Skip to content

Commit

Permalink
Add notes related to goals (#4532)
Browse files Browse the repository at this point in the history
* add year built to table

* prefer non-null values for cross cycle columns

* base for goal notes

* goal crd tested

* update tested

* refactor to only permit update

* attatch goal note to property on goal property filter and display

* permissioning

* backfill historical notes

* post save goal to create goal notes

* create historical notes on property creation

* historical note view

* precommit

* access historical notes and refactor ui grid edit cells

* precommit

* migration order

* migration order

* migration order

* update property retrieval

* precommit

* colors

---------

Co-authored-by: Hannah Eslinger <[email protected]>
  • Loading branch information
perryr16 and haneslinger authored Feb 23, 2024
1 parent da889df commit cb8c7af
Show file tree
Hide file tree
Showing 22 changed files with 539 additions and 22 deletions.
4 changes: 4 additions & 0 deletions seed/api/v3/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@
from seed.views.v3.filter_group import FilterGroupViewSet
from seed.views.v3.gbr_properties import GBRPropertyViewSet
from seed.views.v3.geocode import GeocodeViewSet
from seed.views.v3.goal_notes import GoalNoteViewSet
from seed.views.v3.goals import GoalViewSet
from seed.views.v3.green_assessment_properties import (
GreenAssessmentPropertyViewSet
)
from seed.views.v3.green_assessment_urls import GreenAssessmentURLViewSet
from seed.views.v3.green_assessments import GreenAssessmentViewSet
from seed.views.v3.historical_notes import HistoricalNoteViewSet
from seed.views.v3.import_files import ImportFileViewSet
from seed.views.v3.label_inventories import LabelInventoryViewSet
from seed.views.v3.labels import LabelViewSet
Expand Down Expand Up @@ -125,6 +127,8 @@
properties_router.register(r'notes', NoteViewSet, basename='property-notes')
properties_router.register(r'scenarios', PropertyScenarioViewSet, basename='property-scenarios')
properties_router.register(r'events', EventViewSet, basename='property-events')
properties_router.register(r'goal_notes', GoalNoteViewSet, basename='property-goal-notes')
properties_router.register(r'historical_notes', HistoricalNoteViewSet, basename='property-historical-notes')

# This is a third level router, so we need to register it with the second level router
meters_router = nested_routers.NestedSimpleRouter(properties_router, r'meters', lookup='meter')
Expand Down
26 changes: 26 additions & 0 deletions seed/migrations/0217_goalnote.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 3.2.23 on 2024-02-01 22:04

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


class Migration(migrations.Migration):

dependencies = [
('seed', '0216_goal'),
]

operations = [
migrations.CreateModel(
name='GoalNote',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('question', models.CharField(blank=True, choices=[('Is this a new construction or acquisition?', 'Is this a new construction or acquisition?'), ('Do you have data to report?', 'Do you have data to report?'), ('Is this value correct?', 'Is this value correct?'), ('Are these values correct?', 'Are these values correct?'), ('Other or multiple flags; explain in Additional Notes field', 'Other or multiple flags; explain in Additional Notes field')], max_length=1024, null=True)),
('resolution', models.CharField(blank=True, max_length=1024, null=True)),
('passed_checks', models.BooleanField(default=False)),
('new_or_acquired', models.BooleanField(default=False)),
('goal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='seed.goal')),
('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='seed.property')),
],
),
]
37 changes: 37 additions & 0 deletions seed/migrations/0218_historicalnote.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Generated by Django 3.2.23 on 2024-02-12 20:27

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


@transaction.atomic
def backfill_historical_notes(apps, schema_editor):
Property = apps.get_model("seed", "Property")
HistoricalNote = apps.get_model("seed", "HistoricalNote")

properties_to_update = Property.objects.filter(historical_note__isnull=True)

historical_notes_to_create = [
HistoricalNote(property=property, text='')
for property in properties_to_update
]
HistoricalNote.objects.bulk_create(historical_notes_to_create)


class Migration(migrations.Migration):

dependencies = [
('seed', '0217_goalnote'),
]

operations = [
migrations.CreateModel(
name='HistoricalNote',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('text', models.TextField(blank=True)),
('property', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='historical_note', to='seed.property')),
],
),
migrations.RunPython(backfill_historical_notes)
]
1 change: 1 addition & 0 deletions seed/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
from .ubid_models import * # noqa
from .uniformat import * # noqa
from .goals import * # noqa
from .goal_notes import * # noqa

from .certification import ( # noqa
GreenAssessment,
Expand Down
30 changes: 30 additions & 0 deletions seed/models/goal_notes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""
SEED Platform (TM), Copyright (c) Alliance for Sustainable Energy, LLC, and other contributors.
See also https://github.com/seed-platform/seed/main/LICENSE.md
"""
from django.db import models

from seed.models import Goal, Property


class GoalNote(models.Model):
QUESTION_CHOICES = (
('Is this a new construction or acquisition?', 'Is this a new construction or acquisition?'),
('Do you have data to report?', 'Do you have data to report?'),
('Is this value correct?', 'Is this value correct?'),
('Are these values correct?', 'Are these values correct?'),
('Other or multiple flags; explain in Additional Notes field', 'Other or multiple flags; explain in Additional Notes field'),
)

goal = models.ForeignKey(Goal, on_delete=models.CASCADE)
property = models.ForeignKey(Property, on_delete=models.CASCADE)

question = models.CharField(max_length=1024, choices=QUESTION_CHOICES, blank=True, null=True)
resolution = models.CharField(max_length=1024, blank=True, null=True)
passed_checks = models.BooleanField(default=False)
new_or_acquired = models.BooleanField(default=False)

def serialize(self):
from seed.serializers.goal_notes import GoalNoteSerializer
serializer = GoalNoteSerializer(self)
return serializer.data
45 changes: 44 additions & 1 deletion seed/models/goals.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,20 @@
SEED Platform (TM), Copyright (c) Alliance for Sustainable Energy, LLC, and other contributors.
See also https://github.com/seed-platform/seed/main/LICENSE.md
"""

from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import Q
from django.db.models.signals import post_save
from django.dispatch import receiver

from seed.models import AccessLevelInstance, Column, Cycle, Organization
from seed.models import (
AccessLevelInstance,
Column,
Cycle,
Organization,
Property
)


class Goal(models.Model):
Expand All @@ -28,3 +38,36 @@ def eui_columns(self):
""" Preferred column order """
eui_columns = [self.eui_column1, self.eui_column2, self.eui_column3]
return [column for column in eui_columns if column]

def properties(self):
properties = Property.objects.filter(
Q(views__cycle=self.baseline_cycle) |
Q(views__cycle=self.current_cycle),
access_level_instance__lft__gte=self.access_level_instance.lft,
access_level_instance__rgt__lte=self.access_level_instance.rgt
).distinct()

return properties


@receiver(post_save, sender=Goal)
def post_save_goal(sender, instance, **kwargs):
from seed.models import GoalNote

# retrieve a flat set of all property ids associated with this goal
goal_property_ids = set(instance.properties().values_list('id', flat=True))

# retrieve a flat set of all property ids from the previous goal (through goal note which has not been created/updated yet)
previous_property_ids = set(instance.goalnote_set.values_list('property_id', flat=True))

# create, or update has added more properties to the goal
new_property_ids = goal_property_ids - previous_property_ids
# update has removed properties from the goal
removed_property_ids = previous_property_ids - goal_property_ids

if new_property_ids:
new_goal_notes = [GoalNote(goal=instance, property_id=id) for id in new_property_ids]
GoalNote.objects.bulk_create(new_goal_notes)

if removed_property_ids:
GoalNote.objects.filter(goal=instance, property_id__in=removed_property_ids).delete()
12 changes: 11 additions & 1 deletion seed/models/notes.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from seed.landing.models import SEEDUser as User
from seed.lib.superperms.orgs.models import Organization
from seed.models import MAX_NAME_LENGTH, PropertyView, TaxLotView
from seed.models import MAX_NAME_LENGTH, Property, PropertyView, TaxLotView
from seed.utils.generic import obj_to_dict


Expand Down Expand Up @@ -108,3 +108,13 @@ def create_from_edit(self, user_id, view, new_values, previous_values):

def to_dict(self):
return obj_to_dict(self)


class HistoricalNote(models.Model):
text = models.TextField(blank=True)
property = models.OneToOneField(Property, on_delete=models.CASCADE, related_name='historical_note')

def serialize(self):
from seed.serializers.historical_notes import HistoricalNoteSerializer
serializer = HistoricalNoteSerializer(self)
return serializer.data
7 changes: 7 additions & 0 deletions seed/models/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,13 @@ def set_default_access_level_instance(sender, instance, **kwargs):
raise ValidationError("cannot change property's ALI to Ali different that related taxlots.")


@receiver(post_save, sender=Property)
def post_save_property(sender, instance, created, **kwargs):
if created:
from seed.models import HistoricalNote
HistoricalNote.objects.get_or_create(property=instance)


class PropertyState(models.Model):
"""Store a single property. This contains all the state information about the property
Expand Down
7 changes: 7 additions & 0 deletions seed/models/tax_lot_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ def serialize(
show_columns: Optional[list[int]],
columns_from_database: list[dict],
include_related: bool = True,
goal_id: int = False,
) -> list[dict]:
"""
This method takes a list of TaxLotViews or PropertyViews and returns the data along
Expand Down Expand Up @@ -269,6 +270,12 @@ def serialize(
if obj_dict.get('measures'):
del obj_dict['measures']

# add goal note data
if goal_id:
goal_note = obj.property.goalnote_set.filter(goal=goal_id).first()
obj_dict['goal_note'] = goal_note.serialize() if goal_note else None
obj_dict['historical_note'] = obj.property.historical_note.serialize()

results.append(obj_dict)

return results
Expand Down
14 changes: 14 additions & 0 deletions seed/serializers/goal_notes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""
SEED Platform (TM), Copyright (c) Alliance for Sustainable Energy, LLC, and other contributors.
See also https://github.com/seed-platform/seed/main/LICENSE.md
"""
from rest_framework import serializers

from seed.models import GoalNote


class GoalNoteSerializer(serializers.ModelSerializer):

class Meta:
model = GoalNote
fields = '__all__'
14 changes: 14 additions & 0 deletions seed/serializers/historical_notes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""
SEED Platform (TM), Copyright (c) Alliance for Sustainable Energy, LLC, and other contributors.
See also https://github.com/seed-platform/seed/main/LICENSE.md
"""
from rest_framework import serializers

from seed.models import HistoricalNote


class HistoricalNoteSerializer(serializers.ModelSerializer):

class Meta:
model = HistoricalNote
fields = '__all__'
Loading

0 comments on commit cb8c7af

Please sign in to comment.