views.py 22.6 KB
Newer Older
1
# Create your views here.
2
import sys, logging, json, random, time
3

4 5
import dateutil.parser

6
from django.http import HttpResponse
7
from django.shortcuts import render_to_response, redirect, render
Alan Mitchell's avatar
Alan Mitchell committed
8 9
from django.contrib.auth.decorators import login_required
from django.views.decorators.csrf import csrf_exempt
Alan Mitchell's avatar
Alan Mitchell committed
10
from django.urls import reverse
11
from django.conf import settings
12
from django.templatetags.static import static
13

14 15 16 17 18 19
from . import models
from . import logging_setup
from . import view_util
from . import storereads
from .reports import basechart
from .readingdb import bmsdata
Alan Mitchell's avatar
Alan Mitchell committed
20
from bmsapp.periodic_scripts import ecobee
21
import bmsapp.scripts.backup_readingdb
22 23
from .lora import decoder

Alan Mitchell's avatar
Alan Mitchell committed
24 25 26 27

# Make a logger for this module
_logger = logging.getLogger('bms.' + __name__)

28 29
# Some context variables to include when rendering all templates
DEFAULT_NAV_LINKS = ( ('Data Charts and Reports', 'reports', True),
30
                      ('Training Videos and Project Reports', 'training-anthc'),
31 32
                    )

33 34
TMPL_CONTEXT = {'bmsapp_title_text': getattr(settings, 'BMSAPP_TITLE_TEXT', 'Facility Monitoring'),
                'bmsapp_header': getattr(settings, 'BMSAPP_HEADER', 'Facility Monitoring'),
35
                'bmsapp_footer': getattr(settings, 'BMSAPP_FOOTER', 'Thanks to Alaska Housing Finance Corporation for providing most of the source code for this application.'),
36
                'bmsapp_nav_links': getattr(settings, 'BMSAPP_NAV_LINKS', DEFAULT_NAV_LINKS),
37
                'version_date': view_util.version_date_string(),
38 39
               }

40 41 42 43 44
def base_context():
    '''
    Returns a Template rendering context with some basic variables.
    Had to do this because I could not run the 'reverse' method from the module level.
    '''
45 46 47
    # get the html for the list of organizations and ID of the selected
    # organization.
    orgs_html, _ = view_util.organization_list_html()
48 49 50 51
    
    # determine whether organization selector should be hidden based on how 
    # many options there are in the select drop down.
    orgs_hide = (orgs_html.count('value') == 1)
52

53
    ctx = TMPL_CONTEXT.copy()
54
    ctx['orgs_html'] = orgs_html
55
    ctx['orgs_hide'] = orgs_hide
56
    ctx['bmsapp_nav_link_base_url'] = reverse('index')
57 58 59 60

    # Only show Logout button if Django Lockdown app is being used.
    ctx['logout_show'] = ('lockdown' in settings.INSTALLED_APPS)

61 62
    return ctx

Alan Mitchell's avatar
Alan Mitchell committed
63 64 65 66
def index(request):
    '''
    The main home page for the site, which redirects to the desired page to show for the home page.
    '''
67 68 69
    # find the index page in the set of navigation links
    for lnk in TMPL_CONTEXT['bmsapp_nav_links']:
        if len(lnk)==3 and lnk[2]==True:
70
            return redirect( reverse('wildcard', args=(lnk[1],)) )
Alan Mitchell's avatar
Alan Mitchell committed
71

72
def reports(request):
Alan Mitchell's avatar
Alan Mitchell committed
73
    '''
74 75
    The main graphs/reports page.  If 'bldg_id' is the building to be selected;
    if None, the first building in the list is selected.
Alan Mitchell's avatar
Alan Mitchell committed
76 77
    '''

78 79
    # get the html for the list of building groups and the ID of the selected 
    # group.
80
    group_html, group_id_selected = view_util.group_list_html(0)
Alan Mitchell's avatar
Alan Mitchell committed
81

82 83
    # get the html for the list of buildings and the ID of the a selected building
    # (the first building)
84
    bldgs_html, bldg_id_selected = view_util.bldg_list_html(0, group_id_selected, None)
Alan Mitchell's avatar
Alan Mitchell committed
85

86
    # get the html for the list of charts, selecting the first one.  Returns the actual ID
87
    # of the chart selected.  The org_id of 0 indicates all organizations are being shown.
88
    chart_list_html, chart_id_selected = view_util.chart_list_html(0, bldg_id_selected)
Alan Mitchell's avatar
Alan Mitchell committed
89

90 91 92 93
    # get the option item html for the list of sensors associated with this building,
    # selecting the first sensor.
    sensor_list_html = view_util.sensor_list_html(bldg_id_selected)

94
    ctx = base_context()
95
    ctx.update({'groups_html': group_html,
96
                'bldgs_html': bldgs_html,
97
                'chart_list_html': chart_list_html,
98 99
                'sensor_list_html': sensor_list_html,
                'curtime': int(time.time())})
100
    
101
    return render_to_response('bmsapp/reports.html', ctx)
Alan Mitchell's avatar
Alan Mitchell committed
102

103
def get_report_results(request):
104 105 106
    """Method called to return the main content of a particular chart
    or report.
    """
Alan Mitchell's avatar
Alan Mitchell committed
107 108
    try:
        # Make the chart object
Ian Moore's avatar
Ian Moore committed
109
        chart_obj = basechart.get_chart_object(request)
Alan Mitchell's avatar
Alan Mitchell committed
110 111
        result = chart_obj.result()
    
Ian Moore's avatar
Ian Moore committed
112
    except Exception as e:
Alan Mitchell's avatar
Alan Mitchell committed
113 114 115 116
        _logger.exception('Error in get_report_results')
        result = {'html': 'Error in get_report_results', 'objects': []}

    finally:
117 118 119 120 121 122 123 124
        if type(result) is HttpResponse:
            # the chart object directly produced an HttpResponse object
            # so just return it directly.
            return result
        else:
            # if the chart object does not produce an HttpResponse object, then
            # the result from the chart object is assumed to be a JSON object.
            return HttpResponse(json.dumps(result), content_type="application/json")
125

126
def get_embedded_results(request):
127 128
    """Method called to return the main content of a particular chart or report
       embedded as javascript.
129 130 131
    """
    try:
        # Make the chart object
Ian Moore's avatar
Ian Moore committed
132
        chart_obj = basechart.get_chart_object(request)
133 134 135
        result = chart_obj.result()
    
    except Exception as e:
136 137
        _logger.exception('Error in get_embedded_results')
        result = {'html': 'Error in get_embedded_results', 'objects': []}
138 139 140 141 142 143 144

    finally:
        if type(result) is HttpResponse:
            # the chart object directly produced an HttpResponse object
            # so just return it directly.
            return result
        else:
145
            script_content = view_util.get_embedded_results_script(request, result)
146
            return HttpResponse(script_content, content_type="application/javascript")
147

148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
def energy_reports(request):
    """Presents the BMON Essential Energy Reports page.
    """
    # determine the base Energy Reports URL to pass to the template
    if hasattr(settings, 'ENERGY_REPORTS_URL') and settings.ENERGY_REPORTS_URL is not None:
        energy_reports_url = settings.ENERGY_REPORTS_URL
        if energy_reports_url[-1] != '/':
            # add a slash at end, as it did not contain one.
            energy_reports_url += '/'
        error_message = ''
    else:
        energy_reports_url = ''
        error_message = 'Energy Reports are not Available.  Contact your System Administrator for more information.'

    ctx = base_context()
    ctx.update(
        {
            'energy_reports_url': energy_reports_url,
            'error_message': error_message,
        }
    )
    
    return render_to_response('bmsapp/energy-reports.html', ctx)

172
def custom_report_list(request):
173 174 175 176
    """The main Custom Reports page - lists available custom reports for the
    organization identified by the query parameter 'select_org'.
    """
    org_id = int(request.GET.get('select_org', '0'))
Ian Moore's avatar
Ian Moore committed
177
    ctx = base_context()
178
    ctx.update({
179
        'org_id': org_id,
180
        'customReports': view_util.custom_reports(org_id)
181
        })
Ian Moore's avatar
Ian Moore committed
182 183 184
    
    return render_to_response('bmsapp/customReports.html', ctx)

185
def custom_report(request, requested_report):
Ian Moore's avatar
Ian Moore committed
186 187 188 189
    '''
    Display a specific custom report
    '''

Ian Moore's avatar
Ian Moore committed
190
    report_id = requested_report
Ian Moore's avatar
Ian Moore committed
191
    ctx = base_context()
Ian Moore's avatar
Ian Moore committed
192 193 194 195 196 197 198

    try:
        report = models.CustomReport.objects.get(id=report_id)
        report_title = report.title
        ctx.update({'report_title': report_title, 'report_content': view_util.custom_report_html(report_id)})
    except Exception as ex:
        ctx.update({'report_title': 'Report Not Found', 'report_content': 'Report Not Found: ' + ex.message})
Ian Moore's avatar
Ian Moore committed
199 200 201
    
    return render_to_response('bmsapp/customReport.html', ctx)

202 203 204 205 206 207 208 209 210 211 212
def store_key_is_valid(the_key):
    '''Returns True if the 'the_key' is a valid sensor reading storage key.
    '''
    if type(settings.BMSAPP_STORE_KEY) in (tuple, list):
        # there are multiple valid storage keys
        return True if the_key in settings.BMSAPP_STORE_KEY else False
    else:
        # there is only one valid storage key
        return the_key==settings.BMSAPP_STORE_KEY


213
@csrf_exempt    # needed to accept HTTP POST requests from systems other than this one.
214
def store_reading(request, reading_id):
Alan Mitchell's avatar
Alan Mitchell committed
215
    '''
216 217 218
    Stores a sensor or calculated reading in the sensor reading database.  'reading_id' is the ID of the
    reading to store.  Other information about the reading is in the GET parameter or POST data of the
    request.
Alan Mitchell's avatar
Alan Mitchell committed
219 220 221
    '''
    try:
        # determine whether request was a GET or POST and extract data
222
        req_data = request.GET.dict() if request.method == 'GET' else request.POST.dict()
Alan Mitchell's avatar
Alan Mitchell committed
223

224 225 226 227
        # Test for a valid key for storing readings.  Key should be unique for each
        # installation.
        storeKey = req_data['storeKey']
        del req_data['storeKey']    # for safety, get the key out of the dictionary
228 229 230 231
        if store_key_is_valid(storeKey):
            msg = storereads.store(reading_id, req_data)
            return HttpResponse(msg)
        else:
232
            _logger.warning('Invalid Storage Key in Reading Post: %s', storeKey)
233
            return HttpResponse('Invalid Key')
Alan Mitchell's avatar
Alan Mitchell committed
234 235

    except:
236
        _logger.exception('Error Storing Reading for ID=%s: %s' % (reading_id, req_data))
237 238
        return HttpResponse(sys.exc_info()[1])

239

240 241 242
@csrf_exempt    # needed to accept HTTP POST requests from systems other than this one.
def store_readings(request):
    '''
243
    Stores a set of sensor readings in the sensor reading database.  The readings
244 245 246
    are in the POST data encoded in JSON and there may be additional information in
    the query string.  See 'storereads.store_many' for details on the data formats
    supported of the request data.
247 248
    '''
    try:
249
        # The post data is JSON, so decode it.
250 251
        req_data = json.loads(request.body)

252 253 254
        # Add any query parameters into the dictionary
        req_data.update(request.GET.dict())

255 256
        # Test for a valid key for storing readings.  Key should be unique for each
        # installation.
257
        storeKey = req_data.get('storeKey', 'bad')    # if no storeKey, 'bad' will be returned
258
        if store_key_is_valid(storeKey):
259 260
            # remove storeKey for security
            del(req_data['storeKey'])
261
            _logger.debug('Sensor Readings: %s' % req_data)
262
            msg = storereads.store_many(req_data)
263 264
            return HttpResponse(msg)
        else:
265
            _logger.warning('Invalid Storage Key in Reading Post: %s', storeKey)
266 267 268 269 270
            return HttpResponse('Invalid Key')

    except:
        _logger.exception('Error Storing Reading')
        return HttpResponse(sys.exc_info()[1])
Alan Mitchell's avatar
Alan Mitchell committed
271

272 273 274 275 276
@csrf_exempt    # needed to accept HTTP POST requests from systems other than this one.
def store_readings_things(request):
    '''
    Stores a set of sensor readings from the Things Network in the sensor reading 
    database. The readings are assumed to originate from an HTTP Integration on an
277 278
    Application in the Things Network.  The BMON Store Key is in a custom HTTP header.
    The readings and other data are in the POST data encoded in JSON.
279
    '''
280

281 282 283 284 285
    try:

        # The post data is JSON, so decode it.
        req_data = json.loads(request.body)

286 287 288
        # See if the store key is valid.  It's stored in the "store-key" header, which
        # is found in the "HTTP_STORE_KEY" key in the META dictionary.
        storeKey = request.META.get('HTTP_STORE_KEY', 'None_Present')
289 290

        if store_key_is_valid(storeKey):
291 292 293 294
            data = decoder.decode(req_data)
            if len(data['fields']) == 0:
                return HttpResponse('No Data Found')

295
            readings = []
296 297 298 299
            ts = data['ts']
            eui = data['device_eui']
            for fld, val in data['fields'].items():
                readings.append( [ts, f'{eui}_{fld}', val] )
300

301 302
            # Also extract the best SNR of the gateways that received this.
            readings.append([ts, f'{eui}_snr', data['snr'])
303

304
            msg = storereads.store_many({'readings': readings})
305
            return HttpResponse(msg)
306

307 308 309 310 311 312 313 314
        else:
            _logger.warning('Invalid Storage Key in Reading Post: %s', storeKey)
            return HttpResponse('Invalid Key')

    except:
        _logger.exception('Error Storing Reading')
        return HttpResponse(sys.exc_info()[1])

315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371
@csrf_exempt    # needed to accept HTTP POST requests from systems other than this one.
def store_readings_radio_bridge(request):
    '''
    Stores a set of sensor readings from a Radio Bridge LoRaWAN sensor in the sensor reading 
    database. The readings are assumed to originate from an HTTP Integration on an
    Application in the Things Network.  The BMON Store Key is in a custom HTTP header.
    The readings and other data are in the POST data encoded in JSON.
    '''

    try:

        # The post data is JSON, so decode it.
        req_data = json.loads(request.body)

        # Return if this is a message that does not have any data in it, like an 
        # activate or join message.
        if 'payload_fields' not in req_data:
            return HttpResponse('No Data')

        # See if the store key is valid.  It's stored in the "store-key" header, which
        # is found in the "HTTP_STORE_KEY" key in the META dictionary.
        storeKey = request.META.get('HTTP_STORE_KEY', 'None_Present')

        if store_key_is_valid(storeKey):
            readings = []
            ts = dateutil.parser.parse(req_data['metadata']['time']).timestamp()
            hdw_serial = req_data['hardware_serial']

            pf = req_data['payload_fields']
            event = pf['EVENT_TYPE']
            if event == '01':     # Supervisory Event
                readings.append( [ts, f'{hdw_serial}_battery', float(pf['BATTERY_LEVEL'])] )

            elif event == '07':    # Contact event
                val = float(pf['SENSOR_STATE'])
                # Invert their logic: they have a 0 when the contacts are closed
                val = val * -1 + 1
                readings.append( [ts, f'{hdw_serial}_state', val] )

            elif event == '09':   # Temperature Event
                readings.append( [ts, f'{hdw_serial}_temp', pf['TEMPERATURE'] * 1.8 + 32.] )

            else:
                return HttpResponse('No Data')

            msg = storereads.store_many({'readings': readings})

            return HttpResponse(msg)

        else:
            _logger.warning('Invalid Storage Key in Reading Post: %s', storeKey)
            return HttpResponse('Invalid Key')

    except:
        _logger.exception('Error Storing Reading')
        return HttpResponse(sys.exc_info()[1])

372 373 374 375 376
@csrf_exempt
def store_reading_old(request, store_key):
    '''
    Stores a reading that uses an older URL pattern.
    '''
377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393

    try:
        if store_key == settings.BMSAPP_STORE_KEY_OLD:

            # determine whether request was a GET or POST and extract data
            req_data = request.GET.dict() if request.method == 'GET' else request.POST.dict()

            # pull the reading id out of the request parameters
            reading_id = req_data['id']

            storereads.store(reading_id, req_data)
            return HttpResponse('OK')

        else:
            return HttpResponse('Invalid Key')

    except:
394
        _logger.exception('Error Storing Reading for: %s' %  req_data)
395
        return HttpResponse('Error Storing Reading')
396 397 398 399 400 401 402 403 404 405 406 407 408 409 410

def make_store_key(request):
    '''
    Makes a random 12 character store key
    '''
    k = ''
    for i in range(12):
        val = random.randint(0, 61)
        if val > 35:
            val += 61
        elif val > 9:
            val += 55
        else:
            val += 48
        k += chr(val)
Alan Mitchell's avatar
Alan Mitchell committed
411

412
    return HttpResponse(k)
Alan Mitchell's avatar
Alan Mitchell committed
413

414 415 416 417 418 419 420 421 422
def group_list(request, org_id):
    """Returns a list of building groups associated with the organization identified
    by 'org_id'.  The return value is an html snippet of option elements, one
    for each building group.
    """
    group_html, _ = view_util.group_list_html(int(org_id))

    return HttpResponse(group_html)

423
def bldg_list(request, org_id, group_id):
424
    '''Returns a list of buildings in the organization identified by 'org_id'
425
    and the group identified by the primary key ID of 'group_id'. 
426

427 428 429
    The return value is an html snippet of option elements, one for each building.
    '''

430
    bldgs_html, _ = view_util.bldg_list_html(int(org_id), int(group_id))
431 432 433

    return HttpResponse(bldgs_html)

434
def chart_sensor_list(request, org_id, bldg_id):
Alan Mitchell's avatar
Alan Mitchell committed
435
    '''
436 437 438
    Returns a list of charts and a list of sensors appropriate for a building
    identified by the primary key ID of 'bldg_id'.  'bldg_id' could be the string
    'multi', in which case the list of multi-building charts is returned, and
439 440
    only multi-building charts appropriate for the Organization identified by
    'org_id' are returned.  A list of sensors appropriate for 'bldg_id' is
441 442 443 444
    also returned.  If 'bldg_id' is 'multi' then no sensors are returned.
    The return lists are html snippets of option elements.  The two different
    option element lists are returned in a JSON object, with the keys 'charts'
    and 'sensors'.
Alan Mitchell's avatar
Alan Mitchell committed
445 446
    '''

447 448 449 450
    # try to convert the selected building value to an integer (might be the 
    # string 'multi') so that it will match the integer IDs in the database.
    bldg_id = view_util.to_int(bldg_id)
    
451
    org_id = int(org_id)
Alan Mitchell's avatar
Alan Mitchell committed
452

453
    charts_html, id_selected = view_util.chart_list_html(org_id, bldg_id)
454 455
    sensor_html = view_util.sensor_list_html(bldg_id)
    result = {'charts': charts_html, 'sensors': sensor_html}
Alan Mitchell's avatar
Alan Mitchell committed
456

457
    return HttpResponse(json.dumps(result), content_type="application/json")
Alan Mitchell's avatar
Alan Mitchell committed
458

459 460 461 462 463
def get_readings(request, reading_id):
    """Returns all the rows for one sensor in JSON format.
    'reading_id' is the id of the sensor/reading being requested.
    """

464
    # open the database
465
    db = bmsdata.BMSdata()
466 467 468 469
    result = db.rowsForOneID(reading_id)

    return HttpResponse(json.dumps(result), content_type="application/json")

Alan Mitchell's avatar
Alan Mitchell committed
470
@login_required(login_url='../admin/login/')
471 472 473 474
def show_log(request):
    '''
    Returns the application's log file, without formatting.
    '''
475
    return HttpResponse('<pre>%s</pre>' % open(logging_setup.LOG_FILE).read())
476

Alan Mitchell's avatar
Alan Mitchell committed
477 478 479 480
def show_video(request, filename, width, height):
    '''
    A Page to show a training video.  'filename' is the Flash file name of the video, without
    the 'swf' extension, 'width' and 'height' are the width and height in pixels of the viewport.
481 482
    A 'hide_back_link' GET parameter is optional; if set to 1, it will hide the 'Back to Video
    List' link on the page.
Alan Mitchell's avatar
Alan Mitchell committed
483
    '''
484
    hide_back_link = True if request.GET.get('hide_back_link') == '1' else False
Alan Mitchell's avatar
Alan Mitchell committed
485

486 487
    return render_to_response('bmsapp/video.html', 
        {'filename': filename, 'width': width, 'height': height, 'hide_back_link': hide_back_link})
Alan Mitchell's avatar
Alan Mitchell committed
488

489 490 491 492
def map_json(request):
    """Returns the JSON data necessary to draw the Google map of the sites.
    This view is called from the map.html template.
    """
493
    ret = {"name": "BMON Sites",
494
        "type": "FeatureCollection",
495 496 497
        "crs": {"type": "name", "properties": {"name": "urn:ogc:def:crs:OGC:1.3:CRS83"}},
        "features": []}

498 499 500 501 502 503 504
    org_id = int(request.GET.get('select_org', '0'))
    if org_id != 0:
        bldgs = models.Organization.objects.get(id=org_id).buildings.all()
    else:
        bldgs = models.Building.objects.all()

    for bldg in bldgs:
505 506 507 508 509 510 511
        ret['features'].append( {"type": "Feature", 
                                 "geometry": {"type": "Point", 
                                              "coordinates": [bldg.longitude, bldg.latitude]
                                              },
                                 "properties": {"facilityName": bldg.title, 
                                                "facilityID": bldg.id, 
                                                "message": "", 
512
                                                "href": '{}?select_org={}&select_bldg={}'.format(request.build_absolute_uri('../reports/'), org_id, bldg.id)
513 514
                                                }
                                 } )
515 516 517 518


    return HttpResponse(json.dumps(ret), content_type="application/json")

Alan Mitchell's avatar
Alan Mitchell committed
519
@login_required(login_url='../admin/login/')
520 521 522 523 524 525 526
def ecobee_auth(request):
    """Used to generated a form so that a System Admin can obtain access keys
    for reading data from the Ecobee thermostat server.
    """

    ctx = base_context()
    if request.method == 'GET':
527
        # Get a PIN and auth code
Alan Mitchell's avatar
Alan Mitchell committed
528
        results = ecobee.get_pin()
529
        ctx.update(results)
530
        return render(request, 'bmsapp/ecobee_authorization.html', ctx)
531 532 533

    elif request.method == 'POST':
        req = request.POST.dict()
534
        # request access and refresh tokens
Alan Mitchell's avatar
Alan Mitchell committed
535
        success, access_token, refresh_token = ecobee.get_tokens(req['code'])
536
        ctx.update({'success': success, 'access_token': access_token, 'refresh_token': refresh_token})
537 538
        return render_to_response('bmsapp/ecobee_auth_result.html', ctx)

Alan Mitchell's avatar
Alan Mitchell committed
539
@login_required(login_url='../admin/login/')
540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565
def unassigned_sensors(request):
    """Shows sensors that are in the Reading Database but not assigned to a building.
    """

    db = bmsdata.BMSdata()
    sensor_list = []
    for sens_id in db.sensor_id_list():
        sensor_info = {'id': sens_id, 'title': '', 'cur_value': '', 'minutes_ago': ''}
        add_sensor = False

        qs = models.Sensor.objects.filter(sensor_id=sens_id)
        if len(qs)==0:
            # Sensor does not even have a sensor object
            add_sensor = True
        else:
            sensor = qs[0]   # get the actual Sensor object
            # see if this sensor has links to a building
            links = models.BldgToSensor.objects.filter(sensor=sensor)
            if len(links)==0:
                # no links, so found an unassigned sensor
                add_sensor = True
                sensor_info['title'] = sensor.title

        if add_sensor:
            last_read = db.last_read(sens_id)
            if last_read:
566
                val = last_read['val']
567
                sensor_info['cur_value'] = '%.5g' % val if abs(val)<1e5 else str(val)
568 569 570 571 572 573 574 575
                sensor_info['minutes_ago'] = '%.1f' % ((time.time() - last_read['ts'])/60.0)

            sensor_list.append(sensor_info)

    ctx = base_context()
    ctx.update({'sensor_list': sensor_list})
    return render_to_response('bmsapp/unassigned-sensors.html', ctx)

Alan Mitchell's avatar
Alan Mitchell committed
576
@login_required(login_url='../admin/login/')
577 578 579 580 581 582
def backup_reading_db(request):
    """Causes a backup of the sensor reading database to occur.
    """
    bmsapp.scripts.backup_readingdb.run()
    return HttpResponse('Sensor Reading Backup Complete!')

583 584 585 586 587 588
def wildcard(request, template_name):
    '''
    Used if a URL component doesn't match any of the predefied URL patterns.  Renders
    the template indicated by template_name, adding an '.html' to the name.
    '''
    return render_to_response('bmsapp/%s.html' % template_name, base_context())
Alan Mitchell's avatar
Alan Mitchell committed
589

Alan Mitchell's avatar
Alan Mitchell committed
590
@login_required(login_url='../admin/login/')
Alan Mitchell's avatar
Alan Mitchell committed
591 592 593
def password_change_done(request):
    return render_to_response('registration/password_change_done.html',
        {'user': request.user})