Skip to content
This repository has been archived by the owner on Jan 14, 2023. It is now read-only.

Adapt the app for Councilmatic 2.5 #34

Merged
merged 24 commits into from
Jul 31, 2019
Merged

Conversation

jeancochrane
Copy link

@jeancochrane jeancochrane commented Jul 23, 2019

Overview

Make sure the app is compatible with django-councilmatic v2.5. The major changes in this update include:

  • Support Django 2.0+
  • Refactor queries in send_notifications management command to use the ORM and the new OCD model structure
  • Write some basic tests for the app

Notes

Testing instructions

  • Make a virtualenv for the project and install it for testing with pip install -e .[tests]
  • Run pytest and confirm all tests pass

@jeancochrane jeancochrane changed the base branch from master to 2.5 July 23, 2019 18:24
@jeancochrane jeancochrane requested a review from hancush July 23, 2019 18:43
@hancush
Copy link
Member

hancush commented Jul 23, 2019

hm, @jeancochrane, i'm getting test failures, even after installing django-councilmatic as instructed:

(django-councilmatic-notifications) call-me-hank:django-councilmatic-notifications hannah$ pytest
================================================ test session starts ================================================
platform darwin -- Python 3.7.3, pytest-5.0.1, py-1.8.0, pluggy-0.12.0
Django settings: tests.test_config (from ini file)
rootdir: /Users/hannah/projects/django-councilmatic-notifications, inifile: setup.cfg
plugins: django-3.5.1, mock-1.10.4
collected 13 items

tests/test_management_commands.py .F.....F...                                                                 [ 84%]
tests/test_views.py ..                                                                                        [100%]

===================================================== FAILURES ======================================================
__________________________________ test_find_bill_action_updates_skips_null_dates ___________________________________

self = <django.db.backends.utils.CursorWrapper object at 0x10f9b6320>
sql = 'SELECT "opencivicdata_billaction"."id", "opencivicdata_billaction"."bill_id", "opencivicdata_billaction"."organizatio...D ("opencivicdata_billaction"."date")::timestamp with time zone >= %s) ORDER BY "opencivicdata_billaction"."order" ASC'
params = ('ocd-bill/19cf632a-8323-4c8f-afb3-9d5294b2e7cd', datetime.datetime(2019, 7, 23, 15, 7, 45, 542694, tzinfo=<DstTzInfo 'America/Chicago' CDT-1 day, 19:00:00 DST>))
ignored_wrapper_args = (False, {'connection': <django.contrib.gis.db.backends.postgis.base.DatabaseWrapper object at 0x10bf66e10>, 'cursor': <django.db.backends.utils.CursorWrapper object at 0x10f9b6320>})

    def _execute(self, sql, params, *ignored_wrapper_args):
        self.db.validate_no_broken_transaction()
        with self.db.wrap_database_errors:
            if params is None:
                return self.cursor.execute(sql)
            else:
>               return self.cursor.execute(sql, params)
E               psycopg2.errors.InvalidDatetimeFormat: invalid input syntax for type timestamp with time zone: ""

../../.virtualenvs/django-councilmatic-notifications/lib/python3.7/site-packages/django/db/backends/utils.py:85: InvalidDatetimeFormat

The above exception was the direct cause of the following exception:

new_bill = <Bill: test bill>
new_bill_actions = (<BillAction: test bill action on >, <BillAction: test bill action on >)

    @pytest.mark.django_db
    def test_find_bill_action_updates_skips_null_dates(new_bill, new_bill_actions):
        for bill_action in new_bill_actions:
            # Null dates are saved as empty strings in the OCD data model
            bill_action.date = ''
            bill_action.save()
        command = Command()
>       bill_action_updates = command.find_bill_action_updates([new_bill.id], minutes=15)

tests/test_management_commands.py:33:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
notifications/management/commands/send_notifications.py:209: in find_bill_action_updates
    for action in new_actions:
../../.virtualenvs/django-councilmatic-notifications/lib/python3.7/site-packages/django/db/models/query.py:268: in __iter__
    self._fetch_all()
../../.virtualenvs/django-councilmatic-notifications/lib/python3.7/site-packages/django/db/models/query.py:1186: in _fetch_all
    self._result_cache = list(self._iterable_class(self))
../../.virtualenvs/django-councilmatic-notifications/lib/python3.7/site-packages/django/db/models/query.py:54: in __iter__
    results = compiler.execute_sql(chunked_fetch=self.chunked_fetch, chunk_size=self.chunk_size)
../../.virtualenvs/django-councilmatic-notifications/lib/python3.7/site-packages/django/db/models/sql/compiler.py:1065: in execute_sql
    cursor.execute(sql, params)
../../.virtualenvs/django-councilmatic-notifications/lib/python3.7/site-packages/django/db/backends/utils.py:68: in execute
    return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)
../../.virtualenvs/django-councilmatic-notifications/lib/python3.7/site-packages/django/db/backends/utils.py:77: in _execute_with_wrappers
    return executor(sql, params, many, context)
../../.virtualenvs/django-councilmatic-notifications/lib/python3.7/site-packages/django/db/backends/utils.py:85: in _execute
    return self.cursor.execute(sql, params)
../../.virtualenvs/django-councilmatic-notifications/lib/python3.7/site-packages/django/db/utils.py:89: in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <django.db.backends.utils.CursorWrapper object at 0x10f9b6320>
sql = 'SELECT "opencivicdata_billaction"."id", "opencivicdata_billaction"."bill_id", "opencivicdata_billaction"."organizatio...D ("opencivicdata_billaction"."date")::timestamp with time zone >= %s) ORDER BY "opencivicdata_billaction"."order" ASC'
params = ('ocd-bill/19cf632a-8323-4c8f-afb3-9d5294b2e7cd', datetime.datetime(2019, 7, 23, 15, 7, 45, 542694, tzinfo=<DstTzInfo 'America/Chicago' CDT-1 day, 19:00:00 DST>))
ignored_wrapper_args = (False, {'connection': <django.contrib.gis.db.backends.postgis.base.DatabaseWrapper object at 0x10bf66e10>, 'cursor': <django.db.backends.utils.CursorWrapper object at 0x10f9b6320>})

    def _execute(self, sql, params, *ignored_wrapper_args):
        self.db.validate_no_broken_transaction()
        with self.db.wrap_database_errors:
            if params is None:
                return self.cursor.execute(sql)
            else:
>               return self.cursor.execute(sql, params)
E               django.db.utils.DataError: invalid input syntax for type timestamp with time zone: ""

../../.virtualenvs/django-councilmatic-notifications/lib/python3.7/site-packages/django/db/backends/utils.py:85: DataError
_______________________________________ test_find_new_events_skips_null_dates _______________________________________

self = <django.db.backends.utils.CursorWrapper object at 0x10f839d68>
sql = 'SELECT "opencivicdata_event"."created_at", "opencivicdata_event"."updated_at", "opencivicdata_event"."extras", "openc...created_at" >= %s AND ("opencivicdata_event"."start_date")::timestamp with time zone >= %s) ORDER BY "start_time" DESC'
params = (datetime.datetime(2019, 7, 23, 15, 7, 45, 892043, tzinfo=<DstTzInfo 'America/Chicago' CDT-1 day, 19:00:00 DST>), datetime.datetime(2019, 7, 23, 15, 22, 45, 892065, tzinfo=<DstTzInfo 'America/Chicago' CDT-1 day, 19:00:00 DST>))
ignored_wrapper_args = (False, {'connection': <django.contrib.gis.db.backends.postgis.base.DatabaseWrapper object at 0x10bf66e10>, 'cursor': <django.db.backends.utils.CursorWrapper object at 0x10f839d68>})

    def _execute(self, sql, params, *ignored_wrapper_args):
        self.db.validate_no_broken_transaction()
        with self.db.wrap_database_errors:
            if params is None:
                return self.cursor.execute(sql)
            else:
>               return self.cursor.execute(sql, params)
E               psycopg2.errors.InvalidDatetimeFormat: invalid input syntax for type timestamp with time zone: ""

../../.virtualenvs/django-councilmatic-notifications/lib/python3.7/site-packages/django/db/backends/utils.py:85: InvalidDatetimeFormat

The above exception was the direct cause of the following exception:

new_events = (<Event: test event 1 ()>, <Event: test event 2 ()>)

    @pytest.mark.django_db
    def test_find_new_events_skips_null_dates(new_events):
        for event in new_events:
            # Null dates are saved as empty strings in the OCD data model
            event.start_date = ''
            event.save()
        command = Command()
>       found_events = command.find_new_events()

tests/test_management_commands.py:121:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
notifications/management/commands/send_notifications.py:385: in find_new_events
    for event in new_events_q:
../../.virtualenvs/django-councilmatic-notifications/lib/python3.7/site-packages/django/db/models/query.py:268: in __iter__
    self._fetch_all()
../../.virtualenvs/django-councilmatic-notifications/lib/python3.7/site-packages/django/db/models/query.py:1186: in _fetch_all
    self._result_cache = list(self._iterable_class(self))
../../.virtualenvs/django-councilmatic-notifications/lib/python3.7/site-packages/django/db/models/query.py:54: in __iter__
    results = compiler.execute_sql(chunked_fetch=self.chunked_fetch, chunk_size=self.chunk_size)
../../.virtualenvs/django-councilmatic-notifications/lib/python3.7/site-packages/django/db/models/sql/compiler.py:1065: in execute_sql
    cursor.execute(sql, params)
../../.virtualenvs/django-councilmatic-notifications/lib/python3.7/site-packages/django/db/backends/utils.py:68: in execute
    return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)
../../.virtualenvs/django-councilmatic-notifications/lib/python3.7/site-packages/django/db/backends/utils.py:77: in _execute_with_wrappers
    return executor(sql, params, many, context)
../../.virtualenvs/django-councilmatic-notifications/lib/python3.7/site-packages/django/db/backends/utils.py:85: in _execute
    return self.cursor.execute(sql, params)
../../.virtualenvs/django-councilmatic-notifications/lib/python3.7/site-packages/django/db/utils.py:89: in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <django.db.backends.utils.CursorWrapper object at 0x10f839d68>
sql = 'SELECT "opencivicdata_event"."created_at", "opencivicdata_event"."updated_at", "opencivicdata_event"."extras", "openc...created_at" >= %s AND ("opencivicdata_event"."start_date")::timestamp with time zone >= %s) ORDER BY "start_time" DESC'
params = (datetime.datetime(2019, 7, 23, 15, 7, 45, 892043, tzinfo=<DstTzInfo 'America/Chicago' CDT-1 day, 19:00:00 DST>), datetime.datetime(2019, 7, 23, 15, 22, 45, 892065, tzinfo=<DstTzInfo 'America/Chicago' CDT-1 day, 19:00:00 DST>))
ignored_wrapper_args = (False, {'connection': <django.contrib.gis.db.backends.postgis.base.DatabaseWrapper object at 0x10bf66e10>, 'cursor': <django.db.backends.utils.CursorWrapper object at 0x10f839d68>})

    def _execute(self, sql, params, *ignored_wrapper_args):
        self.db.validate_no_broken_transaction()
        with self.db.wrap_database_errors:
            if params is None:
                return self.cursor.execute(sql)
            else:
>               return self.cursor.execute(sql, params)
E               django.db.utils.DataError: invalid input syntax for type timestamp with time zone: ""

../../.virtualenvs/django-councilmatic-notifications/lib/python3.7/site-packages/django/db/backends/utils.py:85: DataError
======================================= 2 failed, 11 passed in 20.69 seconds ========================================

@jeancochrane
Copy link
Author

@hancush Did you switch to the new branch (feature/jfc/councilmatic-2.5)? I removed the test test_find_bill_action_updates_skips_null_dates in this branch but it seems to be showing up in your traceback.

@hancush
Copy link
Member

hancush commented Jul 23, 2019

hhhhhmmmmmmmmmmmmm @ myself. thanks, @jeancochrane, you're right. sorry about that.

@jeancochrane
Copy link
Author

@hancush Since django-councilmatic now has a v2.5.0 release, I updated this PR to pin django-councilmatic>=2.5 and set the version of this app to 1.0.0. You should now be able to set up this env simply by running pip install -e .[tests].

Copy link
Member

@hancush hancush left a comment

Choose a reason for hiding this comment

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

this is great, @jeancochrane. thank you so much for this thoughtful refactor. left a few commends/questions inline.

notifications/management/commands/send_notifications.py Outdated Show resolved Hide resolved
from django.contrib.postgres.fields import JSONField
try:
HAYSTACK_URL = settings.HAYSTACK_CONNECTIONS['default']['URL']
except KeyError:
Copy link
Member

Choose a reason for hiding this comment

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

this is totally up to your judgment: do you think we should log a warning that solr is not configured if the app starts without this setting, either in addition to or instead of logging when the management command is sent?

Copy link
Author

Choose a reason for hiding this comment

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

I like that idea a lot, but do you have any sense of how we'd hook into the app start event to emit the warning? Not sure when that happens and I can't find anything from a cursory search.

Copy link
Member

Choose a reason for hiding this comment

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

i think that this will get evaluated when you start the app, no extra steps required. (added a print statement to the except block to test.)

(chi-councilmatic) call-me-hank:chi-councilmatic hannah$ python manage.py runserver
no haystack url

that said, as i was testing that assumption, i learned haystack itself will raise a fatal exception if you don't define the default connection, so maybe we don't need to do anything ourselves.

    raise ImproperlyConfigured("The default alias '%s' must be included in the HAYSTACK_CONNECTIONS setting." % DEFAULT_ALIAS)
django.core.exceptions.ImproperlyConfigured: The default alias 'default' must be included in the HAYSTACK_CONNECTIONS setting.

what do you think?

Copy link
Author

Choose a reason for hiding this comment

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

Nice! I think the built-in error is definitely more informative than ours anyway. The only open question to me is whether or not there's a use-case for running this app without search support. If so, it might make more sense to handle the error gently with a warning rather than raise a fatal error IMO. But if we imagine that wiring up Haystack is a necessary prerequisite for the app, then I think we can safely get rid of our error and rely on Haystack to complain.

Copy link
Member

Choose a reason for hiding this comment

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

haystack is a pre-req of django-councilmatic, so if we can't envision running notifications without django-councilmatic, then i think haystack is a necessary pre-req here. does that logic check out?

Copy link
Author

Choose a reason for hiding this comment

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

Makes sense to me 👍

Copy link
Member

Choose a reason for hiding this comment

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

i think we we landed on the app not being able to run without search, i.e., we can rely on haystack to raise the configuration errors.

is there any reason to keep the error handling in:

  • this check?
  • BillSearchSubscription.get_updates?
  • send_notifications?

(apologies if this is obvious, i may need some more caffeine.)

Copy link
Author

Choose a reason for hiding this comment

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

To me, these extra checks seem like reasonable defensive programming in the face of an uncertain integration. We expect Haystack to raise a configuration error if its configuration dictionary is not properly defined, but we also don't control Haystack or its startup logic, so we can't have perfect confidence that it will error out when we want it to. Plus, when we access HAYSTACK_URL, it's in a nested dictionary on the settings object, so if any part of settings.HAYSTACK_CONNECTIONS['default']['URL'] is not defined the user will get a super unhlpeful error. Does this judgment seem reasonable to you, or do you think it's overly confusing to be extra cautious around HAYSTACK_URL?

Copy link
Member

Choose a reason for hiding this comment

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

while it's true we don't control haystack, we do control the version of haystack we want to run, so we have a way of ensuring consistency if future releases of haystack behave differently than the one we're using now, which does raise the configuration error as we expect.

that said, i do find more useful exceptions a compelling reason to preserve the custom exception handling code. this isn't a change i require.

Copy link
Author

Choose a reason for hiding this comment

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

Cool 👍

notifications/models.py Show resolved Hide resolved
notifications/models.py Show resolved Hide resolved
Copy link
Author

@jeancochrane jeancochrane left a comment

Choose a reason for hiding this comment

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

Thanks for your comments @hancush! I followed up with a couple questions for you. For the ones that have immediate action steps, I'll go ahead and make the changes and then re-request your review.

from django.contrib.postgres.fields import JSONField
try:
HAYSTACK_URL = settings.HAYSTACK_CONNECTIONS['default']['URL']
except KeyError:
Copy link
Author

Choose a reason for hiding this comment

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

I like that idea a lot, but do you have any sense of how we'd hook into the app start event to emit the warning? Not sure when that happens and I can't find anything from a cursory search.

notifications/models.py Show resolved Hide resolved
notifications/management/commands/send_notifications.py Outdated Show resolved Hide resolved
Copy link
Author

@jeancochrane jeancochrane left a comment

Choose a reason for hiding this comment

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

@hancush I think I responded to all of your comments! Look like there's anything I missed?

Copy link
Member

@hancush hancush left a comment

Choose a reason for hiding this comment

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

although it was bigger than anticipated, this is a better refactor than i could have imagined! great work, @jeancochrane, and thank you. i left a couple of comments inline, but i'm happy to approve.

cursor.execute(new_bills, [ocd_ids])

bills = []
def _is_empty(self, elem):
Copy link
Member

Choose a reason for hiding this comment

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

does this lil guy need to stick around now that you've defined it locally?

Copy link
Author

Choose a reason for hiding this comment

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

Oops, great catch! Removed in f74e508.

from django.contrib.postgres.fields import JSONField
try:
HAYSTACK_URL = settings.HAYSTACK_CONNECTIONS['default']['URL']
except KeyError:
Copy link
Member

Choose a reason for hiding this comment

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

i think we we landed on the app not being able to run without search, i.e., we can rely on haystack to raise the configuration errors.

is there any reason to keep the error handling in:

  • this check?
  • BillSearchSubscription.get_updates?
  • send_notifications?

(apologies if this is obvious, i may need some more caffeine.)

@jeancochrane jeancochrane merged commit 7a28a3b into 2.5 Jul 31, 2019
@jeancochrane jeancochrane deleted the feature/jfc/councilmatic-2.5 branch July 31, 2019 16:43
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants