PUT, DELETE and address Changes
- Added PUT, PATCH and DELETE to modify data in the database. - refactored modules - completed docstrings - data is now served via api/ and web pages via /
This commit is contained in:
parent
9cc39c56ab
commit
80197208f8
7
transport_accessibility/api/urls.py
Normal file
7
transport_accessibility/api/urls.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("models/", views.data, name="data"),
|
||||||
|
path("timetable/", views.timetable, name="timetable"),
|
||||||
|
]
|
||||||
|
|
@ -1,3 +1,136 @@
|
||||||
from django.shortcuts import render
|
"""
|
||||||
|
Views
|
||||||
|
=====
|
||||||
|
|
||||||
|
Views serving (mostly JSON) data via HTTP, no actual web pages.
|
||||||
|
|
||||||
|
Functions
|
||||||
|
---------
|
||||||
|
timetable
|
||||||
|
Fetches timetables for given routes on api/timetable/
|
||||||
|
data
|
||||||
|
Serves api/models/
|
||||||
|
GET
|
||||||
|
Fetches models given their primary keys
|
||||||
|
PUT
|
||||||
|
Creates new model objects or updates them with complete representations. If object with the given primary keys exist, they will be deleted and replaced.
|
||||||
|
PATCH
|
||||||
|
Updates models, identified by their primary keys without deleting them. Can be incomplete representations.
|
||||||
|
DELETE
|
||||||
|
Deletes models, identified by their primary keys.
|
||||||
|
"""
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed, HttpRequest
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.core import serializers
|
||||||
|
import django.db.models
|
||||||
|
from MySQLdb import IntegrityError
|
||||||
|
from pt_map.models import *
|
||||||
|
import json
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from pt_map.query import *
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def timetable(request):
|
||||||
|
"""
|
||||||
|
Lookup timetable data for given routes.
|
||||||
|
|
||||||
|
Request
|
||||||
|
-------
|
||||||
|
GET
|
||||||
|
Find timetables for all routes passed via GET.
|
||||||
|
Successful response is a Json representation of a dict of timetables in the following form:
|
||||||
|
{
|
||||||
|
route_id (from GET): {
|
||||||
|
'stop_sequence': [stop_ids for all stops the route server, in order],
|
||||||
|
'stop_times': {
|
||||||
|
stop_id (from stop_sequence): [str in the format HH:MM representing stop times]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
if request.method == "GET":
|
||||||
|
try:
|
||||||
|
routes = obj_from_get(request.GET)[Route]
|
||||||
|
trips = get_trips(routes)
|
||||||
|
stop_sequences = get_stop_sequences(routes, trips)
|
||||||
|
timetables = {r.route_id: get_timetable(r, trips[r["route_id"]], stop_sequences[r["route_id"]]) for r in routes}
|
||||||
|
return HttpResponse(json.dumps(timetables), content_type="application/json")
|
||||||
|
except Route.DoesNotExist:
|
||||||
|
return HttpResponseBadRequest("Route not found.")
|
||||||
|
return HttpResponseNotAllowed(["GET"])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
def data(request):
|
||||||
|
"""
|
||||||
|
Handle database requests from the frontend. Using Http semantics to specify what to do with the data.
|
||||||
|
|
||||||
|
Request
|
||||||
|
-------
|
||||||
|
PUT
|
||||||
|
Create a new object if no object with the given primary key exists in the database or delete and replace an existing object.
|
||||||
|
Body must be a json dict of lists of fully specified, valid models. Primary keys can be omitted and will be ignored if the element does not exist in the database.
|
||||||
|
If primary keys are given, the elements are deleted and replaced. Note that if there is an error in creating the new object, the object to replace will still probably already have been deleted.
|
||||||
|
Successful response is 200 with a list of primary keys of the created and replaced objects.
|
||||||
|
PATCH
|
||||||
|
Modify an existing objects given the instructions in the body.
|
||||||
|
Body must be a json dict of lists of fields to change and their valid values existing objects in the database, identified by their valid primary keys.
|
||||||
|
Responds 400 if any of the primary keys given does not exist.
|
||||||
|
Successful response is 200 with a list of the primary keys of the modified objects.
|
||||||
|
GET
|
||||||
|
Return json of models identified by primary keys.
|
||||||
|
Responds 400 if any of the requested pks does not exist.
|
||||||
|
DELETE
|
||||||
|
Delete models with given primary keys if they exist.
|
||||||
|
Responds 400 if any of the primary keys given does not exist in the database.
|
||||||
|
Successful response is 200 and the number of deleted models.
|
||||||
|
"""
|
||||||
|
if request.method in ["PUT", "PATCH", "DELETE"]:
|
||||||
|
new = 0
|
||||||
|
modified = 0
|
||||||
|
if not request.META["CONTENT_TYPE"] == 'application/json':
|
||||||
|
HttpResponseBadRequest('Request must be JSON.')
|
||||||
|
try:
|
||||||
|
for o in serializers.deserialize('json', request.body):
|
||||||
|
try:
|
||||||
|
obj = o.object.__class__.objects.get(pk=o.object.pk)
|
||||||
|
modified += 1
|
||||||
|
if request.method == "PATCH":
|
||||||
|
for f,v in o.object.__dict__.items():
|
||||||
|
if v:
|
||||||
|
setattr(obj, f, v)
|
||||||
|
obj.save()
|
||||||
|
else:
|
||||||
|
obj.delete()
|
||||||
|
if request.method == "PUT":
|
||||||
|
o.save()
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
if not request.method == "PUT":
|
||||||
|
return HttpResponseBadRequest("Object(s) not found. {modified} objects touched.")
|
||||||
|
new += 1
|
||||||
|
o.save()
|
||||||
|
except django.db.IntegrityError:
|
||||||
|
return HttpResponseBadRequest("Could not write to database. Probably the objects you were trying to create or update where not compliant with GTFS's or the API's specification.")
|
||||||
|
except IntegrityError:
|
||||||
|
return HttpResponseBadRequest("There was an error while trying to write to the database. Probably a foreign key could not be resolved. Did you specify the objects in the correct order, if they depend on each other?")
|
||||||
|
except serializers.base.DeserializationError:
|
||||||
|
return HttpResponseBadRequest("Could not process the JSON you sent me correctly. Did you comply with the format required by the API and used valid JSON?")
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return HttpResponseBadRequest("Invalid JSON.")
|
||||||
|
except django.db.utils.DataError:
|
||||||
|
return HttpResponseBadRequest("One of your objects has fields that do not fit in their corresponding fields in the database. Did you comply to all data type requirements?")
|
||||||
|
return HttpResponse(f"OK. {new} new objects, {modified} replaced.") if request.method == "PUT" else HttpResponse(f"OK. {'Patched' if request.method == 'PATCH' else 'Deleted'} {modified} objects.")
|
||||||
|
elif request.method == "GET":
|
||||||
|
try:
|
||||||
|
pks = get_pks_from_get(request.GET)
|
||||||
|
except ValueError:
|
||||||
|
return HttpResponseBadRequest("No valid pks given.")
|
||||||
|
try:
|
||||||
|
return HttpResponse(json.dumps({mdl._meta.object_name: serializers.serialize('json', v) for mdl,v in obj_from_get(request.GET).items()}), content_type='application/json')
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
return HttpResponseBadRequest("Object(s) not found.")
|
||||||
|
return HttpResponseNotAllowed(['PUT', 'PATCH', 'GET'])
|
||||||
|
|
||||||
# Create your views here.
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
"""
|
"""
|
||||||
Constant defining different variation of the file names in GTFS / our model names, mapped to corresponding models.
|
Constants useful to quickly look up often used references to models and their fields.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pt_map.models
|
import pt_map.models
|
||||||
|
|
@ -176,38 +176,6 @@ reversed_file_mapping = {
|
||||||
case_swap = {'Agency': 'agency', 'Stop': 'stops', 'Route': 'routes', 'Trip': 'trips', 'StopTime': 'stop_times', 'Calendar': 'calendar', 'CalendarDate': 'calendar_dates', 'FareAttribute': 'fare_attributes', 'FareRule': 'fare_rules', 'Timeframe': 'timeframes', 'FareMedium': 'fare_media', 'FareProduct': 'fare_products', 'FareLegRule': 'fare_leg_rules', 'FareTransferRule': 'fare_transfer_rules', 'Area': 'areas', 'StopArea': 'stop_areas', 'Network': 'networks', 'RouteNetwork': 'route_networks', 'Shape': 'shapes', 'Frequency': 'frequencies', 'Transfer': 'transfers', 'Pathway': 'pathways', 'Level': 'levels', 'LocationGroup': 'location_groups', 'LocationGroupStop': 'location_group_stops', 'LocationsGeojson': 'locations_geojson', 'BookingRule': 'booking_rules', 'Translation': 'translations', 'FeedInfo': 'feed_info', 'Attribution': 'attributions'}
|
case_swap = {'Agency': 'agency', 'Stop': 'stops', 'Route': 'routes', 'Trip': 'trips', 'StopTime': 'stop_times', 'Calendar': 'calendar', 'CalendarDate': 'calendar_dates', 'FareAttribute': 'fare_attributes', 'FareRule': 'fare_rules', 'Timeframe': 'timeframes', 'FareMedium': 'fare_media', 'FareProduct': 'fare_products', 'FareLegRule': 'fare_leg_rules', 'FareTransferRule': 'fare_transfer_rules', 'Area': 'areas', 'StopArea': 'stop_areas', 'Network': 'networks', 'RouteNetwork': 'route_networks', 'Shape': 'shapes', 'Frequency': 'frequencies', 'Transfer': 'transfers', 'Pathway': 'pathways', 'Level': 'levels', 'LocationGroup': 'location_groups', 'LocationGroupStop': 'location_group_stops', 'LocationsGeojson': 'locations_geojson', 'BookingRule': 'booking_rules', 'Translation': 'translations', 'FeedInfo': 'feed_info', 'Attribution': 'attributions'}
|
||||||
|
|
||||||
|
|
||||||
#primary_keys = { pt_map.models.FeedInfo: None,
|
|
||||||
# pt_map.models.Agency: "agency_id",
|
|
||||||
# pt_map.models.Level: "level_id",
|
|
||||||
# pt_map.models.Stop: "stop_id",
|
|
||||||
# pt_map.models.Route: "route_id",
|
|
||||||
# pt_map.models.Shape: "shape_id",
|
|
||||||
# pt_map.models.Calendar: "service_id",
|
|
||||||
# pt_map.models.CalendarDate: None,
|
|
||||||
# pt_map.models.Trip: "trip_id",
|
|
||||||
# pt_map.models.LocationGroup: "location_group_id",
|
|
||||||
# pt_map.models.LocationsGeojson: None,
|
|
||||||
# pt_map.models.StopTime: None,
|
|
||||||
# pt_map.models.FareAttribute: "fare_id",
|
|
||||||
# pt_map.models.FareRule: None,
|
|
||||||
# pt_map.models.Frequency: None,
|
|
||||||
# pt_map.models.Transfer: None,
|
|
||||||
# pt_map.models.Pathway: "pathway_id",
|
|
||||||
# pt_map.models.BookingRule: "booking_rule_id",
|
|
||||||
# pt_map.models.Translation: None,
|
|
||||||
# pt_map.models.Attribution: "attribution_id",
|
|
||||||
# pt_map.models.LocationGroupStop: None,
|
|
||||||
# pt_map.models.Network: "network_id",
|
|
||||||
# pt_map.models.RouteNetwork: None,
|
|
||||||
# pt_map.models.Area: None,
|
|
||||||
# pt_map.models.StopArea: None,
|
|
||||||
# pt_map.models.FareMedium: "fare_media_id",
|
|
||||||
# pt_map.models.FareProduct: None,
|
|
||||||
# pt_map.models.Timeframe: None,
|
|
||||||
# pt_map.models.FareLegRule: None,
|
|
||||||
# pt_map.models.FareTransferRule: None,
|
|
||||||
#}
|
|
||||||
|
|
||||||
primary_keys = {
|
primary_keys = {
|
||||||
pt_map.models.FeedInfo: "feed_id",
|
pt_map.models.FeedInfo: "feed_id",
|
||||||
pt_map.models.Agency: "agency_id",
|
pt_map.models.Agency: "agency_id",
|
||||||
|
|
|
||||||
|
|
@ -1,57 +1,183 @@
|
||||||
"""
|
"""
|
||||||
Query
|
Query
|
||||||
=====
|
-----
|
||||||
|
|
||||||
Interface between backend/database and Views. Aims to abstract database lookups for the frontend as well as possible.
|
Module to handle database IO while abstracting the specific SQL and django model filtering.
|
||||||
|
|
||||||
Contents
|
|
||||||
--------
|
|
||||||
Classes
|
|
||||||
-------
|
|
||||||
|
|
||||||
Functions
|
Functions
|
||||||
---------
|
---------
|
||||||
|
|
||||||
Public variables
|
|
||||||
----------------
|
|
||||||
"""
|
"""
|
||||||
import django.db.models
|
from pt_map.models import *
|
||||||
import pt_map.models
|
from django.db import models
|
||||||
from pt_map.class_names import class_names
|
from .class_names import *
|
||||||
|
from django.http import HttpRequest
|
||||||
|
from datetime import datetime, date
|
||||||
|
|
||||||
class GTFSQuery:
|
def get_field_names(model: models.Model) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Base datatype conveniently storing data requiring queries involving multiple tables as if they were a GTFS Feed as described by the GTFS specification.
|
Given a model, returns a list of the name strings of all the model's fields.
|
||||||
Main abstraction element between data handling and frontend.
|
"""
|
||||||
|
return [field.name for field in model._meta.fields]
|
||||||
|
|
||||||
Attributes
|
def get_pks_from_get(req_get: dict[str, str]) -> dict[str, list[models.Model]]:
|
||||||
----------
|
"""
|
||||||
|
Extract primary keys from a request.GET dict-like and find the corresponding classes.
|
||||||
|
|
||||||
Methods
|
|
||||||
-------
|
|
||||||
"""
|
|
||||||
def __init__(self, queries: dict[str, django.db.models.query.QuerySet]):
|
|
||||||
"""
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
queries : dict[str, django.db.models.query.QuerySet]
|
req_get : dict[str, str]
|
||||||
dict containing
|
dict-like object, a HTTP Requests GET data.
|
||||||
keys: str specifying the file of the GTFS reference they represent either as the file name omitting the extension or as Model name in CamelCase or as model name all lower case.
|
|
||||||
values: QuerySets of the specified models
|
Returns
|
||||||
|
-------
|
||||||
|
dict[str, list[str]]
|
||||||
|
dict mapping a model to a list of id fields passed with GET
|
||||||
|
"""
|
||||||
|
result = {}
|
||||||
|
for k in req_get.keys():
|
||||||
|
if k in classes_by_primary_keys.keys():
|
||||||
|
result[classes_by_primary_keys[k]] = req_get.getlist(k)
|
||||||
|
if not result:
|
||||||
|
raise ValueError("No pks found.")
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_obj_by_pk(mdl: models.Model, pks: list[str]) -> list[mdl.Model]:
|
||||||
|
"""
|
||||||
|
Given a model, and a list of corresponding primary keys, return a list of objects of the given model identified by the given primary keys.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
mdl: models.Model
|
||||||
|
Model class to look for
|
||||||
|
pks: list[str]
|
||||||
|
primary keys of the objects to return
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
list[mdl]
|
||||||
|
Objects corresponding to primary keys in pk.
|
||||||
|
|
||||||
Raises
|
Raises
|
||||||
------
|
------
|
||||||
TypeError
|
mdl.DoesNotExist
|
||||||
If queries is not present or of bad type
|
If at least one object from the list of pks could not be found.
|
||||||
ValueError
|
|
||||||
If queries contains a Key not specified as a file in the GTFS reference
|
|
||||||
"""
|
"""
|
||||||
if not queries or not isinstance(queries, dict):
|
return [obj for obj in [mdl.objects.get(**{primary_keys[mdl]: pk}) for pk in pks] if obj]
|
||||||
raise TypeError("Missing dict of QuerySets")
|
|
||||||
for key,value in queries.items():
|
|
||||||
for names in class_names:
|
|
||||||
if names.get(key):
|
|
||||||
cls = names[key]
|
|
||||||
if not cls:
|
|
||||||
raise ValueError("Bad GTFS file name")
|
|
||||||
|
|
||||||
|
def obj_from_get(req_get: dict[str,str]) -> dict[cls, list[models.Model]]:
|
||||||
|
"""
|
||||||
|
Given the GET data of a HTTP Request, return a dict with the requested model classes as keys and lists of the requested model objects as values.
|
||||||
|
"""
|
||||||
|
return {mdl: get_obj_by_pk(mdl, keys) for mdl, keys in get_pks_from_get(req_get).items()}
|
||||||
|
|
||||||
|
def get_timetable(r: pt_map.models.Route, trips_r: list[pt_map.models.Trip], stop_sequence: list[str]):
|
||||||
|
"""
|
||||||
|
Given a pt_map.models.Route, calculate the timetable for all its stops.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
r : pt_map.models.Route
|
||||||
|
Route, the timetable should be calculated for
|
||||||
|
trips_r : list(pt_map.Trip)
|
||||||
|
List of trips travelling on the Route r
|
||||||
|
stop_sequence : list(str)
|
||||||
|
List of stop_ids the Route r serves. Currently the first trip is taken as reference for stops and sequence.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
dict{"stop_sequence": list(str), "stop_times": dict[str, list(str)]}
|
||||||
|
Dict containing two elements:
|
||||||
|
"stop_sequence" : list(str)
|
||||||
|
list of stop_ids the route serves
|
||||||
|
"stop_times" : dict(str, list(str))
|
||||||
|
dict mapping stop_ids from stop_sequence to time strings the route is serving the stop at
|
||||||
|
"""
|
||||||
|
timetable = {"stop_sequence": stop_sequence}
|
||||||
|
sts = {}
|
||||||
|
for stop in stop_sequence:
|
||||||
|
times = []
|
||||||
|
for t in trips_r:
|
||||||
|
for st in StopTime.objects.filter(trip_id=t.trip_id):
|
||||||
|
times.append(st.departure_time.strftime("%H:%M"))
|
||||||
|
sts[stop] = times
|
||||||
|
timetable["stop_times"] = sts
|
||||||
|
return timetable
|
||||||
|
|
||||||
|
def get_all_stops() -> dict[str, dict[str,str]]:
|
||||||
|
"""
|
||||||
|
Return all Stop object stored in the database.
|
||||||
|
|
||||||
|
Representation of the result:
|
||||||
|
dict:
|
||||||
|
{
|
||||||
|
stop_id (str): {
|
||||||
|
'stop_name': pt_map.models.Stop.stop_name,
|
||||||
|
'stop_lat': pt_map.models.Stop.stop_lat,
|
||||||
|
'stop_lon': pt_map.models.Stop.stop_lon,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
return {s.stop_id: {name: getattr(s, name) for name in ['stop_name', 'stop_lat', 'stop_lon']} for s in Stop.objects.all()}
|
||||||
|
|
||||||
|
def get_all_routes() -> list[dict[str, str]]:
|
||||||
|
"""
|
||||||
|
Return a list of all Route objects found in the database.
|
||||||
|
|
||||||
|
Representation of the result:
|
||||||
|
list:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
'route_id': pt_map.models.Route.route_id,
|
||||||
|
'route_type': pt_map.models.Route.route_type,
|
||||||
|
'route_name': pt_map.models.Route.route_short_name if set else pt_map.models.Route.route_long_name,
|
||||||
|
'agency_id': pt_map.models.Route.agency_id.agency_id,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
route_name = lambda r : r.route_short_name if r.route_short_name else r.route_long_name
|
||||||
|
return [{"route_id": r.route_id, "route_type": r.route_type, "route_name": route_name(r), "agency_id": r.agency_id.agency_id} for r in Route.objects.all()]
|
||||||
|
|
||||||
|
def get_trips(routes: list[pt_map.models.Route]) -> dict[str, list[pt_map.models.Trip]]:
|
||||||
|
"""
|
||||||
|
Return a list of all Trips associated with a Route in the argument.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
routes: list[str]
|
||||||
|
List of primary keys for the Routes to search the trips for.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
dict[str, list[pt_map.models.Trip]]
|
||||||
|
Keys: route_ids from parameter
|
||||||
|
Values: lists of corresponding trip objects.
|
||||||
|
"""
|
||||||
|
return {r["route_id"]: [t for t in Trip.objects.filter(route_id_id=r["route_id"])] for r in routes}
|
||||||
|
|
||||||
|
def get_stop_sequences(routes: list[pt_map.models.Route], trips: dict[str,list[pt_map.models.Trip]]=None) -> dict[str, list[str]]:
|
||||||
|
"""
|
||||||
|
For all given routes, return a list of stops in the order of appearance along the route. The first trip in the list of trips is used to define the sequence.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
routes: list[pt_map.models.Route]
|
||||||
|
List of pt_map.models.Route to find stop sequences for
|
||||||
|
trips: dict[str,list[pt_map.models.Trip]]
|
||||||
|
List of at least one trip for each Route in routes. If none, all are calculated and the first used for the sequence.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
dict[str, list[str]]
|
||||||
|
Keys: route_ids
|
||||||
|
Values: Lists of stop_ids in the order of appearance in the first Trip in the routes' trips list given.
|
||||||
|
"""
|
||||||
|
if not trips:
|
||||||
|
trips = get_trips(routes)
|
||||||
|
stop_sequences = {}
|
||||||
|
for r in routes:
|
||||||
|
seq = []
|
||||||
|
t = trips[r["route_id"]]
|
||||||
|
for s in StopTime.objects.filter(trip_id_id__exact=t[0].trip_id):
|
||||||
|
seq.append(s)
|
||||||
|
stop_sequences[r["route_id"]] = [s.stop_id.stop_id for s in sorted(seq, key=lambda st : st.stop_sequence)]
|
||||||
|
return stop_sequences
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,4 @@ from . import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", views.index, name="index"),
|
path("", views.index, name="index"),
|
||||||
path("data/", views.data, name="data"),
|
|
||||||
path("timetable/", views.timetable, name="timetable")
|
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,188 +1,29 @@
|
||||||
"""
|
"""
|
||||||
Views
|
Views
|
||||||
=====
|
=====
|
||||||
Views reacting to Http Requests by interfacing between backend and frontend.
|
Views serving browser viewable, HTML web pages.
|
||||||
|
|
||||||
Functions
|
Functions
|
||||||
---------
|
---------
|
||||||
index(request)
|
index
|
||||||
Home page
|
Home page
|
||||||
"""
|
"""
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed, HttpRequest
|
from . import query
|
||||||
from django.core.exceptions import BadRequest, ObjectDoesNotExist
|
|
||||||
from django.core import serializers
|
|
||||||
import django.db.models
|
|
||||||
from MySQLdb import IntegrityError
|
|
||||||
from .models import *
|
|
||||||
from .forms import *
|
|
||||||
import json
|
|
||||||
from datetime import datetime, date
|
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
|
||||||
from .class_names import *
|
|
||||||
|
|
||||||
|
|
||||||
class GTFSSerializer(serializers.json.Serializer):
|
|
||||||
def serialize(self, queryset, **options):
|
|
||||||
return json.dumps([{field: obj['fields'][field] for field in obj['fields'] if obj['fields'][field] == 0 or obj['fields'][field]} for obj in json.loads(super().serialize(queryset, **options))])
|
|
||||||
|
|
||||||
def get_timetable(r, trips_r, stop_sequence):
|
|
||||||
"""
|
|
||||||
Given a pt_map.models.Route, calculate the timetable for all its stops.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
r : pt_map.models.Route
|
|
||||||
Route, the timetable should be calculated for
|
|
||||||
trips : dict(str, list(pt_map.Trip))
|
|
||||||
Dictionary mapping all trips to route_ids they travel on
|
|
||||||
stop_sequences : dict(str, list(str))
|
|
||||||
Dict mapping route_ids to lists of stop_ids they serve. Currently the first trip is taken as reference for stops and sequence.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
dict{"stop_sequence": list(str), "stop_times": dict(str, list(str)}
|
|
||||||
Dict containing two elements:
|
|
||||||
"stop_sequence" : list(str)
|
|
||||||
list of stop_ids the route serves
|
|
||||||
"stop_times" : dict(str, list(str))
|
|
||||||
dict mapping stop_ids from stop_sequence to time strings the route is serving the stop at
|
|
||||||
"""
|
|
||||||
timetable = {"stop_sequence": stop_sequence}
|
|
||||||
sts = {}
|
|
||||||
for stop in stop_sequence:
|
|
||||||
times = []
|
|
||||||
for t in trips_r:
|
|
||||||
for st in StopTime.objects.filter(trip_id=t.trip_id):
|
|
||||||
times.append(st.departure_time.strftime("%H:%M"))
|
|
||||||
sts[stop] = times
|
|
||||||
timetable["stop_times"] = sts
|
|
||||||
return timetable
|
|
||||||
|
|
||||||
def index(request):
|
def index(request):
|
||||||
stops = {s.stop_id: {name: getattr(s, name) for name in ['stop_name', 'stop_lat', 'stop_lon']} for s in Stop.objects.all()}
|
"""
|
||||||
route_name = lambda r : r.route_short_name if r.route_short_name else r.route_long_name
|
Home page view serving the default index page.
|
||||||
routes = [{"route_id": r.route_id, "route_type": r.route_type, "route_name": route_name(r), "agency_id": r.agency_id.agency_id} for r in Route.objects.all()]
|
|
||||||
trips = {r["route_id"]: [t for t in Trip.objects.filter(route_id_id=r["route_id"])] for r in routes}
|
Context
|
||||||
stop_sequences = {}
|
------
|
||||||
for r in routes:
|
"Stops": Json Representation of all stops found in the database
|
||||||
seq = []
|
"Routes": Json Representation of all routes found in the database
|
||||||
t = trips[r["route_id"]]
|
"""
|
||||||
for s in StopTime.objects.filter(trip_id_id__exact=t[0].trip_id):
|
context = {
|
||||||
seq.append(s)
|
"stops": json.dumps(query.get_all_stops()),
|
||||||
stop_sequences[r["route_id"]] = [s.stop_id.stop_id for s in sorted(seq, key=lambda st : st.stop_sequence)]
|
"routes": json.dumps(query.get_all_routes()),
|
||||||
timetable = {}
|
}
|
||||||
if request.GET.get("timetable"):
|
|
||||||
try:
|
|
||||||
r = Route.objects.get(route_id=request.GET.get("timetable"))
|
|
||||||
timetable = get_timetable(r, trips[r.route_id], stop_sequences[r.route_id])
|
|
||||||
except Route.DoesNotExist:
|
|
||||||
print(f"Invalid request for Route with id {request.GET['timetable']}")
|
|
||||||
context = {"stops": json.dumps(stops), "routes": json.dumps(routes), "timetable": json.dumps(timetable)}
|
|
||||||
return render(request, "map.html", context)
|
return render(request, "map.html", context)
|
||||||
|
|
||||||
def get_field_names(model: models.Model):
|
|
||||||
return [field.name for field in model._meta.fields]
|
|
||||||
|
|
||||||
def timetable(request):
|
|
||||||
if request.method == "GET":
|
|
||||||
try:
|
|
||||||
r = Route.objects.get(route_id=request.GET["route_id"])
|
|
||||||
trips_r = [t for t in Trip.objects.filter(route_id_id=r.route_id)]
|
|
||||||
stop_sequence = [s.stop_id.stop_id for s in sorted([s for s in StopTime.objects.filter(trip_id_id__exact=trips_r[0].trip_id)], key=lambda st : st.stop_sequence)]
|
|
||||||
timetable = get_timetable(r, trips_r, stop_sequence)
|
|
||||||
return HttpResponse(json.dumps(timetable), content_type="text/json")
|
|
||||||
except KeyError:
|
|
||||||
return HttpResponseBadRequest("route_id missing or malformed.")
|
|
||||||
except Route.DoesNotExist:
|
|
||||||
return HttpResponseBadRequest("Route not found.")
|
|
||||||
return HttpResponseNotAllowed(["GET"])
|
|
||||||
|
|
||||||
def get_pks_from_get(req_get):
|
|
||||||
result = {}
|
|
||||||
for k in req_get.keys():
|
|
||||||
if k in classes_by_primary_keys.keys():
|
|
||||||
result[classes_by_primary_keys[k]] = req_get.getlist(k)
|
|
||||||
if not result:
|
|
||||||
raise ValueError("No pks found.")
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_obj_by_pk(mdl: models.Model, pks: list[str]):
|
|
||||||
return [obj for obj in [mdl.objects.get(**{primary_keys[mdl]: pk}) for pk in pks] if obj]
|
|
||||||
|
|
||||||
def obj_from_get(req_get) -> str:
|
|
||||||
print({mdl: get_obj_by_pk(mdl, keys) for mdl, keys in get_pks_from_get(req_get).items()})
|
|
||||||
return {mdl: get_obj_by_pk(mdl, keys) for mdl, keys in get_pks_from_get(req_get).items()}
|
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
|
||||||
def data(request):
|
|
||||||
"""
|
|
||||||
Handle database requests from the frontend. Using Http semantics to specify what to do with the data.
|
|
||||||
|
|
||||||
Request
|
|
||||||
-------
|
|
||||||
PUT
|
|
||||||
Create a new object if no object with the given primary key exists in the database or delete and replace an existing object.
|
|
||||||
Body must be a json dict of lists of fully specified, valid models. Primary keys can be omitted and will be ignored if the element does not exist in the database.
|
|
||||||
If primary keys are given, the elements are deleted and replaced. Note that if there is an error in creating the new object, the object to replace will still probably already have been deleted.
|
|
||||||
Successful response is 200 with a list of primary keys of the created and replaced objects.
|
|
||||||
PATCH
|
|
||||||
Modify an existing objects given the instructions in the body.
|
|
||||||
Body must be a json dict of lists of fields to change and their valid values existing objects in the database, identified by their valid primary keys.
|
|
||||||
Responds 400 if any of the primary keys given does not exist.
|
|
||||||
Successful response is 200 with a list of the primary keys of the modified objects.
|
|
||||||
GET
|
|
||||||
Return json of models identified by primary keys.
|
|
||||||
Responds 400 if any of the requested pks does not exist.
|
|
||||||
DELETE
|
|
||||||
Delete models with given primary keys if they exist.
|
|
||||||
Responds 400 if any of the primary keys given does not exist in the database.
|
|
||||||
Successful response is 200 and the number of deleted models.
|
|
||||||
"""
|
|
||||||
if request.method in ["PUT", "PATCH", "DELETE"]:
|
|
||||||
new = 0
|
|
||||||
modified = 0
|
|
||||||
if not request.META["CONTENT_TYPE"] == 'application/json':
|
|
||||||
HttpResponseBadRequest('Request must be JSON.')
|
|
||||||
try:
|
|
||||||
for o in serializers.deserialize('json', request.body):
|
|
||||||
try:
|
|
||||||
obj = o.object.__class__.objects.get(pk=o.object.pk)
|
|
||||||
modified += 1
|
|
||||||
if request.method == "PATCH":
|
|
||||||
for f,v in o.object.__dict__.items():
|
|
||||||
if v:
|
|
||||||
setattr(obj, f, v)
|
|
||||||
obj.save()
|
|
||||||
else:
|
|
||||||
obj.delete()
|
|
||||||
if request.method == "PUT":
|
|
||||||
o.save()
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
if not request.method == "PUT":
|
|
||||||
return HttpResponseBadRequest("Object(s) not found. {modified} objects touched.")
|
|
||||||
new += 1
|
|
||||||
o.save()
|
|
||||||
except django.db.IntegrityError:
|
|
||||||
return HttpResponseBadRequest("Could not write to database. Probably the objects you were trying to create or update where not compliant with GTFS's or the API's specification.")
|
|
||||||
except IntegrityError:
|
|
||||||
return HttpResponseBadRequest("There was an error while trying to write to the database. Probably a foreign key could not be resolved. Did you specify the objects in the correct order, if they depend on each other?")
|
|
||||||
except serializers.base.DeserializationError:
|
|
||||||
return HttpResponseBadRequest("Could not process the JSON you sent me correctly. Did you comply with the format required by the API and used valid JSON?")
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
return HttpResponseBadRequest("Invalid JSON.")
|
|
||||||
except django.db.utils.DataError:
|
|
||||||
return HttpResponseBadRequest("One of your objects has fields that do not fit in their corresponding fields in the database. Did you comply to all data type requirements?")
|
|
||||||
return HttpResponse(f"OK. {new} new objects, {modified} replaced.") if request.method == "PUT" else HttpResponse(f"OK. {'Patched' if request.method == 'PATCH' else 'Deleted'} {modified} objects.")
|
|
||||||
elif request.method == "GET":
|
|
||||||
try:
|
|
||||||
pks = get_pks_from_get(request.GET)
|
|
||||||
except ValueError:
|
|
||||||
return HttpResponseBadRequest("No valid pks given.")
|
|
||||||
try:
|
|
||||||
return HttpResponse(json.dumps({mdl._meta.object_name: GTFSSerializer().serialize(v) for mdl,v in obj_from_get(request.GET).items()}), content_type='application/json')
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
return HttpResponseBadRequest("Object(s) not found.")
|
|
||||||
return HttpResponseNotAllowed(['PUT', 'PATCH', 'GET'])
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ ALLOWED_HOSTS = []
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
|
'api.apps.ApiConfig',
|
||||||
'pt_map.apps.PtMapConfig',
|
'pt_map.apps.PtMapConfig',
|
||||||
'django.contrib.admin',
|
'django.contrib.admin',
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
|
|
|
||||||
|
|
@ -6,5 +6,6 @@ from django.urls import path, include
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('', include("pt_map.urls"))
|
path('api/', include("api.urls")),
|
||||||
|
path('', include("pt_map.urls")),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user