From a49480c8f310f286dc9c7c4b6fa56ac501b38ebd Mon Sep 17 00:00:00 2001 From: David Miller Date: Thu, 1 Nov 2018 15:53:34 +0000 Subject: [PATCH 01/25] Update for Django 1.11.16 compatibility. Fixes tests, removes runtests.py nomigrations, prevents use of Context() rather than a dict in search. --- opal/core/pathway/tests/test_template_tags.py | 11 +++++------ opal/core/search/extract.py | 2 +- runtests.py | 7 ------- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/opal/core/pathway/tests/test_template_tags.py b/opal/core/pathway/tests/test_template_tags.py index 0c5c42e7f..5f2745183 100644 --- a/opal/core/pathway/tests/test_template_tags.py +++ b/opal/core/pathway/tests/test_template_tags.py @@ -28,13 +28,12 @@ def test_global_template_context_not_changed(self, get_form_template): self.assertFalse(rendered.strip().endswith("editing.colour")) def test_nested_template_context(self, get_form_template): - template = Template('{% load pathways %}{% multisave models.Colour %}') - models = dict(models=dict(Colour=Colour), some_test_var="onions") - template.render(Context(models)) - self.assertEqual( - get_form_template.render.call_args[0][0]["some_test_var"], - 'onions' + template = Template( + '{% load pathways %}{% multisave models.Colour %}OMG: {{ some_test_var }}' ) + models = dict(models=dict(Colour=Colour), some_test_var="onions") + resp = template.render(Context(models)) + self.assertIn('OMG: onions', resp) def test_add_common_context(self, get_form_template): ctx = template_tags.add_common_context({}, Colour) diff --git a/opal/core/search/extract.py b/opal/core/search/extract.py index 6edf1280c..3b85de2dc 100644 --- a/opal/core/search/extract.py +++ b/opal/core/search/extract.py @@ -222,7 +222,7 @@ def get_data_dictionary(): def write_data_dictionary(file_name): dictionary = get_data_dictionary() t = loader.get_template("extract_data_schema.html") - ctx = Context(dict(schema=dictionary)) + ctx = dict(schema=dictionary) rendered = t.render(ctx) with open(file_name, "w") as f: f.write(rendered) diff --git a/runtests.py b/runtests.py index 58d369523..82d699e5f 100644 --- a/runtests.py +++ b/runtests.py @@ -59,13 +59,6 @@ 'opal.core.pathway.tests.pathway_test', 'opal.core.pathway', ), - MIGRATION_MODULES={ - 'auth': 'opal.nomigrations', - 'contenttypes': 'opal.nomigrations', - 'staticfiles': 'opal.nomigrations', - 'reversion': 'opal.nomigrations', - 'opal': 'opal.nomigrations' - }, TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', From 0d3f5acc3a5851872ee35e3ffaf7908222e96308 Mon Sep 17 00:00:00 2001 From: David Miller Date: Thu, 1 Nov 2018 16:23:33 +0000 Subject: [PATCH 02/25] Broad refactor without functionality impact: Silence warnings from Django2.1 removals after running python -Wd runtests.py --- opal/core/application.py | 2 +- opal/core/log.py | 2 +- opal/core/pathway/pathways.py | 2 +- opal/core/patient_lists.py | 2 +- opal/core/search/views.py | 10 ++++++--- opal/middleware.py | 2 +- opal/migrations/0001_initial.py | 16 +++++++------- opal/migrations/0003_auto_20150922_1825.py | 8 +++---- .../0009_glossolaliasubscription.py | 2 +- opal/migrations/0011_patientrecordaccess.py | 4 ++-- opal/migrations/0013_inpatientadmission.py | 6 ++--- opal/models.py | 22 +++++++++---------- opal/tests/models.py | 2 +- opal/tests/test_templatetags_forms.py | 4 ++-- opal/urls.py | 2 +- 15 files changed, 45 insertions(+), 41 deletions(-) diff --git a/opal/core/application.py b/opal/core/application.py index 7ab9bccb6..b324fc199 100644 --- a/opal/core/application.py +++ b/opal/core/application.py @@ -5,7 +5,7 @@ import itertools import os -from django.core.urlresolvers import reverse +from django.urls import reverse from opal.core import plugins, menus diff --git a/opal/core/log.py b/opal/core/log.py index 73d08e8e6..65a1d76cc 100644 --- a/opal/core/log.py +++ b/opal/core/log.py @@ -23,7 +23,7 @@ def emit(self, record): # In case the error occurrs before the authentication middleware # has run we need to check that the request has a user if hasattr(record.request, "user"): - if record.request.user.is_authenticated(): + if record.request.user.is_authenticated: user = record.request.user.username m = "Request to host {0} on application {1} from user {2} with {3}" diff --git a/opal/core/pathway/pathways.py b/opal/core/pathway/pathways.py index 1fc20e367..cacffeca0 100644 --- a/opal/core/pathway/pathways.py +++ b/opal/core/pathway/pathways.py @@ -5,7 +5,7 @@ import json from collections import defaultdict -from django.core.urlresolvers import reverse +from django.urls import reverse from django.db import models, transaction from django.utils.text import slugify from six import string_types diff --git a/opal/core/patient_lists.py b/opal/core/patient_lists.py index 2c84c94f2..e34f62415 100644 --- a/opal/core/patient_lists.py +++ b/opal/core/patient_lists.py @@ -351,7 +351,7 @@ def to_dict(klass, user=None, **kw): tag_slugs = {} tag_list = [i for i in TaggedPatientList.for_user(user)] - if user.is_authenticated(): + if user.is_authenticated: for taglist in tag_list: slug = taglist().get_slug() tag = taglist.tag diff --git a/opal/core/search/views.py b/opal/core/search/views.py index 74ced533b..becd33836 100644 --- a/opal/core/search/views.py +++ b/opal/core/search/views.py @@ -45,7 +45,7 @@ class ExtractTemplateView(LoginRequiredMixin, TemplateView): def ajax_login_required(view): @wraps(view) def wrapper(request, *args, **kwargs): - if not request.user.is_authenticated(): + if not request.user.is_authenticated: raise PermissionDenied return view(request, *args, **kwargs) return wrapper @@ -54,7 +54,7 @@ def wrapper(request, *args, **kwargs): def ajax_login_required_view(view): @wraps(view) def wrapper(self, *args, **kwargs): - if not self.request.user.is_authenticated(): + if not self.request.user.is_authenticated: raise PermissionDenied return view(self, *args, **kwargs) return wrapper @@ -159,7 +159,11 @@ def post(self, *args, **kwargs): ) episodes = query.get_episodes() fname = zip_archive(episodes, query.description(), self.request.user) - resp = HttpResponse(open(fname, 'rb').read()) + + with open(fname, 'rb') as download: + content = download.read() + + resp = HttpResponse(content) disp = 'attachment; filename="{0}extract{1}.zip"'.format( settings.OPAL_BRAND_NAME, datetime.datetime.now().isoformat()) resp['Content-Disposition'] = disp diff --git a/opal/middleware.py b/opal/middleware.py index 1ed084768..f7d5f755d 100644 --- a/opal/middleware.py +++ b/opal/middleware.py @@ -17,4 +17,4 @@ def process_request(self, request): class DjangoReversionWorkaround(object): def process_request(self, request): - access = request.user.is_authenticated() # noqa: + access = request.user.is_authenticated # noqa: diff --git a/opal/migrations/0001_initial.py b/opal/migrations/0001_initial.py index 494eca847..57d01e55c 100644 --- a/opal/migrations/0001_initial.py +++ b/opal/migrations/0001_initial.py @@ -208,7 +208,7 @@ class Migration(migrations.Migration): ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('name', models.CharField(max_length=200)), ('criteria', models.TextField()), - ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), ], options={ }, @@ -592,7 +592,7 @@ class Migration(migrations.Migration): ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('name', models.CharField(max_length=255)), ('object_id', models.PositiveIntegerField()), - ('content_type', models.ForeignKey(to='contenttypes.ContentType')), + ('content_type', models.ForeignKey(to='contenttypes.ContentType', on_delete=models.CASCADE)), ], options={ }, @@ -602,7 +602,7 @@ class Migration(migrations.Migration): name='Tagging', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('episode', models.ForeignKey(to='opal.Episode')), + ('episode', models.ForeignKey(to='opal.Episode', on_delete=models.CASCADE)), ], options={ }, @@ -619,7 +619,7 @@ class Migration(migrations.Migration): ('restricted', models.BooleanField(default=False, help_text=b'Whether this team is restricted to only a subset of users')), ('direct_add', models.BooleanField(default=True)), ('show_all', models.BooleanField(default=False)), - ('parent', models.ForeignKey(blank=True, to='opal.Team', null=True)), + ('parent', models.ForeignKey(blank=True, to='opal.Team', null=True, on_delete=models.CASCADE)), ('useful_numbers', models.ManyToManyField(to='opal.ContactNumber', blank=True)), ], options={ @@ -647,7 +647,7 @@ class Migration(migrations.Migration): ('readonly', models.BooleanField(default=False, help_text=b'This user will only be able to read data - they have no write/edit permissions')), ('restricted_only', models.BooleanField(default=False, help_text=b'This user will only see teams that they have been specifically added to')), ('roles', models.ManyToManyField(to='opal.Role')), - ('user', models.OneToOneField(related_name=b'profile', to=settings.AUTH_USER_MODEL)), + ('user', models.OneToOneField(related_name=b'profile', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), ], options={ }, @@ -668,13 +668,13 @@ class Migration(migrations.Migration): migrations.AddField( model_name='tagging', name='team', - field=models.ForeignKey(blank=True, to='opal.Team', null=True), + field=models.ForeignKey(blank=True, to='opal.Team', null=True, on_delete=models.CASCADE), preserve_default=True, ), migrations.AddField( model_name='tagging', name='user', - field=models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True), + field=models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE), preserve_default=True, ), migrations.AlterUniqueTogether( @@ -684,7 +684,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='episode', name='patient', - field=models.ForeignKey(to='opal.Patient'), + field=models.ForeignKey(to='opal.Patient', on_delete=models.CASCADE), preserve_default=True, ), ] diff --git a/opal/migrations/0003_auto_20150922_1825.py b/opal/migrations/0003_auto_20150922_1825.py index 0e065ecea..5a0726b27 100644 --- a/opal/migrations/0003_auto_20150922_1825.py +++ b/opal/migrations/0003_auto_20150922_1825.py @@ -21,7 +21,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='episode', name='created_by', - field=models.ForeignKey(related_name='created_opal_episode_subrecords', blank=True, to=settings.AUTH_USER_MODEL, null=True), + field=models.ForeignKey(related_name='created_opal_episode_subrecords', blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE), ), migrations.AddField( model_name='episode', @@ -31,7 +31,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='episode', name='updated_by', - field=models.ForeignKey(related_name='updated_opal_episode_subrecords', blank=True, to=settings.AUTH_USER_MODEL, null=True), + field=models.ForeignKey(related_name='updated_opal_episode_subrecords', blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE), ), migrations.AddField( model_name='tagging', @@ -41,7 +41,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='tagging', name='created_by', - field=models.ForeignKey(related_name='created_opal_tagging_subrecords', blank=True, to=settings.AUTH_USER_MODEL, null=True), + field=models.ForeignKey(related_name='created_opal_tagging_subrecords', blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE), ), migrations.AddField( model_name='tagging', @@ -51,6 +51,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='tagging', name='updated_by', - field=models.ForeignKey(related_name='updated_opal_tagging_subrecords', blank=True, to=settings.AUTH_USER_MODEL, null=True), + field=models.ForeignKey(related_name='updated_opal_tagging_subrecords', blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE), ), ] diff --git a/opal/migrations/0009_glossolaliasubscription.py b/opal/migrations/0009_glossolaliasubscription.py index 91b9b97d7..d0ba4da4c 100644 --- a/opal/migrations/0009_glossolaliasubscription.py +++ b/opal/migrations/0009_glossolaliasubscription.py @@ -17,7 +17,7 @@ class Migration(migrations.Migration): ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('subscription_type', models.CharField(default=b'all_information', max_length=2, choices=[(b'all_information', b'All Information'), (b'core_demographics', b'Core Demographics')])), ('gloss_id', models.IntegerField()), - ('patient', models.ForeignKey(to='opal.Patient')), + ('patient', models.ForeignKey(to='opal.Patient', on_delete=models.CASCADE)), ], ), ] diff --git a/opal/migrations/0011_patientrecordaccess.py b/opal/migrations/0011_patientrecordaccess.py index 5fefc3486..83da7ff1d 100644 --- a/opal/migrations/0011_patientrecordaccess.py +++ b/opal/migrations/0011_patientrecordaccess.py @@ -18,8 +18,8 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('created', models.DateTimeField(auto_now_add=True)), - ('patient', models.ForeignKey(to='opal.Patient')), - ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ('patient', models.ForeignKey(to='opal.Patient', on_delete=models.CASCADE)), + ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), ], ), ] diff --git a/opal/migrations/0013_inpatientadmission.py b/opal/migrations/0013_inpatientadmission.py index 3179645c2..a34d0c360 100644 --- a/opal/migrations/0013_inpatientadmission.py +++ b/opal/migrations/0013_inpatientadmission.py @@ -28,9 +28,9 @@ class Migration(migrations.Migration): ('bed', models.CharField(max_length=255, blank=True)), ('admission_diagnosis', models.CharField(max_length=255, blank=True)), ('external_identifier', models.CharField(max_length=255, blank=True)), - ('created_by', models.ForeignKey(related_name='created_opal_inpatientadmission_subrecords', blank=True, to=settings.AUTH_USER_MODEL, null=True)), - ('patient', models.ForeignKey(to='opal.Patient')), - ('updated_by', models.ForeignKey(related_name='updated_opal_inpatientadmission_subrecords', blank=True, to=settings.AUTH_USER_MODEL, null=True)), + ('created_by', models.ForeignKey(related_name='created_opal_inpatientadmission_subrecords', blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE)), + ('patient', models.ForeignKey(to='opal.Patient', on_delete=models.CASCADE)), + ('updated_by', models.ForeignKey(related_name='updated_opal_inpatientadmission_subrecords', blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE)), ], options={ 'abstract': False, diff --git a/opal/models.py b/opal/models.py index f4f405d85..b886a0281 100644 --- a/opal/models.py +++ b/opal/models.py @@ -16,7 +16,7 @@ from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericForeignKey -from django.core.urlresolvers import reverse +from django.urls import reverse from django.core.exceptions import FieldDoesNotExist from django.utils.encoding import force_str from six import b @@ -424,7 +424,7 @@ class Filter(models.Model): """ Saved filters for users extracting data. """ - user = models.ForeignKey(User) + user = models.ForeignKey(User, on_delete=models.CASCADE) name = models.CharField(max_length=200) criteria = models.TextField() @@ -451,7 +451,7 @@ def __unicode__(self): class Synonym(models.Model): name = models.CharField(max_length=255) - content_type = models.ForeignKey(ContentType) + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() content_object = GenericForeignKey('content_type', 'object_id') @@ -604,8 +604,8 @@ def save(self, *args, **kwargs): class PatientRecordAccess(models.Model): created = models.DateTimeField(auto_now_add=True) - user = models.ForeignKey(User) - patient = models.ForeignKey(Patient) + user = models.ForeignKey(User, on_delete=models.CASCADE) + patient = models.ForeignKey(Patient, on_delete=models.CASCADE) def to_dict(self, user): return dict( @@ -686,7 +686,7 @@ class Episode(UpdatesFromDictMixin, TrackedModel): category_name = models.CharField( max_length=200, default=get_default_episode_type ) - patient = models.ForeignKey(Patient) + patient = models.ForeignKey(Patient, on_delete=models.CASCADE) active = models.BooleanField(default=False) start = models.DateField(null=True, blank=True) end = models.DateField(blank=True, null=True) @@ -1026,7 +1026,7 @@ def bulk_update_from_dicts( class PatientSubrecord(Subrecord): - patient = models.ForeignKey(Patient) + patient = models.ForeignKey(Patient, on_delete=models.CASCADE) class Meta: abstract = True @@ -1034,7 +1034,7 @@ class Meta: class EpisodeSubrecord(Subrecord): - episode = models.ForeignKey(Episode, null=False) + episode = models.ForeignKey(Episode, null=False, on_delete=models.CASCADE) class Meta: abstract = True @@ -1044,8 +1044,8 @@ class Tagging(TrackedModel, models.Model): _is_singleton = True _advanced_searchable = True - user = models.ForeignKey(User, null=True, blank=True) - episode = models.ForeignKey(Episode, null=False) + user = models.ForeignKey(User, null=True, blank=True, on_delete=models.CASCADE) + episode = models.ForeignKey(Episode, null=False, on_delete=models.CASCADE) archived = models.BooleanField(default=False) value = models.CharField(max_length=200, blank=True, null=True) @@ -1588,7 +1588,7 @@ class UserProfile(models.Model): HELP_PW = "Force this user to change their password on the " \ "next login" - user = models.OneToOneField(User, related_name='profile') + user = models.OneToOneField(User, related_name='profile', on_delete=models.CASCADE) force_password_change = models.BooleanField(default=True, help_text=b(HELP_PW)) can_extract = models.BooleanField(default=False, diff --git a/opal/tests/models.py b/opal/tests/models.py index e0f56cc4b..efbc82ae1 100644 --- a/opal/tests/models.py +++ b/opal/tests/models.py @@ -73,7 +73,7 @@ class HouseOwner(models.PatientSubrecord): class House(dmodels.Model): address = dmodels.CharField(max_length=200) - house_owner = dmodels.ForeignKey(HouseOwner, null=True, blank=True) + house_owner = dmodels.ForeignKey(HouseOwner, null=True, blank=True, on_delete=dmodels.CASCADE) class Dog(lookuplists.LookupList): diff --git a/opal/tests/test_templatetags_forms.py b/opal/tests/test_templatetags_forms.py index 7b153208e..7f5f8cc94 100644 --- a/opal/tests/test_templatetags_forms.py +++ b/opal/tests/test_templatetags_forms.py @@ -82,14 +82,14 @@ def test_infer_choice_fields_from_charfield_with_serialiser(self, js): def test_infer_element_name(self): ctx = infer_from_subrecord_field_path("Birthday.birth_date") - self.assertEquals( + self.assertEqual( ctx["element_name"], "editing.birthday._client.id + '_birth_date'" ) def test_infer_element_type_number(self): ctx = infer_from_subrecord_field_path("FavouriteNumber.number") - self.assertEquals( + self.assertEqual( ctx["element_type"], "number" ) diff --git a/opal/urls.py b/opal/urls.py index 05b21066b..4a50e001d 100644 --- a/opal/urls.py +++ b/opal/urls.py @@ -30,7 +30,7 @@ url(r'^accounts/banned', views.BannedView.as_view(), name='banned'), - url(r'^admin/', include(admin.site.urls)), + url(r'^admin/', admin.site.urls), # Template views url(r'^templates/patient_list.html/(?P[0-9a-z_\-]+)/?$', From eab0b3a0bf6600459add0d81c7517412981d4311 Mon Sep 17 00:00:00 2001 From: David Miller Date: Thu, 1 Nov 2018 16:26:36 +0000 Subject: [PATCH 03/25] Update is_authenticated call to use attribute not method --- opal/core/application.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opal/core/application.py b/opal/core/application.py index b324fc199..26770a7c7 100644 --- a/opal/core/application.py +++ b/opal/core/application.py @@ -134,7 +134,7 @@ def get_menu_items(klass, user=None): items = [] items += klass.menuitems if user: - if not user.is_authenticated(): + if not user.is_authenticated: return [] else: items.append(logout) From 538666569499c0edcfe69536c3f55dd13bbd1a79 Mon Sep 17 00:00:00 2001 From: David Miller Date: Fri, 2 Nov 2018 15:09:15 +0000 Subject: [PATCH 04/25] Bump Django, Django Rest Framework and Django Reversion in setup.py --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 429b948dd..dd1562067 100644 --- a/setup.py +++ b/setup.py @@ -49,10 +49,10 @@ 'letter==0.4.1', 'jinja2==2.10', 'requests==2.18.4', - 'django==1.10.8', - 'django-reversion==1.10.2', + 'django==2.0', + 'django-reversion==3.0.1', 'django-axes==1.7.0', - 'djangorestframework==3.4.7', + 'djangorestframework==3.7.4', 'django-compressor==2.2', 'python-dateutil==2.4.2', 'django-celery==3.2.2', From 791ef8f3e5ddc1c04b02cd298fbbc2d524dd65a9 Mon Sep 17 00:00:00 2001 From: David Miller Date: Fri, 2 Nov 2018 15:09:54 +0000 Subject: [PATCH 05/25] Fix warnings from Django 1.11.x with python -Wd and migration that prevents tests running on Django 2.0 --- opal/core/pathway/tests/test_api.py | 2 +- opal/core/pathway/tests/test_pathways.py | 2 +- opal/core/pathway/tests/test_steps.py | 2 +- opal/core/pathway/tests/test_views.py | 2 +- opal/migrations/0001_initial.py | 2 +- opal/models.py | 2 +- opal/views.py | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/opal/core/pathway/tests/test_api.py b/opal/core/pathway/tests/test_api.py index 3c1148c98..7c5068e0a 100644 --- a/opal/core/pathway/tests/test_api.py +++ b/opal/core/pathway/tests/test_api.py @@ -1,6 +1,6 @@ import json from opal.core.test import OpalTestCase -from django.core.urlresolvers import reverse +from django.urls import reverse from mock import patch, MagicMock from opal.core.pathway.tests.pathway_test import pathways as test_pathways diff --git a/opal/core/pathway/tests/test_pathways.py b/opal/core/pathway/tests/test_pathways.py index 07f79f30f..adb07b09c 100644 --- a/opal/core/pathway/tests/test_pathways.py +++ b/opal/core/pathway/tests/test_pathways.py @@ -5,7 +5,7 @@ from opal.core.exceptions import InitializationError from django.utils import timezone from django.contrib.auth.models import User -from django.core.urlresolvers import reverse +from django.urls import reverse from opal.core import exceptions from opal.core.test import OpalTestCase from opal.core.views import OpalSerializer diff --git a/opal/core/pathway/tests/test_steps.py b/opal/core/pathway/tests/test_steps.py index daf7832b9..1dc709e1c 100644 --- a/opal/core/pathway/tests/test_steps.py +++ b/opal/core/pathway/tests/test_steps.py @@ -1,7 +1,7 @@ """ unittests for opal.core.pathway.steps """ -from django.core.urlresolvers import reverse +from django.urls import reverse from mock import MagicMock from opal.core import exceptions diff --git a/opal/core/pathway/tests/test_views.py b/opal/core/pathway/tests/test_views.py index f9ccc7afb..c309d0978 100644 --- a/opal/core/pathway/tests/test_views.py +++ b/opal/core/pathway/tests/test_views.py @@ -1,5 +1,5 @@ from opal.core.test import OpalTestCase -from django.core.urlresolvers import reverse +from django.urls import reverse from opal.core.pathway.tests.pathway_test import pathways as test_pathways diff --git a/opal/migrations/0001_initial.py b/opal/migrations/0001_initial.py index 57d01e55c..7a5e70d60 100644 --- a/opal/migrations/0001_initial.py +++ b/opal/migrations/0001_initial.py @@ -647,7 +647,7 @@ class Migration(migrations.Migration): ('readonly', models.BooleanField(default=False, help_text=b'This user will only be able to read data - they have no write/edit permissions')), ('restricted_only', models.BooleanField(default=False, help_text=b'This user will only see teams that they have been specifically added to')), ('roles', models.ManyToManyField(to='opal.Role')), - ('user', models.OneToOneField(related_name=b'profile', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), + ('user', models.OneToOneField(related_name='profile', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), ], options={ }, diff --git a/opal/models.py b/opal/models.py index b886a0281..322edca5d 100644 --- a/opal/models.py +++ b/opal/models.py @@ -140,7 +140,7 @@ def get_human_readable_type(cls, field_name): t = "One of the {}" else: t = "Some of the {}" - related = field_type.rel.to + related = field_type.remote_field.model return t.format(related._meta.verbose_name_plural.title()) enum = cls.get_field_enum(field_name) diff --git a/opal/views.py b/opal/views.py index 2ab4fcafe..afd0f6b1a 100644 --- a/opal/views.py +++ b/opal/views.py @@ -1,7 +1,7 @@ """ Module entrypoint for core Opal views """ -from django.core.urlresolvers import reverse +from django.urls import reverse from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.views import login from django.http import HttpResponseForbidden, HttpResponseNotFound From c174786d2221d7dcfb4a852920be1c84451104c6 Mon Sep 17 00:00:00 2001 From: David Miller Date: Fri, 2 Nov 2018 15:41:27 +0000 Subject: [PATCH 06/25] Update middleware to use new style middleware classes --- opal/middleware.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/opal/middleware.py b/opal/middleware.py index f7d5f755d..72109eaaa 100644 --- a/opal/middleware.py +++ b/opal/middleware.py @@ -1,13 +1,14 @@ """ Opal Middlewares """ +from django.utils.deprecation import MiddlewareMixin # Hat tip: # http://kevinzhang.org/posts/django-angularjs-and-csrf-xsrf-protection.html ANGULAR_HEADER_NAME = 'HTTP_X_XSRF_TOKEN' -class AngularCSRFRename(object): +class AngularCSRFRename(MiddlewareMixin): def process_request(self, request): if ANGULAR_HEADER_NAME in request.META: token = request.META[ANGULAR_HEADER_NAME] @@ -15,6 +16,6 @@ def process_request(self, request): del request.META[ANGULAR_HEADER_NAME] -class DjangoReversionWorkaround(object): +class DjangoReversionWorkaround(MiddlewareMixin): def process_request(self, request): access = request.user.is_authenticated # noqa: From 68cc6a4a7140ef6a9071e195e28daa84f3198daf Mon Sep 17 00:00:00 2001 From: David Miller Date: Fri, 2 Nov 2018 15:42:04 +0000 Subject: [PATCH 07/25] Remove axes middleware and update to new middleware setting --- runtests.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/runtests.py b/runtests.py index 82d699e5f..8c72ba2f5 100644 --- a/runtests.py +++ b/runtests.py @@ -32,7 +32,7 @@ OPAL_BRAND_NAME = 'opal', INTEGRATING=False, DEFAULT_DOMAIN='localhost', - MIDDLEWARE_CLASSES=( + MIDDLEWARE=( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'opal.middleware.AngularCSRFRename', @@ -40,8 +40,7 @@ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'opal.middleware.DjangoReversionWorkaround', - 'reversion.middleware.RevisionMiddleware', - 'axes.middleware.FailedLoginMiddleware', + 'reversion.middleware.RevisionMiddleware' ), INSTALLED_APPS=( 'django.contrib.auth', From 1c8c35f04ab6f24607fe914283e17d05e7b3c33c Mon Sep 17 00:00:00 2001 From: David Miller Date: Fri, 2 Nov 2018 15:42:27 +0000 Subject: [PATCH 08/25] Add rest framework auth classes setting to runtests --- runtests.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/runtests.py b/runtests.py index 8c72ba2f5..336678667 100644 --- a/runtests.py +++ b/runtests.py @@ -103,6 +103,12 @@ 'propagate': True, }, } + }, + REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework.authentication.TokenAuthentication', + 'rest_framework.authentication.SessionAuthentication', + ) } ) From 679a5616504dbe8001e8a338d52f7af3ef984fb3 Mon Sep 17 00:00:00 2001 From: David Miller Date: Fri, 2 Nov 2018 15:54:08 +0000 Subject: [PATCH 09/25] Update tests to pass on Django 2.0 --- opal/core/pathway/tests/test_url.py | 2 +- opal/core/search/tests/test_api.py | 4 ++-- opal/core/search/tests/test_extract.py | 2 +- opal/tests/test_api.py | 4 ++-- opal/tests/test_patient_lists.py | 2 +- opal/tests/test_urls.py | 2 +- opal/tests/test_views.py | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/opal/core/pathway/tests/test_url.py b/opal/core/pathway/tests/test_url.py index 941a2793e..d6578d575 100644 --- a/opal/core/pathway/tests/test_url.py +++ b/opal/core/pathway/tests/test_url.py @@ -1,5 +1,5 @@ from opal.core.test import OpalTestCase -from django.core.urlresolvers import reverse +from django.urls import reverse class PathwayReverseUrlTests(OpalTestCase): diff --git a/opal/core/search/tests/test_api.py b/opal/core/search/tests/test_api.py index b41b7ca9f..6b8d05f9d 100644 --- a/opal/core/search/tests/test_api.py +++ b/opal/core/search/tests/test_api.py @@ -21,10 +21,10 @@ def setUp(self): self.patient, self.episode = self.new_patient_and_episode_please() self.request = self.rf.get("/") - def test_403(self): + def test_401(self): url = reverse('extract-schema-list', request=self.request) response = self.client.get(url) self.assertEqual( response.status_code, - status.HTTP_403_FORBIDDEN + 401 ) diff --git a/opal/core/search/tests/test_extract.py b/opal/core/search/tests/test_extract.py index a3ae7bc3b..0c87864fa 100644 --- a/opal/core/search/tests/test_extract.py +++ b/opal/core/search/tests/test_extract.py @@ -5,7 +5,7 @@ import json import os -from django.core.urlresolvers import reverse +from django.urls import reverse from django.test import override_settings from mock import mock_open, patch, Mock, MagicMock diff --git a/opal/tests/test_api.py b/opal/tests/test_api.py index 6cf199d27..503e5cc7e 100644 --- a/opal/tests/test_api.py +++ b/opal/tests/test_api.py @@ -59,12 +59,12 @@ def get_urls(self): ) ] - def test_403(self): + def test_401(self): for url in self.get_urls(): response = self.client.get(url) self.assertEqual( response.status_code, - status.HTTP_403_FORBIDDEN + 401 ) diff --git a/opal/tests/test_patient_lists.py b/opal/tests/test_patient_lists.py index e97c8536f..50261eb43 100644 --- a/opal/tests/test_patient_lists.py +++ b/opal/tests/test_patient_lists.py @@ -3,7 +3,7 @@ """ import os -from django.core.urlresolvers import reverse +from django.urls import reverse from django.contrib.auth.models import User from mock import MagicMock, PropertyMock, patch diff --git a/opal/tests/test_urls.py b/opal/tests/test_urls.py index 9b64c3fc0..5ceeadc97 100644 --- a/opal/tests/test_urls.py +++ b/opal/tests/test_urls.py @@ -1,5 +1,5 @@ from opal.core.test import OpalTestCase -from django.core.urlresolvers import reverse +from django.urls import reverse class ReverseUrlTests(OpalTestCase): diff --git a/opal/tests/test_views.py b/opal/tests/test_views.py index 38187d580..236e116cd 100644 --- a/opal/tests/test_views.py +++ b/opal/tests/test_views.py @@ -4,7 +4,7 @@ import os from django import http -from django.core.urlresolvers import reverse +from django.urls import reverse from django.http import QueryDict from django.test import TestCase from django.test.client import RequestFactory From a979032dea181825e17a183d7bfe32b6ca09b6b0 Mon Sep 17 00:00:00 2001 From: David Miller Date: Fri, 2 Nov 2018 15:58:59 +0000 Subject: [PATCH 10/25] Bump setup.py Django version to 2.0.9 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index dd1562067..b08e1653a 100644 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ 'letter==0.4.1', 'jinja2==2.10', 'requests==2.18.4', - 'django==2.0', + 'django==2.0.9', 'django-reversion==3.0.1', 'django-axes==1.7.0', 'djangorestframework==3.7.4', From c83768c71ac61a3352c2a5dbb61cea65dd30c9ce Mon Sep 17 00:00:00 2001 From: David Miller Date: Fri, 2 Nov 2018 16:03:50 +0000 Subject: [PATCH 11/25] Documentation and pypi classifiers for removal of Python 2 support --- .travis.yml | 3 +-- changelog.md | 9 +++++++++ setup.py | 3 +-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6b161a5b8..c5389266c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,8 @@ language: python python: - - "2.7" - - "3.4" - "3.5" - "3.6" + - "3.7" services: - postgresql install: diff --git a/changelog.md b/changelog.md index 210a968b9..f2b54ff21 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,11 @@ ### 0.13.0 (Major Release) +#### Removes support for Python 2.x + +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, 3.7 + #### Coding systems for lookuplists Lookuplist entries may now have an associated coding system and code value stored against them. @@ -91,6 +97,9 @@ including the `reopen_episode_modal.html` template and the url/view at `template #### Updates to the Dependency Graph +* Django: 1.10.8 -> 2.0.9 +* Django Rest Framework: 3.4.7 -> 3.7.4 +* Django Reversion: 1.10.2 -> 3.0.1 * Letter: 0.4.1 -> 0.5 diff --git a/setup.py b/setup.py index cdcd1a0af..4e4662f83 100644 --- a/setup.py +++ b/setup.py @@ -61,9 +61,8 @@ ], classifiers = [ "Programming Language :: Python", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", ], ) From 256540114a1fb6aa9f1e06946ed7ce174e9e688d Mon Sep 17 00:00:00 2001 From: David Miller Date: Fri, 2 Nov 2018 16:06:14 +0000 Subject: [PATCH 12/25] Upgrade guide for python upgrade and Django version bump --- doc/docs/reference/upgrading.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/doc/docs/reference/upgrading.md b/doc/docs/reference/upgrading.md index 80442960a..69f544907 100644 --- a/doc/docs/reference/upgrading.md +++ b/doc/docs/reference/upgrading.md @@ -5,6 +5,15 @@ application to a later version where there are extra steps required. ### 0.13.0 -> 0.12.0 - 0.11.2 +#### Python versions + +Opal 0.13.0 drops support for Python 2.x +If you have not already done so, you will need to upgrade your application to Python 3 +in order to upgrade. + +You may also like to run the tests for your application with the 'show warnings' +flag e.g. `python -Wd manage.py test` + #### Upgrading Opal How you do this depends on how you have configured your application. You will need to @@ -16,6 +25,9 @@ you have specified them in for instance, a requirements.txt. # requirements.txt opal==0.10.0 + django==2.0.9 + django-reversion==3.0.1 + djangorestframework==3.7.4 letter==0.5 #### Free text or foreign key fields are now, by default case insensitive From 508bbe46b5900dddce209f36f0c15e4b9dfff172 Mon Sep 17 00:00:00 2001 From: David Miller Date: Fri, 2 Nov 2018 16:07:21 +0000 Subject: [PATCH 13/25] Fixup:remove unused import --- opal/core/search/extract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opal/core/search/extract.py b/opal/core/search/extract.py index 3b85de2dc..213fc3aac 100644 --- a/opal/core/search/extract.py +++ b/opal/core/search/extract.py @@ -10,7 +10,7 @@ import tempfile import zipfile -from django.template import Context, loader +from django.template import loader from django.utils.encoding import force_bytes from six import text_type From 443d803119e469e7ec0240091c88c54830e469e0 Mon Sep 17 00:00:00 2001 From: David Miller Date: Fri, 2 Nov 2018 16:09:07 +0000 Subject: [PATCH 14/25] Fixup: Bare except and line lengths for linter --- opal/core/fields.py | 2 +- opal/models.py | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/opal/core/fields.py b/opal/core/fields.py index 8973c1f36..3fea6fd5a 100644 --- a/opal/core/fields.py +++ b/opal/core/fields.py @@ -158,7 +158,7 @@ def __get__(self, inst, cls): return self try: foreign_obj = getattr(inst, self.fk_field_name) - except: + except Exception: return 'Unknown Lookuplist Entry' # foreign_obj = getattr(inst, self.fk_field_name) if foreign_obj is None: diff --git a/opal/models.py b/opal/models.py index cc1f9c402..16e67fa47 100644 --- a/opal/models.py +++ b/opal/models.py @@ -1024,7 +1024,9 @@ class Tagging(TrackedModel, models.Model): _is_singleton = True _advanced_searchable = True - user = models.ForeignKey(User, null=True, blank=True, on_delete=models.CASCADE) + user = models.ForeignKey( + User, null=True, blank=True, on_delete=models.CASCADE + ) episode = models.ForeignKey(Episode, null=False, on_delete=models.CASCADE) archived = models.BooleanField(default=False) value = models.CharField(max_length=200, blank=True, null=True) @@ -1550,7 +1552,10 @@ class UserProfile(models.Model): HELP_PW = "Force this user to change their password on the " \ "next login" - user = models.OneToOneField(User, related_name='profile', on_delete=models.CASCADE) + user = models.OneToOneField( + User, related_name='profile', + on_delete=models.CASCADE + ) force_password_change = models.BooleanField(default=True, help_text=b(HELP_PW)) can_extract = models.BooleanField(default=False, From cca3897762e0ede2949c9268b8941a15683246d3 Mon Sep 17 00:00:00 2001 From: David Miller Date: Fri, 2 Nov 2018 16:12:07 +0000 Subject: [PATCH 15/25] Try experimental 3.7 python --- .travis.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c5389266c..9cd37aab9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,13 @@ language: python python: - "3.5" - "3.6" - - "3.7" + +matrix: + include: + - python: 3.7 + dist: xenial + sudo: true + services: - postgresql install: From 21bd8ccb2e0939d5b0921eab4c7fbdb6013c2a07 Mon Sep 17 00:00:00 2001 From: David Miller Date: Fri, 2 Nov 2018 16:58:10 +0000 Subject: [PATCH 16/25] Remove Python 3.7 explicit support as travis is a pain to run 3.7 on --- .travis.yml | 6 ------ changelog.md | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9cd37aab9..30ae99d4c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,12 +3,6 @@ python: - "3.5" - "3.6" -matrix: - include: - - python: 3.7 - dist: xenial - sudo: true - services: - postgresql install: diff --git a/changelog.md b/changelog.md index f2b54ff21..90659be98 100644 --- a/changelog.md +++ b/changelog.md @@ -4,7 +4,7 @@ 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, 3.7 +Opal is now tested against Python 3.5, 3.6 #### Coding systems for lookuplists From e3cc92a496c3889368e61dfb39aa58af2449a684 Mon Sep 17 00:00:00 2001 From: David Miller Date: Fri, 2 Nov 2018 16:58:59 +0000 Subject: [PATCH 17/25] Add migrations for test models --- opal/tests/migrations/0001_initial.py | 17 + .../migrations/0002_auto_20181102_1642.py | 568 ++++++++++++++++++ ...erialisablemodel_updatablemodelinstance.py | 53 ++ opal/tests/migrations/__init__.py | 0 opal/tests/models.py | 55 +- opal/tests/test_models_mixins.py | 25 +- runtests.py | 2 - 7 files changed, 677 insertions(+), 43 deletions(-) create mode 100644 opal/tests/migrations/0001_initial.py create mode 100644 opal/tests/migrations/0002_auto_20181102_1642.py create mode 100644 opal/tests/migrations/0003_datingmodel_gettermodel_serialisablemodel_updatablemodelinstance.py create mode 100644 opal/tests/migrations/__init__.py diff --git a/opal/tests/migrations/0001_initial.py b/opal/tests/migrations/0001_initial.py new file mode 100644 index 000000000..eeae49924 --- /dev/null +++ b/opal/tests/migrations/0001_initial.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings +import opal.models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('contenttypes', '0001_initial'), + ] + + operations = [ + ] diff --git a/opal/tests/migrations/0002_auto_20181102_1642.py b/opal/tests/migrations/0002_auto_20181102_1642.py new file mode 100644 index 000000000..12b8abfda --- /dev/null +++ b/opal/tests/migrations/0002_auto_20181102_1642.py @@ -0,0 +1,568 @@ +# Generated by Django 2.0.9 on 2018-11-02 16:42 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import opal.models +import opal.utils + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('opal', '0036_merge_20181030_1654'), + ('tests', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='AbstractDog', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(blank=True, null=True)), + ('updated', models.DateTimeField(blank=True, null=True)), + ('consistency_token', models.CharField(max_length=8)), + ('name', models.CharField(max_length=200)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_tests_abstractdog_subrecords', to=settings.AUTH_USER_MODEL)), + ('patient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='opal.Patient')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_tests_abstractdog_subrecords', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + bases=(opal.models.UpdatesFromDictMixin, opal.models.ToDictMixin, models.Model, opal.utils.AbstractBase), + ), + migrations.CreateModel( + name='AbstractHatWearer', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(blank=True, null=True)), + ('updated', models.DateTimeField(blank=True, null=True)), + ('consistency_token', models.CharField(max_length=8)), + ('name', models.CharField(max_length=200)), + ('wearing_a_hat', models.BooleanField(default=True)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_tests_abstracthatwearer_subrecords', to=settings.AUTH_USER_MODEL)), + ('episode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='opal.Episode')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_tests_abstracthatwearer_subrecords', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + bases=(opal.models.UpdatesFromDictMixin, opal.models.ToDictMixin, models.Model, opal.utils.AbstractBase), + ), + migrations.CreateModel( + name='Birthday', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(blank=True, null=True)), + ('updated', models.DateTimeField(blank=True, null=True)), + ('consistency_token', models.CharField(max_length=8)), + ('birth_date', models.DateField(blank=True)), + ('party', models.DateTimeField(blank=True, null=True)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_tests_birthday_subrecords', to=settings.AUTH_USER_MODEL)), + ('patient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='opal.Patient')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_tests_birthday_subrecords', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + bases=(opal.models.UpdatesFromDictMixin, opal.models.ToDictMixin, models.Model), + ), + migrations.CreateModel( + name='Colour', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(blank=True, null=True)), + ('updated', models.DateTimeField(blank=True, null=True)), + ('consistency_token', models.CharField(max_length=8)), + ('name', models.CharField(blank=True, max_length=200, null=True)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_tests_colour_subrecords', to=settings.AUTH_USER_MODEL)), + ('episode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='opal.Episode')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_tests_colour_subrecords', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + bases=(opal.models.UpdatesFromDictMixin, opal.models.ToDictMixin, models.Model), + ), + migrations.CreateModel( + name='Demographics', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(blank=True, null=True)), + ('updated', models.DateTimeField(blank=True, null=True)), + ('consistency_token', models.CharField(max_length=8)), + ('hospital_number', models.CharField(blank=True, help_text=b'The unique identifier for this patient at the hospital.', max_length=255)), + ('nhs_number', models.CharField(blank=True, max_length=255, null=True, verbose_name=b'NHS Number')), + ('surname', models.CharField(blank=True, max_length=255)), + ('first_name', models.CharField(blank=True, max_length=255)), + ('middle_name', models.CharField(blank=True, max_length=255, null=True)), + ('date_of_birth', models.DateField(blank=True, null=True, verbose_name=b'Date of Birth')), + ('religion', models.CharField(blank=True, max_length=255, null=True)), + ('date_of_death', models.DateField(blank=True, null=True, verbose_name=b'Date of Death')), + ('post_code', models.CharField(blank=True, max_length=20, null=True)), + ('gp_practice_code', models.CharField(blank=True, max_length=20, null=True, verbose_name=b'GP Practice Code')), + ('death_indicator', models.BooleanField(default=False, help_text=b'This field will be True if the patient is deceased.')), + ('birth_place_ft', models.CharField(blank=True, default=b'', max_length=255, null=True)), + ('title_ft', models.CharField(blank=True, default=b'', max_length=255, null=True)), + ('marital_status_ft', models.CharField(blank=True, default=b'', max_length=255, null=True)), + ('ethnicity_ft', models.CharField(blank=True, default=b'', max_length=255, null=True)), + ('sex_ft', models.CharField(blank=True, default=b'', max_length=255, null=True)), + ('birth_place_fk', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='opal.Destination')), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_tests_demographics_subrecords', to=settings.AUTH_USER_MODEL)), + ('ethnicity_fk', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='opal.Ethnicity')), + ('marital_status_fk', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='opal.MaritalStatus')), + ('patient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='opal.Patient')), + ('sex_fk', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='opal.Gender')), + ('title_fk', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='opal.Title')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_tests_demographics_subrecords', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + 'verbose_name_plural': 'Demographics', + }, + bases=(opal.models.UpdatesFromDictMixin, opal.models.ToDictMixin, models.Model), + ), + migrations.CreateModel( + name='Dinner', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(blank=True, null=True)), + ('updated', models.DateTimeField(blank=True, null=True)), + ('consistency_token', models.CharField(max_length=8)), + ('food', models.CharField(blank=True, max_length=256, null=True)), + ('time', models.TimeField(blank=True, null=True)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_tests_dinner_subrecords', to=settings.AUTH_USER_MODEL)), + ('episode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='opal.Episode')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_tests_dinner_subrecords', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + bases=(opal.models.UpdatesFromDictMixin, opal.models.ToDictMixin, models.Model), + ), + migrations.CreateModel( + name='Dog', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, unique=True)), + ('system', models.CharField(blank=True, max_length=255, null=True)), + ('code', models.CharField(blank=True, max_length=255, null=True)), + ('version', models.CharField(blank=True, max_length=255, null=True)), + ], + options={ + 'abstract': False, + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='DogOwner', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(blank=True, null=True)), + ('updated', models.DateTimeField(blank=True, null=True)), + ('consistency_token', models.CharField(max_length=8)), + ('name', models.CharField(default='Catherine', max_length=200)), + ('ownership_start_date', models.DateField(blank=True, null=True, verbose_name='OSD')), + ('dog_ft', models.CharField(blank=True, default=b'', max_length=255, null=True)), + ('least_favourite_dog_ft', models.CharField(blank=True, default=b'', max_length=255, null=True)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_tests_dogowner_subrecords', to=settings.AUTH_USER_MODEL)), + ('dog_fk', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='tests.Dog')), + ('episode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='opal.Episode')), + ('least_favourite_dog_fk', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='hated_dogs', to='tests.Dog')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_tests_dogowner_subrecords', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + bases=(opal.models.UpdatesFromDictMixin, opal.models.ToDictMixin, models.Model), + ), + migrations.CreateModel( + name='EntitledHatWearer', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(blank=True, null=True)), + ('updated', models.DateTimeField(blank=True, null=True)), + ('consistency_token', models.CharField(max_length=8)), + ('name', models.CharField(max_length=200)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_tests_entitledhatwearer_subrecords', to=settings.AUTH_USER_MODEL)), + ('episode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='opal.Episode')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_tests_entitledhatwearer_subrecords', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Entitled Wearer of Hats', + }, + bases=(opal.models.UpdatesFromDictMixin, opal.models.ToDictMixin, models.Model), + ), + migrations.CreateModel( + name='EpisodeName', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(blank=True, null=True)), + ('updated', models.DateTimeField(blank=True, null=True)), + ('consistency_token', models.CharField(max_length=8)), + ('name', models.CharField(blank=True, max_length=200, null=True)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_tests_episodename_subrecords', to=settings.AUTH_USER_MODEL)), + ('episode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='opal.Episode')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_tests_episodename_subrecords', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + bases=(opal.models.UpdatesFromDictMixin, opal.models.ToDictMixin, models.Model), + ), + migrations.CreateModel( + name='ExternalSubRecord', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('external_system', models.CharField(blank=True, max_length=255, null=True)), + ('external_identifier', models.CharField(blank=True, max_length=255, null=True)), + ('created', models.DateTimeField(blank=True, null=True)), + ('updated', models.DateTimeField(blank=True, null=True)), + ('consistency_token', models.CharField(max_length=8)), + ('name', models.CharField(blank=True, max_length=200, null=True)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_tests_externalsubrecord_subrecords', to=settings.AUTH_USER_MODEL)), + ('episode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='opal.Episode')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_tests_externalsubrecord_subrecords', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + bases=(opal.models.UpdatesFromDictMixin, opal.models.ToDictMixin, models.Model), + ), + migrations.CreateModel( + name='FamousLastWords', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(blank=True, null=True)), + ('updated', models.DateTimeField(blank=True, null=True)), + ('consistency_token', models.CharField(max_length=8)), + ('words', models.CharField(blank=True, max_length=200, null=True, verbose_name='only words')), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_tests_famouslastwords_subrecords', to=settings.AUTH_USER_MODEL)), + ('patient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='opal.Patient')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_tests_famouslastwords_subrecords', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + bases=(opal.models.UpdatesFromDictMixin, opal.models.ToDictMixin, models.Model), + ), + migrations.CreateModel( + name='FavouriteColour', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(blank=True, null=True)), + ('updated', models.DateTimeField(blank=True, null=True)), + ('consistency_token', models.CharField(max_length=8)), + ('name', models.CharField(blank=True, choices=[('purple', 'purple'), ('yellow', 'yellow'), ('blue', 'blue')], help_text='orange is the new black', max_length=200, null=True)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_tests_favouritecolour_subrecords', to=settings.AUTH_USER_MODEL)), + ('patient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='opal.Patient')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_tests_favouritecolour_subrecords', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + bases=(opal.models.UpdatesFromDictMixin, opal.models.ToDictMixin, models.Model), + ), + migrations.CreateModel( + name='FavouriteDogs', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(blank=True, null=True)), + ('updated', models.DateTimeField(blank=True, null=True)), + ('consistency_token', models.CharField(max_length=8)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_tests_favouritedogs_subrecords', to=settings.AUTH_USER_MODEL)), + ('dogs', models.ManyToManyField(related_name='favourite_dogs', to='tests.Dog')), + ('patient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='opal.Patient')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_tests_favouritedogs_subrecords', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + bases=(opal.models.UpdatesFromDictMixin, opal.models.ToDictMixin, models.Model), + ), + migrations.CreateModel( + name='FavouriteNumber', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(blank=True, null=True)), + ('updated', models.DateTimeField(blank=True, null=True)), + ('consistency_token', models.CharField(max_length=8)), + ('number', models.IntegerField(blank=True, null=True)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_tests_favouritenumber_subrecords', to=settings.AUTH_USER_MODEL)), + ('patient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='opal.Patient')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_tests_favouritenumber_subrecords', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + bases=(opal.models.UpdatesFromDictMixin, opal.models.ToDictMixin, models.Model), + ), + migrations.CreateModel( + name='GhostHat', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, unique=True)), + ('system', models.CharField(blank=True, max_length=255, null=True)), + ('code', models.CharField(blank=True, max_length=255, null=True)), + ('version', models.CharField(blank=True, max_length=255, null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Hat', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, unique=True)), + ('system', models.CharField(blank=True, max_length=255, null=True)), + ('code', models.CharField(blank=True, max_length=255, null=True)), + ('version', models.CharField(blank=True, max_length=255, null=True)), + ], + options={ + 'abstract': False, + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='HatWearer', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(blank=True, null=True)), + ('updated', models.DateTimeField(blank=True, null=True)), + ('consistency_token', models.CharField(max_length=8)), + ('name', models.CharField(max_length=200)), + ('wearing_a_hat', models.BooleanField(default=True)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_tests_hatwearer_subrecords', to=settings.AUTH_USER_MODEL)), + ('episode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='opal.Episode')), + ('hats', models.ManyToManyField(related_name='hat_wearers', to='tests.Hat')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_tests_hatwearer_subrecords', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Wearer of Hats', + }, + bases=(opal.models.UpdatesFromDictMixin, opal.models.ToDictMixin, models.Model), + ), + migrations.CreateModel( + name='HoundOwner', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(blank=True, null=True)), + ('updated', models.DateTimeField(blank=True, null=True)), + ('consistency_token', models.CharField(max_length=8)), + ('name', models.CharField(default='Philipa', max_length=200)), + ('dog_ft', models.CharField(blank=True, default=b'', max_length=255, null=True)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_tests_houndowner_subrecords', to=settings.AUTH_USER_MODEL)), + ('dog_fk', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='tests.Dog')), + ('episode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='opal.Episode')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_tests_houndowner_subrecords', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + bases=(opal.models.UpdatesFromDictMixin, opal.models.ToDictMixin, models.Model), + ), + migrations.CreateModel( + name='House', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('address', models.CharField(max_length=200)), + ], + ), + migrations.CreateModel( + name='HouseOwner', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(blank=True, null=True)), + ('updated', models.DateTimeField(blank=True, null=True)), + ('consistency_token', models.CharField(max_length=8)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_tests_houseowner_subrecords', to=settings.AUTH_USER_MODEL)), + ('patient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='opal.Patient')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_tests_houseowner_subrecords', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + bases=(opal.models.UpdatesFromDictMixin, opal.models.ToDictMixin, models.Model), + ), + migrations.CreateModel( + name='InvisibleDog', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(blank=True, null=True)), + ('updated', models.DateTimeField(blank=True, null=True)), + ('consistency_token', models.CharField(max_length=8)), + ('name', models.CharField(default='Catherine', max_length=200)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_tests_invisibledog_subrecords', to=settings.AUTH_USER_MODEL)), + ('patient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='opal.Patient')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_tests_invisibledog_subrecords', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + bases=(opal.models.UpdatesFromDictMixin, opal.models.ToDictMixin, models.Model), + ), + migrations.CreateModel( + name='InvisibleHatWearer', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(blank=True, null=True)), + ('updated', models.DateTimeField(blank=True, null=True)), + ('consistency_token', models.CharField(max_length=8)), + ('name', models.CharField(max_length=200)), + ('wearing_a_hat', models.BooleanField(default=True)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_tests_invisiblehatwearer_subrecords', to=settings.AUTH_USER_MODEL)), + ('episode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='opal.Episode')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_tests_invisiblehatwearer_subrecords', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Invisible Wearer of Hats', + }, + bases=(opal.models.UpdatesFromDictMixin, opal.models.ToDictMixin, models.Model), + ), + migrations.CreateModel( + name='Location', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(blank=True, null=True)), + ('updated', models.DateTimeField(blank=True, null=True)), + ('consistency_token', models.CharField(max_length=8)), + ('category', models.CharField(blank=True, max_length=255)), + ('hospital', models.CharField(blank=True, max_length=255)), + ('ward', models.CharField(blank=True, max_length=255)), + ('bed', models.CharField(blank=True, max_length=255)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_tests_location_subrecords', to=settings.AUTH_USER_MODEL)), + ('episode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='opal.Episode')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_tests_location_subrecords', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + bases=(opal.models.UpdatesFromDictMixin, opal.models.ToDictMixin, models.Model), + ), + migrations.CreateModel( + name='PatientColour', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(blank=True, null=True)), + ('updated', models.DateTimeField(blank=True, null=True)), + ('consistency_token', models.CharField(max_length=8)), + ('name', models.CharField(blank=True, max_length=200, null=True)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_tests_patientcolour_subrecords', to=settings.AUTH_USER_MODEL)), + ('patient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='opal.Patient')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_tests_patientcolour_subrecords', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + bases=(opal.models.UpdatesFromDictMixin, opal.models.ToDictMixin, models.Model), + ), + migrations.CreateModel( + name='PatientConsultation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(blank=True, null=True)), + ('updated', models.DateTimeField(blank=True, null=True)), + ('consistency_token', models.CharField(max_length=8)), + ('when', models.DateTimeField(blank=True, null=True)), + ('initials', models.CharField(blank=True, help_text=b'The initials of the user who gave the consult.', max_length=255)), + ('discussion', models.TextField(blank=True)), + ('reason_for_interaction_ft', models.CharField(blank=True, default=b'', max_length=255, null=True)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_tests_patientconsultation_subrecords', to=settings.AUTH_USER_MODEL)), + ('episode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='opal.Episode')), + ('reason_for_interaction_fk', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='opal.PatientConsultationReasonForInteraction')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_tests_patientconsultation_subrecords', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + 'verbose_name': 'Patient Consultation', + }, + bases=(opal.models.UpdatesFromDictMixin, opal.models.ToDictMixin, models.Model), + ), + migrations.CreateModel( + name='SensitiveDogOwner', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(blank=True, null=True)), + ('updated', models.DateTimeField(blank=True, null=True)), + ('consistency_token', models.CharField(max_length=8)), + ('name', models.CharField(default='Catherine', max_length=200)), + ('dog_ft', models.CharField(blank=True, default=b'', max_length=255, null=True)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_tests_sensitivedogowner_subrecords', to=settings.AUTH_USER_MODEL)), + ('dog_fk', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='tests.Dog')), + ('episode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='opal.Episode')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_tests_sensitivedogowner_subrecords', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + bases=(opal.models.UpdatesFromDictMixin, opal.models.ToDictMixin, models.Model), + ), + migrations.CreateModel( + name='SpanielOwner', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(blank=True, null=True)), + ('updated', models.DateTimeField(blank=True, null=True)), + ('consistency_token', models.CharField(max_length=8)), + ('name', models.CharField(default='Catherine', max_length=200)), + ('dog_ft', models.CharField(blank=True, default=b'', max_length=255, null=True)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_tests_spanielowner_subrecords', to=settings.AUTH_USER_MODEL)), + ('dog_fk', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='tests.Dog')), + ('episode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='opal.Episode')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_tests_spanielowner_subrecords', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + bases=(opal.models.UpdatesFromDictMixin, opal.models.ToDictMixin, models.Model), + ), + migrations.CreateModel( + name='SymptomComplex', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(blank=True, null=True)), + ('updated', models.DateTimeField(blank=True, null=True)), + ('consistency_token', models.CharField(max_length=8)), + ('duration', models.CharField(blank=True, choices=[(b'3 days or less', b'3 days or less'), (b'4-10 days', b'4-10 days'), (b'11-21 days', b'11-21 days'), (b'22 days to 3 months', b'22 days to 3 months'), (b'over 3 months', b'over 3 months')], help_text=b'The duration for which the patient had been experiencing these symptoms when recorded.', max_length=255, null=True)), + ('details', models.TextField(blank=True, null=True)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_tests_symptomcomplex_subrecords', to=settings.AUTH_USER_MODEL)), + ('episode', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='opal.Episode')), + ('symptoms', models.ManyToManyField(blank=True, related_name='symptoms', to='opal.Symptom')), + ('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_tests_symptomcomplex_subrecords', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + 'verbose_name_plural': 'Symptom complexes', + 'verbose_name': 'Symptoms', + }, + bases=(opal.models.UpdatesFromDictMixin, opal.models.ToDictMixin, models.Model), + ), + migrations.AddField( + model_name='house', + name='house_owner', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='tests.HouseOwner'), + ), + migrations.AlterUniqueTogether( + name='hat', + unique_together={('code', 'system')}, + ), + migrations.AlterUniqueTogether( + name='dog', + unique_together={('code', 'system')}, + ), + migrations.CreateModel( + name='CockerSpanielOwner', + fields=[ + ], + options={ + 'indexes': [], + 'proxy': True, + }, + bases=('tests.spanielowner',), + ), + ] diff --git a/opal/tests/migrations/0003_datingmodel_gettermodel_serialisablemodel_updatablemodelinstance.py b/opal/tests/migrations/0003_datingmodel_gettermodel_serialisablemodel_updatablemodelinstance.py new file mode 100644 index 000000000..a4416c743 --- /dev/null +++ b/opal/tests/migrations/0003_datingmodel_gettermodel_serialisablemodel_updatablemodelinstance.py @@ -0,0 +1,53 @@ +# Generated by Django 2.0.9 on 2018-11-02 16:54 + +from django.db import migrations, models +import django.db.models.deletion +import opal.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tests', '0002_auto_20181102_1642'), + ] + + operations = [ + migrations.CreateModel( + name='DatingModel', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('datetime', models.DateTimeField()), + ], + bases=(opal.models.UpdatesFromDictMixin, models.Model), + ), + migrations.CreateModel( + name='GetterModel', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('foo', models.CharField(blank=True, max_length=200, null=True)), + ], + bases=(opal.models.ToDictMixin, models.Model), + ), + migrations.CreateModel( + name='SerialisableModel', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('pid', models.CharField(blank=True, max_length=200, null=True)), + ('hatty_ft', models.CharField(blank=True, default=b'', max_length=255, null=True)), + ('hatty_fk', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='tests.Hat')), + ], + bases=(opal.models.SerialisableFields, models.Model), + ), + migrations.CreateModel( + name='UpdatableModelInstance', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('foo', models.CharField(blank=True, max_length=200, null=True)), + ('bar', models.CharField(blank=True, max_length=200, null=True)), + ('pid', models.CharField(blank=True, max_length=200, null=True)), + ('hatty_ft', models.CharField(blank=True, default=b'', max_length=255, null=True)), + ('hatty_fk', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='tests.Hat')), + ], + bases=(opal.models.UpdatesFromDictMixin, models.Model), + ), + ] diff --git a/opal/tests/migrations/__init__.py b/opal/tests/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/opal/tests/models.py b/opal/tests/models.py index efbc82ae1..eaeeb74fe 100644 --- a/opal/tests/models.py +++ b/opal/tests/models.py @@ -7,7 +7,7 @@ from opal import models from opal.core import lookuplists from opal.utils import AbstractBase - +from opal.models import UpdatesFromDictMixin, SerialisableFields, ToDictMixin class Birthday(models.PatientSubrecord): birth_date = dmodels.DateField(blank=True) @@ -92,8 +92,8 @@ class DogOwner(models.EpisodeSubrecord): class HoundOwner(models.EpisodeSubrecord): - name = dmodels.CharField(max_length=200, default=lambda: "Philipa") - dog = fields.ForeignKeyOrFreeText(Dog, verbose_name="hound", default=lambda: "spaniel") + name = dmodels.CharField(max_length=200, default="Philipa") + dog = fields.ForeignKeyOrFreeText(Dog, verbose_name="hound", default="spaniel") class FavouriteDogs(models.PatientSubrecord): @@ -191,23 +191,44 @@ class ExternalSubRecord( ): name = dmodels.CharField(max_length=200, blank=True, null=True) -# We shouldn't, but we basically insist on some non-core models being there. -if not getattr(models.Patient, 'demographics_set', None): - class Demographics(models.Demographics): - _is_singleton = True - pid_fields = 'first_name', 'surname', +class Demographics(models.Demographics): + _is_singleton = True + pid_fields = 'first_name', 'surname', + + +class Location(models.Location): + _is_singleton = True + + +class SymptomComplex(models.SymptomComplex): + pass + + +class PatientConsultation(models.PatientConsultation): + pass + + +class DatingModel(UpdatesFromDictMixin, dmodels.Model): + datetime = dmodels.DateTimeField() + consistency_token = None + + +class UpdatableModelInstance(UpdatesFromDictMixin, dmodels.Model): + foo = dmodels.CharField(max_length=200, blank=True, null=True) + bar = dmodels.CharField(max_length=200, blank=True, null=True) + pid = dmodels.CharField(max_length=200, blank=True, null=True) + hatty = fields.ForeignKeyOrFreeText(Hat) + pid_fields = 'pid', 'hatty' -if not getattr(models.Episode, 'location_set', None): - class Location(models.Location): - _is_singleton = True +class GetterModel(ToDictMixin, dmodels.Model): + foo = dmodels.CharField(max_length=200, blank=True, null=True) + def get_foo(self, user): + return "gotten" -if not getattr(models.Episode, 'symptoms', None): - class SymptomComplex(models.SymptomComplex): - pass -if not getattr(models.Episode, 'patientconsultation_set', None): - class PatientConsultation(models.PatientConsultation): - pass +class SerialisableModel(SerialisableFields, dmodels.Model): + pid = dmodels.CharField(max_length=200, blank=True, null=True) + hatty = fields.ForeignKeyOrFreeText(Hat) diff --git a/opal/tests/test_models_mixins.py b/opal/tests/test_models_mixins.py index 5c3d2a090..6dfa73ff6 100644 --- a/opal/tests/test_models_mixins.py +++ b/opal/tests/test_models_mixins.py @@ -11,35 +11,12 @@ from opal.core.fields import ForeignKeyOrFreeText from opal.core.test import OpalTestCase from opal.tests import models as test_models - +from opal.tests.models import DatingModel, UpdatableModelInstance, GetterModel, SerialisableModel from opal.models import ( UpdatesFromDictMixin, SerialisableFields, ToDictMixin ) -class DatingModel(UpdatesFromDictMixin, models.Model): - datetime = models.DateTimeField() - consistency_token = None - - -class UpdatableModelInstance(UpdatesFromDictMixin, models.Model): - foo = models.CharField(max_length=200, blank=True, null=True) - bar = models.CharField(max_length=200, blank=True, null=True) - pid = models.CharField(max_length=200, blank=True, null=True) - hatty = ForeignKeyOrFreeText(test_models.Hat) - pid_fields = 'pid', 'hatty' - - -class GetterModel(ToDictMixin, models.Model): - foo = models.CharField(max_length=200, blank=True, null=True) - - def get_foo(self, user): - return "gotten" - - -class SerialisableModel(SerialisableFields, models.Model): - pid = models.CharField(max_length=200, blank=True, null=True) - hatty = ForeignKeyOrFreeText(test_models.Hat) class SerialisableFieldsTestCase(OpalTestCase): diff --git a/runtests.py b/runtests.py index 336678667..311882873 100644 --- a/runtests.py +++ b/runtests.py @@ -123,8 +123,6 @@ } } - - settings.configure(**test_settings_config) from opal.tests import dummy_opal_application # NOQA From c46f4484097fe4ec7b2933a93bc208906545560e Mon Sep 17 00:00:00 2001 From: David Miller Date: Wed, 14 Nov 2018 14:24:15 +0000 Subject: [PATCH 18/25] Change the default text value of an FKorFT field to '' rather than b''. As of Django2/Python3 only this results in our serializing any empty FKorFT field as "b''" rather than "". This is then displaye in the UI. To avoid this we change the field default. This will require migrations. refs #1641 --- opal/core/fields.py | 2 +- opal/tests/test_api.py | 59 +++++++++++++++++++++++++++++--- opal/tests/test_core_fields.py | 6 ++++ opal/tests/test_models_mixins.py | 28 ++++++++++++++- opal/tests/test_utils_fields.py | 2 +- 5 files changed, 89 insertions(+), 8 deletions(-) diff --git a/opal/core/fields.py b/opal/core/fields.py index 3fea6fd5a..1a7546a3a 100644 --- a/opal/core/fields.py +++ b/opal/core/fields.py @@ -83,7 +83,7 @@ def contribute_to_class(self, cls, name): ) fk_field.contribute_to_class(cls, self.fk_field_name) ft_field = models.CharField( - max_length=255, blank=True, null=True, default=b('') + max_length=255, blank=True, null=True, default='' ) ft_field.contribute_to_class(cls, self.ft_field_name) diff --git a/opal/tests/test_api.py b/opal/tests/test_api.py index 503e5cc7e..1353c0088 100644 --- a/opal/tests/test_api.py +++ b/opal/tests/test_api.py @@ -19,7 +19,9 @@ from rest_framework import status from opal import models -from opal.tests.models import Colour, PatientColour, HatWearer, Hat, Demographics +from opal.tests.models import ( + Colour, PatientColour, HatWearer, Hat, Demographics +) from opal.core import metadata from opal.core.test import OpalTestCase from opal.core.views import json_response @@ -279,11 +281,58 @@ def test_create_unexpected_field(self): response = self.client.post(url, data=data) def test_retrieve(self): - with patch.object(self.model.objects, 'get') as mockget: - mockget.return_value.to_dict.return_value = 'serialized colour' + colour = Colour.objects.create( + episode=self.episode + ) + expected = { + u'consistency_token': u'', + u'created' : None, + u'created_by_id' : None, + u'episode_id' : self.episode.id, + u'id' : colour.id, + u'name' : None, + u'updated' : None, + u'updated_by_id' : None + } + response = self.viewset().retrieve(MagicMock(name='request'), pk=colour.id) + response_decoded = json.loads(response.content.decode('UTF-8')) + self.assertEqual(expected, response_decoded) - response = self.viewset().retrieve(MagicMock(name='request'), pk=1) - self.assertEqual('serialized colour', response.data) + def test_retrieve_with_empty_fk_or_ft(self): + self.maxDiff = None + instance = self.patient.demographics_set.get() + class DemographicsViewSet(api.SubrecordViewSet): + base_name = 'demographics' + model = Demographics + + expected = { + u'consistency_token': u'', + u'created' : None, + u'created_by_id' : None, + u'patient_id' : self.patient.id, + u'id' : instance.id, + u'updated' : None, + u'updated_by_id' : None, + u'date_of_birth' : None, + u'date_of_death' : None, + u'death_indicator' : False, + u'ethnicity' : '', + u'birth_place' : '', + u'first_name' : '', + u'gp_practice_code' : None, + u'hospital_number' : '', + u'marital_status' : '', + u'middle_name' : None, + u'nhs_number' : None, + u'post_code' : None, + u'religion' : None, + u'sex' : '', + u'surname' : '', + u'title' : '' + } + response = DemographicsViewSet().retrieve(MagicMock(name='request'), pk=instance.id) + response_decoded = json.loads(response.content.decode('UTF-8')) + self.assertEqual(expected, response_decoded) def test_with_defined_api_name(self): with patch.object(self.model, "get_api_name") as mock_api_name: diff --git a/opal/tests/test_core_fields.py b/opal/tests/test_core_fields.py index 630c01270..1a74c7362 100644 --- a/opal/tests/test_core_fields.py +++ b/opal/tests/test_core_fields.py @@ -62,6 +62,12 @@ def test_get_raises(self): result = field.__get__(self, ForeignKeyOrFreeText) self.assertEqual(result, 'Unknown Lookuplist Entry') + def test_unset_value(self): + patient, episode = self.new_patient_and_episode_please() + demographics = test_models.Demographics(patient=patient) + demographics.save() + self.assertEqual('', demographics.title) + def test_synonyms_addition(self): ct = ContentType.objects.get_for_model( test_models.Dog diff --git a/opal/tests/test_models_mixins.py b/opal/tests/test_models_mixins.py index 6dfa73ff6..adb71a9ff 100644 --- a/opal/tests/test_models_mixins.py +++ b/opal/tests/test_models_mixins.py @@ -11,7 +11,9 @@ from opal.core.fields import ForeignKeyOrFreeText from opal.core.test import OpalTestCase from opal.tests import models as test_models -from opal.tests.models import DatingModel, UpdatableModelInstance, GetterModel, SerialisableModel +from opal.tests.models import ( + DatingModel, Dinner, UpdatableModelInstance, GetterModel, SerialisableModel +) from opal.models import ( UpdatesFromDictMixin, SerialisableFields, ToDictMixin ) @@ -160,6 +162,30 @@ class ToDictMixinTestCase(OpalTestCase): def setUp(self): self.model_instance = GetterModel(foo="blah") + def test_to_dict(self): + patient, episode = self.new_patient_and_episode_please() + dinner = Dinner.objects.create(episode=episode) + as_dict = dinner.to_dict(self.user) + expected = { + 'food': None, + 'time': None, + 'id': dinner.id, + 'episode_id': episode.id, + 'consistency_token': '', + 'created': None, + 'created_by_id': None, + 'updated': None, + 'updated_by_id': None + } + self.assertEqual(expected, as_dict) + + def test_to_dict_empty_fk_ft_fields(self): + patient, episode = self.new_patient_and_episode_please() + demographics = patient.demographics_set.get() + as_dict = demographics.to_dict(self.user) + for field in ['title', 'sex', 'ethnicity']: + self.assertEqual('', as_dict[field]) + def test_getter_is_used(self): self.assertEqual( self.model_instance.to_dict(self.user), diff --git a/opal/tests/test_utils_fields.py b/opal/tests/test_utils_fields.py index e0c48adad..04d820e69 100644 --- a/opal/tests/test_utils_fields.py +++ b/opal/tests/test_utils_fields.py @@ -18,4 +18,4 @@ def test_set_none(self): instance = self.Model() instance.dog = None - self.assertEqual(b(''), instance.dog) + self.assertEqual('', instance.dog) From 82127b822a3590a1eb065e1f07074b84eed00164 Mon Sep 17 00:00:00 2001 From: David Miller Date: Wed, 14 Nov 2018 14:26:47 +0000 Subject: [PATCH 19/25] Note change to lookuplist ft defaults in changelog --- changelog.md | 2 ++ doc/docs/reference/upgrading.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index d1488d7c8..9db3acbf1 100644 --- a/changelog.md +++ b/changelog.md @@ -109,6 +109,8 @@ including the `reopen_episode_modal.html` template and the url/view at `template * Adds in a footer updated/created by to the form base template +* Changes the default value of `_ft` fields on `ForeignKeyOrFreeTextField` from b'' to ''. This requires a migration + #### Updates to the Dependency Graph * Django: 1.10.8 -> 2.0.9 diff --git a/doc/docs/reference/upgrading.md b/doc/docs/reference/upgrading.md index 69f544907..0e45000d4 100644 --- a/doc/docs/reference/upgrading.md +++ b/doc/docs/reference/upgrading.md @@ -3,7 +3,7 @@ This document provides instructions for specific steps required to upgrading your Opal application to a later version where there are extra steps required. -### 0.13.0 -> 0.12.0 - 0.11.2 +### 0.12.0 - 0.11.2 -> 0.13.0 #### Python versions From 8791ed12183101cddce9810e6c9a7d62e6061709 Mon Sep 17 00:00:00 2001 From: David Miller Date: Wed, 14 Nov 2018 14:40:30 +0000 Subject: [PATCH 20/25] Convert __unicode__ model methods to __str__ refs #1641 --- changelog.md | 2 ++ opal/models.py | 18 +++++++++--------- opal/tests/test_episode.py | 4 ++-- opal/tests/test_patient.py | 4 ++-- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/changelog.md b/changelog.md index 9db3acbf1..56733141e 100644 --- a/changelog.md +++ b/changelog.md @@ -111,6 +111,8 @@ including the `reopen_episode_modal.html` template and the url/view at `template * Changes the default value of `_ft` fields on `ForeignKeyOrFreeTextField` from b'' to ''. This requires a migration +* `__unicode__` model methods have been renamed `__str__` + #### Updates to the Dependency Graph * Django: 1.10.8 -> 2.0.9 diff --git a/opal/models.py b/opal/models.py index a59a6301b..8befe0472 100644 --- a/opal/models.py +++ b/opal/models.py @@ -445,7 +445,7 @@ class ContactNumber(models.Model): name = models.CharField(max_length=255) number = models.CharField(max_length=255) - def __unicode__(self): + def __str__(self): return '{0}: {1}'.format(self.name, self.number) @@ -458,7 +458,7 @@ class Synonym(models.Model): class Meta: unique_together = (('name', 'content_type')) - def __unicode__(self): + def __str__(self): return self.name @@ -474,7 +474,7 @@ class Macro(models.Model): title = models.CharField(max_length=200, help_text=HELP_TITLE) expanded = models.TextField(help_text=HELP_EXPANDED) - def __unicode__(self): + def __str__(self): return self.title @classmethod @@ -490,7 +490,7 @@ class Patient(models.Model): objects = managers.PatientQueryset.as_manager() - def __unicode__(self): + def __str__(self): return 'Patient {0}'.format(self.id) def demographics(self): @@ -693,7 +693,7 @@ def __init__(self, *args, **kwargs): super(Episode, self).__init__(*args, **kwargs) self.__original_active = self.active - def __unicode__(self): + def __str__(self): return 'Episode {0}: {1} - {2}'.format( self.pk, self.start, self.end ) @@ -881,7 +881,7 @@ class Subrecord(UpdatesFromDictMixin, ToDictMixin, TrackedModel, models.Model): class Meta: abstract = True - def __unicode__(self): + def __str__(self): if self.created: return '{0}: {1} {2}'.format( self.get_api_name(), self.id, self.created @@ -1061,7 +1061,7 @@ class Meta: unique_together = (('value', 'episode', 'user')) verbose_name = "Teams" - def __unicode__(self): + def __str__(self): if self.user is not None: return 'User: %s - %s - archived: %s' % ( self.user.username, self.value, self.archived @@ -1561,8 +1561,8 @@ class Meta: class Role(models.Model): name = models.CharField(max_length=200) - def __unicode__(self): - return str(self.name) + def __str__(self): + return self.name class UserProfile(models.Model): diff --git a/opal/tests/test_episode.py b/opal/tests/test_episode.py index 6d9c7ed93..d4d870db7 100644 --- a/opal/tests/test_episode.py +++ b/opal/tests/test_episode.py @@ -57,9 +57,9 @@ def test_default_category_name(self, category, getter): episode = self.patient.create_episode() self.assertEqual('MyEpisodeCategory', episode.category_name) - def test__unicode__(self): + def test__str__(self): expected = 'Episode {}: None - None'.format(self.episode.pk) - self.assertEqual(expected, self.episode.__unicode__()) + self.assertEqual(expected, self.episode.__str__()) def test_category(self): self.episode.category_name = 'Inpatient' diff --git a/opal/tests/test_patient.py b/opal/tests/test_patient.py index 308dd31b1..a82bb4639 100644 --- a/opal/tests/test_patient.py +++ b/opal/tests/test_patient.py @@ -18,9 +18,9 @@ def setUp(self): def test_singleton_subrecord_created(self): self.assertEqual(1, self.patient.famouslastwords_set.count()) - def test__unicode__(self): + def test__str__(self): expected = 'Patient {}'.format(self.patient.pk) - self.assertEqual(expected, self.patient.__unicode__()) + self.assertEqual(expected, self.patient.__str__()) def test_can_create_episode(self): episode = self.patient.create_episode() From 8d5117da9f79655ac84d18266c8104ba8fd3a34f Mon Sep 17 00:00:00 2001 From: David Miller Date: Wed, 14 Nov 2018 14:46:25 +0000 Subject: [PATCH 21/25] Update models to stop using six.b on help text. refs 1641 --- opal/migrations/0037_auto_20181114_1445.py | 43 ++++++++++++++++++ opal/models.py | 53 +++++++++++----------- 2 files changed, 69 insertions(+), 27 deletions(-) create mode 100644 opal/migrations/0037_auto_20181114_1445.py diff --git a/opal/migrations/0037_auto_20181114_1445.py b/opal/migrations/0037_auto_20181114_1445.py new file mode 100644 index 000000000..e7fdec2e3 --- /dev/null +++ b/opal/migrations/0037_auto_20181114_1445.py @@ -0,0 +1,43 @@ +# Generated by Django 2.0.9 on 2018-11-14 14:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('opal', '0036_merge_20181030_1654'), + ] + + operations = [ + migrations.AlterField( + model_name='macro', + name='expanded', + field=models.TextField(help_text='This is the text that it will expand to.'), + ), + migrations.AlterField( + model_name='macro', + name='title', + field=models.CharField(help_text='The text that will display in the dropdown. No spaces!', max_length=200), + ), + migrations.AlterField( + model_name='userprofile', + name='can_extract', + field=models.BooleanField(default=False, help_text='This user will be able to download data from advanced searches'), + ), + migrations.AlterField( + model_name='userprofile', + name='force_password_change', + field=models.BooleanField(default=True, help_text='Force this user to change their password on the next login'), + ), + migrations.AlterField( + model_name='userprofile', + name='readonly', + field=models.BooleanField(default=False, help_text='This user will only be able to read data - they have no write/edit permissions'), + ), + migrations.AlterField( + model_name='userprofile', + name='restricted_only', + field=models.BooleanField(default=False, help_text='This user will only see teams that they have been specifically added to'), + ), + ] diff --git a/opal/models.py b/opal/models.py index 8befe0472..615c8990d 100644 --- a/opal/models.py +++ b/opal/models.py @@ -19,7 +19,6 @@ from django.urls import reverse from django.core.exceptions import FieldDoesNotExist from django.utils.encoding import force_str -from six import b from opal.core import ( application, exceptions, lookuplists, plugins, patient_lists, tagging @@ -468,8 +467,8 @@ class Macro(models.Model): enter "github-style" #foo text blocks from an admin defined list and then have them expand to cover frequent entries. """ - HELP_TITLE = b("The text that will display in the dropdown. No spaces!") - HELP_EXPANDED = b("This is the text that it will expand to.") + HELP_TITLE = "The text that will display in the dropdown. No spaces!" + HELP_EXPANDED = "This is the text that it will expand to." title = models.CharField(max_length=200, help_text=HELP_TITLE) expanded = models.TextField(help_text=HELP_EXPANDED) @@ -1365,10 +1364,10 @@ class Demographics(PatientSubrecord): hospital_number = models.CharField( max_length=255, blank=True, - help_text=b("The unique identifier for this patient at the hospital.") + help_text="The unique identifier for this patient at the hospital." ) nhs_number = models.CharField( - max_length=255, blank=True, null=True, verbose_name=b("NHS Number") + max_length=255, blank=True, null=True, verbose_name="NHS Number" ) surname = models.CharField(max_length=255, blank=True) @@ -1376,24 +1375,24 @@ class Demographics(PatientSubrecord): middle_name = models.CharField(max_length=255, blank=True, null=True) title = ForeignKeyOrFreeText(Title) date_of_birth = models.DateField( - null=True, blank=True, verbose_name=b("Date of Birth") + null=True, blank=True, verbose_name="Date of Birth" ) marital_status = ForeignKeyOrFreeText(MaritalStatus) religion = models.CharField(max_length=255, blank=True, null=True) date_of_death = models.DateField( - null=True, blank=True, verbose_name=b("Date of Death") + null=True, blank=True, verbose_name="Date of Death" ) post_code = models.CharField(max_length=20, blank=True, null=True) gp_practice_code = models.CharField( max_length=20, blank=True, null=True, - verbose_name=b("GP Practice Code") + verbose_name="GP Practice Code" ) birth_place = ForeignKeyOrFreeText(Destination, - verbose_name=b("Country of Birth")) + verbose_name="Country of Birth") ethnicity = ForeignKeyOrFreeText(Ethnicity) death_indicator = models.BooleanField( default=False, - help_text=b("This field will be True if the patient is deceased.") + help_text="This field will be True if the patient is deceased." ) sex = ForeignKeyOrFreeText(Gender) @@ -1434,7 +1433,7 @@ class Treatment(EpisodeSubrecord): route = ForeignKeyOrFreeText(Drugroute) start_date = models.DateField( null=True, blank=True, - help_text=b(HELP_START) + help_text=HELP_START ) end_date = models.DateField(null=True, blank=True) frequency = ForeignKeyOrFreeText(Drugfreq) @@ -1450,8 +1449,8 @@ class Allergies(PatientSubrecord): drug = ForeignKeyOrFreeText(Drug) provisional = models.BooleanField( - default=False, verbose_name=b("Suspected?"), - help_text=b(HELP_PROVISIONAL) + default=False, verbose_name="Suspected?", + help_text=HELP_PROVISIONAL ) details = models.CharField(max_length=255, blank=True) @@ -1471,8 +1470,8 @@ class Diagnosis(EpisodeSubrecord): condition = ForeignKeyOrFreeText(Condition) provisional = models.BooleanField( default=False, - verbose_name=b("Provisional?"), - help_text=b("True if the diagnosis is provisional. Defaults to False") + verbose_name="Provisional?", + help_text="True if the diagnosis is provisional. Defaults to False" ) details = models.CharField(max_length=255, blank=True) date_of_diagnosis = models.DateField(blank=True, null=True) @@ -1583,13 +1582,13 @@ class UserProfile(models.Model): on_delete=models.CASCADE ) force_password_change = models.BooleanField(default=True, - help_text=b(HELP_PW)) + help_text=HELP_PW) can_extract = models.BooleanField(default=False, - help_text=b(HELP_EXTRACT)) + help_text=HELP_EXTRACT) readonly = models.BooleanField(default=False, - help_text=b(HELP_READONLY)) + help_text=HELP_READONLY) restricted_only = models.BooleanField(default=False, - help_text=b(HELP_RESTRICTED)) + help_text=HELP_RESTRICTED) roles = models.ManyToManyField(Role, blank=True) def to_dict(self): @@ -1715,7 +1714,7 @@ class Meta: when = models.DateTimeField(null=True, blank=True) initials = models.CharField( max_length=255, blank=True, - help_text=b("The initials of the user who gave the consult.") + help_text="The initials of the user who gave the consult." ) reason_for_interaction = ForeignKeyOrFreeText( PatientConsultationReasonForInteraction @@ -1742,14 +1741,14 @@ class Meta: Symptom, related_name="symptoms", blank=True ) DURATION_CHOICES = ( - (b('3 days or less'), b('3 days or less')), - (b('4-10 days'), b('4-10 days')), - (b('11-21 days'), b('11-21 days')), - (b('22 days to 3 months'), b('22 days to 3 months')), - (b('over 3 months'), b('over 3 months')) + ('3 days or less', '3 days or less'), + ('4-10 days', '4-10 days'), + ('11-21 days', '11-21 days'), + ('22 days to 3 months', '22 days to 3 months'), + ('over 3 months', 'over 3 months') ) - HELP_DURATION = b("The duration for which the patient had been experiencing \ -these symptoms when recorded.") + HELP_DURATION = "The duration for which the patient had been experiencing \ +these symptoms when recorded." duration = models.CharField( max_length=255, From 0b5d87fc65092f08e729b6178ff53145d3e9e396 Mon Sep 17 00:00:00 2001 From: David Miller Date: Wed, 14 Nov 2018 14:46:51 +0000 Subject: [PATCH 22/25] Fixup: Remove unused import --- opal/core/fields.py | 1 - 1 file changed, 1 deletion(-) diff --git a/opal/core/fields.py b/opal/core/fields.py index 1a7546a3a..18f866a5a 100644 --- a/opal/core/fields.py +++ b/opal/core/fields.py @@ -1,6 +1,5 @@ from django.db import models from django.contrib.contenttypes.models import ContentType -from six import b from django.db.models.signals import pre_delete from opal.utils import _itersubclasses From d683fbd6d68c8129942184cc97da5c2eaadfe5aa Mon Sep 17 00:00:00 2001 From: David Miller Date: Wed, 14 Nov 2018 15:08:16 +0000 Subject: [PATCH 23/25] Fixup: Remove duplicate requests from setup.py --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 6fb29db51..6c5aad8ae 100644 --- a/setup.py +++ b/setup.py @@ -48,7 +48,6 @@ 'ffs>=0.0.8.2', 'letter==0.5', 'jinja2==2.10', - 'requests==2.18.4', 'django==2.0.9', 'requests==2.20.1', 'django-axes==1.7.0', From 4e7e0a64b4f68d2957b0ed486d34987cf404489e Mon Sep 17 00:00:00 2001 From: David Miller Date: Wed, 14 Nov 2018 15:11:56 +0000 Subject: [PATCH 24/25] Fixup: Add Django reversion back to setup.py --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 6c5aad8ae..1e8a9b306 100644 --- a/setup.py +++ b/setup.py @@ -52,6 +52,7 @@ 'requests==2.20.1', 'django-axes==1.7.0', 'djangorestframework==3.7.4', + 'django-reversion==3.0.1', 'django-compressor==2.2', 'python-dateutil==2.7.5', 'django-celery==3.2.2', From a520c0d311cbc1eca1c298ae68073ecd5f911a53 Mon Sep 17 00:00:00 2001 From: David Miller Date: Tue, 18 Dec 2018 13:44:40 +0000 Subject: [PATCH 25/25] Add unittests taht return us to 100% coverage --- opal/tests/test_core_fields.py | 4 ++++ opal/tests/test_models.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/opal/tests/test_core_fields.py b/opal/tests/test_core_fields.py index 1a74c7362..c6764f731 100644 --- a/opal/tests/test_core_fields.py +++ b/opal/tests/test_core_fields.py @@ -68,6 +68,10 @@ def test_unset_value(self): demographics.save() self.assertEqual('', demographics.title) + def test_get_default_when_callable(self): + field = ForeignKeyOrFreeText(test_models.Demographics, default=lambda: 'Nope') + self.assertEqual('Nope', field.get_default()) + def test_synonyms_addition(self): ct = ContentType.objects.get_for_model( test_models.Dog diff --git a/opal/tests/test_models.py b/opal/tests/test_models.py index d20847b7a..18125a26b 100644 --- a/opal/tests/test_models.py +++ b/opal/tests/test_models.py @@ -826,3 +826,31 @@ def test_get_footer(self): ExternalSubRecord.get_modal_footer_template(), "partials/_sourced_modal_footer.html" ) + + +class ContactNumberTestCase(OpalTestCase): + + def test_str(self): + c = models.ContactNumber(name='Jane Doe', number='0777383828') + self.assertEqual('Jane Doe: 0777383828', c.__str__()) + + +class SynonymTestCase(OpalTestCase): + + def test_str(self): + s = models.Synonym(name='Name') + self.assertEqual('Name', s.__str__()) + + +class MacroTestCase(OpalTestCase): + + def test_str(self): + m = models.Macro(title='My Macro') + self.assertEqual('My Macro', m.__str__()) + + +class RoleTestCase(OpalTestCase): + + def test_str(self): + r = models.Role(name='Doctor') + self.assertEqual('Doctor', r.__str__())