diff --git a/.travis.yml b/.travis.yml index 735055cf..bea17945 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,7 @@ services: branches: only: - master + - v2.1 env: global: - SECRET_KEY=SecretKeyForTravis diff --git a/LICENSE b/LICENSE index 8f71f43f..d05ada71 100644 --- a/LICENSE +++ b/LICENSE @@ -1,192 +1,4 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} + Copyright 2016 Department of Parks and Wildlife Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -199,4 +11,3 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - diff --git a/README.md b/README.md index a2b906f6..f00f6815 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ Variables below may also need to be defined (context-dependent): GEOSERVER_WFS_URL="//kmi.dpaw.wa.gov.au/geoserver/ows" PRS_USER_GROUP="PRS user" PRS_PWUSER_GROUP="PRS power user" + BORGCOLLECTOR_API="https://borg.dpaw.wa.gov.au/api/" + SSO_COOKIE_NAME="oim_dpaw_wa_gov_au_sessionid" # debug-toolbar settings: INTERNAL_IP="x.x.x.x" @@ -42,7 +44,7 @@ Variables below may also need to be defined (context-dependent): Use `runserver` to run a local copy of the application: - python manage.py runserver 0.0.0.0:8080 + python manage.py runserver [::]:8080 Run console commands manually: diff --git a/fabfile.py b/fabfile.py index 6bb9e723..1dece2fd 100644 --- a/fabfile.py +++ b/fabfile.py @@ -1,31 +1,31 @@ -import confy +from confy import read_environment_file, env import os from fabric.api import cd, run, local, get, settings from fabric.contrib.files import exists, upload_template -confy.read_environment_file() -DEPLOY_REPO_URL = os.environ['DEPLOY_REPO_URL'] -DEPLOY_TARGET = os.environ['DEPLOY_TARGET'] -DEPLOY_VENV_PATH = os.environ['DEPLOY_VENV_PATH'] -DEPLOY_VENV_NAME = os.environ['DEPLOY_VENV_NAME'] -DEPLOY_DEBUG = os.environ['DEPLOY_DEBUG'] -DEPLOY_PORT = os.environ['DEPLOY_PORT'] -DEPLOY_DATABASE_URL = os.environ['DEPLOY_DATABASE_URL'] -DEPLOY_SECRET_KEY = os.environ['DEPLOY_SECRET_KEY'] -DEPLOY_CSRF_COOKIE_SECURE = os.environ['DEPLOY_CSRF_COOKIE_SECURE'] -DEPLOY_SESSION_COOKIE_SECURE = os.environ['DEPLOY_SESSION_COOKIE_SECURE'] -DEPLOY_USER = os.environ['DEPLOY_USER'] -DEPLOY_DB_NAME = os.environ['DEPLOY_DB_NAME'] -DEPLOY_DB_USER = os.environ['DEPLOY_DB_USER'] -DEPLOY_SUPERUSER_USERNAME = os.environ['DEPLOY_SUPERUSER_USERNAME'] -DEPLOY_SUPERUSER_EMAIL = os.environ['DEPLOY_SUPERUSER_EMAIL'] -DEPLOY_SUPERUSER_PASSWORD = os.environ['DEPLOY_SUPERUSER_PASSWORD'] -DEPLOY_SUPERVISOR_NAME = os.environ['DEPLOY_SUPERVISOR_NAME'] -DEPLOY_EMAIL_HOST = os.environ['DEPLOY_EMAIL_HOST'] -DEPLOY_EMAIL_PORT = os.environ['DEPLOY_EMAIL_PORT'] -DEPLOY_SITE_URL = os.environ['SITE_URL'] -GEOSERVER_WMS_URL = os.environ['GEOSERVER_WMS_URL'] -GEOSERVER_WFS_URL = os.environ['GEOSERVER_WFS_URL'] +read_environment_file() +DEPLOY_REPO_URL = env('DEPLOY_REPO_URL', '') +DEPLOY_TARGET = env('DEPLOY_TARGET', '') +DEPLOY_VENV_PATH = env('DEPLOY_VENV_PATH', '') +DEPLOY_VENV_NAME = env('DEPLOY_VENV_NAME', '') +DEPLOY_DEBUG = env('DEPLOY_DEBUG', '') +DEPLOY_PORT = env('DEPLOY_PORT', '') +DEPLOY_DATABASE_URL = env('DEPLOY_DATABASE_URL', '') +DEPLOY_SECRET_KEY = env('DEPLOY_SECRET_KEY', '') +DEPLOY_CSRF_COOKIE_SECURE = env('DEPLOY_CSRF_COOKIE_SECURE', '') +DEPLOY_SESSION_COOKIE_SECURE = env('DEPLOY_SESSION_COOKIE_SECURE', '') +DEPLOY_USER = env('DEPLOY_USER', '') +DEPLOY_DB_NAME = env('DEPLOY_DB_NAME', 'db') +DEPLOY_DB_USER = env('DEPLOY_DB_USER', 'dbuser') +DEPLOY_SUPERUSER_USERNAME = env('DEPLOY_SUPERUSER_USERNAME', 'superuser') +DEPLOY_SUPERUSER_EMAIL = env('DEPLOY_SUPERUSER_EMAIL', 'test@email.com') +DEPLOY_SUPERUSER_PASSWORD = env('DEPLOY_SUPERUSER_PASSWORD', 'pass') +DEPLOY_SUPERVISOR_NAME = env('DEPLOY_SUPERVISOR_NAME', 'sv') +DEPLOY_EMAIL_HOST = env('DEPLOY_EMAIL_HOST', 'email.host') +DEPLOY_EMAIL_PORT = env('DEPLOY_EMAIL_PORT', '25') +DEPLOY_SITE_URL = env('SITE_URL', 'url') +GEOSERVER_WMS_URL = env('GEOSERVER_WMS_URL', 'url') +GEOSERVER_WFS_URL = env('GEOSERVER_WFS_URL', 'url') def _get_latest_source(): @@ -109,8 +109,8 @@ def _create_db(): """Creates a database on the deploy target. Assumes that PGHOST and PGUSER are set. """ db = { - 'NAME': os.environ['DEPLOY_DB_NAME'], - 'USER': os.environ['DEPLOY_DB_USER'], + 'NAME': DEPLOY_DB_NAME, + 'USER': DEPLOY_DB_USER, } sql = '''CREATE DATABASE {NAME} OWNER {USER}; \c {NAME}'''.format(**db) @@ -126,11 +126,8 @@ def _migrate(): def _create_superuser(): - un = os.environ['DEPLOY_SUPERUSER_USERNAME'] - em = os.environ['DEPLOY_SUPERUSER_EMAIL'] - pw = os.environ['DEPLOY_SUPERUSER_PASSWORD'] script = """from django.contrib.auth.models import User; -User.objects.create_superuser('{}', '{}', '{}')""".format(un, em, pw) +User.objects.create_superuser('{}', '{}', '{}')""".format(DEPLOY_SUPERUSER_USERNAME, DEPLOY_SUPERUSER_EMAIL, DEPLOY_SUPERUSER_PASSWORD) with cd(DEPLOY_TARGET): run_str = 'source {}/{}/bin/activate && echo "{}" | python manage.py shell' run(run_str.format(DEPLOY_VENV_PATH, DEPLOY_VENV_NAME, script), shell='/bin/bash') @@ -175,9 +172,9 @@ def update_repo(): def export_legacy_json(): """Dump, compress and download legacy PRS data to JSON fixtures for import to PRS2. """ - EXPORT_VENV_PATH = os.environ['EXPORT_VENV_PATH'] - EXPORT_VENV_NAME = os.environ['EXPORT_VENV_NAME'] - EXPORT_TARGET = os.environ['EXPORT_TARGET'] + EXPORT_VENV_PATH = env('EXPORT_VENV_PATH', '') + EXPORT_VENV_NAME = env('EXPORT_VENV_NAME', 'venv') + EXPORT_TARGET = env('EXPORT_TARGET', '') models = [ ('auth.Group', 'auth_group.json'), diff --git a/prs2/referral/admin.py b/prs2/referral/admin.py index 3eaad0c1..8239f8d4 100644 --- a/prs2/referral/admin.py +++ b/prs2/referral/admin.py @@ -4,10 +4,11 @@ # Third-party app imports from reversion.admin import VersionAdmin # PRS project imports -from .models import (DopTrigger, Region, OrganisationType, Organisation, TaskType, - TaskState, NoteType, ReferralType, Referral, Task, Record, - Note, Condition, Location, Bookmark, Clearance, Agency, - ConditionCategory, ModelCondition, UserProfile) +from referral.models import ( + DopTrigger, Region, OrganisationType, Organisation, TaskType, + TaskState, NoteType, ReferralType, Referral, Task, Record, + Note, Condition, Location, Bookmark, Clearance, Agency, + ConditionCategory, ModelCondition, UserProfile) class AuditAdmin(VersionAdmin, ModelAdmin): diff --git a/prs2/referral/base.py b/prs2/referral/base.py index 5b094bb2..3c427256 100644 --- a/prs2/referral/base.py +++ b/prs2/referral/base.py @@ -3,7 +3,7 @@ import logging import magic # File MIME-type identification -from reversion import create_revision, set_comment +from reversion.revisions import create_revision, set_comment import threading from django.conf import settings diff --git a/prs2/referral/forms.py b/prs2/referral/forms.py index 4d6e1f40..5019e3ff 100644 --- a/prs2/referral/forms.py +++ b/prs2/referral/forms.py @@ -445,6 +445,12 @@ class Meta: fields = ['records'] +class TagMultipleChoiceField(forms.ModelMultipleChoiceField): + def __init__(self, *args, **kwargs): + kwargs['queryset'] = Tag.objects.all().order_by('name') + super(TagMultipleChoiceField, self).__init__(*args, **kwargs) + + class TaskCompleteForm(BaseForm): ''' * Task outcome @@ -452,7 +458,7 @@ class TaskCompleteForm(BaseForm): * Description * Tags ''' - tags = forms.CharField( + tags = TagMultipleChoiceField( required=False, help_text='''Select all tags relevant to the advice supplied (required for response with advice / condition / objection).''') diff --git a/prs2/referral/migrations/0002_auto_20160401_0929.py b/prs2/referral/migrations/0002_auto_20160401_0929.py new file mode 100644 index 00000000..64360a5c --- /dev/null +++ b/prs2/referral/migrations/0002_auto_20160401_0929.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2016-04-01 01:29 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('referral', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='bookmark', + name='referral', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='referral.Referral'), + ), + migrations.AlterField( + model_name='bookmark', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='referral_user_bookmark', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='clearance', + name='condition', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='referral.Condition'), + ), + migrations.AlterField( + model_name='clearance', + name='task', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='referral.Task'), + ), + migrations.AlterField( + model_name='condition', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='referral.ConditionCategory'), + ), + migrations.AlterField( + model_name='condition', + name='model_condition', + field=models.ForeignKey(blank=True, help_text='Model text on which this condition is based', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='model_condition', to='referral.ModelCondition'), + ), + migrations.AlterField( + model_name='condition', + name='referral', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='referral.Referral'), + ), + migrations.AlterField( + model_name='location', + name='referral', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='referral.Referral'), + ), + migrations.AlterField( + model_name='modelcondition', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='referral.ConditionCategory'), + ), + migrations.AlterField( + model_name='note', + name='referral', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='referral.Referral'), + ), + migrations.AlterField( + model_name='note', + name='type', + field=models.ForeignKey(blank=True, help_text='The type of note (optional).', null=True, on_delete=django.db.models.deletion.PROTECT, to='referral.NoteType', verbose_name='note type'), + ), + migrations.AlterField( + model_name='organisation', + name='type', + field=models.ForeignKey(help_text='The organisation type.', on_delete=django.db.models.deletion.PROTECT, to='referral.OrganisationType'), + ), + migrations.AlterField( + model_name='record', + name='referral', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='referral.Referral'), + ), + migrations.AlterField( + model_name='referral', + name='agency', + field=models.ForeignKey(blank=True, help_text='[Searchable] The agency to which this referral relates.', null=True, on_delete=django.db.models.deletion.PROTECT, to='referral.Agency'), + ), + migrations.AlterField( + model_name='referral', + name='referring_org', + field=models.ForeignKey(help_text='[Searchable] The referring organisation or individual.', on_delete=django.db.models.deletion.PROTECT, to='referral.Organisation', verbose_name='referring organisation'), + ), + migrations.AlterField( + model_name='referral', + name='type', + field=models.ForeignKey(help_text='[Searchable] The referral type; explanation of these categories is also found\n in the PRS User documentation.', on_delete=django.db.models.deletion.PROTECT, to='referral.ReferralType', verbose_name='referral type'), + ), + migrations.AlterField( + model_name='referraltype', + name='initial_task', + field=models.ForeignKey(blank=True, help_text='Optional, but highly recommended.', null=True, on_delete=django.db.models.deletion.PROTECT, to='referral.TaskType'), + ), + migrations.AlterField( + model_name='relatedreferral', + name='from_referral', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='from_referral', to='referral.Referral'), + ), + migrations.AlterField( + model_name='relatedreferral', + name='to_referral', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='to_referral', to='referral.Referral'), + ), + migrations.AlterField( + model_name='task', + name='assigned_user', + field=models.ForeignKey(help_text='The officer responsible for completing the task.', on_delete=django.db.models.deletion.PROTECT, related_name='refer_task_assigned_user', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='task', + name='state', + field=models.ForeignKey(help_text='The status of the task.', on_delete=django.db.models.deletion.PROTECT, to='referral.TaskState', verbose_name='status'), + ), + migrations.AlterField( + model_name='task', + name='type', + field=models.ForeignKey(help_text='The task type.', on_delete=django.db.models.deletion.PROTECT, to='referral.TaskType', verbose_name='task type'), + ), + migrations.AlterField( + model_name='taskstate', + name='task_type', + field=models.ForeignKey(blank=True, help_text='Optional - does this state relate to a single task type only?', null=True, on_delete=django.db.models.deletion.PROTECT, to='referral.TaskType'), + ), + migrations.AlterField( + model_name='tasktype', + name='initial_state', + field=models.ForeignKey(help_text='The initial state for this task type.', on_delete=django.db.models.deletion.PROTECT, to='referral.TaskState'), + ), + migrations.AlterField( + model_name='userprofile', + name='agency', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='referral.Agency'), + ), + ] diff --git a/prs2/referral/models.py b/prs2/referral/models.py index 8f7fee3c..7d499bef 100644 --- a/prs2/referral/models.py +++ b/prs2/referral/models.py @@ -62,9 +62,9 @@ def __unicode__(self): def save(self, force_insert=False, force_update=False, *args, **kwargs): '''Overide save() to cleanse text input fields. ''' - self.name = unidecode(self.name) + self.name = unidecode(unicode(self.name)) if self.description: - self.description = unidecode(self.description) + self.description = unidecode(unicode(self.description)) super(ReferralLookup, self).save(force_insert, force_update) def get_absolute_url(self): @@ -82,7 +82,7 @@ def as_row(self): template = '{name}{description}{modified}' d = copy(self.__dict__) d['url'] = self.get_absolute_url() - d['description'] = unidecode(self.description or '') + d['description'] = unidecode(unicode(self.description) or u'') d['modified'] = self.modified.strftime("%d %b %Y") return unicode(mark_safe(template.format(**d))) @@ -132,7 +132,8 @@ class Organisation(ReferralLookup): ''' Lookup table of Organisations that send planning referrals to DPaW. ''' - type = models.ForeignKey(OrganisationType, help_text='The organisation type.') + type = models.ForeignKey( + OrganisationType, on_delete=models.PROTECT, help_text='The organisation type.') list_name = models.CharField( max_length=100, help_text='''Name as it will appear in the alphabetised selection lists (e.g. "Broome, @@ -208,7 +209,7 @@ class TaskState(ReferralLookup): objection) ''' task_type = models.ForeignKey( - 'TaskType', null=True, blank=True, + 'TaskType', on_delete=models.PROTECT, null=True, blank=True, help_text='Optional - does this state relate to a single task type only?') is_ongoing = models.BooleanField( default=True, @@ -227,7 +228,7 @@ class TaskType(ReferralLookup): for each type. ''' initial_state = models.ForeignKey( - TaskState, limit_choices_to=Q(effective_to__isnull=True), + TaskState, on_delete=models.PROTECT, limit_choices_to=Q(effective_to__isnull=True), help_text='The initial state for this task type.') target_days = models.IntegerField( default=35, @@ -241,7 +242,7 @@ class ReferralType(ReferralLookup): Having a "default" task type is not essential, though highly recommended. ''' initial_task = models.ForeignKey( - TaskType, + TaskType, on_delete=models.PROTECT, limit_choices_to=Q(effective_to__isnull=True), null=True, blank=True, help_text='Optional, but highly recommended.') @@ -292,17 +293,17 @@ class Referral(ReferralBaseModel): A planning referral which has been sent to DPaW for comment. ''' type = models.ForeignKey( - ReferralType, verbose_name='referral type', + ReferralType, on_delete=models.PROTECT, verbose_name='referral type', help_text='''[Searchable] The referral type; explanation of these categories is also found in the PRS User documentation.''') agency = models.ForeignKey( - Agency, blank=True, null=True, + Agency, on_delete=models.PROTECT, blank=True, null=True, help_text='[Searchable] The agency to which this referral relates.') region = models.ManyToManyField( Region, related_name='regions', blank=True, help_text='[Searchable] The region(s) in which this referral belongs.') referring_org = models.ForeignKey( - Organisation, verbose_name='referring organisation', + Organisation, on_delete=models.PROTECT, verbose_name='referring organisation', help_text='[Searchable] The referring organisation or individual.') reference = models.CharField( max_length=100, validators=[MaxLengthValidator(100)], @@ -340,9 +341,9 @@ def save(self, force_insert=False, force_update=False, *args, **kwargs): '''Overide save to cleanse text input to the description, address fields. ''' if self.description: - self.description = unidecode(self.description) + self.description = unidecode(unicode(self.description)) if self.address: - self.address = unidecode(self.address) + self.address = unidecode(unicode(self.address)) super(Referral, self).save(force_insert, force_update) def get_absolute_url(self): @@ -407,8 +408,8 @@ def as_row(self): d['region'] = self.regions_str d['referring_org'] = self.referring_org d['referral_date'] = self.referral_date.strftime('%d %b %Y') or '' - d['address'] = unidecode(self.address or '') - d['description'] = unidecode(self.description or '') + d['address'] = unidecode(unicode(self.address) or u'') + d['description'] = unidecode(unicode(self.description) or u'') return mark_safe(template.format(**d)) def as_tbody(self): @@ -432,9 +433,9 @@ def as_tbody(self): d['dop_triggers'] = self.dop_triggers_str d['referring_org'] = self.referring_org d['file_no'] = self.file_no or '' - d['description'] = unidecode(self.description or '') + d['description'] = unidecode(unicode(self.description) or u'') d['referral_date'] = self.referral_date.strftime('%d-%b-%Y') - d['address'] = unidecode(self.address or '') + d['address'] = unidecode(unicode(self.address) or u'') return mark_safe(template.format(**d).strip()) def add_relationship(self, referral): @@ -473,7 +474,8 @@ def generate_qgis_layer(self): # Read in the base Jinja template. t = Template(open('prs2/referral/templates/qgis_layer.jinja', 'r').read()) # Build geographical extent of associated locations. - xmin, ymin, xmax, ymax = self.location_set.current().filter(poly__isnull=False).extent() + qs = self.location_set.current().filter(poly__isnull=False).aggregate(models.Extent('poly')) + xmin, ymin, xmax, ymax = qs['poly__extent'] d = { 'REFERRAL_PK': self.pk} return t.render(**d) @@ -485,8 +487,10 @@ class RelatedReferral(models.Model): Trying to create this relationship without the intermediate class generated some really odd recursion errors. ''' - from_referral = models.ForeignKey(Referral, related_name='from_referral') - to_referral = models.ForeignKey(Referral, related_name='to_referral') + from_referral = models.ForeignKey( + Referral, on_delete=models.PROTECT, related_name='from_referral') + to_referral = models.ForeignKey( + Referral, on_delete=models.PROTECT, related_name='to_referral') def __unicode__(self): return unicode( @@ -503,10 +507,12 @@ class Task(ReferralBaseModel): This is how we record and manage our workflow. ''' type = models.ForeignKey( - TaskType, verbose_name='task type', help_text='The task type.') + TaskType, on_delete=models.PROTECT, verbose_name='task type', + help_text='The task type.') referral = models.ForeignKey(Referral) assigned_user = models.ForeignKey( - settings.AUTH_USER_MODEL, related_name='refer_task_assigned_user', + settings.AUTH_USER_MODEL, on_delete=models.PROTECT, + related_name='refer_task_assigned_user', help_text='The officer responsible for completing the task.') description = models.TextField( blank=True, null=True, help_text='Description of the task requirements.') @@ -524,7 +530,8 @@ class Task(ReferralBaseModel): stop_time = models.IntegerField( default=0, editable=False, help_text='Cumulative time stopped in days.') state = models.ForeignKey( - TaskState, verbose_name='status', help_text='The status of the task.') + TaskState, on_delete=models.PROTECT, verbose_name='status', + help_text='The status of the task.') headers = [ 'Task', 'Type', 'Task description', 'Address', 'Referral ID', 'Assigned', 'Start', 'Due', 'Completed', 'Status'] @@ -542,7 +549,7 @@ def save(self, force_insert=False, force_update=False, *args, **kwargs): '''Overide save() to cleanse text input to the description field. ''' if self.description: - self.description = unidecode(self.description) + self.description = unidecode(unicode(self.description)) super(Task, self).save(force_insert, force_update) def as_row(self): @@ -606,7 +613,7 @@ def as_row_actions(self): ''' - d['edit_url'] = reverse('task_action', kwargs={'pk': self.pk, 'action': 'edit'}) + d['edit_url'] = reverse('task_action', kwargs={'pk': self.pk, 'action': 'update'}) d['reassign_url'] = reverse( 'task_action', kwargs={ 'pk': self.pk, 'action': 'reassign'}) @@ -678,12 +685,12 @@ def as_row_for_site_home(self): template += '' d = copy(self.__dict__) d['type'] = self.type - d['description'] = unidecode(self.description or '') + d['description'] = unidecode(unicode(self.description) or u'') d['referral_url'] = self.referral.get_absolute_url() d['referral_pk'] = self.referral.pk d['referring_org'] = self.referral.referring_org d['reference'] = self.referral.reference - d['address'] = unidecode(self.referral.address or '') + d['address'] = unidecode(unicode(self.referral.address) or u'') if self.due_date: d['due_date'] = self.due_date.strftime('%d %b %Y') else: @@ -712,12 +719,12 @@ def as_row_for_index_print(self): {due_date}''' d = copy(self.__dict__) d['type'] = self.type - d['description'] = unidecode(self.description or '') + d['description'] = unidecode(unicode(self.description) or u'') d['referral_pk'] = self.referral.pk d['referring_org'] = self.referral.referring_org d['type'] = self.referral.type d['reference'] = self.referral.reference - d['address'] = unidecode(self.referral.address or '') + d['address'] = unidecode(unicode(self.referral.address) or u'') if self.due_date: d['due_date'] = self.due_date.strftime("%d %b %Y") else: @@ -771,7 +778,7 @@ def as_tbody(self): d['restart_date'] = '' if d['stop_time'] == 0: d['stop_time'] = '' - d['description'] = unidecode(self.description or '') + d['description'] = unidecode(unicode(self.description) or u'') return mark_safe(template.format(**d).strip()) def email_user(self, from_email=None): @@ -811,7 +818,7 @@ class Record(ReferralBaseModel): max_length=200, help_text='The name/description of the record (max 200 characters).', validators=[MaxLengthValidator(200)]) - referral = models.ForeignKey(Referral) + referral = models.ForeignKey(Referral, on_delete=models.PROTECT) uploaded_file = models.FileField( blank=True, null=True, @@ -837,9 +844,9 @@ def __unicode__(self): def save(self, force_insert=False, force_update=False, *args, **kwargs): '''Overide save() to cleanse text input fields. ''' - self.name = unidecode(self.name) + self.name = unidecode(unicode(self.name)) if self.description: - self.description = unidecode(self.description) + self.description = unidecode(unicode(self.description)) super(Record, self).save(force_insert, force_update) @property @@ -979,10 +986,10 @@ class Note(ReferralBaseModel): A note or comment about a referral. These notes are meant to supplement formal record-keeping procedures only. HTML-formatted text is allowed. ''' - referral = models.ForeignKey(Referral) + referral = models.ForeignKey(Referral, on_delete=models.PROTECT) type = models.ForeignKey( - NoteType, blank=True, null=True, verbose_name='note type', - help_text='The type of note (optional).') + NoteType, on_delete=models.PROTECT, blank=True, null=True, + verbose_name='note type', help_text='The type of note (optional).') note_html = models.TextField(verbose_name='note') note = models.TextField(editable=False) order_date = models.DateField( @@ -1002,7 +1009,7 @@ def save(self, force_insert=False, force_update=False, *args, **kwargs): ''' Overide the Note model save() to cleanse the HTML used. ''' - self.note_html = unidecode(dewordify_text(self.note_html)) + self.note_html = unidecode(unicode(dewordify_text(self.note_html))) self.note_html = clean.clean_html(self.note_html) # Strip HTML tags and save as plain text. t = fromstring(self.note_html) @@ -1011,7 +1018,7 @@ def save(self, force_insert=False, force_update=False, *args, **kwargs): @property def short_note(self, x=12): - text = unidecode(self.note) + text = unidecode(unicode(self.note)) text = text.replace('\n', ' ').replace('\r', ' ') # Replace newlines. words = text.split(' ') if len(words) > x: @@ -1042,7 +1049,7 @@ def as_row(self): d['order_date'] = self.order_date.strftime('%d %b %Y') else: d['order_date'] = '' - d['note'] = smart_truncate(unidecode(self.note), length=400) + d['note'] = smart_truncate(unicode(unidecode(self.note)), length=400) d['referral_url'] = self.referral.get_absolute_url() d['referral'] = self.referral return mark_safe(template.format(**d)) @@ -1091,7 +1098,7 @@ def as_tbody(self): d['order_date'] = self.order_date.strftime('%d-%b-%Y') else: d['order_date'] = '' - d['note_html'] = unidecode(self.note_html) + d['note_html'] = unidecode(unicode(self.note_html)) return mark_safe(template.format(**d).strip()) @@ -1105,7 +1112,8 @@ class Meta(ReferralLookup.Meta): class ModelCondition(ReferralBaseModel): """Represents a 'model condition' with standard text. """ - category = models.ForeignKey(ConditionCategory, blank=True, null=True) + category = models.ForeignKey( + ConditionCategory, on_delete=models.PROTECT, blank=True, null=True) condition = models.TextField(help_text="Model condition") identifier = models.CharField( max_length=100, blank=True, null=True, @@ -1117,7 +1125,8 @@ class Condition(ReferralBaseModel): """Model type to handle proposed & approved conditions on referrals. Note that referral may be blank; this denotes a "standard" model condition. """ - referral = models.ForeignKey(Referral, blank=True, null=True) + referral = models.ForeignKey( + Referral, on_delete=models.PROTECT, blank=True, null=True) condition = models.TextField(editable=False, blank=True, null=True) condition_html = models.TextField( blank=True, null=True, verbose_name='approved condition', @@ -1135,9 +1144,11 @@ class Condition(ReferralBaseModel): clearance_tasks = models.ManyToManyField( Task, through='Clearance', editable=False, symmetrical=True, related_name='clearance_requests') - category = models.ForeignKey(ConditionCategory, blank=True, null=True) + category = models.ForeignKey( + ConditionCategory, on_delete=models.PROTECT, blank=True, null=True) model_condition = models.ForeignKey( - ModelCondition, blank=True, null=True, related_name='model_condition', + ModelCondition, on_delete=models.PROTECT, blank=True, null=True, + related_name='model_condition', help_text='Model text on which this condition is based') headers = [ 'Condition', 'No.', 'Proposed condition', 'Approved condition', @@ -1146,10 +1157,10 @@ class Condition(ReferralBaseModel): def save(self, force_insert=False, force_update=False, *args, **kwargs): ''' - Overide the models's save() to cleanse the HTML input. + Overide the Condition models's save() to cleanse the HTML input. ''' if self.condition_html: - self.condition_html = unidecode(dewordify_text(self.condition_html)) + self.condition_html = unidecode(unicode(dewordify_text(self.condition_html))) self.condition_html = clean.clean_html(self.condition_html) t = fromstring(self.condition_html) self.condition = unicode(t.text_content()) @@ -1157,7 +1168,7 @@ def save(self, force_insert=False, force_update=False, *args, **kwargs): self.condition_html = '' self.condition = '' if self.proposed_condition_html: - self.proposed_condition_html = unidecode(dewordify_text(self.proposed_condition_html)) + self.proposed_condition_html = unidecode(unicode(dewordify_text(self.proposed_condition_html))) self.proposed_condition_html = clean.clean_html(self.proposed_condition_html) t = fromstring(self.proposed_condition_html) self.proposed_condition = unicode(t.text_content()) @@ -1289,8 +1300,8 @@ class Clearance(models.Model): ''' Intermediate class for relationships between Condition and Task objects. ''' - condition = models.ForeignKey(Condition) - task = models.ForeignKey(Task) + condition = models.ForeignKey(Condition, on_delete=models.PROTECT) + task = models.ForeignKey(Task, on_delete=models.PROTECT) date_created = models.DateField(auto_now_add=True) deposited_plan = models.CharField( max_length=200, null=True, blank=True, @@ -1330,7 +1341,7 @@ def as_row(self): else: d['category'] = '' if self.task.description: - d['task'] = smart_truncate(unidecode(self.task.description), length=400) + d['task'] = smart_truncate(unidecode(unicode(self.task.description)), length=400) else: d['task'] = self.task.type.name d['deposited_plan'] = self.deposited_plan or '' @@ -1354,14 +1365,14 @@ def as_tbody(self): d['referral'] = self.task.referral d['referral_url'] = self.task.referral.get_absolute_url() d['reference'] = self.task.referral.reference - d['referral_desc'] = unidecode(self.task.referral.description or '') + d['referral_desc'] = unidecode(unicode(self.task.referral.description) or u'') d['condition_url'] = reverse( 'prs_object_detail', kwargs={'pk': self.condition.pk, 'model': 'conditions'}) d['condition'] = self.condition d['condition_html'] = self.condition.condition_html d['task_url'] = reverse('prs_object_detail', kwargs={'pk': self.task.pk, 'model': 'tasks'}) d['task'] = self.task - d['task_desc'] = unidecode(self.task.description or '') + d['task_desc'] = unidecode(unicode(self.task.description) or u'') d['deposited_plan'] = self.deposited_plan or '' return mark_safe(template.format(**d).strip()) @@ -1390,7 +1401,7 @@ class Location(ReferralBaseModel): strata_lot_desc = models.TextField(null=True, blank=True) reserve = models.TextField(null=True, blank=True) cadastre_obj_id = models.IntegerField(null=True, blank=True) - referral = models.ForeignKey(Referral) + referral = models.ForeignKey(Referral, on_delete=models.PROTECT) poly = models.PolygonField(srid=4283, null=True, blank=True, help_text='Optional.') address_string = models.TextField(null=True, blank=True, editable=True) headers = ['Location', 'Address', 'Polygon', 'Referral ID'] @@ -1509,9 +1520,10 @@ class Bookmark(ReferralBaseModel): Inherits from the abstract model class ReferralBaseModel. Users are able to bookmark referrals for faster access. ''' - referral = models.ForeignKey(Referral) + referral = models.ForeignKey(Referral, on_delete=models.PROTECT) user = models.ForeignKey( - settings.AUTH_USER_MODEL, related_name='referral_user_bookmark') + settings.AUTH_USER_MODEL, on_delete=models.PROTECT, + related_name='referral_user_bookmark') description = models.CharField( max_length=200, blank=True, null=True, help_text='Maximum 200 characters.', @@ -1522,7 +1534,7 @@ def save(self, force_insert=False, force_update=False, *args, **kwargs): '''Overide save() to cleanse text input to the description field. ''' if self.description: - self.description = unidecode(self.description) + self.description = unidecode(unicode(self.description)) super(Bookmark, self).save(force_insert, force_update) def as_row(self): @@ -1567,7 +1579,7 @@ class UserProfile(models.Model): An extension of the Django auth model, to add additional fields to each User ''' user = models.OneToOneField(settings.AUTH_USER_MODEL) - agency = models.ForeignKey(Agency, blank=True, null=True) + agency = models.ForeignKey(Agency, on_delete=models.PROTECT, blank=True, null=True) # Referral history is a list of 2-tuples: (referral pk, datetime) referral_history = models.TextField(blank=True, null=True) task_history = models.TextField(blank=True, null=True) diff --git a/prs2/referral/static/js/referral_map.js b/prs2/referral/static/js/referral_map.js index c5126bd3..94ae05ea 100644 --- a/prs2/referral/static/js/referral_map.js +++ b/prs2/referral/static/js/referral_map.js @@ -94,6 +94,9 @@ L.control.layers(baseMaps, overlayMaps).addTo(map); // Define scale bar L.control.scale({maxWidth: 500, imperial: false}).addTo(map); +// Add a fullscreen control. +L.control.fullscreen().addTo(map); + // Define geocoder search input. map.addControl(new L.Control.Search({ // OSM Noninatum geocoder URL diff --git a/prs2/referral/templates/referral/change_form.html b/prs2/referral/templates/referral/change_form.html index 6cbe00b2..d15ad77c 100644 --- a/prs2/referral/templates/referral/change_form.html +++ b/prs2/referral/templates/referral/change_form.html @@ -7,7 +7,6 @@ {{ form.media }} - {% endblock %} {% block page_content_inner %} @@ -18,7 +17,6 @@

{{ title }}

{% block extra_js %} {{ block.super }} - - - + +{##} + - + + -{% comment %} - - -{% endcomment %} {% endif %} @@ -203,7 +198,13 @@

Key issues:

// Configure file upload Dropzone element. Dropzone.options.idReferralDropzone = { paramName: "file", - acceptedFiles: ".tif,.jpg,.gif,.png,.doc,.docx,.xls,.xlsx,.csv,.pdf,.txt,.zip,.msg,.qgs", + acceptedFiles: ".tif,.jpg,.jpeg,.gif,.png,.doc,.docx,.xls,.xlsx,.csv,.pdf,.txt,.zip,.msg,.qgs", + init: function() { + this.on("queuecomplete", function(file) { + // Reload the current page on completed upload(s). + location.assign("{% url 'referral_detail' pk=referral.pk related_model='records' %}"); + }); + }, }; // Initialise all DataTables. $.fn.dataTable.moment("dd MMM YYYY", "en-AU"); @@ -223,6 +224,5 @@

Key issues:

// Hide the map viewer by default. $("#id_collapseMap").collapse("hide"); {% endif %} - {% endblock extra_js %} diff --git a/prs2/referral/test_views.py b/prs2/referral/test_views.py index 6d64bcf6..6db3ed4a 100644 --- a/prs2/referral/test_views.py +++ b/prs2/referral/test_views.py @@ -1,4 +1,5 @@ from __future__ import absolute_import, print_function, unicode_literals +from datetime import date from django.contrib.auth import get_user_model from django.core.urlresolvers import reverse from django.test import Client @@ -561,3 +562,93 @@ def test_get(self): self.assertEquals(response.status_code, 200) self.assertTrue(self.ref_tagged in response.context['object_list']) self.assertFalse(self.ref_untagged in response.context['object_list']) + + +class TaskActionTest(PrsViewsTestCase): + + def setUp(self): + super(TaskActionTest, self).setUp() + self.task = Task.objects.all()[0] + + def test_get_update(self): + """Test the Task update view responds + """ + url = reverse('task_action', kwargs={'pk': self.task.pk, 'action': 'update'}) + response = self.client.get(url) + self.assertEquals(response.status_code, 200) + + def test_cant_update_stopped_task(self): + """Test that a stopped task can't be updated + """ + self.task.stop_date = date.today() + self.task.save() + url = reverse('task_action', kwargs={'pk': self.task.pk, 'action': 'update'}) + response = self.client.get(url) + # Response should be a redirect to the object URL. + self.assertRedirects(response, self.task.get_absolute_url()) + # Test that the redirected response contains an error message. + response = self.client.get(url, follow=True) + messages = response.context['messages']._get()[0] + self.assertIsNot(messages[0].message.find("You can't edit a stopped task"), -1) + + def test_cant_stop_completed_task(self): + """Test that a completed task can't be stopped + """ + self.task.complete_date = date.today() + self.task.save() + url = reverse('task_action', kwargs={'pk': self.task.pk, 'action': 'stop'}) + response = self.client.get(url) + # Response should be a redirect to the object URL. + self.assertRedirects(response, self.task.get_absolute_url()) + # Test that the redirected response contains an error message. + response = self.client.get(url, follow=True) + messages = response.context['messages']._get()[0] + self.assertIsNot(messages[0].message.find("You can't stop a completed task"), -1) + + def test_cant_restart_unstopped_task(self): + """Test that a non-stopped task can't be started + """ + url = reverse('task_action', kwargs={'pk': self.task.pk, 'action': 'start'}) + response = self.client.get(url) + # Response should be a redirect to the object URL. + self.assertRedirects(response, self.task.get_absolute_url()) + # Test that the redirected response contains an error message. + response = self.client.get(url, follow=True) + messages = response.context['messages']._get()[0] + self.assertIsNot(messages[0].message.find("You can't restart a non-stopped task"), -1) + + def test_cant_inherit_owned_task_task(self): + """Test that you can't inherit a task assigned to you + """ + self.task.assigned_user = self.n_user + self.task.save() + url = reverse('task_action', kwargs={'pk': self.task.pk, 'action': 'inherit'}) + response = self.client.get(url) + # Response should be a redirect to the object URL. + self.assertRedirects(response, self.task.get_absolute_url()) + # Test that the redirected response contains an error message. + response = self.client.get(url, follow=True) + messages = response.context['messages']._get()[0] + self.assertIsNot(messages[0].message.find("That task is already assigned to you"), -1) + + def test_cant_cancel_completed_task(self): + """Test that a completed task can't be cancelled + """ + self.task.complete_date = date.today() + self.task.save() + url = reverse('task_action', kwargs={'pk': self.task.pk, 'action': 'cancel'}) + response = self.client.get(url) + # Response should be a redirect to the object URL. + self.assertRedirects(response, self.task.get_absolute_url()) + # Test that the redirected response contains an error message. + response = self.client.get(url, follow=True) + messages = response.context['messages']._get()[0] + self.assertIsNot(messages[0].message.find('That task is already completed'), -1) + + def test_cant_add_task_to_task(self): + """Test that a task can't be added to another task + """ + url = reverse('task_action', kwargs={'pk': self.task.pk, 'action': 'add'}) + response = self.client.get(url) + # Response should be a redirect to the object URL. + self.assertRedirects(response, self.task.get_absolute_url()) diff --git a/prs2/referral/urls.py b/prs2/referral/urls.py index e5799a9f..40fd7431 100644 --- a/prs2/referral/urls.py +++ b/prs2/referral/urls.py @@ -1,10 +1,9 @@ -from django.conf.urls import patterns, url +from django.conf.urls import url from referral.models import Referral, Record, Task from referral import views # URL patterns for Referral objects -urlpatterns = patterns( - '', +urlpatterns = [ url(r'^referrals/create/$', views.ReferralCreate.as_view(), name='referral_create'), url(r'^referrals/recent/$', views.ReferralRecent.as_view(), name='referral_recent'), url(r'^referrals/tagged/(?P[-\w]+)/$', views.ReferralTagged.as_view(), name='referral_tagged'), @@ -22,11 +21,10 @@ url(r'^referrals/(?P\d+)/(?P\w+)/create/(?P\w+)/$', views.ReferralCreateChild.as_view(), name='referral_create_child_type'), url(r'^referrals/(?P\d+)/(?P\w+)/(?P\d+)/(?P\w+)/$', views.ReferralCreateChild.as_view(), name='referral_create_child_related'), url(r'^referrals/(?P\d+)/locations/intersecting/(?P\w+)/$', views.LocationIntersects.as_view(), name='referral_intersecting_locations'), -) +] # URL patterns for other model types requiring specific views -urlpatterns += patterns( - '', +urlpatterns += [ url(r'^bookmarks/$', views.BookmarkList.as_view(), name='bookmark_list'), url(r'^tags/$', views.TagList.as_view(), name='tag_list'), url(r'^tags/replace/$', views.TagReplace.as_view(), name='tag_replace'), @@ -35,11 +33,10 @@ url(r'^conditions/(?P\d+)/clearance/$', views.ConditionClearanceCreate.as_view(), name='condition_clearance_add'), url(r'^records/(?P\d+)/infobase/$', views.InfobaseShortcut.as_view(), name='infobase_shortcut'), url(r'^records/(?P\d+)/download/$', views.ReferralDownloadView.as_view(model=Record, file_field='uploaded_file'), name='download_record'), -) +] # Other static/functional URLs -urlpatterns += patterns( - '', +urlpatterns += [ url(r'^help/$', views.HelpPage.as_view(), name='help_page'), url(r'^search/$', views.GeneralSearch.as_view(), name='prs_general_search'), url(r'^stopped-tasks/$', views.SiteHome.as_view(stopped_tasks=True), name='stopped_tasks_list'), @@ -53,4 +50,4 @@ url(r'^(?P\w+)/(?P\d+)/delete/$', views.PrsObjectDelete.as_view(), name='prs_object_delete'), url(r'^(?P\w+)/(?P\d+)/tag/$', views.PrsObjectTag.as_view(), name='prs_object_tag'), url(r'^$', views.SiteHome.as_view(printable=False), name='site_home'), -) +] diff --git a/prs2/referral/utils.py b/prs2/referral/utils.py index 389ed8fc..7c69a54a 100644 --- a/prs2/referral/utils.py +++ b/prs2/referral/utils.py @@ -1,13 +1,15 @@ -# Django imports +from confy import env +from datetime import datetime from django.apps import apps from django.db.models import Q from django.db.models.base import ModelBase from django.utils.safestring import mark_safe from django.contrib import admin -import re from django.utils.encoding import smart_str -from datetime import datetime +from dpaw_utils.requests.api import post as post_sso import json +from reversion.models import Version +import re def is_model_or_string(model): @@ -217,9 +219,45 @@ def is_prs_power_user(request): return True -def is_superuser(request): - return request.user.is_superuser +def prs_user(request): + return is_prs_user(request) or is_prs_power_user(request) or request.user.is_superuser -def prs_user(request): - return is_prs_user(request) or is_prs_power_user(request) or is_superuser(request) +def update_revision_history(app_model): + """Function to bulk-update Version objects where the data model + is changed. This function is for reference, as these change will tend to + be one-off and customised. + + Example: the order_date field was added the the Record model, then later + changed from DateTime to Date. This change caused the deserialisation step + to fail for Record versions with a serialised DateTime. + """ + for v in Version.objects.all(): + # Deserialise the object version. + data = json.loads(v.serialized_data)[0] + if data['model'] == app_model: # Example: referral.record + pass + """ + # Do something to the deserialised data here, e.g.: + if 'order_date' in data['fields']: + if data['fields']['order_date']: + data['fields']['order_date'] = data['fields']['order_date'][:10] + v.serialized_data = json.dumps([data]) + v.save() + else: + data['fields']['order_date'] = '' + v.serialized_data = json.dumps([data]) + v.save() + """ + + +def borgcollector_harvest(request, publishes=['prs_locations']): + """Convenience function to manually run a Borg Collector harvest + job for the PRS locations layer. + + Docs: https://github.com/parksandwildlife/borgcollector + """ + api_url = env('BORGCOLLECTOR_API', 'https://borg.dpaw.wa.gov.au/api/') + 'jobs/' + # Send a POST request to the API endpoint. + r = post_sso(user_request=request, url=api_url, data=json.dumps({'publishes': publishes})) + return r diff --git a/prs2/referral/views.py b/prs2/referral/views.py index c023f04b..daf9a71c 100644 --- a/prs2/referral/views.py +++ b/prs2/referral/views.py @@ -15,8 +15,10 @@ from django.utils.safestring import mark_safe from django.views.decorators.csrf import csrf_exempt from django.views.generic import View, ListView, TemplateView, FormView +from django_downloadview import ObjectDownloadView import json import re +from taggit.models import Tag from referral.models import ( Task, Clearance, Referral, Condition, Note, Record, Location, @@ -25,7 +27,7 @@ from referral.utils import ( is_model_or_string, breadcrumbs_li, smart_truncate, get_query, user_task_history, user_referral_history, filter_queryset, - prs_user, is_prs_power_user) + prs_user, is_prs_power_user, borgcollector_harvest) from referral.forms import ( ReferralCreateForm, NoteForm, NoteAddExistingForm, RecordCreateForm, RecordAddExistingForm, TaskCreateForm, @@ -38,8 +40,6 @@ from referral.views_base import ( PrsObjectDetail, PrsObjectList, PrsObjectCreate, PrsObjectUpdate, PrsObjectDelete, PrsObjectHistory, PrsObjectTag) -from django_downloadview import ObjectDownloadView -from taggit.models import Tag class SiteHome(LoginRequiredMixin, ListView): @@ -282,6 +282,7 @@ def get_context_data(self, **kwargs): obj_tab = 'tab_{}'.format(m._meta.model_name) obj_list = '{}_list'.format(m._meta.model_name) if m.objects.current().filter(referral=ref): + context['{}_count'.format(m._meta.object_name.lower())] = m.objects.current().filter(referral=ref).count() obj_qs = m.objects.current().filter(referral=ref) headers = copy(m.headers) headers.remove('Referral ID') @@ -296,6 +297,7 @@ def get_context_data(self, **kwargs): context[obj_tab] = mark_safe(obj_tab_html) context[obj_list] = obj_qs else: + context['{}_count'.format(m._meta.object_name.lower())] = 0 context[obj_tab] = 'No {} found for this referral'.format( m._meta.verbose_name_plural) context[obj_list] = None @@ -691,6 +693,7 @@ def get_context_data(self, **kwargs): ] context['breadcrumb_trail'] = breadcrumbs_li(links) context['title'] = 'CREATE LOCATION(S)' + context['address'] = ref.address # Add any existing referral locations serialised as GeoJSON. if any([l.poly for l in ref.location_set.current()]): context['geojson_locations'] = serialize( @@ -736,6 +739,14 @@ def post(self, request, *args, **kwargs): locations.append(l) messages.success(request, '{} location(s) created.'.format(len(forms))) + + # Call the Borg Collector publish API endpoint to create a manual job + # to update the prs_locations layer. + resp = borgcollector_harvest(self.request) + logger.info('Borg Collector API response status was {}'.format(resp.status_code)) + logger.info('Borg Collector API response: {}'.format(resp.content)) + + # Test for intersecting locations. intersecting_locations = self.polygon_intersects(locations) if intersecting_locations: # Redirect to a view where users can create relationships between referrals. @@ -857,7 +868,7 @@ def post(self, request, *args, **kargs): class TaskAction(PrsObjectUpdate): ''' A customised view is used for editing Tasks because of the additional business logic. - ``action`` includes add, stop, start, reassign, complete, cancel, inherit, edit, + ``action`` includes add, stop, start, reassign, complete, cancel, inherit, update, addrecord, addnewrecord, addnote, addnewnote NOTE: does not include the 'delete' action (handled by separate view). ''' @@ -875,7 +886,7 @@ def get(self, request, *args, **kwargs): action = self.kwargs['action'] task = self.get_object() - if action == 'edit' and task.stop_date and not task.restart_date: + if action == 'update' and task.stop_date and not task.restart_date: messages.error(request, "You can't edit a stopped task - restart the task first!") return redirect(task.get_absolute_url()) if action == 'stop' and task.complete_date: @@ -885,10 +896,10 @@ def get(self, request, *args, **kwargs): messages.error(request, "You can't restart a non-stopped task!") return redirect(task.get_absolute_url()) if action == 'inherit' and task.assigned_user == request.user: + messages.info(request, 'That task is already assigned to you.') return redirect(task.get_absolute_url()) - if action == 'complete' and task.complete_date: - return redirect(task.get_absolute_url()) - if action == 'cancel' and task.complete_date: + if action in ['complete', 'cancel'] and task.complete_date: + messages.info(request, 'That task is already completed.') return redirect(task.get_absolute_url()) # We can't (yet) add a task to a task. if action == 'add': @@ -976,7 +987,7 @@ def form_valid(self, form): ) if self.request.POST.get('email_user'): obj.email_user(self.request.user.email) - elif action == 'edit': + elif action == 'update': if obj.restart_date and obj.stop_date: obj.stop_time = (obj.restart_date - obj.stop_date).days elif action == 'complete': @@ -1038,10 +1049,9 @@ def form_valid(self, form): messages.warning(self.request, msg) return self.form_invalid(form) elif obj.state in trigger_outcome and form_data['tags']: - tag_names = form_data['tags'].split(',') # Save tags on the parent referral. - for name in tag_names: - obj.referral.tags.add(name) + for tag in form_data['tags']: + obj.referral.tags.add(tag) obj.modifier = self.request.user obj.save() @@ -1143,8 +1153,7 @@ def get(self, request, *args, **kwargs): return super(TagList, self).get(request, *args, **kwargs) def get_queryset(self): - qs = super(TagList, self).get_queryset() - return qs.order_by('name') + return Tag.objects.all().order_by('name') class TagReplace(LoginRequiredMixin, FormView): @@ -1280,6 +1289,11 @@ def post(self, request, *args, **kwargs): i.delete() ref.delete() messages.success(request, '{0} deleted.'.format(self.model._meta.object_name)) + # Call the Borg Collector publish API endpoint to create a manual job + # to update the prs_locations layer. + resp = borgcollector_harvest(self.request) + logger.info('Borg Collector API response status was {}'.format(resp.status_code)) + logger.info('Borg Collector API response: {}'.format(resp.content)) return redirect('site_home') diff --git a/prs2/referral/views_base.py b/prs2/referral/views_base.py index f4217d90..69501757 100644 --- a/prs2/referral/views_base.py +++ b/prs2/referral/views_base.py @@ -13,11 +13,16 @@ from django.views.generic import ( View, ListView, DetailView, CreateView, UpdateView, DeleteView) import json -from reversion import get_for_object +import logging +from reversion.revisions import get_for_object from taggit.models import Tag -from .forms import FORMS_MAP, ReferralForm -from .utils import is_model_or_string, breadcrumbs_li, get_query, prs_user +from referral.forms import FORMS_MAP, ReferralForm +from referral.utils import ( + is_model_or_string, breadcrumbs_li, get_query, prs_user, + borgcollector_harvest) + +logger = logging.getLogger('prs.log') class PrsObjectList(LoginRequiredMixin, ListView): @@ -42,7 +47,7 @@ def get_queryset(self): By default, the queryset return recently-modified ones objects first. ''' qs = super(PrsObjectList, self).get_queryset() - if 'effective_to' in self.model._meta.get_all_field_names(): + if 'effective_to' in [f.name for f in self.model._meta.get_fields()]: qs = qs.filter(effective_to=None) # Did we pass in a search string? If so, filter the queryset and # return it. @@ -72,7 +77,6 @@ def get_context_data(self, **kwargs): context['page_title'] = ' | '.join([settings.APPLICATION_ACRONYM, title]) links = [(reverse('site_home'), 'Home'), (None, title)] context['breadcrumb_trail'] = breadcrumbs_li(links) - context['queryset_count'] = self.get_queryset().count() return context @@ -135,7 +139,7 @@ def form_valid(self, form): # and redirects to get_success_url(). self.object = form.save(commit=False) # Handle models that inherit from Audit abstract model. - f = self.model._meta.get_all_field_names() + f = [field.name for field in self.model._meta.get_fields()] if 'creator' in f and 'modifier' in f: self.object.creator, self.object.modifier = self.request.user, self.request.user # Handle slug fields. @@ -257,7 +261,7 @@ def get_form_class(self): def get_context_data(self, **kwargs): context = super(PrsObjectUpdate, self).get_context_data(**kwargs) obj = self.get_object() - context['title'] = 'UPDATE {}'.format(self.get_object().__unicode__()).upper() + context['title'] = 'UPDATE {}'.format(obj._meta.object_name).upper() context['page_title'] = 'PRS | {} | {} | Update'.format( obj._meta.verbose_name_plural.capitalize(), obj.pk) @@ -347,7 +351,7 @@ def get_context_data(self, **kwargs): context = super(PrsObjectDelete, self).get_context_data(**kwargs) context['object_type'] = self.model._meta.verbose_name obj = self.get_object() - context['title'] = 'DELETE: {}'.format(obj) + context['title'] = 'DELETE {}'.format(obj._meta.object_name.upper()) context['page_title'] = ' | '.join([ settings.APPLICATION_ACRONYM, 'Delete {}'.format(self.get_object().__unicode__())]) @@ -390,6 +394,15 @@ def delete(self, request, *args, **kwargs): else: success_url = self.get_success_url() obj.delete() + + # For Location objects: + # Call the Borg Collector publish API endpoint to create a manual job + # to update the prs_locations layer. + if obj._meta.object_name == 'Location': + resp = borgcollector_harvest(self.request) + logger.info('Borg Collector API response status was {}'.format(resp.status_code)) + logger.info('Borg Collector API response: {}'.format(resp.content)) + messages.success(self.request, '{0} has been deleted.'.format(obj)) return HttpResponseRedirect(success_url) diff --git a/prs2/reports/views.py b/prs2/reports/views.py index 4c45333a..d9789612 100644 --- a/prs2/reports/views.py +++ b/prs2/reports/views.py @@ -3,7 +3,8 @@ from django.http import HttpResponse from django.views.generic import TemplateView from openpyxl import Workbook -from referral.models import Referral, Clearance, Task +from openpyxl.styles import Font +from referral.models import Referral, Clearance, Task, TaskType from referral.utils import breadcrumbs_li, is_model_or_string, prs_user @@ -41,6 +42,10 @@ def get(self, request): # Generate a blank Excel workbook. wb = Workbook() ws = wb.active # The worksheet + # Default font for all cells. + arial = Font(name='Arial', size=10) + # Define a date style. + date_style = 'dd/mm/yyyy' # Generate a HTTPResponse object to write to. response = HttpResponse( @@ -49,7 +54,7 @@ def get(self, request): if model == Referral: response['Content-Disposition'] = 'attachment; filename=prs_referrals.xlsx' # Filter referral objects according to the parameters. - referrals = Referral.objects.filter(**query_params) + referrals = Referral.objects.current().filter(**query_params) # Write the column headers to the new worksheet. headers = [ 'Referral ID', 'Region(s)', 'Referrer', 'Type', 'Reference', @@ -58,134 +63,200 @@ def get(self, request): for col, value in enumerate(headers, 1): cell = ws.cell(row=1, column=col) cell.value = value + cell.font = arial # Write the referral values to the worksheet. for row, r in enumerate(referrals, 2): # Start at row 2 cell = ws.cell(row=row, column=1) cell.value = r.pk + cell.font = arial cell = ws.cell(row=row, column=2) cell.value = r.regions_str + cell.font = arial cell = ws.cell(row=row, column=3) cell.value = r.referring_org.name + cell.font = arial cell = ws.cell(row=row, column=4) cell.value = r.type.name + cell.font = arial cell = ws.cell(row=row, column=5) cell.value = r.reference + cell.font = arial cell = ws.cell(row=row, column=6) - cell.value = r.referral_date.strftime('%d/%b/%Y') + cell.value = r.referral_date + cell.number_format = date_style + cell.font = arial cell = ws.cell(row=row, column=7) cell.value = r.description + cell.font = arial cell = ws.cell(row=row, column=8) cell.value = r.address + cell.font = arial cell = ws.cell(row=row, column=9) cell.value = ', '.join([t.name for t in r.dop_triggers.all()]) + cell.font = arial cell = ws.cell(row=row, column=10) cell.value = ', '.join([t.name for t in r.tags.all()]) + cell.font = arial cell = ws.cell(row=row, column=11) cell.value = r.file_no + cell.font = arial elif model == Clearance: response['Content-Disposition'] = 'attachment; filename=prs_clearance_requests.xlsx' # Filter clearance objects according to the parameters. - clearances = Clearance.objects.filter(**query_params) + clearances = Clearance.objects.current().filter(**query_params) # Write the column headers to the new worksheet. headers = [ 'Referral ID', 'Region(s)', 'Reference', 'Condition no.', 'Approved condition', 'Category', 'Task description', 'Deposited plan no.', 'Assigned user', 'Status', 'Start date', - 'Due date', 'Complete date'] + 'Due date', 'Complete date', 'Stop date', 'Restart date', + 'Total stop days'] for col, value in enumerate(headers, 1): cell = ws.cell(row=1, column=col) cell.value = value + cell.font = arial # Write the clearance values to the worksheet. for row, c in enumerate(clearances, 2): # Start at row 2 cell = ws.cell(row=row, column=1) cell.value = c.condition.referral.pk + cell.font = arial cell = ws.cell(row=row, column=2) cell.value = c.condition.referral.regions_str + cell.font = arial cell = ws.cell(row=row, column=3) cell.value = c.condition.referral.reference + cell.font = arial cell = ws.cell(row=row, column=4) cell.value = c.condition.identifier + cell.font = arial cell = ws.cell(row=row, column=5) cell.value = c.condition.condition + cell.font = arial cell = ws.cell(row=row, column=6) + cell.font = arial if c.condition.category: cell.value = c.condition.category.name cell = ws.cell(row=row, column=7) cell.value = c.task.description + cell.font = arial cell = ws.cell(row=row, column=8) cell.value = c.deposited_plan + cell.font = arial cell = ws.cell(row=row, column=9) cell.value = c.task.assigned_user.get_full_name() + cell.font = arial cell = ws.cell(row=row, column=10) cell.value = c.task.state.name + cell.font = arial cell = ws.cell(row=row, column=11) - if c.task.start_date: - cell.value = c.task.start_date.strftime('%d/%b/%Y') + cell.value = c.task.start_date + cell.number_format = date_style + cell.font = arial cell = ws.cell(row=row, column=12) - if c.task.due_date: - cell.value = c.task.due_date.strftime('%d/%b/%Y') + cell.value = c.task.due_date + cell.number_format = date_style + cell.font = arial cell = ws.cell(row=row, column=13) - if c.task.complete_date: - cell.value = c.task.complete_date.strftime('%d/%b/%Y') + cell.value = c.task.complete_date + cell.number_format = date_style + cell.font = arial + cell = ws.cell(row=row, column=14) + cell.value = c.task.stop_date + cell.number_format = date_style + cell.font = arial + cell = ws.cell(row=row, column=15) + cell.value = c.task.restart_date + cell.number_format = date_style + cell.font = arial + cell = ws.cell(row=row, column=16) + cell.value = c.task.stop_time + cell.font = arial elif model == Task: response['Content-Disposition'] = 'attachment; filename=prs_tasks.xlsx' # Filter task objects according to the parameters. - tasks = Task.objects.filter(**query_params) + tasks = Task.objects.current().filter(**query_params) + # Business rule: filter out 'Condition clearance' task types. + cr = TaskType.objects.get(name='Conditions clearance request') + tasks = tasks.exclude(type=cr) # Write the column headers to the new worksheet. headers = [ 'Task ID', 'Region(s)', 'Referral ID', 'Referred by', 'Referral type', 'Reference', 'Referral received', 'Task type', 'Task status', 'Assigned user', 'Task start', 'Task due', 'Task complete', 'Stop date', 'Restart date', 'Total stop days', - 'File no.', 'DoP triggers', 'Referral description'] + 'File no.', 'DoP triggers', 'Referral description', 'Referral address'] for col, value in enumerate(headers, 1): cell = ws.cell(row=1, column=col) cell.value = value + cell.font = arial # Write the task values to the worksheet. for row, t in enumerate(tasks, 2): # Start at row 2 cell = ws.cell(row=row, column=1) cell.value = t.pk + cell.font = arial cell = ws.cell(row=row, column=2) cell.value = t.referral.regions_str + cell.font = arial cell = ws.cell(row=row, column=3) cell.value = t.referral.pk + cell.font = arial cell = ws.cell(row=row, column=4) cell.value = t.referral.referring_org.name + cell.font = arial cell = ws.cell(row=row, column=5) cell.value = t.referral.type.name + cell.font = arial cell = ws.cell(row=row, column=6) cell.value = t.referral.reference + cell.font = arial cell = ws.cell(row=row, column=7) - cell.value = t.referral.referral_date.strftime('%d/%b/%Y') + cell.value = t.referral.referral_date + cell.number_format = date_style + cell.font = arial cell = ws.cell(row=row, column=8) cell.value = t.type.name + cell.font = arial cell = ws.cell(row=row, column=9) cell.value = t.state.name + cell.font = arial cell = ws.cell(row=row, column=10) cell.value = t.assigned_user.get_full_name() + cell.font = arial cell = ws.cell(row=row, column=11) - if t.start_date: - cell.value = t.start_date.strftime('%d/%b/%Y') + cell.value = t.start_date + cell.number_format = date_style + cell.font = arial cell = ws.cell(row=row, column=12) - if t.due_date: - cell.value = t.due_date.strftime('%d/%b/%Y') + cell.value = t.due_date + cell.number_format = date_style + cell.font = arial cell = ws.cell(row=row, column=13) - if t.complete_date: - cell.value = t.complete_date.strftime('%d/%b/%Y') + cell.value = t.complete_date + cell.number_format = date_style + cell.font = arial cell = ws.cell(row=row, column=14) - if t.stop_date: - cell.value = t.stop_date.strftime('%d/%b/%Y') + cell.value = t.stop_date + cell.number_format = date_style + cell.font = arial cell = ws.cell(row=row, column=15) - if t.restart_date: - cell.value = t.restart_date.strftime('%d/%b/%Y') + cell.value = t.restart_date + cell.number_format = date_style + cell.font = arial cell = ws.cell(row=row, column=16) cell.value = t.stop_time + cell.font = arial cell = ws.cell(row=row, column=17) cell.value = t.referral.file_no + cell.font = arial cell = ws.cell(row=row, column=18) cell.value = ', '.join([i.name for i in t.referral.dop_triggers.all()]) + cell.font = arial cell = ws.cell(row=row, column=19) cell.value = t.referral.description + cell.font = arial + cell = ws.cell(row=row, column=20) + cell.value = t.referral.address + cell.font = arial wb.save(response) # Save the workbook contents to the response. return response diff --git a/prs2/settings.py b/prs2/settings.py index 9e00a6d3..60faf73e 100644 --- a/prs2/settings.py +++ b/prs2/settings.py @@ -97,10 +97,9 @@ ('Cho Lamb', 'cho.lamb@dpaw.wa.gov.au', '9442 0309'), ) LOGIN_URL = '/login/' -#LOGIN_REDIRECT_URL = '/' APPLICATION_TITLE = 'Planning Referral System' APPLICATION_ACRONYM = 'PRS' -APPLICATION_VERSION_NO = '2.0.3' +APPLICATION_VERSION_NO = '2.1' APPLICATION_ALERTS_EMAIL = 'PRS-Alerts@dpaw.wa.gov.au' SITE_URL = env('SITE_URL', 'localhost') PRS_USER_GROUP = env('PRS_USER_GROUP', 'PRS user') @@ -177,9 +176,10 @@ os.mkdir(os.path.join(BASE_DIR, 'logs')) LOGGING = { 'version': 1, + 'disable_existing_loggers': False, 'formatters': { - 'verbose': { - 'format': '%(levelname)s %(asctime)s %(module)s %(message)s' + 'simple': { + 'format': '%(levelname)s %(message)s' }, }, 'handlers': { @@ -187,8 +187,8 @@ 'level': 'INFO', 'class': 'logging.handlers.RotatingFileHandler', 'filename': os.path.join(BASE_DIR, 'logs', 'prs.log'), - 'formatter': 'verbose', - 'maxBytes': '16777216' + 'formatter': 'simple', + 'maxBytes': 1024 * 1024 * 5 }, }, 'loggers': { @@ -196,7 +196,7 @@ 'handlers': ['file'], 'level': 'INFO' }, - 'log': { + 'prs.log': { 'handlers': ['file'], 'level': 'INFO' }, @@ -204,9 +204,10 @@ } DEBUG_LOGGING = { 'version': 1, + 'disable_existing_loggers': False, 'formatters': { 'verbose': { - 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' + 'format': '%(levelname)s %(asctime)s %(module)s %(message)s' }, }, 'handlers': { @@ -215,7 +216,7 @@ 'class': 'logging.handlers.RotatingFileHandler', 'filename': os.path.join(BASE_DIR, 'logs', 'debug-prs.log'), 'formatter': 'verbose', - 'maxBytes': '16777216' + 'maxBytes': 1024 * 1024 * 5 }, }, 'loggers': { @@ -223,7 +224,7 @@ 'handlers': ['file'], 'level': 'DEBUG' }, - 'log': { + 'prs.log': { 'handlers': ['file'], 'level': 'DEBUG' }, diff --git a/prs2/templates/site_home.html b/prs2/templates/site_home.html index 5e75165f..97bde54a 100644 --- a/prs2/templates/site_home.html +++ b/prs2/templates/site_home.html @@ -14,7 +14,7 @@

{% if stopped_tasks %}STOPPED TASKS{% else %}ONGOING TASKS{% endif %}

{% else %}
{% endif %} -{% if object_list %} +{% if object_list %}{# List of non-stopped tasks #}
@@ -25,8 +25,8 @@

{% if stopped_tasks %}STOPPED TASKS{% else %}ONGOING TASKS{% endif %}

- {% for object in object_list %} - {{ object.as_row_for_site_home|safe }} + {% for task in object_list %} + {{ task.as_row_for_site_home }} {% endfor %} diff --git a/prs2/urls.py b/prs2/urls.py index adf5eeb5..e81319a6 100644 --- a/prs2/urls.py +++ b/prs2/urls.py @@ -1,32 +1,27 @@ from django.conf import settings -from django.conf.urls import patterns, include, url +from django.conf.urls import include, url from django.contrib import admin +from django.contrib.auth.views import login, logout from api import v1_api admin.autodiscover() -urlpatterns = patterns( - '', +urlpatterns = [ url(r'^admin/', include(admin.site.urls)), - url(r'^login/$', 'django.contrib.auth.views.login', name='login', - kwargs={'template_name': 'login.html'}), - url(r'^logout/$', 'django.contrib.auth.views.logout', name='logout', - kwargs={'template_name': 'logged_out.html'}), + url(r'^login/$', login, name='login', kwargs={'template_name': 'login.html'}), + url(r'^logout/$', logout, name='logout', kwargs={'template_name': 'logged_out.html'}), url(r'^explorer/', include('explorer.urls')), # django-sql-explorer -) +] # Additional URLS for development/debug. if settings.DEBUG: # Add in Debug Toolbar URLs. import debug_toolbar - urlpatterns += patterns( - '', (r'^__debug__/', include(debug_toolbar.urls)), - ) + urlpatterns.append(url(r'^__debug__/', include(debug_toolbar.urls))) # PRS project URLs - must be placed after the debug_toolbar URLs. -urlpatterns += patterns( - '', +urlpatterns += [ url(r'^api/', include(v1_api.urls)), # All API views are registered in api.py url(r'^reports/', include('reports.urls')), url(r'^', include('referral.urls')), -) +] diff --git a/requirements.txt b/requirements.txt index e1f2d557..a8a569c1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,32 +1,31 @@ # Project requirements -Django==1.8.7 -django-extensions==1.5.9 -https://static.dpaw.wa.gov.au/static/py/dpaw-utils/dist/dpaw-utils-0.3a3.tar.gz +Django==1.9.7 +django-extensions==1.6.1 +https://static.dpaw.wa.gov.au/static/py/dpaw-utils/dist/dpaw-utils-0.3a11.tar.gz +django-reversion==1.10.1 +django-debug-toolbar==1.4 +django-tastypie==0.13.3 +django-taggit==0.19.0 +django-braces==1.8.1 +django-autoslug==1.9.3 +django-crispy-forms==1.6.0 +django-sql-explorer==0.9.2 +django-model-utils==2.4 +django-storages==1.4 +django-bootstrap-pagination==1.6.1 +dj-static==0.0.6 Unipath==1.1 +Unidecode==0.04.18 Fabric==1.10.1 jinja2==2.8 -dj-static==0.0.6 -django-model-utils==2.3.1 -django-reversion==1.9.3 -django-guardian==1.3 -django-storages==1.1.8 -django-debug-toolbar==1.3.2 Pillow==2.7.0 -django-taggit==0.17.3 coverage==3.7.1 -mixer==5.1.8 -django-braces==1.8.1 +mixer==5.5.5 +django-webtest==1.7.8 python-magic==0.4.10 lxml==3.4.0 -django-crispy-forms==1.5.2 -django-bootstrap-pagination==1.5.1 django-downloadview==1.8 webtemplate-dpaw==0.4.2 -django-tastypie==0.12.2 selenium==2.46.1 -django-autoslug==1.8.0 openpyxl==2.2.6 -django-sql-explorer==0.8 tqdm==3.4.0 -Unidecode==0.04.18 -django-webtest==1.7.8 diff --git a/wercker.yml b/wercker.yml deleted file mode 100644 index a878e329..00000000 --- a/wercker.yml +++ /dev/null @@ -1,52 +0,0 @@ -# This references the default Python container from -# the Docker Hub with the 2.7 tag: -# https://registry.hub.docker.com/_/python/ -# If you want to use a slim Python container with -# version 3.4.3 you would use: python:3.4-slim -# If you want Google's container you would reference google/python -# Read more about containers on our dev center -# http://devcenter.wercker.com/docs/containers/index.html -box: wercker/python -# You can also use services such as databases. Read more on our dev center: -# http://devcenter.wercker.com/docs/services/index.html -services: - - mies/postgis@0.0.5 - -# This is the build pipeline. Pipelines are the core of wercker -# Read more about pipelines on our dev center -# http://devcenter.wercker.com/docs/pipelines/index.html -build: - # The steps that will be executed on build - # Steps make up the actions in your pipeline - # Read more about steps on our dev center: - # http://devcenter.wercker.com/docs/steps/index.html - steps: - # A step that sets up the python virtual environment - - virtualenv: - name: setup virtual environment - install_wheel: false # Enable wheel to speed up builds (experimental) - - # # Use this virtualenv step for python 3.2 - # - virtualenv - # name: setup virtual environment - # python_location: /usr/bin/python3.2 - - # A step that executes `pip install` command. - # https://github.com/wercker/step-pip-install - - pip-install - - # # This pip-install clears the local wheel cache - # - pip-install: - # clean_wheel_dir: true - - # A custom script step, name value is used in the UI - # and the code value contains the command that get executed - - script: - name: echo python information - code: | - echo "python version $(python --version) running" - echo "pip version $(pip --version) running" - - script: - name: run django tests - code: | - python manage.py test referral