Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add boundary views and serializers #113

Merged
merged 6 commits into from
Oct 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ deployment/ansible/roles/azavea.*
# Django
/src/django/static/
/src/django/data/
/.venv

# JS
node_modules/
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add navigation bar and logout button [#109](https://github.com/azavea/iow-boundary-tool/pull/109)
- Style submission detail page [#104](https://github.com/azavea/iow-boundary-tool/pull/104)
- Add ADR for REST API [#115](https://github.com/azavea/iow-boundary-tool/pull/115)
- Add boundary list and detail views/serializers [#113](https://github.com/azavea/iow-boundary-tool/pull/113)

### Changed

Expand Down
75 changes: 68 additions & 7 deletions src/django/api/management/commands/resetdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
from api.models import Utility, User
from api.models.boundary import Boundary
from api.models.user import Roles
from api.models.submission import Approval, Submission
from api.models.state import State
from api.models.submission import Approval, Submission, Review, Annotation

from ..test_shapes import (
RALEIGH_FAKE_RECTANGLE,
RALEIGH_FAKE_TRIANGLE,
RALEIGH_FAKE_ZIGZAG,
POINT_IN_RALEIGH_FAKE_TRIANGLE,
)


Expand All @@ -22,7 +24,7 @@ def handle(self, *args, **options):
call_command("reset_schema", "--noinput")
call_command("migrate")

# Create test utility located in Raleigh.
# Create test utilities located in Raleigh.
mstone121 marked this conversation as resolved.
Show resolved Hide resolved
test_utility = Utility(pwsid="123456789", name="Azavea Test Utility")
test_utility.save()

Expand All @@ -32,12 +34,15 @@ def handle(self, *args, **options):
password="password",
has_admin_generated_password=False,
)

validator = User.objects.create_user(
email="v1@azavea.com",
password="password",
has_admin_generated_password=False,
role=Roles.VALIDATOR,
)
validator.states.add(State.objects.get(pk='NC'))

contributor = User.objects.create_user(
email="c1@azavea.com",
password="password",
Expand All @@ -46,21 +51,25 @@ def handle(self, *args, **options):
)
contributor.utilities.add(test_utility)

# Create test boundary and submissions.
# Create test boundaries and submissions.
# Use tz-aware datetimes to avoid warnings.
boundary = Boundary.objects.create(utility=test_utility)
boundary_1 = Boundary.objects.create(utility=test_utility)
boundary_2 = Boundary.objects.create(utility=test_utility)
boundary_3 = Boundary.objects.create(utility=test_utility)
boundary_4 = Boundary.objects.create(utility=test_utility)
boundary_5 = Boundary.objects.create(utility=test_utility)

# draft
Submission.objects.create(
boundary=boundary,
boundary=boundary_1,
shape=RALEIGH_FAKE_ZIGZAG,
created_at=datetime(2022, 10, 3, hour=15, tzinfo=timezone.utc),
created_by=contributor,
)

# submitted
Submission.objects.create(
boundary=boundary,
boundary=boundary_2,
shape=RALEIGH_FAKE_TRIANGLE,
created_at=datetime(2022, 10, 2, hour=11, tzinfo=timezone.utc),
created_by=contributor,
Expand All @@ -69,8 +78,60 @@ def handle(self, *args, **options):
notes="Notes for the test submission.",
)

# reviewing
submitted = Submission.objects.create(
boundary=boundary_3,
shape=RALEIGH_FAKE_TRIANGLE,
created_at=datetime(2022, 10, 2, hour=4, tzinfo=timezone.utc),
created_by=contributor,
submitted_at=datetime(2022, 10, 2, hour=6, tzinfo=timezone.utc),
submitted_by=contributor,
notes="Notes for the test submission.",
)

review = Review.objects.create(
submission=submitted,
reviewed_by=validator,
notes="Notes for the review.",
created_at=datetime(2022, 10, 2, hour=10, tzinfo=timezone.utc),
)

Annotation.objects.create(
review=review,
location=POINT_IN_RALEIGH_FAKE_TRIANGLE,
comment="Comment on review",
created_at=datetime(2022, 10, 2, hour=10, minute=5, tzinfo=timezone.utc),
)

# needs revision
submitted = Submission.objects.create(
boundary=boundary_4,
shape=RALEIGH_FAKE_TRIANGLE,
created_at=datetime(2022, 10, 2, hour=4, tzinfo=timezone.utc),
created_by=contributor,
submitted_at=datetime(2022, 10, 2, hour=6, tzinfo=timezone.utc),
submitted_by=contributor,
notes="Notes for the test submission.",
)

review = Review.objects.create(
submission=submitted,
reviewed_by=validator,
reviewed_at=datetime(2022, 10, 2, hour=11, tzinfo=timezone.utc),
notes="Notes for the review.",
created_at=datetime(2022, 10, 2, hour=10, tzinfo=timezone.utc),
)

Annotation.objects.create(
review=review,
location=POINT_IN_RALEIGH_FAKE_TRIANGLE,
comment="Comment on review",
created_at=datetime(2022, 10, 2, hour=10, minute=5, tzinfo=timezone.utc),
)

# approved
approved = Submission.objects.create(
boundary=boundary,
boundary=boundary_5,
shape=RALEIGH_FAKE_RECTANGLE,
created_at=datetime(2022, 10, 1, hour=8, tzinfo=timezone.utc),
created_by=contributor,
Expand Down
11 changes: 11 additions & 0 deletions src/django/api/management/test_shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,17 @@
}
"""
)
POINT_IN_RALEIGH_FAKE_TRIANGLE = GEOSGeometry(
"""
{
"type": "Point",
"coordinates": [
-78.70000000000000,
35.80000000000000
]
}
"""
)


RALEIGH_FAKE_ZIGZAG = GEOSGeometry(
Expand Down
49 changes: 49 additions & 0 deletions src/django/api/models/boundary.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
from enum import Enum
from django.utils.functional import cached_property
from django.db import models

from .utility import Utility

__all__ = ["Boundary"]


class BOUNDARY_STATUS(Enum):
DRAFT = "Draft"
SUBMITTED = "Submitted"
IN_REVIEW = "In Review"
NEEDS_REVISIONS = "Needs Revisions"
APPROVED = "Approved"


class Boundary(models.Model):
utility = models.ForeignKey(Utility, on_delete=models.PROTECT)
archived_at = models.DateTimeField(blank=True, null=True)
Expand All @@ -14,3 +24,42 @@ class Meta:

def __str__(self):
return f"{self.utility} Boundary"

@cached_property
def last_modified(self):
if self.status == BOUNDARY_STATUS.DRAFT:
return self.latest_submission.updated_at

if self.status == BOUNDARY_STATUS.SUBMITTED:
return self.latest_submission.submitted_at

if self.status == BOUNDARY_STATUS.IN_REVIEW:
return self.latest_submission.review.created_at

if self.status == BOUNDARY_STATUS.NEEDS_REVISIONS:
return self.latest_submission.review.reviewed_at

if self.status == BOUNDARY_STATUS.APPROVED:
return self.latest_submission.approval.approved_at

@cached_property
def status(self):
if self.latest_submission.submitted_at is None:
return BOUNDARY_STATUS.DRAFT

if hasattr(self.latest_submission, 'approval'):
return BOUNDARY_STATUS.APPROVED

if hasattr(self.latest_submission, 'review'):
review = self.latest_submission.review

if review.reviewed_at is None:
return BOUNDARY_STATUS.IN_REVIEW

return BOUNDARY_STATUS.NEEDS_REVISIONS

return BOUNDARY_STATUS.SUBMITTED

@cached_property
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work caching this 👍

def latest_submission(self):
return self.submissions.latest()
23 changes: 19 additions & 4 deletions src/django/api/models/submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ def limit_by_validator_or_admin():


class Submission(models.Model):
boundary = models.ForeignKey(Boundary, on_delete=models.PROTECT)
boundary = models.ForeignKey(
Boundary, on_delete=models.PROTECT, related_name='submissions'
)
shape = gis_models.PolygonField(geography=True)
created_at = models.DateTimeField(auto_now_add=True)
created_by = models.ForeignKey(
Expand All @@ -28,6 +30,9 @@ class Submission(models.Model):
upload_edited_at = models.DateTimeField(null=True, blank=True)
notes = models.TextField(blank=True)

class Meta:
get_latest_by = 'created_at'

def clean(self):
if self.submitted_at is not None and self.submitted_by is None:
raise ValidationError("Must define User submitting.")
Expand All @@ -38,7 +43,9 @@ def __str__(self):


class Review(models.Model):
submission = models.ForeignKey(Submission, on_delete=models.PROTECT)
submission = models.OneToOneField(
Submission, on_delete=models.PROTECT, related_name='review'
)
reviewed_by = models.ForeignKey(
User,
on_delete=models.PROTECT,
Expand All @@ -57,19 +64,27 @@ def clean(self):


class Approval(models.Model):
submission = models.ForeignKey(Submission, on_delete=models.PROTECT)
submission = models.OneToOneField(
Submission, on_delete=models.PROTECT, related_name='approval'
)
approved_at = models.DateTimeField(auto_now_add=True)
approved_by = models.ForeignKey(
User, on_delete=models.PROTECT, limit_choices_to=limit_by_validator_or_admin
)


class Annotation(models.Model):
review = models.ForeignKey(Review, on_delete=models.PROTECT)
review = models.ForeignKey(
Review, on_delete=models.PROTECT, related_name='annotations'
)
location = gis_models.PointField(geography=True)
comment = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
resolved_at = models.DateTimeField(null=True, blank=True)

def __str__(self):
return self.comment

@property
def resolved(self):
return self.resolved_at is not None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice shortcut

63 changes: 63 additions & 0 deletions src/django/api/serializers/boundary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from rest_framework.serializers import (
ModelSerializer,
CharField,
ChoiceField,
DateTimeField,
)

from ..models.boundary import Boundary, BOUNDARY_STATUS
from ..models.utility import Utility
from ..models.submission import Submission, Review, Annotation


class StatusField(ChoiceField):
def __init__(self):
super().__init__(choices=[status for status in BOUNDARY_STATUS])

def to_representation(self, status):
return status.value


class BoundaryListSerializer(ModelSerializer):
location = CharField(source='utility.name')
pwsid = CharField(source='utility.pwsid')
last_modified = DateTimeField()
status = StatusField()

class Meta:
model = Boundary
fields = ['id', 'location', 'pwsid', 'last_modified', 'status']


class BoundaryDetailSerializer(ModelSerializer):
class UtilitySerializer(ModelSerializer):
class Meta:
model = Utility
fields = ['name', 'pwsid']

class SubmissionSerializer(ModelSerializer):
class ReviewSerializer(ModelSerializer):
class AnnotationSerializer(ModelSerializer):
class Meta:
model = Annotation
fields = ['location', 'comment', 'resolved']

annotations = AnnotationSerializer(many=True)

class Meta:
model = Review
fields = ['annotations', 'notes']

review = ReviewSerializer(required=False)

class Meta:
model = Submission
fields = ['shape', 'notes', 'review']

utility = UtilitySerializer()
status = StatusField()
submission = SubmissionSerializer(source='latest_submission')

class Meta:
model = Boundary
fields = ['utility', 'status', 'submission']
11 changes: 8 additions & 3 deletions src/django/api/urls.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
from django.urls import include, path
from rest_framework.urlpatterns import format_suffix_patterns
from api import views

from .views import Login, Logout
from .views.boundary import BoundaryDetailView, BoundaryListView

urlpatterns = [
path("auth/login/", views.Login.as_view()),
path("auth/logout/", views.Logout.as_view()),
path("auth/login/", Login.as_view()),
path("auth/logout/", Logout.as_view()),
path("auth/", include("dj_rest_auth.urls")),
path("boundaries/", BoundaryListView.as_view()),
path("boundaries/<int:id>/", BoundaryDetailView.as_view()),
]

urlpatterns = format_suffix_patterns(urlpatterns)
Loading