Skip to content

Commit

Permalink
Merge upstream v0.13.0
Browse files Browse the repository at this point in the history
  • Loading branch information
davidmiller committed Nov 2, 2018
2 parents e3cc92a + b13c275 commit f1c0d76
Show file tree
Hide file tree
Showing 17 changed files with 235 additions and 85 deletions.
14 changes: 14 additions & 0 deletions changelog.md
Expand Up @@ -6,6 +6,20 @@ Due to the upgrade to Django 2.x, Opal no longer supports Python 2.x.

Opal is now tested against Python 3.5, 3.6

#### Episode.active

The field `Episode.active` was previously implicitly set when calling `.set_tag_names()` to
something equivalent to the value of `bool(len(tag_names) > 0)`.

As of 0.13.0 the value of `Episode.active` is checked whenever `.save()` is called, prior
to the database call. The correct value is looked up via `Episode.category.is_active()`.

The default calculation of `.active` has also changed to be roughly equivalent to
` bool(self.episode.end is None)`.

Applications are now able to easily _change_ this behaviour by overriding the `.is_active`
method of the relevant `EpisodeCategory`.

#### Coding systems for lookuplists

Lookuplist entries may now have an associated coding system and code value stored against them.
Expand Down
43 changes: 23 additions & 20 deletions doc/custom_theme/base.html
Expand Up @@ -12,10 +12,16 @@

<title>{% if page_title %}{{ page_title }} - {% endif %}{{ site_name }}</title>

<link href="{{ base_url }}/css/bootstrap-custom.min.css" rel="stylesheet">
<link href="{{ base_url }}/css/font-awesome-4.0.3.css" rel="stylesheet">
<link rel="stylesheet" href="{{ base_url }}/css/highlight.css">
<link href="{{ base_url }}/css/base.css" rel="stylesheet">
<link href="{{ 'css/bootstrap-custom.min.css'|url }}" rel="stylesheet">
<link href="{{ 'css/font-awesome.min.css'|url }}" rel="stylesheet">
<link href="{{ 'css/base.css'|url }}" rel="stylesheet">
{%- if config.theme.highlightjs %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/{{ config.theme.hljs_style }}.min.css">
{%- endif %}
<!-- <link href="{{ base_url }}/css/bootstrap-custom.min.css" rel="stylesheet"> -->
<!-- <link href="{{ base_url }}/css/font-awesome-4.0.3.css" rel="stylesheet"> -->
<!-- <link rel="stylesheet" href="{{ base_url }}/css/highlight.css"> -->
<!-- <link href="{{ base_url }}/css/base.css" rel="stylesheet"> -->
{%- for path in extra_css %}
<link href="{{ path }}" rel="stylesheet">
{%- endfor %}
Expand All @@ -26,21 +32,9 @@
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
<![endif]-->

{% if google_analytics %}
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');

ga('create', '{{ google_analytics[0] }}', '{{ google_analytics[1] }}');
ga('send', 'pageview');
</script>
{% endif %}
</head>

<body>

{% include "nav.html" %}

<div class="container">
Expand All @@ -57,11 +51,20 @@
<a href="http://openhealthcare.org.uk" target="_blank">Open Health Care UK</i></a>.
</center>
</footer>
<script>
var base_url = {{ base_url | tojson }},
shortcuts = {{ config.theme.shortcuts | tojson }};
</script>

<script src="{{ base_url }}/js/jquery-1.10.2.min.js"></script>
<script src="{{ base_url }}/js/bootstrap-3.0.3.min.js"></script>
<script src="{{ base_url }}/js/highlight.pack.js"></script>
<script src="{{ base_url }}/js/base.js"></script>
<script src="{{ 'js/jquery-1.10.2.min.js'|url }}" defer></script>
<script src="{{ 'js/bootstrap-3.0.3.min.js'|url }}" defer></script>
{%- if config.theme.highlightjs %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js"></script>
{%- for lang in config.theme.hljs_languages %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/languages/{{lang}}.min.js"></script>
{%- endfor %}
<script>hljs.initHighlightingOnLoad();</script>
{%- endif %}
{%- for path in extra_javascript %}
<script src="{{ path }}"></script>
{%- endfor %}
Expand Down
1 change: 1 addition & 0 deletions doc/custom_theme/content.html
@@ -0,0 +1 @@
{{ page.content }}
2 changes: 1 addition & 1 deletion doc/custom_theme/nav.html
Expand Up @@ -35,7 +35,7 @@
<!-- </li> -->
{% else %}
<li {% if nav_item.active %}class="active"{% endif %}>
<a href="{{ nav_item.url }}">{{ nav_item.title }}</a>
<a href="{{ nav_item.url | url }}">{{ nav_item.title }}</a>
</li>
{% endif %}
{% endfor %}
Expand Down
2 changes: 1 addition & 1 deletion doc/custom_theme/toc.html
@@ -1,7 +1,7 @@
<div class="bs-sidebar hidden-print affix well" role="complementary">
<h3>Contents</h3>
<ul class="nav bs-sidenav">
{% for toc_item in toc %}
{% for toc_item in page.toc %}
<li class="main {% if toc_item.active %}active{% endif %}"><a href="{{ toc_item.url }}">{{ toc_item.title }}</a></li>
{% for toc_item in toc_item.children %}
<li><a href="{{ toc_item.url }}">{{ toc_item.title }}</a></li>
Expand Down
71 changes: 52 additions & 19 deletions doc/docs/guides/episodes.md
Expand Up @@ -54,25 +54,6 @@ class Application(application.OpalApplication):
default_episode_category = MyCategory.display_name
```

## Episode stages

An Episode will frequently consist of a number of possible stages. For instance,
for an inpatient episode, a patient will first be an inpatient, and then an
be discharged, with an optional interim follow up stage for inpatients who have been
discharged but requrie further follow up.

Opal stores the stage of an episode as a string in the `stage` property of an
`Episode`. The valid possible stages for a category are accessed from the
`get_stages` method of the category.

```
episode.category.get_stages()
# ['Inpatient', 'Followup', 'Discharged']
episode.category.has_stage('Followup')
# True
```

## Defining your own EpisodeCategory

As EpisodeCategory is a [discoverable](discoverable) we can define our own to
Expand All @@ -92,3 +73,55 @@ class DropInClinicEpisode(episodes.EpisodeCategory):
detail_template = "detail/drop_in.html"

```

## Episode.active

The field `.active` is used to distinguish Episodes which are ongoing. This field
is set implicitly by the `Episode.save()` method, and there will generally be no
need to set this field directly as part of application code.

Whether an Episode is considered active is determined by the `.is_active()` method
of the relevant EpisodeCategory.

The default implementation considers any Episode without an `.end` date to be active
and any Episode with one to be inactive.

Applications may customise this behaviour by overriding the `.is_active()` method.

For instance, to create a category which considered any Episode older than 2 weeks to
be inactive, one might override the method as follows:

```python
# yourapp/episode_categories.py
import datetime
from opal.core import episodes


class TwoWeeksAndStopCaringEpisode(episodes.EpisodeCategory):
def is_active(self):
delta = datetime.date.today() - self.episode.start
return bool(delta >= datetime.timedelta(days=14))

```

(Note that this would not alter the value in the database immediately after those 2
weeks, but would alter the value the next time the `Episode.save()` method was called.)

## Episode stages

An Episode will frequently consist of a number of possible stages. For instance,
for an inpatient episode, a patient will first be an inpatient, and then an
be discharged, with an optional interim follow up stage for inpatients who have been
discharged but requrie further follow up.

Opal stores the stage of an episode as a string in the `stage` property of an
`Episode`. The valid possible stages for a category are accessed from the
`get_stages` method of the category.

```
episode.category.get_stages()
# ['Inpatient', 'Followup', 'Discharged']
episode.category.has_stage('Followup')
# True
```
11 changes: 11 additions & 0 deletions doc/docs/reference/episode_categories.md
Expand Up @@ -65,6 +65,17 @@ InpatientEpisode(episode).has_stage('Inpatient')
# -> True
```

### EpisodeCategory.is_active()

Predicate function to determine whether this episode is active.

The default implementation looks to see whether an end date has
been set on the episode.

```python
InpatientEpisode(Episode()).is_active()
# -> True
```

### EpisodeCategory.set_stage(stage, user, data)

Expand Down
11 changes: 7 additions & 4 deletions doc/mkdocs.yml
@@ -1,5 +1,6 @@
site_name: Opal
docs_dir: docs

pages:
- Home: index.md
- installation.md
Expand Down Expand Up @@ -110,11 +111,13 @@ pages:



theme: mkdocs
theme: cerulean
theme: cosmo
theme_dir: 'custom_theme'
#theme: mkdocs
theme:
name: cosmo
custom_dir: 'custom_theme/'

extra_css:
- opaldocs.css

dev_addr: 0.0.0.0:8965
include_next_prev: false
Expand Down
6 changes: 3 additions & 3 deletions doc/requirements.txt
@@ -1,3 +1,3 @@
mkdocs-bootstrap==0.1.1
mkdocs-bootswatch==0.4.0
mkdocs==0.15.3
mkdocs-bootstrap==1.0.1
mkdocs-bootswatch==1.0
mkdocs==1.0.4
9 changes: 9 additions & 0 deletions opal/core/episodes.py
Expand Up @@ -50,6 +50,15 @@ def episode_visible_to(kls, episode, user):
def __init__(self, episode):
self.episode = episode

def is_active(self):
"""
Predicate function to determine whether this episode is active.
The default implementation looks to see whether an end date has
been set on the episode.
"""
return bool(self.episode.end is None)

def get_stages(self):
"""
Return the list of string stages for this category
Expand Down
4 changes: 4 additions & 0 deletions opal/core/exceptions.py
Expand Up @@ -39,5 +39,9 @@ class MissingTemplateError(Error):
pass


class UnexpectedEpisodeCategoryNameError(Error):
pass


class InvalidDataError(Error):
pass
58 changes: 42 additions & 16 deletions opal/models.py
Expand Up @@ -689,14 +689,42 @@ class Episode(UpdatesFromDictMixin, TrackedModel):

objects = managers.EpisodeQueryset.as_manager()

def __init__(self, *args, **kwargs):
super(Episode, self).__init__(*args, **kwargs)
self.__original_active = self.active

def __unicode__(self):
return 'Episode {0}: {1} - {2}'.format(
self.pk, self.start, self.end
)

def save(self, *args, **kwargs):
created = not bool(self.id)

current_active_value = self.active
category_active_value = self.category.is_active()

if current_active_value != category_active_value: # Disagreement
if current_active_value != self.__original_active:
# The value of self.active has been set by some code somewhere
# not by __init__() e.g. the original database value at the
# time of instance initalization.
#
# Rather than overriding this silently we should raise a
# ValueError.
msg = "Value of Episode.active has been set to {} but " \
"category.is_active() returns {}"
raise ValueError(
msg.format(current_active_value, category_active_value)
)

self.active = category_active_value
super(Episode, self).save(*args, **kwargs)

# Re-set this in case we changed it once post initialization and then
# the user subsequently saves this instance again
self.__original_active = self.active

if created:
for subclass in episode_subrecords():
if subclass._is_singleton:
Expand All @@ -705,10 +733,16 @@ def save(self, *args, **kwargs):
@property
def category(self):
from opal.core import episodes
category = episodes.EpisodeCategory.filter(
categories = episodes.EpisodeCategory.filter(
display_name=self.category_name
)[0]
return category(self)
)
if len(categories) == 0:
msg = "Unable to find EpisodeCategory for category name {0}"
msg = msg.format(self.category_name)
raise exceptions.UnexpectedEpisodeCategoryNameError(msg)
else:
category = categories[0]
return category(self)

def visible_to(self, user):
"""
Expand All @@ -731,20 +765,12 @@ def set_stage(self, stage, user, data):

def set_tag_names(self, tag_names, user):
"""
1. Set the episode.active status
2. Special case mine
3. Archive dangling tags not in our current list.
4. Add new tags.
5. Ensure that we're setting the parents of child tags
6. There is no step 6.
1. Special case mine
2. Archive dangling tags not in our current list.
3. Add new tags.
4. Ensure that we're setting the parents of child tags
5. There is no step 6.
"""
if len(tag_names) and not self.active:
self.active = True
self.save()
elif not len(tag_names) and self.active:
self.active = False
self.save()

if "mine" not in tag_names:
self.tagging_set.filter(user=user,
value='mine').update(archived=True)
Expand Down
13 changes: 12 additions & 1 deletion opal/tests/test_core_episodes.py
@@ -1,6 +1,8 @@
"""
Unittests for opal.core.episodes
"""
import datetime

from django.contrib.auth.models import User

from opal.core import test
Expand All @@ -17,7 +19,7 @@ def setUp(self):
)
self.patient = Patient.objects.create()
self.inpatient_episode = self.patient.create_episode(
category_name=episodes.InpatientEpisode
category_name=episodes.InpatientEpisode.display_name
)

def test_episode_categories(self):
Expand All @@ -42,6 +44,15 @@ def test_for_category(self):
self.assertEqual(episodes.InpatientEpisode,
episodes.EpisodeCategory.get('inpatient'))

def test_is_active(self):
category = episodes.InpatientEpisode(self.inpatient_episode)
self.assertTrue(category.is_active())

def test_is_active_end_date_set(self):
self.inpatient_episode.end = datetime.date.today()
category = episodes.InpatientEpisode(self.inpatient_episode)
self.assertFalse(category.is_active())

def test_get_stages(self):
stages = [
'Inpatient',
Expand Down

0 comments on commit f1c0d76

Please sign in to comment.