2
\$\begingroup\$

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:

  1. Go to website and brought to a landing page with a search bar.
  2. Type in the name of a location.
  3. Be shown all the cameras in that location, and next to each camera is a button to trigger its siren.
  4. 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:

Search results page


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)),
]
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.