1
\$\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.

My code: 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. I've set up a barebones repository with my code: https://github.com/hgducharme/barebones

What the user sees:

Search results page


views.py

### views.py

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
import json, ast, requests, pprint
from zcrmsdk import ZCRMRecord, ZCRMRestClient, ZCRMModule, ZCRMException

def siren_search(request):

    # Define the term in the search
    term = request.GET.get('query', '').strip().lower()
    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 active raptor cameras at the desired jobsite
            deployed = search_zoho(term)

            # Sort the list so they show up in HTML as: PANTHER1, PANTHER2, PANTHER3, ..., etc. with 'N/A' at the end
            context = {
                'cameras': sorted(deployed, key=lambda camera: (camera['site_id'] == 'N/A', camera['site_id']) ),
                'term': term
            }

        # Catch any error that the Zoho API might output
        except ZCRMException as error:

            # If Zoho didn't find any results
            if error.status_code == 400 or error.status_code == 204:
                pass
            else:
                errorString = f'There was an error with accessing Zoho: {error}'
                messages.add_message(request, messages.INFO, errorString)
            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 to {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')
        # for some reason json.loads() doesn't work, so use literal_eval ()
        camera_list = ast.literal_eval(camera_json)

        # 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 one and a half 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)



### How can this API call be optimized? Maybe caching or doing something with the session?
def search_zoho(jobsite):

    # Initialize the API client and search for cameras with a jobsite that matches the search
    # This must be in uppercase because the sites on ZOHO are all uppercase
    jobsite = jobsite.upper()
    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 properties about the camera to filter results
        camera = camera_record.field_data
        status = (camera['Status'].lower() if 'Status' in camera else 'None')
        camera_type = (camera['Type'].lower() if 'Type' in camera else 'None')
        site = (camera['Site']['name'].upper() if 'Site' in camera else 'None')

        # 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, else set the default to 'None'
            asset_name = (camera['Vendor_Name'].upper() if 'Vendor_Name' in camera else 'N/A' )
            site_id = (camera['Multiple_Assets'].upper() if 'Multiple_Assets' in camera else 'N/A')

            # 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 # TODO: I'm using 0.5 seconds right now so I don't annoy the office

    # Pulse the siren on the camera for the preset time
    username = settings.RAPTOR_AUTHENTICATION['username']
    password = settings.RAPTOR_AUTHENTICATION['password']
    pulseSiren = requests.get(camera_url, auth=(username, password), timeout=2)
    return camera_url

siren_search.html

{% extends "base.html" %}
{% load staticfiles %}

{% block page_title %} Siren System {% endblock %}

{% block styles %}
<link href="{% static 'css/camera_search.css' %}" rel="stylesheet">
{% endblock %}


{% block search_title %}
<h3>Siren system</h3>
{% endblock %}

{% block search_form %}
<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>
{% endblock %}

{% block page_content %}

<div class="container">

    {% if not cameras %}
    {% if term != '' %}
    <!-- No results alert -->
    <div class="row">
        <div id="no-results" class="col-xl-8 col-md-7 col-sm-8 col-8 mx-auto">
            <div id="no-results-text" class="alert alert-info text-center my-auto" role="alert">No raptor cameras
                were
                found at the jobsite '{{ term }}'</div>
        </div>
    </div>
    {% endif %}

    {% else %}
    <div class="results-container container-fluid d-flex flex-column">

        <!-- Trigger all sirens button -->
        <div class="row">
            <div id="trigger-all" class="col-xl-6 col-md-5 col-sm-6 col-6 mx-auto">
                <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 at {{ term }}</button>
                </form>
            </div>
        </div>

        <!-- Camera results table -->
        <div class="row">
            <div class="col-xl-8 col-md-7 col-sm-8 col-8 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>
                            <!-- If site ID is available, bold it and put it in the siren button -->
                            {% if camera.site_id != 'N/A' %}
                            <td class="align-middle"><b>{{ camera.site_id }}</b></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 }}">{{ camera.site_id }}</button>
                                </form>
                            </td>

                            <!-- else, take bold off site ID ('N/A') and put the asset name in the siren button-->
                            {% else %}

                            <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 }}">{{ camera.asset_name }}</button>
                                </form>
                            </td>

                            {% endif %}
                        </tr>
                        {% endfor %}
                    </tbody>

                </table>
            </div>
        </div> <!-- End entire table section -->

    </div><!-- End {# else #} block-->
    {% endif %}

    <!-- Error messages -->
    {% 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> <!-- End container holding everything below navbar -->

{% endblock %} {% block javascript %}
<script type="text/javascript" src="{% static 'js/search.js' %}"></script>
{% endblock %}

/project/app/urls.py

from django.urls import path, re_path
from . import views

app_name = 'camera_search'

urlpatterns = [
    path('', views.siren_search, name = 'siren_search'),
]

/project/urls.py

## This is the main urls.py file

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('camera-info/', include('camera_info.urls', namespace="camera_info")),
    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.