Skip to content

Commit

Permalink
Merge pull request #82 from azavea/rm/add-submissions-data-models
Browse files Browse the repository at this point in the history
Create Remaining Initial Django Models
  • Loading branch information
rachelekm committed Sep 30, 2022
2 parents 246402f + 2b771db commit bb2faf0
Show file tree
Hide file tree
Showing 20 changed files with 494 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -20,6 +20,7 @@ deployment/ansible/roles/azavea.*

# Django
/src/django/static/
/src/django/data/

# JS
node_modules/
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add utility model and test development users [#73](https://github.com/azavea/iow-boundary-tool/pull/73)
- Add login interface [#77](https://github.com/azavea/iow-boundary-tool/pull/77)
- Add reset password functionality [#79](https://github.com/azavea/iow-boundary-tool/pull/79)
- Create Remaining Initial Django Models [#82](https://github.com/azavea/iow-boundary-tool/pull/82)

### Changed

Expand Down
96 changes: 96 additions & 0 deletions doc/arch/adr-001-data-models.md
@@ -0,0 +1,96 @@
# IOW Entity Relationship Diagram

## Context

Below is the Entity Relationship Diagram illustrating the initial data model as discussed in Issue #69. Not included in the diagram at the time of creation are the file fields for `ReferenceImage` and `Submission` that would reference those respective file S3 uploads (to be implemented in Issue #81).

```mermaid
erDiagram
Role {
string description
}
User }o--o{ Utility : "has"
User }o--|{ State : "within"
User }o--|| Role : is
User {
Role role FK
email email
boolean is_staff
boolean is_active
date date_joined
password password
boolean is_locked_out
phone phone
State state FK
Utility utilities
}
Utility }o--|| State : "within"
Utility {
string pwsid
string name
address address
point location
State state FK
}
State {
string id "max_length=2"
string name
MultiPolygon shape
json options
}
Boundary }o--|| Utility : "for"
Boundary {
Utility utility FK
date archived_at
}
Submission }o--|| User : "by"
Submission }|--|| Boundary: "for"
Submission {
Boundary boundary FK
Polygon shape
date created_at
User created_by FK
date submitted_at
User submitted_by FK
date updated_at
string upload_filename
date upload_edited_at
text notes
}
Review }o--|| User : "by"
Review |o--|| Submission : "for"
Review {
User reviewed_by FK
Submission submission FK
date created_at
date reviewed_at
text notes
}
Annotation }o--|| Review : "of"
Annotation {
Review review FK
point location
text comment
date created_at
date resolved_at
}
Approval }o--|| User : "by"
Approval |o--|| Submission : "for"
Approval {
User approved_by FK
Submission submission FK
date approved_at
}
Reference_Image }o--|| Boundary : "for"
Reference_Image }o--|| User : "by"
Reference_Image {
string filename
User uploaded_by FK
Boundary boundary FK
date uploaded_at
boolean is_visible
json distortion
}
```
25 changes: 25 additions & 0 deletions scripts/fetch-data
Expand Up @@ -10,13 +10,26 @@ MUNICIPAL_BOUNDARIES_DATA_LOCATION="https://linc.osbm.nc.gov/explore/dataset/mun
MUNICIPAL_BOUNDARIES_FILENAME=muni.geo.json
DATA_DIR=src/app/public/data

DEFAULT_STATE_NC_GEOJSON_URL="https://public.opendatasoft.com/explore/dataset/us-state-boundaries/download/?format=geojson&refine.name=North+Carolina&timezone=America/New_York&lang=en"
NC_GEOJSON_FILENAME=NC_BOUNDARY.geojson
DJANGO_DATA_DIR=src/django/data

function usage() {
echo -n \
"Usage: $(basename "$0")
Execute Yarn CLI commands.
"
}

function fetchDefaultDBData() {
if [ ! -d "$DJANGO_DATA_DIR" ]; then
mkdir -p "$DJANGO_DATA_DIR"
fi

echo "Fetching default state boundary geojson data from: $DEFAULT_STATE_NC_GEOJSON_URL"
curl "${DEFAULT_STATE_NC_GEOJSON_URL}" -o "$DJANGO_DATA_DIR/$NC_GEOJSON_FILENAME"
}

function fetchData() {
if [ ! -d "$DATA_DIR" ]; then
mkdir -p "$DATA_DIR"
Expand All @@ -41,5 +54,17 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
else
fetchData
fi

if [[ -f "$DJANGO_DATA_DIR/$DEFAULT_STATE_NC_GEOJSON_URL" ]]; then
if [ "${1:-}" = "--force" ]; then
echo "Warning: forcing data refetching!"
rm "$DJANGO_DATA_DIR/$NC_GEOJSON_FILENAME"
fetchDefaultDBData
else
echo "Skipping default django data fetching: file exists."
fi
else
fetchDefaultDBData
fi
fi
fi
1 change: 1 addition & 0 deletions scripts/setup
Expand Up @@ -24,6 +24,7 @@ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
export IOW_BOUNDARY_TOOL_SETTINGS_BUCKET=iow-boundary-tool-development-config-us-east-1

./scripts/bootstrap
./scripts/fetch-data
./scripts/update
./scripts/resetdb
else
Expand Down
17 changes: 16 additions & 1 deletion src/django/api/admin.py
Expand Up @@ -2,6 +2,10 @@
from django.contrib.auth.admin import UserAdmin
from rest_framework.authtoken.models import TokenProxy
from .models.user import User, Utility
from .models.state import State
from .models.boundary import Boundary
from .models.submission import Submission, Approval, Review, Annotation
from .models.reference_image import ReferenceImage


class EmailAsUsernameUserAdmin(UserAdmin):
Expand All @@ -10,7 +14,7 @@ class EmailAsUsernameUserAdmin(UserAdmin):
ordering = ("email",)

fieldsets = (
(None, {"fields": ("email", "password", "role", "utilities")}),
(None, {"fields": ("email", "password", "role", "utilities", "states")}),
(
"Permissions",
{
Expand Down Expand Up @@ -39,6 +43,17 @@ class EmailAsUsernameUserAdmin(UserAdmin):
)


submission_stage_models = [
Boundary,
ReferenceImage,
Submission,
Review,
Approval,
Annotation,
]

admin.site.register(User, EmailAsUsernameUserAdmin)
admin.site.register(Utility)
admin.site.unregister(TokenProxy)
admin.site.register(State)
admin.site.register(submission_stage_models)
31 changes: 31 additions & 0 deletions src/django/api/migrations/0006_create_state.py
@@ -0,0 +1,31 @@
# Generated by Django 3.2.13 on 2022-09-29 22:41

import django.contrib.gis.db.models.fields
import django.contrib.postgres.fields.citext
from django.contrib.postgres.operations import CITextExtension
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('api', '0005_create_utility'),
]

operations = [
CITextExtension(),
migrations.CreateModel(
name='State',
fields=[
('id', django.contrib.postgres.fields.citext.CICharField(max_length=2, primary_key=True, serialize=False, unique=True)),
('name', models.CharField(max_length=127)),
('shape', django.contrib.gis.db.models.fields.MultiPolygonField(geography=True, srid=4326)),
('options', models.JSONField(blank=True, help_text='This JSON dictionary contains state-specific configuration.', null=True)),
],
),
migrations.AddField(
model_name='user',
name='states',
field=models.ManyToManyField(blank=True, related_name='users', to='api.State'),
),
]
30 changes: 30 additions & 0 deletions src/django/api/migrations/0007_add_default_state.py
@@ -0,0 +1,30 @@
# Generated by Django 3.2.13 on 2022-09-29 22:41

import os
import json
from django.conf import settings
from django.db import migrations
from django.contrib.gis.geos import GEOSGeometry, MultiPolygon

def add_default_state_nc(apps, schema_editor):
# Utility relies on a many-to-one relationship with State
# We should populate db with a State so we have ability to
# add upcoming state field on Utility with default FK id
State = apps.get_model('api', 'State')
with open(os.path.join(settings.BASE_DIR, 'data/NC_BOUNDARY.geojson'), 'r') as f:
NC_GEOJSON = json.load(f)
NC_POLYGON = GEOSGeometry(str(NC_GEOJSON['features'][0]['geometry']))
# Turn Polygon into type MultiPolygon to match shape field
NC_MULTIPOLYGON = MultiPolygon([NC_POLYGON])
NC_STATE = State(id="NC", name="North Carolina", shape=NC_MULTIPOLYGON)
NC_STATE.save()

class Migration(migrations.Migration):

dependencies = [
('api', '0006_create_state'),
]

operations = [
migrations.RunPython(add_default_state_nc, migrations.RunPython.noop),
]
19 changes: 19 additions & 0 deletions src/django/api/migrations/0008_add_utility_state_field.py
@@ -0,0 +1,19 @@
# Generated by Django 3.2.13 on 2022-09-29 22:50

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('api', '0007_add_default_state'),
]

operations = [
migrations.AddField(
model_name='utility',
name='state',
field=models.ForeignKey(default="NC", on_delete=django.db.models.deletion.PROTECT, to='api.state'),
),
]
25 changes: 25 additions & 0 deletions src/django/api/migrations/0009_create_boundary.py
@@ -0,0 +1,25 @@
# Generated by Django 3.2.13 on 2022-09-29 23:06

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('api', '0008_add_utility_state_field'),
]

operations = [
migrations.CreateModel(
name='Boundary',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('archived_at', models.DateTimeField(blank=True, null=True)),
('utility', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='api.utility')),
],
options={
'verbose_name_plural': 'boundaries',
},
),
]
44 changes: 44 additions & 0 deletions src/django/api/migrations/0010_create_submission_and_ref_image.py
@@ -0,0 +1,44 @@
# Generated by Django 3.2.13 on 2022-09-29 23:21

from django.conf import settings
import django.contrib.gis.db.models.fields
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('api', '0009_create_boundary'),
]

operations = [
migrations.CreateModel(
name='Submission',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('shape', django.contrib.gis.db.models.fields.PolygonField(geography=True, srid=4326)),
('created_at', models.DateTimeField(auto_now_add=True)),
('submitted_at', models.DateTimeField(blank=True, null=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('upload_filename', models.CharField(blank=True, max_length=255)),
('upload_edited_at', models.DateTimeField(blank=True, null=True)),
('notes', models.TextField(blank=True)),
('boundary', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='api.boundary')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='creator', to=settings.AUTH_USER_MODEL)),
('submitted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='submitter', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='ReferenceImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('filename', models.CharField(blank=True, max_length=255)),
('uploaded_at', models.DateTimeField()),
('is_visible', models.BooleanField(default=True)),
('distortion', models.JSONField(blank=True, null=True)),
('boundary', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='api.boundary')),
('uploaded_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
],
),
]
36 changes: 36 additions & 0 deletions src/django/api/migrations/0011_create_approval_and_review.py
@@ -0,0 +1,36 @@
# Generated by Django 3.2.13 on 2022-09-29 23:24

import api.models.submission
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('api', '0010_create_submission_and_ref_image'),
]

operations = [
migrations.CreateModel(
name='Review',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('reviewed_at', models.DateTimeField(blank=True, null=True)),
('notes', models.TextField(blank=True)),
('reviewed_by', models.ForeignKey(blank=True, limit_choices_to=api.models.submission.limit_by_validator_or_admin, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
('submission', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='api.submission')),
],
),
migrations.CreateModel(
name='Approval',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('approved_at', models.DateTimeField(auto_now_add=True)),
('approved_by', models.ForeignKey(limit_choices_to=api.models.submission.limit_by_validator_or_admin, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
('submission', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='api.submission')),
],
),
]

0 comments on commit bb2faf0

Please sign in to comment.