diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b8da9361..85ccfff52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,38 @@ +### 0.10.1 (Minor Release) + +#### Plugin API end points can now override application end points + +A change to the order that APIs are registered with Django Rest Framework allows +plugins to now override the core Opal application APIs. + +#### Fonts are now locally sourced + +Fonts are now served from Opal's static assets rather than from the Google CDN. + +#### print/screen stylesheets have been collapsed into opal.css + +Print/screen differences are now in opal.css with media tags. + +#### Google Analytics is now deferred + +The loading in of Google Analytics is now deferred to the bottom of the body +tag to allow the page to load without waiting on analytics scripts to load. + +#### Scaffold version control failures + +The `startplugin` and `startproject` commands initialize a git repository by +default. If we (The `subprocess` module) cannot find the `git` command, we now +continue with a message printed to screen rather than raising an exception. + +#### Episode.objects.serialised now uses select_related + +`ForeignKeyOrFreeText` fields now have their ForeignKey items preselected when +we use `Episode.objects.serialised`. This provides a speed boost for applications +with moderately heavy `ForeignKeyOrFreeText` usage. + +(Approx 30-40% in our tests.) + + ### 0.10.0 (Major Release) This is a major release with breaking changes from upstream dependencies. diff --git a/README.md b/README.md index 27f1ceb7a..ca31fe381 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ Opal ==== -[![Build Status](https://travis-ci.org/openhealthcare/opal.svg?branch=v0.10.0)](https://travis-ci.org/openhealthcare/opal) -[![Coverage Status](https://coveralls.io/repos/github/openhealthcare/opal/badge.svg?branch=v0.10.0)](https://coveralls.io/github/openhealthcare/opal?branch=v0.10.0) +[![Build Status](https://travis-ci.org/openhealthcare/opal.svg?branch=v0.10.1)](https://travis-ci.org/openhealthcare/opal) +[![Coverage Status](https://coveralls.io/repos/github/openhealthcare/opal/badge.svg?branch=v0.10.1)](https://coveralls.io/github/openhealthcare/opal?branch=v0.10.1) [![PyPI version](https://badge.fury.io/py/opal.svg)](https://badge.fury.io/py/opal) diff --git a/doc/docs/reference/upgrading.md b/doc/docs/reference/upgrading.md index 1be28911c..d14b45324 100644 --- a/doc/docs/reference/upgrading.md +++ b/doc/docs/reference/upgrading.md @@ -3,7 +3,21 @@ This document provides instructions for specific steps required to upgrading your Opal application to a later version where there are extra steps required. -### 0.9.1 -> 0.10.0 + +### 0.10.0 -> 0.10.1 + +#### Upgrading Opal + +How you do this depends on how you have configured your application, but updating your +requirements.txt to update the version should work. + + # requirements.txt + opal==0.10.1 + +There are no migrations or additional commands for this upgrae, and we are not aware of +any backwards incompatible changes. + +### 0.9.0 -> 0.10.0 #### Upgrading Opal @@ -103,21 +117,6 @@ logs into second will throw a CSRF failure because Django invalidates CSRF tokens on login. -### 0.9.0 -> 0.9.1 - -#### Upgrading Opal - -How you do this depends on how you have configured your application, but updating your -requirements.txt to update the version should work. - - # requirements.txt - opal==0.9.1 - -After re-installing (via for instance `pip install -r requirements.txt`) you will need to -run the migrations for Opal 0.9.1 - - $ python manage.py migrate opal - ### 0.8.3 -> 0.9.0 `episode.date_of_episode`, `episode.date_of_admission` and `episode.discharge_date` are all deprecated. diff --git a/doc/mkdocs.yml b/doc/mkdocs.yml index 9aea5c071..57ff76b1c 100644 --- a/doc/mkdocs.yml +++ b/doc/mkdocs.yml @@ -117,7 +117,7 @@ dev_addr: 0.0.0.0:8965 include_next_prev: false extra: - version: v0.10.0 + version: v0.10.1 markdown_extensions: - fenced_code diff --git a/opal/_version.py b/opal/_version.py index 71d8e4615..c4bf81830 100644 --- a/opal/_version.py +++ b/opal/_version.py @@ -1,4 +1,4 @@ """ Declare our current version string """ -__version__ = '0.10.0' +__version__ = '0.10.1.rc1' diff --git a/opal/core/api.py b/opal/core/api.py index 47fff4a90..6ba20ab14 100644 --- a/opal/core/api.py +++ b/opal/core/api.py @@ -390,32 +390,39 @@ def retrieve(self, request, pk=None): return json_response(patientlist.to_dict(request.user)) -router.register('patient', PatientViewSet) -router.register('episode', EpisodeViewSet) -router.register('record', RecordViewSet) -router.register('userprofile', UserProfileViewSet) -router.register('user', UserViewSet) -router.register('tagging', TaggingViewSet) -router.register('patientlist', PatientListViewSet) -router.register('patientrecordaccess', PatientRecordAccessViewSet) +def register_plugin_apis(): + for plugin in plugins.OpalPlugin.list(): + for api in plugin.get_apis(): + router.register(*api) -router.register('referencedata', ReferenceDataViewSet) -router.register('metadata', MetadataViewSet) -for subrecord in subrecords(): - sub_name = subrecord.get_api_name() +def register_subrecords(): + for subrecord in subrecords(): + sub_name = subrecord.get_api_name() - class SubViewSet(SubrecordViewSet): - base_name = sub_name - model = subrecord + class SubViewSet(SubrecordViewSet): + base_name = sub_name + model = subrecord - router.register(sub_name, SubViewSet) + router.register(sub_name, SubViewSet) -def register_plugin_apis(): - for plugin in plugins.OpalPlugin.list(): - for api in plugin.get_apis(): - router.register(*api) +def initialize_router(): + # plugin apis get initialised first, so that plugins + # can + register_plugin_apis() + router.register('patient', PatientViewSet) + router.register('episode', EpisodeViewSet) + router.register('record', RecordViewSet) + router.register('userprofile', UserProfileViewSet) + router.register('user', UserViewSet) + router.register('tagging', TaggingViewSet) + router.register('patientlist', PatientListViewSet) + router.register('patientrecordaccess', PatientRecordAccessViewSet) + + router.register('referencedata', ReferenceDataViewSet) + router.register('metadata', MetadataViewSet) + register_subrecords() -register_plugin_apis() +initialize_router() diff --git a/opal/core/scaffold.py b/opal/core/scaffold.py index 815dc9d0b..4c72b4d05 100644 --- a/opal/core/scaffold.py +++ b/opal/core/scaffold.py @@ -2,9 +2,11 @@ Opal scaffolding and code generation """ import inspect +import errno import os import subprocess import sys + from django.core import management from django.utils.crypto import get_random_string from django.apps import apps @@ -57,6 +59,9 @@ def create_lookuplists(root_dir): def call(cmd, **kwargs): + """ + Call an external program in a subprocess + """ write("Calling: {}".format(' '.join(cmd))) try: subprocess.check_call(cmd, **kwargs) @@ -65,6 +70,26 @@ def call(cmd, **kwargs): sys.exit(1) +def call_if_exists(cmd, failure_message, **kwargs): + """ + Call an external program in a subprocess if it exists. + + Returns True. + + If it does not exist, write a failure message and return False + without raising an exception + """ + try: + call(cmd, **kwargs) + return True + except OSError as e: + if e.errno == errno.ENOENT: + write(failure_message) + return False + else: + raise + + def start_plugin(name, USERLAND): name = name @@ -103,7 +128,11 @@ def start_plugin(name, USERLAND): services = jsdir/'services' services.mkdir() # 5. Initialize git repo - call(('git', 'init'), cwd=root, stdout=subprocess.PIPE) + call_if_exists( + ('git', 'init'), + 'Unable to locate git; Skipping git repository initialization.', + cwd=root, stdout=subprocess.PIPE + ) write('Plugin complete at {0}'.format(reponame)) return @@ -252,7 +281,11 @@ def manage(command): manage('createopalsuperuser') # 10. Initialise git repo - call(('git', 'init'), cwd=project_dir, stdout=subprocess.PIPE) + call_if_exists( + ('git', 'init'), + 'Unable to locate git; Skipping git repository initialization.', + cwd=project_dir, stdout=subprocess.PIPE + ) # 11. Load referencedata shipped with Opal manage('load_lookup_lists') diff --git a/opal/managers.py b/opal/managers.py index 8baf4a081..05e428f62 100644 --- a/opal/managers.py +++ b/opal/managers.py @@ -10,6 +10,7 @@ from opal.core.subrecords import ( episode_subrecords, patient_subrecords ) +from opal.core.fields import ForeignKeyOrFreeText from functools import reduce @@ -32,6 +33,23 @@ def search(self, some_query): return qs +def prefetch(qs): + """ + Given a Queryset QS, examine the model for `ForeignKeyOrFreetext` + fields or `ManyToMany` fields and add `select_related` or + `prefetch_related` calls to the queryset as appropriate to reduce + the total number of database queries required to serialise the + contents of the queryset. + """ + for name, value in list(vars(qs.model).items()): + if isinstance(value, ForeignKeyOrFreeText): + qs = qs.select_related(value.fk_field_name) + + for related in qs.model._meta.many_to_many: + qs = qs.prefetch_related(related.attname) + return qs + + class EpisodeQueryset(models.QuerySet): def search(self, some_query): @@ -51,7 +69,9 @@ def serialised_episode_subrecords(self, episodes, user): for model in episode_subrecords(): name = model.get_api_name() - subrecords = model.objects.filter(episode__in=episodes) + subrecords = prefetch( + model.objects.filter(episode__in=episodes) + ) for related in model._meta.many_to_many: subrecords = subrecords.prefetch_related(related.attname) @@ -74,7 +94,9 @@ def serialised(self, user, episodes, episode_subs = self.serialised_episode_subrecords(episodes, user) for model in patient_subrecords(): name = model.get_api_name() - subrecords = model.objects.filter(patient__in=patient_ids) + subrecords = prefetch( + model.objects.filter(patient__in=patient_ids) + ) for sub in subrecords: patient_subs[sub.patient_id][name].append(sub.to_dict(user)) diff --git a/opal/static/css/opal.css b/opal/static/css/opal.css index 34afe77b7..2d759272d 100644 --- a/opal/static/css/opal.css +++ b/opal/static/css/opal.css @@ -1,3 +1,82 @@ +/* lato-300 - latin */ +@font-face { + font-family: 'Lato'; + font-style: normal; + font-weight: 300; + src: url('../fonts/lato-v14-latin-300.eot'); /* IE9 Compat Modes */ + src: local('Lato Light'), local('Lato-Light'), + url('../fonts/lato-v14-latin-300.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ + url('../fonts/lato-v14-latin-300.woff2') format('woff2'), /* Super Modern Browsers */ + url('../fonts/lato-v14-latin-300.woff') format('woff'), /* Modern Browsers */ + url('../fonts/lato-v14-latin-300.ttf') format('truetype'), /* Safari, Android, iOS */ + url('../fonts/lato-v14-latin-300.svg#Lato') format('svg'); /* Legacy iOS */ +} +/* lato-regular - latin */ +@font-face { + font-family: 'Lato'; + font-style: normal; + font-weight: 400; + src: url('../fonts/lato-v14-latin-regular.eot'); /* IE9 Compat Modes */ + src: local('Lato Regular'), local('Lato-Regular'), + url('../fonts/lato-v14-latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ + url('../fonts/lato-v14-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */ + url('../fonts/lato-v14-latin-regular.woff') format('woff'), /* Modern Browsers */ + url('../fonts/lato-v14-latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */ + url('../fonts/lato-v14-latin-regular.svg#Lato') format('svg'); /* Legacy iOS */ +} +/* lato-700 - latin */ +@font-face { + font-family: 'Lato'; + font-style: normal; + font-weight: 700; + src: url('../fonts/lato-v14-latin-700.eot'); /* IE9 Compat Modes */ + src: local('Lato Bold'), local('Lato-Bold'), + url('../fonts/lato-v14-latin-700.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ + url('../fonts/lato-v14-latin-700.woff2') format('woff2'), /* Super Modern Browsers */ + url('../fonts/lato-v14-latin-700.woff') format('woff'), /* Modern Browsers */ + url('../fonts/lato-v14-latin-700.ttf') format('truetype'), /* Safari, Android, iOS */ + url('../fonts/lato-v14-latin-700.svg#Lato') format('svg'); /* Legacy iOS */ +} + + +@media print{ + @page { + size: landscape; + } + .patient-list-container { + overflow: visible; + overflow-y: visible; + } + + li { + padding-top: 0px; + padding-bottom: 0px; + line-height: 12px; + } + + .screen-only { + display: none; + } + + /* + * it needs important because bootstrap + */ + .always-on-print-block{ + display: block !important; + } + + table { + font-size: 12px + } +} + +@media screen{ + .print-only { + display: none; + } +} + + /* Bootstrap resets */ html, .outer-container{ height: 100%; diff --git a/opal/static/css/print.css b/opal/static/css/print.css deleted file mode 100644 index fe081f6ab..000000000 --- a/opal/static/css/print.css +++ /dev/null @@ -1,30 +0,0 @@ -@media print{ - @page { - size: landscape; - } - .patient-list-container { - overflow: visible; - overflow-y: visible; - } -} - -li { - padding-top: 0px; - padding-bottom: 0px; - line-height: 12px; -} - -.screen-only { - display: none; -} - -/* -* it needs important because bootstrap -*/ -.always-on-print-block{ - display: block !important; -} - -table { - font-size: 12px -} diff --git a/opal/static/css/screen.css b/opal/static/css/screen.css deleted file mode 100644 index def51c29c..000000000 --- a/opal/static/css/screen.css +++ /dev/null @@ -1,3 +0,0 @@ -.print-only { - display: none; -} diff --git a/opal/static/fonts/lato-v14-latin-300.eot b/opal/static/fonts/lato-v14-latin-300.eot new file mode 100644 index 000000000..e8b79c348 Binary files /dev/null and b/opal/static/fonts/lato-v14-latin-300.eot differ diff --git a/opal/static/fonts/lato-v14-latin-300.svg b/opal/static/fonts/lato-v14-latin-300.svg new file mode 100644 index 000000000..11b626f87 --- /dev/null +++ b/opal/static/fonts/lato-v14-latin-300.svg @@ -0,0 +1,435 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opal/static/fonts/lato-v14-latin-300.ttf b/opal/static/fonts/lato-v14-latin-300.ttf new file mode 100644 index 000000000..f326d26c2 Binary files /dev/null and b/opal/static/fonts/lato-v14-latin-300.ttf differ diff --git a/opal/static/fonts/lato-v14-latin-300.woff b/opal/static/fonts/lato-v14-latin-300.woff new file mode 100644 index 000000000..ab45ab76b Binary files /dev/null and b/opal/static/fonts/lato-v14-latin-300.woff differ diff --git a/opal/static/fonts/lato-v14-latin-300.woff2 b/opal/static/fonts/lato-v14-latin-300.woff2 new file mode 100644 index 000000000..136337fdc Binary files /dev/null and b/opal/static/fonts/lato-v14-latin-300.woff2 differ diff --git a/opal/static/fonts/lato-v14-latin-700.eot b/opal/static/fonts/lato-v14-latin-700.eot new file mode 100644 index 000000000..9d8bfb614 Binary files /dev/null and b/opal/static/fonts/lato-v14-latin-700.eot differ diff --git a/opal/static/fonts/lato-v14-latin-700.svg b/opal/static/fonts/lato-v14-latin-700.svg new file mode 100644 index 000000000..077653d20 --- /dev/null +++ b/opal/static/fonts/lato-v14-latin-700.svg @@ -0,0 +1,438 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opal/static/fonts/lato-v14-latin-700.ttf b/opal/static/fonts/lato-v14-latin-700.ttf new file mode 100644 index 000000000..eeea013c5 Binary files /dev/null and b/opal/static/fonts/lato-v14-latin-700.ttf differ diff --git a/opal/static/fonts/lato-v14-latin-700.woff b/opal/static/fonts/lato-v14-latin-700.woff new file mode 100644 index 000000000..1d9d75bc6 Binary files /dev/null and b/opal/static/fonts/lato-v14-latin-700.woff differ diff --git a/opal/static/fonts/lato-v14-latin-700.woff2 b/opal/static/fonts/lato-v14-latin-700.woff2 new file mode 100644 index 000000000..d88f1af8c Binary files /dev/null and b/opal/static/fonts/lato-v14-latin-700.woff2 differ diff --git a/opal/static/fonts/lato-v14-latin-regular.eot b/opal/static/fonts/lato-v14-latin-regular.eot new file mode 100644 index 000000000..2400e1284 Binary files /dev/null and b/opal/static/fonts/lato-v14-latin-regular.eot differ diff --git a/opal/static/fonts/lato-v14-latin-regular.svg b/opal/static/fonts/lato-v14-latin-regular.svg new file mode 100644 index 000000000..55b43fb86 --- /dev/null +++ b/opal/static/fonts/lato-v14-latin-regular.svg @@ -0,0 +1,435 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opal/static/fonts/lato-v14-latin-regular.ttf b/opal/static/fonts/lato-v14-latin-regular.ttf new file mode 100644 index 000000000..fa245a8a8 Binary files /dev/null and b/opal/static/fonts/lato-v14-latin-regular.ttf differ diff --git a/opal/static/fonts/lato-v14-latin-regular.woff b/opal/static/fonts/lato-v14-latin-regular.woff new file mode 100644 index 000000000..97ab144d9 Binary files /dev/null and b/opal/static/fonts/lato-v14-latin-regular.woff differ diff --git a/opal/static/fonts/lato-v14-latin-regular.woff2 b/opal/static/fonts/lato-v14-latin-regular.woff2 new file mode 100644 index 000000000..b14c76cab Binary files /dev/null and b/opal/static/fonts/lato-v14-latin-regular.woff2 differ diff --git a/opal/templates/base.html b/opal/templates/base.html index febfad8d5..9bde9a908 100644 --- a/opal/templates/base.html +++ b/opal/templates/base.html @@ -21,8 +21,8 @@ LOG_OUT_DURATION: {{OPAL_LOG_OUT_DURATION}} } var version = '{{VERSION_NUMBER}}'; - - + + {% compress js %} {% core_javascripts 'opal.upstream.deps' %} {% block opal_js %} @@ -36,21 +36,6 @@ {% if OPAL_FLOW_SERVICE %}'{{ OPAL_FLOW_SERVICE }}'{% else %}null{% endif %} ); - - {% core_javascripts 'opal.utils' %} {% core_javascripts 'opal.services' %} @@ -68,26 +53,20 @@ {% endblock opal_js %} {% endcompress %} - - - + - + - - - - - {% compress css %} - - {% plugin_stylesheets %} - {% application_stylesheets %} - {% endcompress %} + {% compress css %} + + {% plugin_stylesheets %} + {% application_stylesheets %} + {% endcompress %} @@ -119,6 +98,22 @@

}); + + + diff --git a/opal/tests/test_api.py b/opal/tests/test_api.py index 2d4ae7591..a9c00835b 100644 --- a/opal/tests/test_api.py +++ b/opal/tests/test_api.py @@ -947,13 +947,34 @@ def test_retrieve_episodes_not_found(self): self.assertEqual(404, response.status_code) -class RegisterPluginsTestCase(OpalTestCase): +class RegisterTestCase(OpalTestCase): @patch('opal.core.api.plugins.OpalPlugin.list') - def test_register(self, plugins): + def test_register_plugins(self, plugins): mock_plugin = MagicMock(name='Mock Plugin') mock_plugin.get_apis.return_value = [('thingapi', None)] plugins.return_value = [mock_plugin] with patch.object(api.router, 'register') as register: api.register_plugin_apis() register.assert_called_with('thingapi', None) + + @patch("opal.core.api.router.register") + @patch("opal.core.api.subrecords") + def test_register_subrecords(self, subrecords, register): + subrecords.return_value = [HatWearer] + api.register_subrecords() + self.assertEqual(register.call_count, 1) + self.assertTrue(register.call_args[0][0][0], HatWearer.get_api_name()) + + @patch("opal.core.api.router.register") + @patch('opal.core.api.plugins.OpalPlugin.list') + def test_register_plugin_order(self, plugins, register): + # plugins should be registered first + mock_plugin = MagicMock(name='Mock Plugin') + mock_plugin.get_apis.return_value = [('thingapi', None)] + plugins.return_value = [mock_plugin] + api.initialize_router() + call_args_list = register.call_args_list + self.assertEqual( + register.call_args_list[0][0][0], "thingapi" + ) diff --git a/opal/tests/test_managers.py b/opal/tests/test_managers.py index 483a99ddf..0beede8b3 100644 --- a/opal/tests/test_managers.py +++ b/opal/tests/test_managers.py @@ -3,7 +3,71 @@ """ from opal.core.test import OpalTestCase from opal.models import Patient, Episode -from opal.tests.models import Hat, HatWearer, Dog, DogOwner +from opal.tests import models as test_models +from opal.managers import prefetch + + +class PrefetchTestCase(OpalTestCase): + def test_prefech_fk_or_ft(self): + _, episode_1 = self.new_patient_and_episode_please() + _, episode_2 = self.new_patient_and_episode_please() + test_models.Dog.objects.create( + name="Fido" + ) + test_models.Dog.objects.create( + name="Spot" + ) + hound_owner_1 = test_models.HoundOwner.objects.create( + episode=episode_1 + ) + hound_owner_1.dog = "Fido" + hound_owner_1.save() + hound_owner_2 = test_models.HoundOwner.objects.create( + episode=episode_2 + ) + hound_owner_2.dog = "Spot" + hound_owner_2.save() + qs = test_models.HoundOwner.objects.all() + + # testing without prefetch + with self.assertNumQueries(4): + self.assertEqual(qs[0].dog, "Fido") + self.assertEqual(qs[1].dog, "Spot") + + # testign with prefetch + with self.assertNumQueries(2): + qs = prefetch(qs) + self.assertEqual(qs[0].dog, "Fido") + self.assertEqual(qs[1].dog, "Spot") + + def test_many_to_many(self): + bowler = test_models.Hat.objects.create(name="Bowler") + _, episode_1 = self.new_patient_and_episode_please() + _, episode_2 = self.new_patient_and_episode_please() + hat_wearer_1 = test_models.HatWearer.objects.create( + episode=episode_1 + ) + hat_wearer_1.hats.add(bowler) + + hat_wearer_2 = test_models.HatWearer.objects.create( + episode=episode_2 + ) + hat_wearer_2.hats.add(bowler) + + qs = test_models.HatWearer.objects.all() + + # testing without prefetch + with self.assertNumQueries(3): + for i in qs: + for hat in i.hats.all(): + self.assertEqual(hat.name, "Bowler") + + with self.assertNumQueries(2): + qs = prefetch(qs) + for i in qs: + for hat in i.hats.all(): + self.assertEqual(hat.name, "Bowler") + class PatientManagerTestCase(OpalTestCase): def setUp(self): @@ -71,19 +135,19 @@ def setUp(self): self.episode = self.patient.create_episode() # make sure many to many serialisation goes as epected - top = Hat.objects.create(name="top") - hw = HatWearer.objects.create(episode=self.episode) + top = test_models.Hat.objects.create(name="top") + hw = test_models.HatWearer.objects.create(episode=self.episode) hw.hats.add(top) # make sure free text or foreign key serialisation goes as expected # for actual foriegn keys - Dog.objects.create(name="Jemima") - do = DogOwner.objects.create(episode=self.episode) + test_models.Dog.objects.create(name="Jemima") + do = test_models.DogOwner.objects.create(episode=self.episode) do.dog = "Jemima" do.save() # make sure it goes as expected for strings - DogOwner.objects.create(episode=self.episode, dog="Philip") + test_models.DogOwner.objects.create(episode=self.episode, dog="Philip") def test_search_returns_both_episodes(self): self.patient_1, self.episode_1_1 = self.new_patient_and_episode_please() diff --git a/opal/tests/test_scaffold.py b/opal/tests/test_scaffold.py index e9d4030e1..e2877f442 100644 --- a/opal/tests/test_scaffold.py +++ b/opal/tests/test_scaffold.py @@ -21,6 +21,62 @@ ) +@patch('subprocess.check_call') +class CallTestCase(OpalTestCase): + def test_writes_message(self, cc): + with patch.object(scaffold, 'write'): + scaffold.call(('yes', 'please')) + scaffold.write.assert_called_with('Calling: yes please') + + def test_calls_with_args(self, cc): + scaffold.call(('yes', 'please')) + cc.assert_called_with(('yes', 'please')) + + def test_exits_on_error(self, cc): + with patch.object(scaffold.sys, 'exit') as exiter: + cc.side_effect = subprocess.CalledProcessError(None, None) + scaffold.call(('oh' 'noes')) + exiter.assert_called_with(1) + + +@patch('opal.core.scaffold.call') +class CallIfExistsTestCase(OpalTestCase): + def test_success(self, c): + self.assertEqual( + True, + scaffold.call_if_exists(('hello', 'world'), 'Sorry, no greetings') + ) + + def test_file_not_found_err(self, c): + if getattr(__builtins__, 'FileNotFoundError', None): + c.side_effect = FileNotFoundError(2, os.strerror(2)) + with patch.object(scaffold, 'write'): + return_value = scaffold.call_if_exists( + ('hello', 'world'), + 'Sorry no greetings' + ) + self.assertEqual(False, return_value) + scaffold.write.assert_any_call('Sorry no greetings') + + def test_oserror(self, c): + c.side_effect = OSError(2, os.strerror(2)) + with patch.object(scaffold, 'write'): + return_value = scaffold.call_if_exists( + ('hello', 'world'), + 'Sorry no greetings' + ) + self.assertEqual(False, return_value) + scaffold.write.assert_any_call('Sorry no greetings') + + def test_other_oserror(self, c): + with self.assertRaises(OSError): + c.side_effect = OSError(3, os.strerror(3)) + scaffold.call_if_exists( + ('hello', 'world'), + 'No such process would be a weird error to get here' + ) + + @patch('subprocess.check_call') class StartpluginTestCase(OpalTestCase): def setUp(self): @@ -109,6 +165,7 @@ def test_creates_requirements(self, subpr): contents = r.read() self.assertIn('opal=={}'.format(opal.__version__), contents) + @patch('subprocess.check_call') @patch.object(scaffold.management, 'call_command') class StartprojectTestCase(OpalTestCase):