├── .env-dist
├── .gitignore
├── LICENSE
├── Procfile
├── README.md
├── alerts
├── __init__.py
├── admin.py
├── apps.py
├── management
│ └── commands
│ │ └── send_alerts.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0002_alert_sent.py
│ └── __init__.py
├── models.py
├── tests.py
└── views.py
├── api
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│ └── __init__.py
├── models.py
├── readme.md
├── tests.py
├── urls.py
└── views.py
├── courtbot
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py
├── lambda_function.py
├── manage.py
├── requirements.txt
├── static
├── css
│ └── styles.css
└── images
│ ├── COURTBOT.png
│ ├── CourtbotBanner.png
│ ├── CourtbotBannerr.png
│ ├── brigade.png
│ ├── cfm.png
│ ├── cft.png
│ ├── icon-isolated-short.png
│ ├── icon-isolated-short.psd
│ ├── icon-isolated.png
│ ├── icon-isolated.psd
│ ├── lawyer-client.jpg
│ ├── logo-no-border.png
│ └── logo-no-border.psd
├── staticfiles
└── .gitkeep
└── website
├── __init__.py
├── admin.py
├── apps.py
├── migrations
└── __init__.py
├── models.py
├── templates
├── base_generic.html
├── index.html
└── privacy.html
├── tests.py
├── urls.py
└── views.py
/.env-dist:
--------------------------------------------------------------------------------
1 | DJANGO_DEBUG=True
2 |
3 | TWILIO_ACCOUNT_SID=
4 | TWILIO_AUTH_TOKEN=
5 | TWILIO_FROM_NUMBER=
6 | TWILIO_TO_NUMBER= #only use for test_twilio.py
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | __pycache__
3 | db.sqlite3
4 | env/
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014, Code for America
2 |
3 | Permission to use, copy, modify, and distribute this software for any
4 | purpose with or without fee is hereby granted, provided that the above
5 | copyright notice and this permission notice appear in all copies.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: newrelic-admin run-program gunicorn courtbot.wsgi
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # courtbot-python
2 | This is an experimental reimplementation of courtbot using python. It uses the
3 | [oscn](https://pypi.org/project/oscn/) PyPI library and twilio.
4 |
5 | ## Development
6 | ### Requirements
7 | * python
8 | * A twilio account
9 | * PostgreSQL
10 |
11 | ### Install & Set up locally
12 | 1. `pip install -r requirements.txt` (if you get an error, make sure `postgresql` is installed, then add the folder containing pg_config to your path `export PATH=”/usr/local/bin:/Library/PostgreSQL/9.6/bin:$PATH”`)
13 | 2. `cp .env-dist .env` and put your own values into `.env`
14 | 3. `python manage.py runserver`
15 |
16 |
17 | ### Use ngrok to test twilio callbacks
18 | To test twilio callbacks, run ngrok to set up a public domain for your
19 | localhost:8000 ...
20 |
21 | `ngrok http 8000`
22 |
23 | ... then use the public domain url for your number's Messaging "a messages
24 | comes in" webhook. E.g., http://34ae567a.ngrok.io/sms/twilio
25 |
26 | Note: super annoying, but every time you restart ngrok you'll have to update
27 | your twilio number's messaging webhook again, and you'll need to change your
28 | `ALLOWED_HOSTS` to include the new ngrok host. Unless you pay for ngrok.
29 |
--------------------------------------------------------------------------------
/alerts/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefortulsa/courtbot-python/2d8f0048fae086d4aabc0ed40d001aba750e623d/alerts/__init__.py
--------------------------------------------------------------------------------
/alerts/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | from .models import Alert
4 |
5 |
6 | class AlertAdmin(admin.ModelAdmin):
7 | pass
8 |
9 |
10 | admin.site.register(Alert, AlertAdmin)
11 |
--------------------------------------------------------------------------------
/alerts/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class AlertsConfig(AppConfig):
5 | name = 'alerts'
6 |
--------------------------------------------------------------------------------
/alerts/management/commands/send_alerts.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | from django.core.management.base import BaseCommand, CommandError
4 |
5 | from ...models import Alert
6 |
7 | from twilio.rest import Client
8 | from decouple import config
9 |
10 | TWILIO_ACCOUNT_SID = config('TWILIO_ACCOUNT_SID')
11 | TWILIO_AUTH_TOKEN = config('TWILIO_AUTH_TOKEN')
12 | TWILIO_FROM_NUMBER = config('TWILIO_FROM_NUMBER')
13 |
14 |
15 | class Command(BaseCommand):
16 | help = 'Sends un-sent alerts for the current hour'
17 |
18 | def handle(self, *args, **options):
19 | unsent_alerts = Alert.objects.filter(sent=False, when=datetime.today())
20 | client = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)
21 | for unsent_alert in unsent_alerts:
22 | message = client.messages.create(
23 | to=str(unsent_alert.to),
24 | from_="+19189134069",
25 | body=unsent_alert.what)
26 | unsent_alert.sent = True
27 | unsent_alert.save()
28 | print('Sent message "' + unsent_alert.what + '" to ' + str(unsent_alert.to))
29 |
--------------------------------------------------------------------------------
/alerts/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.1.5 on 2019-02-17 18:58
2 |
3 | from django.db import migrations, models
4 | import phonenumber_field.modelfields
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | initial = True
10 |
11 | dependencies = [
12 | ]
13 |
14 | operations = [
15 | migrations.CreateModel(
16 | name='Alert',
17 | fields=[
18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
19 | ('when', models.DateField()),
20 | ('to', phonenumber_field.modelfields.PhoneNumberField(max_length=128)),
21 | ('what', models.CharField(max_length=255)),
22 | ],
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/alerts/migrations/0002_alert_sent.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.1.5 on 2019-02-17 19:49
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('alerts', '0001_initial'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='alert',
15 | name='sent',
16 | field=models.BooleanField(default=False),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/alerts/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefortulsa/courtbot-python/2d8f0048fae086d4aabc0ed40d001aba750e623d/alerts/migrations/__init__.py
--------------------------------------------------------------------------------
/alerts/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | from phonenumber_field.modelfields import PhoneNumberField
4 |
5 |
6 | class Alert(models.Model):
7 | when = models.DateField()
8 | to = PhoneNumberField()
9 | what = models.CharField(max_length=255)
10 | sent = models.BooleanField(default=False)
11 |
--------------------------------------------------------------------------------
/alerts/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/alerts/views.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render
2 |
3 | # Create your views here.
4 |
--------------------------------------------------------------------------------
/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefortulsa/courtbot-python/2d8f0048fae086d4aabc0ed40d001aba750e623d/api/__init__.py
--------------------------------------------------------------------------------
/api/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 |
--------------------------------------------------------------------------------
/api/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class ApiConfig(AppConfig):
5 | name = 'api'
6 |
--------------------------------------------------------------------------------
/api/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefortulsa/courtbot-python/2d8f0048fae086d4aabc0ed40d001aba750e623d/api/migrations/__init__.py
--------------------------------------------------------------------------------
/api/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | # Create your models here.
4 |
--------------------------------------------------------------------------------
/api/readme.md:
--------------------------------------------------------------------------------
1 | The courbot API revolves around 2 endpoints.
2 |
3 | * A `GET` endpoint that receives case query parameters and returns the court date.
4 | * A `POST` endpoint that receives the court date and a phone number, and creates 2 text alerts: for 1 day before and 1 week before.
5 |
6 | For a brigade to add themselves to courbot, they need to implement the first
7 | `GET` API endpoint, and [file an
8 | issue](https://github.com/codefortulsa/courtbot-python/issues/new) to add the
9 | endpoint url to courtbot. If your API endpoint needs new case query
10 | parameters in order to find the court date, please include them in the issue so
11 | we can update the courtbot code to pass the required query parameters to your
12 | endpoint.
13 |
14 | # `GET` court date endpoint
15 | This endpoint receives lookup data as url parameters and returns a JSON
16 | response with an `arraignment_datetime` field containing the court date.
17 |
18 | ## Example url
19 | ***`https://courtbot-python.herokuapp.com/api/case?year=2019&county=Tulsa&case_num=1`***
20 |
21 | ## Example Response
22 | ```
23 | {
24 | "arraignment_datetime": "2019-01-07T09:00:00", // required
25 | "case": { // optional
26 | "type": "CF",
27 | "year": "2019",
28 | "county": "Tulsa",
29 | "number": "1"
30 | }
31 | }
32 | ```
33 |
34 | # `POST` alerts endpoint
35 | This endpoint receives the `arraignment_datetime` and a `phone_num` and creates
36 | the text message alerts to be sent.
37 |
38 | ## Example URL
39 | ***`https://courtbot-python.herokuapp.com/api/reminders`***
40 |
41 | ## Example `POST` data
42 | ```
43 | case_num=CF-2014-5203&phone_num=19182615259&arraignment_datetime=2019-09-17T08:00:00
44 | ```
45 |
46 | # Expected Post Response
47 | ```
48 | {
49 | "status": "201 Created",
50 | "week_before_datetime": "2018-12-31T09:00:00",
51 | "day_before_datetime": "2019-01-06T09:00:00"
52 | }
53 | ```
54 |
55 |
--------------------------------------------------------------------------------
/api/tests.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timedelta
2 | import json
3 |
4 | from django.test import RequestFactory, TestCase
5 |
6 | from .views import (
7 | reminders,
8 | eligible_jurisdiction,
9 | find_arraignment_or_return_False,
10 | )
11 | from alerts.models import Alert
12 |
13 |
14 | class testReminders(TestCase):
15 | def setUp(self):
16 | self.factory = RequestFactory()
17 | class Event:
18 | case_index
19 | Docket
20 | Event
21 | Party
22 | Reporter
23 |
24 | def _post(self, url, params):
25 | return self.factory.post(url, params)
26 |
27 | def _get(self, url, params):
28 | return self.factory.get(url, params)
29 |
30 | def testReminderWithArraignmentIn8Days(self):
31 | arraignment_datetime = (datetime.today() + timedelta(days=8)).strftime('%Y-%m-%dT%H:%M:%S')
32 | request = self._post('/api/reminders', {
33 | "arraignment_datetime": arraignment_datetime,
34 | "case_num": 1,
35 | "phone_num": "+1-918-555-5555",
36 | })
37 | response = reminders(request)
38 | resp_json = json.loads(response.content)
39 | self.assertEqual(Alert.objects.all().count(), 2)
40 | self.assertEqual(resp_json['status'], '201 Created')
41 |
42 | def testReminderWithArraignmentIn2Days(self):
43 | arraignment_datetime = (datetime.today() + timedelta(days=2)).strftime('%Y-%m-%dT%H:%M:%S')
44 | request = self._post('/api/reminders', {
45 | "arraignment_datetime": arraignment_datetime,
46 | "case_num": 2,
47 | "phone_num": "+1-918-555-5555",
48 | })
49 | response = reminders(request)
50 | resp_json = json.loads(response.content)
51 | self.assertEqual(Alert.objects.all().count(), 1)
52 | self.assertEqual(resp_json['status'], '201 Created')
53 |
54 | def testReminderWithArraignmentToday(self):
55 | arraignment_datetime = datetime.today().strftime('%Y-%m-%dT%H:%M:%S')
56 | request = self._post('/api/reminders', {
57 | "arraignment_datetime": arraignment_datetime,
58 | "case_num": 3,
59 | "phone_num": "+1-918-555-5555",
60 | })
61 | response = reminders(request)
62 | resp_json = json.loads(response.content)
63 | self.assertEqual(Alert.objects.all().count(), 0)
64 | self.assertEqual(resp_json['status'], '410 Gone')
65 |
66 | def testPreventDuplicateReminder(self):
67 | arraignment_datetime = (datetime.today() + timedelta(days=8)).strftime('%Y-%m-%dT%H:%M:%S')
68 | request = self._post('/api/reminders', {
69 | "arraignment_datetime": arraignment_datetime,
70 | "case_num": 1,
71 | "phone_num": "+1-918-555-5555",
72 | })
73 | response = reminders(request)
74 | resp_json = json.loads(response.content)
75 | self.assertEqual(Alert.objects.all().count(), 2)
76 | self.assertEqual(resp_json['status'], '201 Created')
77 | reminders(request) # Submitting a duplicate reminder
78 | self.assertEqual(Alert.objects.all().count(), 2)
79 | self.assertEqual(resp_json['status'], '201 Created')
80 |
81 | def testEligibleJurisdictions(self):
82 | request = self._get('/api/eligible_jurisdiction', {
83 | "state": 'OK'
84 | })
85 | response = eligible_jurisdiction(request)
86 | resp_json = json.loads(response.content)
87 | print(resp_json)
88 | self.assertEqual(resp_json, {
89 | 'jurisdiction_type': 'county',
90 | 'eligible_jurisdictions': [
91 | 'adair', 'alfalfa', 'appellate', 'atoka', 'beaver', 'beckham',
92 | 'blaine', 'bryan', 'caddo', 'canadian', 'carter', 'cherokee',
93 | 'choctaw', 'cimarron', 'cleveland', 'coal', 'comanche',
94 | 'cotton', 'craig', 'creek', 'bristow', 'drumright', 'custer',
95 | 'delaware', 'dewey', 'ellis', 'garfield', 'garvin', 'grady',
96 | 'grant', 'greer', 'harmon', 'harper', 'haskell', 'hughes',
97 | 'jackson', 'jefferson', 'johnston', 'kay', 'poncacity',
98 | 'kingfisher', 'kiowa', 'latimer', 'leflore', 'lincoln',
99 | 'logan', 'love', 'major', 'marshall', 'mayes', 'mcclain',
100 | 'mccurtain', 'mcintosh', 'murray', 'muskogee', 'noble',
101 | 'nowata', 'okfuskee', 'oklahoma', 'okmulgee', 'henryetta',
102 | 'osage', 'ottawa', 'payne', 'pawnee', 'pittsburg', 'pontotoc',
103 | 'pottawatomie', 'pushmataha', 'rogermills', 'rogers',
104 | 'seminole', 'sequoyah', 'stephens', 'texas', 'tillman',
105 | 'tulsa', 'wagoner', 'washington', 'washita', 'woods',
106 | 'woodward']})
107 |
--------------------------------------------------------------------------------
/api/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 |
4 | from . import views
5 |
6 |
7 | urlpatterns = [
8 | path('case', views.case, name='case'),
9 | path('reminders', views.reminders, name='reminders'),
10 | path('eligible-jurisdiction', views.eligible_jurisdiction, name='eligible_jurisdiction'),
11 | ]
12 |
--------------------------------------------------------------------------------
/api/views.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timedelta
2 | import re
3 |
4 | from django.http import JsonResponse, HttpResponse
5 | from django.shortcuts import render
6 | from django.views.decorators.csrf import csrf_exempt
7 |
8 | import oscn
9 |
10 |
11 | from alerts.models import Alert
12 |
13 |
14 | @csrf_exempt
15 | def case(request):
16 | year_regex = re.compile(r'^[0-9]{4}$')
17 | this_year = datetime.today().year
18 |
19 | if request.method == 'GET':
20 | year = request.GET.get('year', 'NOT PROVIDED')
21 | county = request.GET.get('county', 'NOT PROVIDED')
22 | case_num = request.GET.get('case_num', 'NOT PROVIDED')
23 |
24 | if not year_regex.match(year) or int(year) > this_year or int(year) < 1900:
25 | err_msg = (
26 | f'invalid year: {year}')
27 | return JsonResponse({'error': err_msg})
28 | try:
29 | case = oscn.request.Case(year=year, county=county, number=case_num)
30 | except Exception as exc:
31 | print(exc)
32 | err_msg = (
33 | f'Unable to find case with the following information: '
34 | f'year {year}, county {county}, case number {case_num}')
35 | return JsonResponse({'error': err_msg})
36 |
37 | arraignment_event = find_arraignment_or_return_False(case.events)
38 | if not arraignment_event:
39 | err_msg = (
40 | f'Unable to find arraignment event with the following '
41 | f'year {year}, county {county}, case number {case_num}')
42 | return JsonResponse({'error': err_msg})
43 | arraignment_datetime = parse_datetime_from_oscn_event_string(
44 | arraignment_event.Event
45 | )
46 |
47 | return JsonResponse({
48 | 'case': {
49 | 'type': case.type,
50 | 'year': case.year,
51 | 'county': case.county,
52 | 'number': case.number,
53 | },
54 | 'arraignment_datetime': arraignment_datetime
55 | })
56 | return HttpResponse(status=405)
57 |
58 |
59 | @csrf_exempt
60 | def reminders(request):
61 | case_num = request.POST['case_num']
62 | phone_num = request.POST['phone_num']
63 | arraignment_datetime = datetime.strptime(
64 | request.POST['arraignment_datetime'], "%Y-%m-%dT%H:%M:%S"
65 | )
66 |
67 | week_alert_datetime = arraignment_datetime - timedelta(days=7)
68 | day_alert_datetime = arraignment_datetime - timedelta(days=1)
69 | message = {
70 | "status":"201 Created"
71 | }
72 | if week_alert_datetime > datetime.today():
73 | Alert.objects.get_or_create(
74 | when=week_alert_datetime,
75 | what=f'Arraignment for case {case_num} in 1 week at {arraignment_datetime}',
76 | to=phone_num
77 | )
78 | message['week_before_datetime'] = week_alert_datetime
79 | if day_alert_datetime > datetime.today():
80 | Alert.objects.get_or_create(
81 | when=day_alert_datetime,
82 | what=f'Arraignment for case {case_num} in 1 day at {arraignment_datetime}',
83 | to=phone_num
84 | )
85 | message['day_before_datetime'] = day_alert_datetime
86 | return JsonResponse(message, status=201)
87 | else:
88 | return JsonResponse({
89 | "status":"410 Gone", #https://www.codetinkerer.com/2015/12/04/choosing-an-http-status-code.html
90 | "error":f'Arraignment for case {case_num} has already passed'
91 | }, status=410)
92 |
93 |
94 | @csrf_exempt
95 | def eligible_jurisdiction(request):
96 | if request.method == 'GET':
97 | state = request.GET.get('state', 'NOT PROVIDED')
98 |
99 | if state == 'OK':
100 | jurisdiction_type = 'county'
101 | eligible_jurisdictions = oscn.counties
102 | return JsonResponse({
103 | 'jurisdiction_type': jurisdiction_type,
104 | 'eligible_jurisdictions': eligible_jurisdictions,
105 | })
106 | else:
107 | err_msg = (
108 | f'That eligible jurisdiction list is not available '
109 | f'at this time.')
110 | return JsonResponse({'error': err_msg})
111 |
112 | return HttpResponse(status=405)
113 |
114 |
115 | def find_arraignment_or_return_False(events):
116 | for event in events:
117 | if not "Event" in dir(event):
118 | continue
119 | if "arraignment" in event.Event.lower():
120 | return event
121 | return False
122 |
123 |
124 | def parse_datetime_from_oscn_event_string(event):
125 | event = event.replace('ARRAIGNMENT', '').rstrip()
126 | return datetime.strptime(event, "%A, %B %d, %Y at %I:%M %p")
127 |
--------------------------------------------------------------------------------
/courtbot/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefortulsa/courtbot-python/2d8f0048fae086d4aabc0ed40d001aba750e623d/courtbot/__init__.py
--------------------------------------------------------------------------------
/courtbot/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for courtbot project.
3 |
4 | Generated by 'django-admin startproject' using Django 2.1.5.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/2.1/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/2.1/ref/settings/
11 | """
12 |
13 | import os
14 |
15 | from decouple import config
16 | import django_heroku
17 |
18 |
19 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
20 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
21 |
22 |
23 | # Quick-start development settings - unsuitable for production
24 | # See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/
25 |
26 | # SECURITY WARNING: keep the secret key used in production secret!
27 | SECRET_KEY = config('SECRET_KEY', default='unsafe-secret-key-for-dev')
28 |
29 | # SECURITY WARNING: don't run with debug turned on in production!
30 | DEBUG = config('DJANGO_DEBUG', default=False, cast=bool)
31 |
32 | ALLOWED_HOSTS = [
33 | config('DJANGO_ALLOWED_HOSTS', default="127.0.0.1")
34 | ]
35 |
36 |
37 | # Application definition
38 |
39 | INSTALLED_APPS = [
40 | 'django.contrib.admin',
41 | 'django.contrib.auth',
42 | 'django.contrib.contenttypes',
43 | 'django.contrib.sessions',
44 | 'django.contrib.messages',
45 | 'django.contrib.staticfiles',
46 |
47 | 'phonenumber_field',
48 |
49 | 'website',
50 | 'alerts',
51 | ]
52 |
53 | MIDDLEWARE = [
54 | 'django.middleware.security.SecurityMiddleware',
55 | 'django.contrib.sessions.middleware.SessionMiddleware',
56 | 'django.middleware.common.CommonMiddleware',
57 | 'django.middleware.csrf.CsrfViewMiddleware',
58 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
59 | 'django.contrib.messages.middleware.MessageMiddleware',
60 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
61 |
62 | 'whitenoise.middleware.WhiteNoiseMiddleware',
63 | ]
64 |
65 | ROOT_URLCONF = 'courtbot.urls'
66 |
67 | TEMPLATES = [
68 | {
69 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
70 | 'DIRS': [
71 | os.path.join(BASE_DIR, 'templates'),
72 | ],
73 | 'APP_DIRS': True,
74 | 'OPTIONS': {
75 | 'context_processors': [
76 | 'django.template.context_processors.debug',
77 | 'django.template.context_processors.request',
78 | 'django.contrib.auth.context_processors.auth',
79 | 'django.contrib.messages.context_processors.messages',
80 | ],
81 | },
82 | },
83 | ]
84 |
85 | WSGI_APPLICATION = 'courtbot.wsgi.application'
86 |
87 |
88 | # Database
89 | # https://docs.djangoproject.com/en/2.1/ref/settings/#databases
90 |
91 | DATABASES = {
92 | 'default': {
93 | 'ENGINE': 'django.db.backends.sqlite3',
94 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
95 | }
96 | }
97 |
98 |
99 | # Password validation
100 | # https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators
101 |
102 | AUTH_PASSWORD_VALIDATORS = [
103 | {
104 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
105 | },
106 | {
107 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
108 | },
109 | {
110 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
111 | },
112 | {
113 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
114 | },
115 | ]
116 |
117 |
118 | # Internationalization
119 | # https://docs.djangoproject.com/en/2.1/topics/i18n/
120 |
121 | LANGUAGE_CODE = 'en-us'
122 |
123 | TIME_ZONE = 'UTC'
124 |
125 | USE_I18N = True
126 |
127 | USE_L10N = True
128 |
129 | USE_TZ = True
130 |
131 |
132 | # Static files (CSS, JavaScript, Images)
133 | # https://docs.djangoproject.com/en/2.1/howto/static-files/
134 | STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
135 | STATIC_URL = '/static/'
136 |
137 | STATICFILES_DIRS = [
138 | os.path.join(BASE_DIR, 'static'),
139 | ]
140 |
141 | STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
142 |
143 | SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
144 | SECURE_SSL_REDIRECT = config('SECURE_SSL_REDIRECT', default=not DEBUG)
145 |
146 | django_heroku.settings(locals())
147 |
--------------------------------------------------------------------------------
/courtbot/urls.py:
--------------------------------------------------------------------------------
1 | """courtbot URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/2.1/topics/http/urls/
5 | Examples:
6 | Function views
7 | 1. Add an import: from my_app import views
8 | 2. Add a URL to urlpatterns: path('', views.home, name='home')
9 | Class-based views
10 | 1. Add an import: from other_app.views import Home
11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
12 | Including another URLconf
13 | 1. Import the include() function: from django.urls import include, path
14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
15 | """
16 | from django.contrib import admin
17 | from django.urls import include, path
18 | from django.conf import settings
19 | from django.views.generic import RedirectView
20 |
21 | urlpatterns = [
22 | path('api/', include('api.urls')),
23 | path('admin/', admin.site.urls),
24 | path('', include('website.urls')),
25 | ]
26 |
--------------------------------------------------------------------------------
/courtbot/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for courtbot project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'courtbot.settings')
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/lambda_function.py:
--------------------------------------------------------------------------------
1 | """
2 | This sample demonstrates an implementation of the Lex Code Hook Interface
3 | in order to serve a sample bot which manages orders for flowers.
4 | Bot, Intent, and Slot models which are compatible with this sample can be found in the Lex Console
5 | as part of the 'OrderFlowers' template.
6 |
7 | For instructions on how to set up and test this bot, as well as additional samples,
8 | visit the Lex Getting Started documentation http://docs.aws.amazon.com/lex/latest/dg/getting-started.html.
9 |
10 | 20190604 : seyeon : Initial validations for year, county, and case number
11 | 20190713 : seyeon : Added GET call to courtbot-python herokuapp
12 | """
13 | import datetime
14 | import dateutil.parser
15 | import json
16 | import logging
17 | import os
18 | import re
19 | import urllib.request
20 | import time
21 |
22 |
23 | logger = logging.getLogger()
24 | logger.setLevel(logging.DEBUG)
25 |
26 |
27 | """ --- Helpers to build responses which match the structure of the necessary dialog actions --- """
28 |
29 |
30 | def get_slots(intent_request):
31 | return intent_request['currentIntent']['slots']
32 |
33 |
34 | def elicit_slot(session_attributes, intent_name, slots, slot_to_elicit, message):
35 | return {
36 | 'sessionAttributes': session_attributes,
37 | 'dialogAction': {
38 | 'type': 'ElicitSlot',
39 | 'intentName': intent_name,
40 | 'slots': slots,
41 | 'slotToElicit': slot_to_elicit,
42 | 'message': message
43 | }
44 | }
45 |
46 | def confirm_slot(session_attributes, intent_name, slots, message):
47 | return {
48 | 'sessionAttributes': session_attributes,
49 | 'dialogAction': {
50 | 'type': 'ConfirmIntent',
51 | 'intentName': intent_name,
52 | 'slots': slots,
53 | 'message': message
54 | }
55 | }
56 |
57 | def close(session_attributes, fulfillment_state, message):
58 | response = {
59 | 'sessionAttributes': session_attributes,
60 | 'dialogAction': {
61 | 'type': 'Close',
62 | 'fulfillmentState': fulfillment_state,
63 | 'message': message
64 | }
65 | }
66 |
67 | return response
68 |
69 |
70 | def delegate(session_attributes, slots):
71 | return {
72 | 'sessionAttributes': session_attributes,
73 | 'dialogAction': {
74 | 'type': 'Delegate',
75 | 'slots': slots
76 | }
77 | }
78 |
79 |
80 | """ --- Helper Functions --- """
81 |
82 |
83 | def parse_int(n):
84 | try:
85 | return int(n)
86 | except ValueError:
87 | return float('nan')
88 |
89 |
90 | def build_validation_result(is_valid, violated_slot, message_content):
91 | if message_content is None:
92 | return {
93 | "isValid": is_valid,
94 | "violatedSlot": violated_slot,
95 | }
96 |
97 | return {
98 | 'isValid': is_valid,
99 | 'violatedSlot': violated_slot,
100 | 'message': {'contentType': 'PlainText', 'content': message_content}
101 | }
102 |
103 |
104 | def isvalid_date(date):
105 | try:
106 | dateutil.parser.parse(date)
107 | return True
108 | except ValueError:
109 | return False
110 |
111 | def validate_eligible_county(county):
112 | counties = ['Tulsa', 'Rogers', 'Muskogee', 'Wagoner']
113 | normalized_counties = []
114 | for eligible_county in counties:
115 | normalized_counties.append(eligible_county.lower())
116 | if county is not None and county.lower() not in normalized_counties:
117 | eligible_counties = ', '.join(str(c) for c in counties)
118 | message = ('We do not have {0} as a serviceable area, '
119 | 'would you like to look up case from one of our eligible '
120 | 'counties? Counties we currently service are: {1}'.format(
121 | county, eligible_counties))
122 | return False, 'County', message
123 | return True, None, None
124 |
125 | def validate_year(year):
126 | message = None
127 | year_range = 15
128 | valid_year = 0
129 | try:
130 | valid_year = int(year)
131 | except ValueError:
132 | message = ('Please enter the year as a 4 digit number, such as 2019. '
133 | 'What year is the case in?')
134 | return False, 'Year', message
135 | if len(year) != 4:
136 | message = ('Please enter the year as a 4 digit number, such as 2019. '
137 | 'What year is the case in?')
138 | return False, 'Year', message
139 | if abs(valid_year - 2019) > year_range:
140 | # range in which cases are searchable
141 | message = ('Unfortunately we cannot look up cases older or later than '
142 | '{0} years. Would you like to look up a case between the '
143 | '{0} years in the past or future?'.format(year_range))
144 | return False, 'Year', message
145 | return True, None, message
146 |
147 | def validate_case_number(county, year, case_number, intent_request):
148 | regex = '\w{2,}-\w{4}-\w{1,}$'
149 | if not re.match(regex, case_number):
150 | message = 'Please enter the case number formatted like CF-2019-1234'
151 | return False, 'CaseID', message
152 |
153 | # call the endpoint for validating case number
154 | courtbot_url = (f'http://courtbot-python.herokuapp.com/api/case?'
155 | f'county={county}&year={year}&case_num={case_number}')
156 | # response = requests.get(url=courtbot_url)
157 | response = urllib.request.urlopen(courtbot_url)
158 | case_info = json.load(response)
159 | message = None
160 | if 'arraignment_datetime' in case_info:
161 | event_date = case_info.get('arraignment_datetime')
162 | output_session_attributes = intent_request['sessionAttributes'] if intent_request['sessionAttributes'] is not None else {}
163 | output_session_attributes['event_date'] = event_date
164 | output_session_attributes['case_id'] = case_number
165 | event_date = datetime.datetime.strptime(
166 | event_date, '%Y-%m-%dT%H:%M:%S')
167 | message = f'You have an event coming up at {event_date}. Would you like to set a reminder?'
168 | else:
169 | message = case_info.get('error')
170 | return False, 'CaseID', message
171 | return True, None, message
172 |
173 | def validate_reminder(reminder, intent_request):
174 | bool_reminder = None
175 | message = ''
176 | output_session_attributes = intent_request['sessionAttributes'] if intent_request['sessionAttributes'] is not None else {}
177 | if reminder.lower() in ['y', 'yes']:
178 | bool_reminder = True
179 | elif reminder.lower() in ['n', 'no']:
180 | bool_reminder = False
181 | else:
182 | message = 'Please enter "y" or "yes" to setup a reminder.'
183 | return False, 'Reminder', message
184 | return True, None, message
185 |
186 | def validate_phone_num(phone_num, intent_request):
187 | num_list = phone_num.split('-')
188 | num_str = '1' + ''.join(num_list)
189 | if len(num_str) != 11:
190 | message = 'Please enter a phone number formatted like 516-111-2222'
191 | return False, 'PhoneNumber', message
192 |
193 | output_session_attributes = intent_request['sessionAttributes'] if intent_request['sessionAttributes'] is not None else {}
194 | case_id = output_session_attributes['case_id']
195 | event_date = output_session_attributes['event_date']
196 | courtbot_url = f'http://courtbot-python.herokuapp.com/api/reminders'
197 | reminder_data = (f'case_num={case_id}&phone_num={phone_num}&'
198 | f'arraignment_datetime={event_date}'
199 | ).encode(encoding='UTF-8')
200 | response = urllib.request.urlopen(courtbot_url, data=reminder_data)
201 | reminder_info = json.load(response)
202 | if 'week_before_datetime' in reminder_info:
203 | week_before_datetime = reminder_info.get('week_before_datetime')
204 | day_before_datetime = reminder_info.get('day_before_datetime')
205 | output_session_attributes['week_before_datetime'] = week_before_datetime
206 | output_session_attributes['day_before_datetime'] = day_before_datetime
207 | output_session_attributes['confirm_reminder'] = True
208 | output_session_attributes['phone_num'] = num_str
209 | message = (
210 | f'We will send you a reminder text on {week_before_datetime} '
211 | f'and {week_before_datetime}.')
212 | return True, None, message
213 | message = 'Something was wrong while setting up the reminder.'
214 | return False, 'PhoneNumber', message
215 |
216 | def validate_case_info(
217 | county, year, case_number, reminder, phone_num, intent_request):
218 | is_valid, slot, message = validate_eligible_county(county)
219 | if not is_valid:
220 | return build_validation_result(is_valid, slot, message)
221 |
222 | if year is not None:
223 | is_valid, slot, message = validate_year(year)
224 | if not is_valid:
225 | return build_validation_result(is_valid, slot, message)
226 |
227 | if case_number is not None:
228 | is_valid, slot, message = validate_case_number(
229 | county, year, case_number, intent_request)
230 | if not is_valid:
231 | return build_validation_result(is_valid, slot, message)
232 | # return build_validation_result(is_valid, slot, message)
233 |
234 | if reminder is not None:
235 | is_valid, slot, message = validate_reminder(reminder, intent_request)
236 | if not is_valid:
237 | return build_validation_result(is_valid, slot, message)
238 |
239 | if phone_num is not None:
240 | is_valid, slot, message = validate_phone_num(phone_num, intent_request)
241 | if not is_valid:
242 | return build_validation_result(is_valid, slot, message)
243 | return build_validation_result(True, None, message)
244 |
245 |
246 | """ --- Functions that control the bot's behavior --- """
247 |
248 |
249 | def get_case_info(intent_request):
250 | """
251 | Performs dialog management and fulfillment for ordering flowers.
252 | Beyond fulfillment, the implementation of this intent demonstrates the use of the elicitSlot dialog action
253 | in slot validation and re-prompting.
254 | """
255 |
256 | county = get_slots(intent_request)["County"]
257 | year = get_slots(intent_request)["Year"]
258 | case_number = get_slots(intent_request)["CaseID"]
259 | reminder = get_slots(intent_request)["Reminder"]
260 | phone_number = get_slots(intent_request)["PhoneNumber"]
261 | source = intent_request['invocationSource']
262 |
263 | if source == 'DialogCodeHook':
264 | # Perform basic validation on the supplied input slots.
265 | # Use the elicitSlot dialog action to re-prompt for the first violation detected.
266 | slots = get_slots(intent_request)
267 |
268 | validation_result = validate_case_info(
269 | county, year, case_number, reminder, phone_number, intent_request)
270 | if not validation_result['isValid']:
271 | slots[validation_result['violatedSlot']] = None
272 | return elicit_slot(intent_request['sessionAttributes'],
273 | intent_request['currentIntent']['name'],
274 | slots,
275 | validation_result['violatedSlot'],
276 | validation_result['message'])
277 |
278 | # Pass the price of the flowers back through session attributes to be used in various prompts defined
279 | # on the bot model.
280 | output_session_attributes = intent_request['sessionAttributes'] if intent_request['sessionAttributes'] is not None else {}
281 | return delegate(output_session_attributes, get_slots(intent_request))
282 |
283 | output_session_attributes = intent_request['sessionAttributes'] if intent_request['sessionAttributes'] is not None else {}
284 | week_before_datetime = output_session_attributes['week_before_datetime']
285 | day_before_datetime = output_session_attributes['day_before_datetime']
286 | phone_num = output_session_attributes.get('phone_num', 'NO NUMBER FOUND')
287 | success_message = (f'You will get a text on {week_before_datetime} and {day_before_datetime} on the number {phone_num}. '
288 | 'Thank you for using Courtbot. '
289 | 'For any comments or concerns contact us at '
290 | 'https://www.okcourtbot.com/#faq')
291 | return close(intent_request['sessionAttributes'],
292 | 'Fulfilled',
293 | {'contentType': 'PlainText', 'content': success_message})
294 |
295 |
296 | """ --- Intents --- """
297 |
298 |
299 | def dispatch(intent_request):
300 | """
301 | Called when the user specifies an intent for this bot.
302 | """
303 |
304 | logger.debug('dispatch userId={}, intentName={}'.format(intent_request['userId'], intent_request['currentIntent']['name']))
305 |
306 | intent_name = intent_request['currentIntent']['name']
307 |
308 | # Dispatch to your bot's intent handlers
309 | if intent_name == 'GetCaseInfo':
310 | return get_case_info(intent_request)
311 |
312 | raise Exception('Intent with name ' + intent_name + ' not supported')
313 |
314 |
315 | """ --- Main handler --- """
316 |
317 |
318 | def lambda_handler(event, context):
319 | """
320 | Route the incoming request based on intent.
321 | The JSON body of the request is provided in the event slot.
322 | """
323 | # By default, treat the user request as coming from the America/New_York time zone.
324 | os.environ['TZ'] = 'America/New_York'
325 | time.tzset()
326 | logger.debug('event.bot.name={}'.format(event['bot']['name']))
327 |
328 | return dispatch(event)
329 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == '__main__':
6 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'courtbot.settings')
7 | try:
8 | from django.core.management import execute_from_command_line
9 | except ImportError as exc:
10 | raise ImportError(
11 | "Couldn't import Django. Are you sure it's installed and "
12 | "available on your PYTHONPATH environment variable? Did you "
13 | "forget to activate a virtual environment?"
14 | ) from exc
15 | execute_from_command_line(sys.argv)
16 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Django==2.2.24
2 | gunicorn==19.9.0
3 | oscn==0.0.0.32
4 | python-decouple==3.1
5 | twilio==6.24.0
6 | django-heroku==0.3.1
7 | django-phonenumber-field==2.2.0
8 | phonenumbers==8.10.5
9 | psycopg2==2.8.6
10 | whitenoise==4.1.2
11 | newrelic==5.10.0.138
12 |
--------------------------------------------------------------------------------
/static/css/styles.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: 'Montserrat', sans-serif;
3 | color: #302f09;
4 | font-size: 16px;
5 | }
6 |
7 | .center {
8 | text-align: center;
9 | }
10 |
11 | strong, .dark {
12 | color: #484623;
13 | }
14 |
15 | .jumbotron .btn {
16 | margin-top: 15px;
17 | margin-bottom: 15px;
18 | }
19 |
20 | .uppercase {
21 | text-transform: uppercase;
22 | }
23 |
24 | .btn-yellow {
25 | color: #2c2b05;
26 | background-color: #ffe052;
27 | }
28 |
29 | .btn-gold {
30 | color: #fcfbe3;
31 | background-color: #766b22;
32 | }
33 |
34 | section, footer {
35 | padding: 60px 0;
36 | }
37 |
38 | .navbar {
39 | border-bottom: 2px solid black;
40 | }
41 |
42 | .bright-yellow-bkg {
43 | background-color: #fcfbe3;
44 | border-top: 10px solid #ffe052;
45 | border-bottom: 10px solid #ffe052;
46 | color: #2c2b05;
47 | }
48 |
49 | .display-1, h1 {
50 | font-size: 32px;
51 | font-weight: lighter;
52 | }
53 |
54 | .display-2, h2 {
55 | font-size: 28px;
56 | font-weight: bold;
57 | }
58 |
59 | .display-3, h3 {
60 | font-size: 24px;
61 | font-weight: lighter;
62 | }
63 |
64 | h4, .display-4 {
65 | font-size: 18px;
66 | font-weight: bold;
67 | }
68 |
69 | .display-4 {
70 | font-weight: lighter;
71 | }
72 |
73 | .lead {
74 | max-width: 900px;
75 | margin-left: auto;
76 | margin-right: auto;
77 | font-size: 1em;
78 | }
79 |
80 | .CBLtBgClr {
81 | background-color: #F6F3DD;
82 | }
83 |
84 | .CBLtBgClr {
85 | background-color: #F6F3DD;
86 | }
87 |
88 | p {
89 | padding-bottom: 15px;
90 | }
91 |
92 | .facebook {
93 | font-size: 5em;
94 | color: #3B5998;
95 | }
96 |
97 | #sponsor-logo img {
98 | max-width: 200px;
99 | width: 100%;
100 | margin-bottom: 1em;
101 | padding: 0.5em 0;
102 | }
103 |
104 | .app-links {
105 | max-width: 600px;
106 | margin: 0 auto;
107 | }
108 |
109 | .app-links .col-sm-4 {
110 | margin-bottom: 2em;
111 | }
112 |
113 | .app-links a {
114 | background-color: #ffe052;
115 | padding: 0.5em;
116 | border-radius: 5px;
117 | color: #000000;
118 | display: inline-block;
119 | }
120 |
121 | .app-links a:hover {
122 | background-color: #DFC23B;
123 | }
124 |
125 | .app-links em {
126 | font-size: 0.9em;
127 | }
128 |
129 | footer {
130 | color: #999;
131 | background: #EEE;
132 | }
133 |
134 | footer a {
135 | color: #555;
136 | }
137 | footer a:hover {
138 | color: #333;
139 | }
140 |
141 | #case-form {
142 | max-width: 500px;
143 | margin-right: auto;
144 | margin-left: auto;
145 | }
146 |
147 | .form-group {
148 | position: relative;
149 | }
150 |
151 | label {
152 | float: left;
153 | margin-left: 20px;
154 | }
155 |
156 | /* Tablet Breakpoint */
157 | @media all and (min-width: 768px) {
158 | .display-1, h1 {
159 | font-size: 56px;
160 | }
161 |
162 | .display-2, h2 {
163 | font-size: 42px;
164 | }
165 |
166 | .display-3, h3 {
167 | font-size: 36px;
168 | }
169 |
170 | h4 {
171 | font-size: 20px;
172 | }
173 | }
174 |
175 | /* Desktop Breakpoint */
176 | @media all and (min-width: 768px) {
177 | .display-1, h1 {
178 | font-size: 72px;
179 | }
180 |
181 | .display-2, h2 {
182 | font-size: 42px;
183 | }
184 |
185 | .display-3, h3 {
186 | font-size: 32px;
187 | }
188 |
189 | h4, .display-4 {
190 | font-size: 24px;
191 | }
192 | }
193 |
194 | @media(min-width: 1030px) {
195 | .jumbotron .col-sm-4.display-3 br {
196 | display: none;
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/static/images/COURTBOT.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefortulsa/courtbot-python/2d8f0048fae086d4aabc0ed40d001aba750e623d/static/images/COURTBOT.png
--------------------------------------------------------------------------------
/static/images/CourtbotBanner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefortulsa/courtbot-python/2d8f0048fae086d4aabc0ed40d001aba750e623d/static/images/CourtbotBanner.png
--------------------------------------------------------------------------------
/static/images/CourtbotBannerr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefortulsa/courtbot-python/2d8f0048fae086d4aabc0ed40d001aba750e623d/static/images/CourtbotBannerr.png
--------------------------------------------------------------------------------
/static/images/brigade.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefortulsa/courtbot-python/2d8f0048fae086d4aabc0ed40d001aba750e623d/static/images/brigade.png
--------------------------------------------------------------------------------
/static/images/cfm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefortulsa/courtbot-python/2d8f0048fae086d4aabc0ed40d001aba750e623d/static/images/cfm.png
--------------------------------------------------------------------------------
/static/images/cft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefortulsa/courtbot-python/2d8f0048fae086d4aabc0ed40d001aba750e623d/static/images/cft.png
--------------------------------------------------------------------------------
/static/images/icon-isolated-short.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefortulsa/courtbot-python/2d8f0048fae086d4aabc0ed40d001aba750e623d/static/images/icon-isolated-short.png
--------------------------------------------------------------------------------
/static/images/icon-isolated-short.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefortulsa/courtbot-python/2d8f0048fae086d4aabc0ed40d001aba750e623d/static/images/icon-isolated-short.psd
--------------------------------------------------------------------------------
/static/images/icon-isolated.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefortulsa/courtbot-python/2d8f0048fae086d4aabc0ed40d001aba750e623d/static/images/icon-isolated.png
--------------------------------------------------------------------------------
/static/images/icon-isolated.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefortulsa/courtbot-python/2d8f0048fae086d4aabc0ed40d001aba750e623d/static/images/icon-isolated.psd
--------------------------------------------------------------------------------
/static/images/lawyer-client.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefortulsa/courtbot-python/2d8f0048fae086d4aabc0ed40d001aba750e623d/static/images/lawyer-client.jpg
--------------------------------------------------------------------------------
/static/images/logo-no-border.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefortulsa/courtbot-python/2d8f0048fae086d4aabc0ed40d001aba750e623d/static/images/logo-no-border.png
--------------------------------------------------------------------------------
/static/images/logo-no-border.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefortulsa/courtbot-python/2d8f0048fae086d4aabc0ed40d001aba750e623d/static/images/logo-no-border.psd
--------------------------------------------------------------------------------
/staticfiles/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefortulsa/courtbot-python/2d8f0048fae086d4aabc0ed40d001aba750e623d/staticfiles/.gitkeep
--------------------------------------------------------------------------------
/website/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefortulsa/courtbot-python/2d8f0048fae086d4aabc0ed40d001aba750e623d/website/__init__.py
--------------------------------------------------------------------------------
/website/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 |
--------------------------------------------------------------------------------
/website/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class WebsiteConfig(AppConfig):
5 | name = 'website'
6 |
--------------------------------------------------------------------------------
/website/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefortulsa/courtbot-python/2d8f0048fae086d4aabc0ed40d001aba750e623d/website/migrations/__init__.py
--------------------------------------------------------------------------------
/website/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | # Create your models here.
4 |
--------------------------------------------------------------------------------
/website/templates/base_generic.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {% block title %}Free Oklahoma Courtdate Notifications {% endblock %}
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | {% load static %}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
31 |
32 |
33 |
34 |
35 |
36 |
61 |
62 |
63 | {% block content %}{% endblock %}
64 |
65 |
89 |
90 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/website/templates/index.html:
--------------------------------------------------------------------------------
1 | {% extends "base_generic.html" %}
2 |
3 | {% block content %}
4 |
5 |
6 |
7 |
Get Court Date Reminders
8 |
Text your OSCN Case Number:
9 |
12 |
13 | Also available on:
14 |
15 |
16 |
17 |
18 |
SMS Texting
19 |
20 |
21 |
22 |
Facebook Messenger
23 |
24 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | What is CourtBot?
41 | COURTBOT is a text-message app that helps keep everything on track. After cases are booked, clients can text their case number to COURTBOT to receive automatic reminders of court dates. Research shows that reminders work.
42 |
43 |
44 |
45 |
46 |
47 |
48 |
Why CourtBot?
49 |
50 |
51 |
52 | LESS BUREAUCRACY
53 | Around 25% of the people in Tulsa County Jail are waiting for their cases to be heard, costing taxpayers up to $25,000 per day. When clients attend their court cases, more small fines are collected, and jail space is freed for more
54 | serious offenders. Remembering to attend court can help break the cycle of fines and jail time.
55 |
56 |
57 |
58 |
59 | FEWER FINES
60 | Failure-to-appear and failure-to-pay fines can quickly snowball, especially for those living paycheck-to-paycheck. CourtBot helps clients avoid mistakes that burden them and the criminal justice system. Government done well can
61 | help.
62 |
63 |
64 |
65 |
66 |
102 |
135 |
136 |
137 |
138 |
Frequently Asked Questions
139 |
140 |
141 |
142 |
1. How do I know my OSCN Case Number?
143 |
It starts with CF, CM, CRF or CRM and has two dashes.
144 |
145 |
146 |
2. What if I still can't find my case number?
147 |
Check with your public defender or search your name online at www.oscn.net/dockets/search.aspx
148 |
149 |
150 |
151 |
152 |
3. Can other people be reminded of my court date?
153 |
Anyone with your case number will be able to use courtbot to keep track of your court date.
154 |
155 |
156 |
4. How will my information be used?
157 |
Your case information will be pulled into Courtbot's database to be used as a reference for your reminders
158 |
159 |
160 |
5. When will I be reminded of my court case?
161 |
Currently, a notification will be sent the day before you case.
162 |
163 |
164 |
6. How often will I be reminded?
165 |
Currently, you will only be reminded once.
166 |
167 |
168 |
7. How do I unsubscribe?
169 |
170 |
171 |
175 |
176 |
177 |
178 |
179 |
189 |
190 |
191 | {% endblock %}
192 |
--------------------------------------------------------------------------------
/website/templates/privacy.html:
--------------------------------------------------------------------------------
1 | {% extends "base_generic.html" %}
2 |
3 | {% block content %}
4 |
5 |
6 |
7 |
8 |
Privacy Policy
9 |
10 |
Effective date: September 21, 2019
11 |
12 |
13 |
OK CourtBot ("us", "we", or "our") operates the https://www.okcourtbot.com/ website (hereinafter referred to as the "Service").
14 |
15 |
This page informs you of our policies regarding the collection, use, and disclosure of personal data when you use our Service and the choices you have associated with that data.
16 |
17 |
We use your data to provide and improve the Service. By using the Service, you agree to the collection and use of information in accordance with this policy. Unless otherwise defined in this Privacy Policy, the terms used in this Privacy Policy have the same meanings as in our Terms and Conditions, accessible from https://www.okcourtbot.com/
18 |
19 |
20 |
Information Collection And Use
21 |
22 |
We collect several different types of information for various purposes to provide and improve our Service to you.
23 |
24 |
Types of Data Collected
25 |
26 |
Personal Data
27 |
28 |
While using our Service, we may ask you to provide us with certain personally identifiable information that can be used to contact or identify you ("Personal Data"). Personally identifiable information may include, but is not limited to:
29 |
30 |
31 | Phone number Cookies and Usage Data
32 |
33 |
34 |
Usage Data
35 |
36 |
We may also collect information on how the Service is accessed and used ("Usage Data"). This Usage Data may include information such as your computer's Internet Protocol address (e.g. IP address), browser type, browser version, the pages of our Service that you visit, the time and date of your visit, the time spent on those pages, unique device identifiers and other diagnostic data.
37 |
38 |
Tracking & Cookies Data
39 |
We use cookies and similar tracking technologies to track the activity on our Service and hold certain information.
40 |
Cookies are files with small amount of data which may include an anonymous unique identifier. Cookies are sent to your browser from a website and stored on your device. Tracking technologies also used are beacons, tags, and scripts to collect and track information and to improve and analyze our Service.
41 |
You can instruct your browser to refuse all cookies or to indicate when a cookie is being sent. However, if you do not accept cookies, you may not be able to use some portions of our Service.
42 |
Examples of Cookies we use:
43 |
44 | Session Cookies. We use Session Cookies to operate our Service.
45 | Preference Cookies. We use Preference Cookies to remember your preferences and various settings.
46 | Security Cookies. We use Security Cookies for security purposes.
47 |
48 |
49 |
Use of Data
50 |
51 |
OK CourtBot uses the collected data for various purposes:
52 |
53 | To provide and maintain the Service
54 | To notify you about changes to our Service
55 | To allow you to participate in interactive features of our Service when you choose to do so
56 | To provide customer care and support
57 | To provide analysis or valuable information so that we can improve the Service
58 | To monitor the usage of the Service
59 | To detect, prevent and address technical issues
60 |
61 |
62 |
Transfer Of Data
63 |
Your information, including Personal Data, may be transferred to — and maintained on — computers located outside of your state, province, country or other governmental jurisdiction where the data protection laws may differ than those from your jurisdiction.
64 |
If you are located outside United States and choose to provide information to us, please note that we transfer the data, including Personal Data, to United States and process it there.
65 |
Your consent to this Privacy Policy followed by your submission of such information represents your agreement to that transfer.
66 |
OK CourtBot will take all steps reasonably necessary to ensure that your data is treated securely and in accordance with this Privacy Policy and no transfer of your Personal Data will take place to an organization or a country unless there are adequate controls in place including the security of your data and other personal information.
67 |
68 |
Disclosure Of Data
69 |
70 |
Legal Requirements
71 |
OK CourtBot may disclose your Personal Data in the good faith belief that such action is necessary to:
72 |
73 | To comply with a legal obligation
74 | To protect and defend the rights or property of OK CourtBot
75 | To prevent or investigate possible wrongdoing in connection with the Service
76 | To protect the personal safety of users of the Service or the public
77 | To protect against legal liability
78 |
79 |
80 |
Security Of Data
81 |
The security of your data is important to us, but remember that no method of transmission over the Internet, or method of electronic storage is 100% secure. While we strive to use commercially acceptable means to protect your Personal Data, we cannot guarantee its absolute security.
82 |
83 |
Service Providers
84 |
We may employ third party companies and individuals to facilitate our Service ("Service Providers"), to provide the Service on our behalf, to perform Service-related services or to assist us in analyzing how our Service is used.
85 |
These third parties have access to your Personal Data only to perform these tasks on our behalf and are obligated not to disclose or use it for any other purpose.
86 |
87 |
88 |
89 |
Links To Other Sites
90 |
Our Service may contain links to other sites that are not operated by us. If you click on a third party link, you will be directed to that third party's site. We strongly advise you to review the Privacy Policy of every site you visit.
91 |
We have no control over and assume no responsibility for the content, privacy policies or practices of any third party sites or services.
92 |
93 |
94 |
Children's Privacy
95 |
Our Service does not address anyone under the age of 18 ("Children").
96 |
We do not knowingly collect personally identifiable information from anyone under the age of 18. If you are a parent or guardian and you are aware that your Children has provided us with Personal Data, please contact us. If we become aware that we have collected Personal Data from children without verification of parental consent, we take steps to remove that information from our servers.
97 |
98 |
99 |
Changes To This Privacy Policy
100 |
We may update our Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page.
101 |
We will let you know via email and/or a prominent notice on our Service, prior to the change becoming effective and update the "effective date" at the top of this Privacy Policy.
102 |
You are advised to review this Privacy Policy periodically for any changes. Changes to this Privacy Policy are effective when they are posted on this page.
103 |
104 |
105 |
Contact Us
106 |
If you have any questions about this Privacy Policy, please contact us:
107 |
108 | By email: courtbotmuskogee@gmail.com
109 |
110 | By phone number: 4792343025
111 |
112 |
113 |
114 |
115 |
116 | {% endblock %}
117 |
--------------------------------------------------------------------------------
/website/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase, RequestFactory, Client
2 | import json
3 |
4 | class testFormViews(TestCase):
5 |
6 | def setUp(self):
7 | self.factory = RequestFactory()
8 | self.client = Client()
9 |
10 | def test_form_data_view_passed(self):
11 | data = {
12 | "case_num": "CF-2020-1648",
13 | "year": 2020,
14 | "county": "Tulsa",
15 | "phone_num": 918-555-5555,
16 | "add_phone_num": 918-111-1111
17 | }
18 | #resp = self.client.post("https://courtbot-python.herokuapp.com/form_data", data=data, follow=True)
19 | resp = self.client.post("/schedule_reminders", data=data, follow=True)
20 | self.assertIn(str.encode("Arraignment for case CF-2020-1648 has already passed"), resp.content)
21 |
22 | def test_form_data_view_not_found(self):
23 | data = {
24 | "case_num": "1000000000",
25 | "year": 2020,
26 | "county": "Tulsa",
27 | "phone_num": 918-555-5555,
28 | "add_phone_num": 918-111-1111
29 | }
30 | #resp = self.client.post("https://courtbot-python.herokuapp.com/form_data", data=data, follow=True)
31 | resp = self.client.post("/schedule_reminders", data=data, follow=True)
32 |
33 | self.assertIn(str.encode("Unable to find arraignment event with the following year 2020, county Tulsa, case number 1000000000"), resp.content)
34 |
35 | def test_form_data_view_scheduled(self):
36 | data = {
37 | "case_num": "SC-2020-11082",
38 | "year": 2020,
39 | "county": "Tulsa",
40 | "phone_num": 918-555-5555,
41 | "add_phone_num": 918-111-1111
42 | }
43 | #resp = self.client.post("https://courtbot-python.herokuapp.com/form_data", data=data, follow=True)
44 | resp = self.client.post("/schedule_reminders", data=data, follow=True)
45 |
46 | self.assertIn(str.encode("Reminder scheduled"), resp.content)
47 |
48 |
--------------------------------------------------------------------------------
/website/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from . import views
3 |
4 | urlpatterns = [
5 | path('', views.index, name='index'),
6 | path('schedule_reminders', views.schedule_reminders, name='schedule_reminders'),
7 | ]
--------------------------------------------------------------------------------
/website/views.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render, redirect
2 | from datetime import datetime, timedelta
3 | import re
4 |
5 | from django.http import JsonResponse, HttpResponse
6 | from django.shortcuts import render
7 | from django.views.decorators.csrf import csrf_exempt
8 | from django.contrib import messages
9 |
10 | import oscn, requests, json
11 |
12 |
13 | from alerts.models import Alert
14 |
15 |
16 | def index(request):
17 | # """View function for home page of site."""
18 |
19 | # Render the HTML template index.html with the data in the context variable
20 | return render(request, 'index.html')
21 |
22 | def check_valid_case(case_num, year, county):
23 | # Process form data and requests arraignment data form api/case
24 | resp = requests.get(
25 | #f"http://127.0.0.1:8000/api/case?year={year}&county={county}&case_num={case_num}"
26 | f"https://courtbot-python.herokuapp.com/api/case?year={year}&county={county}&case_num={case_num}"
27 | )
28 | resp_json = json.loads(resp.content)
29 | if resp_json.get('error', None):
30 | return resp_json['error'], None
31 | return '', resp_json.get('arraignment_datetime', None)
32 |
33 |
34 | def set_case_reminder(arraignment_datetime, case_num, phone_num):
35 | #reminder_request = requests.post('http://127.0.0.1:8000/api/reminders', {
36 | reminder_request = requests.post('https://courtbot-python.herokuapp.com/api/reminders', {
37 | "arraignment_datetime": arraignment_datetime,
38 | "case_num": case_num,
39 | "phone_num": f"+1-{phone_num}"
40 | })
41 | resp = json.loads(reminder_request.content)
42 | if resp.get('error', None):
43 | return False, resp['error']
44 | message = f'Text reminder for case {case_num} occuring on {arraignment_datetime} was scheduled under {phone_num}.'
45 | return True, message
46 |
47 |
48 | @csrf_exempt
49 | def schedule_reminders(request):
50 | # If valid case and arraignment time, posts reminder data to api/reminder
51 | # Includes option for extra phone number for additional recipient
52 | case_num_list = [
53 | value for key, value in request.POST.items()
54 | if key.find("case_num") > -1 and value
55 | ]
56 | year = request.POST['year']
57 | county = request.POST['county']
58 | phone_num = request.POST['phone_num']
59 | add_num = request.POST.get('add_phone_num', None)
60 | for i, case_num in enumerate(case_num_list):
61 | valid_case_message, arraignment_datetime = check_valid_case(case_num, year, county)
62 | if not arraignment_datetime:
63 | messages.error(request, valid_case_message)
64 | faq_message = (
65 | f'Please check the case for further information using steps provided at http://court.bot/#faq'
66 | )
67 | messages.error(request, faq_message)
68 | else:
69 | reminder_set, reminder_message = set_case_reminder(arraignment_datetime, case_num, phone_num)
70 | # messages.error(request, message)
71 | messages.info(request, reminder_message)
72 | if len(case_num_list)-1 == i and not reminder_set:
73 | return redirect('/#form')
74 | if add_num:
75 | _, another_reminder_message = set_case_reminder(arraignment_datetime, case_num, add_num)
76 | messages.info(request, another_reminder_message)
77 | return redirect('/#form')
78 |
79 |
--------------------------------------------------------------------------------