Skip to content

Commit

Permalink
Add search and save for the remaining tables (#128)
Browse files Browse the repository at this point in the history
* add points of interest to asset map search; implement a simple popup

* add search for parking lots

* add migration to LocalAsset so we can use PicnicGrove.fpd_uid CharField

* make popup relevant to local assets and search assets

* cleanup code

* add signage search + tidy up point markers

* cleanup code

* cleanup code

* cleanup code

* catch and render 400 error from client

* allow null for LocalAsset.asset_id

* bring back docstring

* change CharField to a TextField and create a new migration instead of having the two migrations that do nothing

* remove signage table for now

* remove dead code
  • Loading branch information
smcalilly authored Feb 25, 2022
1 parent e458417 commit bafa250
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 31 deletions.
24 changes: 17 additions & 7 deletions asset_dashboard/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
from rest_framework.permissions import IsAuthenticated

from asset_dashboard.models import Phase, Portfolio, PortfolioPhase, Project, \
LocalAsset, Buildings, TrailsInfo
LocalAsset, Buildings, TrailsInfo, PoiInfo, PicnicGroves
from asset_dashboard.serializers import PortfolioSerializer, UserSerializer, \
PortfolioPhaseSerializer, PhaseSerializer, ProjectSerializer, \
BuildingsSerializer, TrailsSerializer, LocalAssetWriteSerializer, LocalAssetReadSerializer
BuildingsSerializer, TrailsSerializer, LocalAssetWriteSerializer, LocalAssetReadSerializer, \
PointsOfInterestSerializer, PicnicGrovesSerializer, ParkingLotsSerializer


class PortfolioViewSet(viewsets.ModelViewSet):
Expand Down Expand Up @@ -59,15 +60,21 @@ def asset_type(self):
@property
def model_cls(self):
return {
'buildings': Buildings,
'trails': TrailsInfo,
'buildings': {'model': Buildings},
'trails': {'model': TrailsInfo},
'points_of_interest': {'model': PoiInfo},
'picnic_groves': {'model': PicnicGroves},
'parking_lots': {'model': PoiInfo, 'check_for_not_null': True}
}.get(self.asset_type, Buildings)

@property
def serializer_cls(self):
return {
'buildings': BuildingsSerializer,
'trails': TrailsSerializer,
'points_of_interest': PointsOfInterestSerializer,
'picnic_groves': PicnicGrovesSerializer,
'parking_lots': ParkingLotsSerializer
}.get(self.asset_type, BuildingsSerializer)

def get_serializer_class(self, *args, **kwargs):
Expand All @@ -77,16 +84,19 @@ def get_queryset(self, *args, **kwargs):
search_filter = Q()

if query := self.request.query_params.get('q', False):

for field, field_type in self.model_cls.Search.fields:
for field, field_type in self.model_cls.get('model').Search.fields:
try:
field_type(query)
except (ValueError, TypeError):
continue
else:
search_filter |= Q(**{f'{field}__icontains': query})

return self.model_cls.objects.filter(search_filter)
if self.model_cls.get('check_for_not_null'):
for field in self.model_cls.get('model').Search.not_null_fields:
search_filter &= Q(**{f'{field}__isnull': False})

return self.model_cls.get('model').objects.filter(search_filter)


class LocalAssetViewSet(viewsets.ModelViewSet):
Expand Down
18 changes: 18 additions & 0 deletions asset_dashboard/migrations/0026_auto_20220216_1720.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2022-02-16 17:20

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('asset_dashboard', '0025_auto_20220126_1731'),
]

operations = [
migrations.AlterField(
model_name='localasset',
name='asset_id',
field=models.TextField(blank=True, null=True),
),
]
38 changes: 22 additions & 16 deletions asset_dashboard/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ class LocalAsset(models.Model):

geom = models.GeometryField(srid=3435)

asset_id = models.IntegerField()
asset_id = models.TextField(null=True, blank=True)
asset_model = models.CharField(max_length=100)
asset_name = models.CharField(max_length=600)

Expand Down Expand Up @@ -700,39 +700,37 @@ class Meta(GISModel.Meta):
remarks = models.TextField()
photos = models.TextField()


class ParkingLots(GISModel):
"""Parking lot polygons, for all public and non-public lots."""

class Meta(GISModel.Meta):
db_table = '"quercus"."parking_lots"'

id = models.AutoField(primary_key=True, db_column='parking_lots_id')
db_table = '"acer"."parking_lots_union_mv"'

id = models.AutoField(primary_key=True, db_column='lot_id')
geom = models.PolygonField(srid=3435, spatial_index=True)

lot_id = models.IntegerField()
zone = models.CharField(max_length=25)
name = models.CharField(max_length=100)
lot_access = models.CharField(max_length=25)
parking_stalls = models.IntegerField()
lot_surface = models.CharField(max_length=25)
lot_part_type = models.CharField(max_length=25)
closed = models.CharField(max_length=10)
comments = models.CharField(max_length=250)
maintained = models.CharField(max_length=10)
closed = models.CharField(max_length=10)
lot_surface = models.CharField(max_length=25)
square_feet = models.DecimalField(max_digits=10, decimal_places=2)
square_yards = models.DecimalField(max_digits=10, decimal_places=2)
acres = models.DecimalField(max_digits=10, decimal_places=2)
square_feet = models.DecimalField(max_digits=10, decimal_places=2)
maintained_by = models.CharField(max_length=50)
maintenance_comment = models.CharField(max_length=250)
accessible_stalls = models.IntegerField()
parking_info_id = models.IntegerField()


class PicnicGroves(GISModel):
"""All picnic groves with active status"""

class Meta(GISModel.Meta):
db_table = '"quercus"."picnicgroves"'

class Search:
fields = (
('fpd_uid', int),
('poi_info__nameid__name', str),
)

id = models.AutoField(primary_key=True, db_column='picnicgrove_id')

Expand Down Expand Up @@ -882,6 +880,14 @@ class Meta(GISModel.Meta):
models.Index(fields=['parking_info_id']),
models.Index(fields=['pointsofinterest_id'])
]

class Search:
fields = (
('fpd_uid', int),
('nameid__name', str),
)

not_null_fields = ['parking_info_id']

id = models.AutoField(primary_key=True, db_column='poi_info_id')

Expand Down
73 changes: 69 additions & 4 deletions asset_dashboard/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
GeometrySerializerMethodField, GeometryField

from asset_dashboard.models import Phase, Portfolio, PortfolioPhase, Project, \
LocalAsset, Buildings, TrailsInfo
LocalAsset, Buildings, TrailsInfo, PoiInfo, PointsOfInterest, PicnicGroves, \
ParkingLots


class UserSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -69,6 +70,17 @@ def to_representation(self, value):
return super().to_representation(value)


class NullableCharField(serializers.CharField):
def __init__(self, *args, allow_null=False, **kwargs):
super().__init__(*args, **kwargs)
self.allow_null = allow_null

def to_representation(self, value):
if self.allow_null and value is None:
return None
return super().to_representation(value)


class BaseLocalAssetSerializer(GeoFeatureModelSerializer):
"""
A base serializer for the LocalAssets because we need
Expand All @@ -81,7 +93,7 @@ class Meta:
fields = ('id', 'geom', 'asset_id', 'asset_type', 'asset_name', 'phase')
geo_field = 'geom'

asset_id = serializers.IntegerField()
asset_id = NullableCharField(allow_null=True)
asset_type = serializers.CharField(source='asset_model')
asset_name = serializers.CharField()
phase = serializers.PrimaryKeyRelatedField(queryset=Phase.objects.all())
Expand All @@ -101,9 +113,9 @@ class LocalAssetReadSerializer(BaseLocalAssetSerializer):
class SourceAssetSerializer(GeoFeatureModelSerializer):
class Meta:
fields = ('source')

source = serializers.SerializerMethodField()

def get_source(self, obj):
return 'search'

Expand Down Expand Up @@ -137,3 +149,56 @@ def get_geom(self, obj):
in viewset -> get_queryset
'''
return obj.trails.geom.transform(4326, clone=True)

class PointsOfInterestSerializer(SourceAssetSerializer):
class Meta:
model = PoiInfo
fields = ('identifier', 'name', 'geom', 'source')
geo_field = 'geom'

identifier = serializers.IntegerField(source='fpd_uid')
name = serializers.SerializerMethodField(source='nameid')
geom = GeometrySerializerMethodField()

def get_geom(self, obj):
return PointsOfInterest.objects.get(
id=obj.pointsofinterest_id
).geom.transform(4326, clone=True)

def get_name(self, obj):
return obj.nameid.name

class PicnicGrovesSerializer(SourceAssetSerializer):
class Meta:
model = PicnicGroves
fields = ('identifier', 'name', 'geom', 'source')
geo_field = 'geom'

identifier = serializers.CharField(source='fpd_uid')
name = serializers.SerializerMethodField(source='poi_info__nameid')
geom = GeometrySerializerMethodField()

def get_geom(self, obj):
return obj.geom.transform(4326, clone=True)

def get_name(self, obj):
return obj.poi_info.nameid.name

class ParkingLotsSerializer(SourceAssetSerializer):
class Meta:
model = PoiInfo # Need to use PoiInfo.name and PoiInfo.fpd_uid to lookup ParkingLots
fields = ('identifier', 'name', 'geom', 'source')
geo_field = 'geom'

identifier = serializers.SerializerMethodField()
name = serializers.SerializerMethodField()
geom = GeometrySerializerMethodField()

def get_geom(self, obj):
return ParkingLots.objects.get(id=obj.parking_info.lot_id).geom.transform(4326, clone=True)

def get_name(self, obj):
return obj.nameid.name

def get_identifier(self, obj):
return obj.fpd_uid
29 changes: 29 additions & 0 deletions asset_dashboard/static/js/components/map_utils/Popup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react'

function Popup({ feature }) {
const properties = feature.properties
return (
<>
{properties &&
<div>
<h6>Asset Info</h6>
<ul className='list-group list-group-flush'>
<li className='list-group-item'>
<strong>ID:</strong> {properties.asset_id ? properties.asset_id : properties.identifier }
</li>
<li className='list-group-item'>
<strong>Name:</strong> {properties.asset_name ? properties.asset_name : properties.name }
</li>
{properties.asset_type
&& <li className='list-group-item'>
<strong>Asset Type:</strong> {properties.asset_type}
</li>
}
</ul>
</div>
}
</>
)
}

export default Popup
12 changes: 12 additions & 0 deletions asset_dashboard/static/js/components/map_utils/circleMarker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default function circleMarker (feature, latlng) {
return L.circleMarker(
latlng,
{
radius: 7,
fillColor: 'green',
weight: 1,
opacity: 1,
fillOpacity: 1
}
)
}
8 changes: 7 additions & 1 deletion asset_dashboard/static/js/components/maps/ListAssetsMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React, { useEffect } from 'react'
import BaseMap from './BaseMap'
import { GeoJSON } from 'react-leaflet'
import zoomToExistingGeometries from '../map_utils/zoomToExistingGeometries'
import circleMarker from '../map_utils/circleMarker'

function ListAssetsMap(props) {

Expand All @@ -18,7 +19,12 @@ function ListAssetsMap(props) {
zoom={11}
whenCreated={onMapCreated}>
{
props?.assets && <GeoJSON data={props.assets} style={{color: 'green'}} />
props?.assets &&
<GeoJSON
data={props.assets}
style={{color: 'green'}}
pointToLayer={circleMarker}
/>
}
</BaseMap>
</div>
Expand Down
Loading

0 comments on commit bafa250

Please sign in to comment.