Web-app summary:
I'm a complete beginner in web development and I made a sample web application that can trigger sirens on security cameras located in remote locations. Here's the functionality seen by the user:
- Go to website and brought to a landing page with a search bar.
- Type in the name of a location.
- Be shown all the cameras in that location, and next to each camera is a button to trigger its siren.
- There is also a button that triggers all the sirens in that location.
Searching for a location:
When the user searches for a location, the back-end routes this query to a function called search_by_jobsite(jobsite)
. This function establishes a connection with a third-party API and queries a mock database on Zoho.com. The results are processed and then returned to the server to be shown on the front-end.
Triggering a siren:
This is honestly extremely simple. When the button is pressed, the request on the back-end is routed to a function called pulse_siren(camera_name)
that just makes a simple HTTP get request to a camera's static IP address.
My thoughts:
After making this web-app I realized that Django was absolutely overkill since I don't even have a local database. I'm essentially just using Django as a web-server and routing. Next time I should use something like Flask, but i'm interested in how I can improve my existing code. When making this project I was primarily focused on actually getting this thing up and running and then optimizing. I feel like my views.py
file should be turned into most class-based-views (as they're called in Django), and I read a lot up on this but I wasn't exactly sure the best way to implement this.
I'm mostly concerned with cleaning up my views.py
file and optimizing it for speed since it takes maybe a second to make the API call.
What the user sees:
views.py
## views.py - this is where all the back-end processing happens for a user's request
from django.shortcuts import render, redirect
from django.http import JsonResponse
from django.core.exceptions import ObjectDoesNotExist
from .models import CameraSystem, CameraModel, ControlByWeb, JobSite
from django.core import serializers
from django.conf import settings
from django.contrib import messages
from .pulse_siren import pulse_siren
import json, ast, requests
from zcrmsdk import ZCRMRecord, ZCRMRestClient, ZCRMModule, ZCRMException
def siren_search(request):
# Define the term in the search
term = request.GET.get('query', '').strip()
context = {'term': term}
# Handle the AJAX search
if 'query' in request.GET and request.is_ajax():
try:
cameras = CameraSystem.objects.filter(site__name__icontains=term)
json_response = serializers.serialize('json', cameras, fields=(
'asset_name', 'site',), use_natural_foreign_keys=True, use_natural_primary_keys=True)
parsed = json.loads(json_response)
print(json.dumps(parsed, indent=4, sort_keys=True))
except ObjectDoesNotExist:
return JsonResponse({'success': False}, status=400)
return JsonResponse(json_response, safe=False, status=200)
# Handle when the user presses enter
elif 'query' in request.GET and term != '' and not request.is_ajax():
try:
# Get the list of cameras at the desired jobsite
deployed = search_by_jobsite(term)
# Sort the list so they show up in HTML as: PANTHER1, PANTHER2, PANTHER3, ..., etc.
context = {
'cameras': sorted(deployed, key=lambda camera: camera['site_id']),
'term': term
}
# Catch any error that the Zoho API might output
except ZCRMException as error:
print(error) # TODO
pass
return render(request, 'siren_search.html', context)
# Handle when the user presses a siren button for just one camera
elif 'pulse-siren' in request.GET:
# Get the current url so the we can show the same search results after they press the button
redirect_url = request.GET.get('current-url')
asset_name = request.GET.get('pulse-siren').upper()
# Pulse the siren on the desired camera, and for anything that happens add a message to the redirect
try:
response = pulse_siren(asset_name)
response.raise_for_status()
messages.add_message(request, messages.INFO, f"Successful for {asset_name}.")
except requests.exceptions.HTTPError as error:
if response.status_code == 401:
errorString = f'Looks like {asset_name} is online, but the system was not able to log in.'
messages.add_message(request, messages.INFO, errorString)
else:
errorString = f'HTTP error for {asset_name}: {error}'
messages.add_message(request, messages.INFO, errorString)
pass
except requests.exceptions.ConnectionError as error:
errorString = f'The system failed to connect for {asset_name}. Check to see if the unit is online. If it is, then then the system may have used a wrong username/password.'
messages.add_message(request, messages.INFO, errorString)
pass
except requests.exceptions.Timeout as error:
errorString = f'Timeout error for {asset_name}: the system tried to connect for three seconds but failed.'
messages.add_message(request, messages.INFO, errorString)
pass
except requests.exceptions.RequestException as error:
errorString = f'Oops, something weird happened for {asset_name}: {error}'
messages.add_message(request, messages.INFO, errorString)
pass
return redirect(redirect_url)
# Handle when the user wants to trigger all sirens
elif 'pulse-all-sirens' in request.GET:
# Get the cameras and the redirecut url
redirect_url = request.GET.get('current-url')
camera_json = request.GET.get('pulse-all-sirens')
camera_list = ast.literal_eval(camera_json) # for some reason json.loads() doesn't work, so use literal_eval ()
# Pulse the siren for each camera on the search results page
for camera in camera_list:
asset_name = camera['asset_name']
# Pulse the siren on the desired camera, and for anything that happens add a message to the redirect
try:
response = pulse_siren(asset_name)
response.raise_for_status()
messages.add_message(request, messages.INFO, f"Successful for {asset_name}.")
except requests.exceptions.HTTPError as error:
errorString = f'HTTP error for {asset_name}: {error}'
messages.add_message(request, messages.INFO, errorString)
pass
except requests.exceptions.ConnectionError as error:
errorString = f'The system failed to connect for {asset_name}: Check to see if the unit is online, but the system may have used a wrong username/password.'
messages.add_message(request, messages.INFO, errorString)
pass
except requests.exceptions.Timeout as error:
errorString = f'Timeout error for {asset_name}: The system tried to connect for three seconds but failed.'
messages.add_message(request, messages.INFO, errorString)
pass
except requests.exceptions.RequestException as error:
errorString = f'Oops, something weird happened for {asset_name}: {error}'
messages.add_message(request, messages.INFO, errorString)
pass
return redirect(redirect_url)
else:
return render(request, 'siren_search.html', context)
def search_by_jobsite(jobsite):
# 'IP': camera['Static_IP'],
# Initialize the API client and search for cameras with a jobsite that matches the search
jobsite = jobsite.upper() # This must be in uppercase because the sites on ZOHO are all uppercase
config = settings.ZOHO_CONFIG
client = ZCRMRestClient.initialize(config)
all_cameras = ZCRMModule.get_instance('Vendors').search_records(f'{jobsite}')
# Iterate over each returned camera
deployed_cameras = []
for camera_record in all_cameras.data:
# Get some properties from the camera to filter results
camera = camera_record.field_data
status = camera['Status'].lower()
camera_type = camera['Type'].lower()
site = camera['Site']['name'].upper()
# Only get the cameras that are deployed at the jobsite of type 'raptor'
if site == jobsite and camera_type == 'raptor':
# Get properties if they exist
asset_name = camera.get('Vendor_Name', 'None')
site_id = camera.get('Multiple_Assets', 'None')
# If the properties exist, style them
if asset_name is not 'None':
asset_name = asset_name.upper()
if site_id is not 'None':
site_id = site_id.upper()
# Append this camera's dictionary to the list of deployed cameras
temp_dictionary = {'asset_name': asset_name, 'site_id': site_id, 'site': site.capitalize()}
deployed_cameras.append(temp_dictionary.copy())
return deployed_cameras
def pulse_siren(asset_name):
# Configure the url for the desired camera
camera_url = 'http://' + asset_name.lower() + settings.CAMERA_URL + settings.PULSE_SIREN_HALF_SECOND
# Pulse the siren on the desired camera
username = settings.RAPTOR_AUTHENTICATION['username']
password = settings.RAPTOR_AUTHENTICATION['password']
pulseSiren = requests.get(camera_url, auth=(username, password), timeout = 3)
return pulseSiren
def get_camera_state(asset_name):
# Configure the url for the desired camera
camera_url = 'http://' + asset_name.lower() + settings.CAMERA_URL + 'state.xml'
# Connect to the camera's interface and grab its state
username = settings.RAPTOR_AUTHENTICATION['username']
password = settings.RAPTOR_AUTHENTICATION['password']
camera_state = requests.get(camera_url, auth=(username, password))
data = xmltodict.parse(camera_state.text)
siren_search.html
<!-- siren_search.html -->
{% extends "base.html" %}
{% load static %}
{% block page_title %} Siren System {% endblock %}
{% block page_content %}
<!-- Search section -->
<div class="container">
<div class="row">
<div id="search-title" class="col-xl-6 col-md-7 col-sm-8 col-8 mx-auto">
<h3>Siren system</h3>
</div>
</div>
<div class="row">
<div class="col-xl-6 col-md-7 col-sm-8 col-8 mx-auto">
<form id="searchform" action="{% url 'camera_search:siren_search' %}" method="GET">
<div class="input-group">
<input id="searchbar" name="query" autocomplete="on" onkeyup=getCameras(this.value) placeholder="Search for the name of a jobsite." class="form-control" type="search" />
</div>
</form>
</div>
</div>
<!-- Show the trigger all button if there's cameras, or show no results -->
<div class="row">
<div id="trigger-all-and-no-results" class="col-xl-6 col-md-7 col-sm-8 col-8 mx-auto">
{% if cameras %}
<form id="trigger-all-form" action="{% url 'camera_search:siren_search' %}" method="GET">
<input type="hidden" name="current-url" value="{{ request.get_full_path }}">
<button id="trigger-all-btn" name="pulse-all-sirens" type="submit" class="btn btn-lg btn-block btn-outline-danger btn-space js-trigger-all-sirens-btn" value="{{ cameras }}">Trigger
all sirens</button>
</form>
{% else %} {% if term != '' %}
<div id="no-results-text" class="alert alert-info text-center my-auto" role="alert">No cameras were found for {{ term }}</div>
{% endif %} {% endif %}
</div>
</div>
<!-- Show the cameras that were found in the search -->
{% if cameras %}
<div class="row">
<div class="col-xl-8 col-md-9 col-sm-10 col-10 mx-auto">
<table class="table table-responsive-sm table-hover table-borderless mx-auto">
<thead>
<tr class="header">
<th scope="col" style="width:30%;">Site ID</th>
<th scope="col" style="width:25%;">Job Site</th>
<th scope="col" style="width:25%;">Asset Name</th>
<th scope="col" style="width:20%;"></th>
</tr>
</thead>
<tbody>
{% for camera in cameras %}
<tr>
<td class="align-middle">{{ camera.site_id }}</td>
<td class="align-middle">{{ camera.site }}</td>
<td class="align-middle">{{ camera.asset_name }}</td>
<td class="align-middle">
<form id="{{ camera.asset_name }}-siren-form" action="{% url 'camera_search:siren_search' %}" method="GET">
<input type="hidden" name="current-url" value="{{ request.get_full_path }}">
<button type="submit" name="pulse-siren" id="{{ camera.asset_name }}-siren-btn" name="button-big-submit" class="btn btn-outline-primary float-right js-single-trigger-btn" onclick="" value="{{ camera.asset_name }}">{% if camera.site_id != 'None' %}{{ camera.site_id }}{% else %}{{ camera.asset_name }}{% endif %}</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<!-- This is a WIP, this was just here so I can practice getting the messages from the server to the front-end -->
{% if messages %}
<ul class="messages">
{% for message in messages %}
<li{% if message.tags %} class="{{ message.tags }}" {% endif %}>{{ message }}</li>
{% endfor %}
</ul>
{% else %}
<!-- show success -->
{% endif %}
</div>
{% endblock %}
{% block javascript %}
<script type="text/javascript" src="{% static 'js/search.js' %}"></script>
{% endblock %}
root/app/urls.py
## root/app/urls.py
# The app's urls file. Users get routed from the main urls.py file to here.
# This could be improved by having different buttons route to different urls.
from django.urls import path, re_path
from . import views
app_name = 'camera_search'
urlpatterns = [
path('', views.siren_search, name = 'siren_search'),
]
root/urls.py
## root/urls.py
# This is the main urls file that handles incoming traffic to the website.
from django.contrib import admin
from django.urls import path, include
from django.views.generic.base import RedirectView
urlpatterns = [
path('admin/', admin.site.urls),
path('siren-search/', include('camera_search.urls', namespace = 'camera_search')),
path('', RedirectView.as_view(pattern_name = 'camera_search:siren_search', permanent=False)),
]