diff --git a/deployment/docker-dev/REQUIREMENTS.txt b/deployment/docker-dev/REQUIREMENTS.txt index 8a1bb96b..3bc48829 100644 --- a/deployment/docker-dev/REQUIREMENTS.txt +++ b/deployment/docker-dev/REQUIREMENTS.txt @@ -1,26 +1,26 @@ -requests +requests==2.11.1 Django>=1.7,<1.8 -django-bootstrap3-datetimepicker -psycopg2 -pytz -celery -django-celery -django-kombu +django-bootstrap3-datetimepicker==2.2.3 +psycopg2==2.6.2 +pytz==2016.7 +celery==3.1.24 +django-celery==3.1.17 +django-kombu==0.9.4 django-leaflet==0.14.0 -django-braces -django-model-utils +django-braces==1.9.0 +django-model-utils==2.6 django-pipeline<1.6,>=1.5 -nodeenv -raven -django-user-map +nodeenv==1.0.0 +raven==5.29.0 +django-user-map==1.1.1 djangorestframework==3.2.5 djangorestframework-gis==0.9.6 -markdown +Markdown==2.6.7 django-filter==0.11.0 -pillow -numpy -hammock -scrapy -python-dateutil -unicodecsv -beautifulsoup4 +Pillow==3.4.2 +numpy==1.11.2 +hammock==0.2.4 +Scrapy==1.2.0 +python-dateutil==2.5.3 +unicodecsv==0.14.1 +beautifulsoup4==4.5.1 diff --git a/deployment/docker-prod/REQUIREMENTS.txt b/deployment/docker-prod/REQUIREMENTS.txt index 8a1bb96b..3bc48829 100644 --- a/deployment/docker-prod/REQUIREMENTS.txt +++ b/deployment/docker-prod/REQUIREMENTS.txt @@ -1,26 +1,26 @@ -requests +requests==2.11.1 Django>=1.7,<1.8 -django-bootstrap3-datetimepicker -psycopg2 -pytz -celery -django-celery -django-kombu +django-bootstrap3-datetimepicker==2.2.3 +psycopg2==2.6.2 +pytz==2016.7 +celery==3.1.24 +django-celery==3.1.17 +django-kombu==0.9.4 django-leaflet==0.14.0 -django-braces -django-model-utils +django-braces==1.9.0 +django-model-utils==2.6 django-pipeline<1.6,>=1.5 -nodeenv -raven -django-user-map +nodeenv==1.0.0 +raven==5.29.0 +django-user-map==1.1.1 djangorestframework==3.2.5 djangorestframework-gis==0.9.6 -markdown +Markdown==2.6.7 django-filter==0.11.0 -pillow -numpy -hammock -scrapy -python-dateutil -unicodecsv -beautifulsoup4 +Pillow==3.4.2 +numpy==1.11.2 +hammock==0.2.4 +Scrapy==1.2.0 +python-dateutil==2.5.3 +unicodecsv==0.14.1 +beautifulsoup4==4.5.1 diff --git a/django_project/core/settings/celery_config.py b/django_project/core/settings/celery_config.py index 87dec8f9..954840c4 100644 --- a/django_project/core/settings/celery_config.py +++ b/django_project/core/settings/celery_config.py @@ -22,6 +22,7 @@ CELERY_DEFAULT_ROUTING_KEY = "default" CELERY_CREATE_MISSING_QUEUES = True CELERYD_CONCURRENCY = 1 +CELERYD_PREFETCH_MULTIPLIER = 1 CELERY_QUEUES = [ Queue('default', routing_key='default'), diff --git a/django_project/core/settings/test_travis.py b/django_project/core/settings/test_travis.py index 63a2b657..afa13082 100644 --- a/django_project/core/settings/test_travis.py +++ b/django_project/core/settings/test_travis.py @@ -12,3 +12,6 @@ 'PORT': '', } } + +# For local testing without celery worker +CELERY_ALWAYS_EAGER = True diff --git a/django_project/core/settings/utils.py b/django_project/core/settings/utils.py index 6c3329df..139ba11c 100644 --- a/django_project/core/settings/utils.py +++ b/django_project/core/settings/utils.py @@ -23,5 +23,6 @@ def ensure_secret_key_file(): with open(secret_path, 'w') as f: f.write("SECRET_KEY = " + repr(secret_key) + "\n") + # Import the secret key ensure_secret_key_file() diff --git a/django_project/realtime/admin.py b/django_project/realtime/admin.py index 07b425a3..9e7d2eff 100644 --- a/django_project/realtime/admin.py +++ b/django_project/realtime/admin.py @@ -70,7 +70,9 @@ class AshReportAdmin(ModelAdmin): class VolcanoAdmin(ModelAdmin): """Admin class for volcano model""" - list_display = ('volcano_name', 'location', 'elevation', 'province', 'district', 'morphology') + list_display = ( + 'volcano_name', 'location', 'elevation', 'province', 'district', + 'morphology') list_filter = ('province', 'district', 'morphology') search_fields = ['volcano_name', 'province', 'district', 'morphology'] diff --git a/django_project/realtime/forms/__init__.py b/django_project/realtime/forms/__init__.py new file mode 100644 index 00000000..571998b9 --- /dev/null +++ b/django_project/realtime/forms/__init__.py @@ -0,0 +1,6 @@ +# coding=utf-8 + +__copyright__ = "Copyright 2016, The InaSAFE Project" +__license__ = "GPL version 3" +__email__ = "info@inasafe.org" +__revision__ = ':%H$' diff --git a/django_project/realtime/forms.py b/django_project/realtime/forms/ash.py similarity index 52% rename from django_project/realtime/forms.py rename to django_project/realtime/forms/ash.py index 66c72972..717d5753 100644 --- a/django_project/realtime/forms.py +++ b/django_project/realtime/forms/ash.py @@ -1,54 +1,12 @@ # coding=utf-8 """Forms for realtime app.""" from bootstrap3_datetime.widgets import DateTimePicker - from django import forms from django.utils.translation import ugettext_lazy as _ -from realtime.models.earthquake import Earthquake from realtime.models.ash import Ash -class EarthquakeForm(forms.ModelForm): - class Meta: - model = Earthquake - fields = [ - 'shake_id', - 'magnitude', - 'time', - 'depth', - 'location', - 'location_description' - ] - - -date_format = 'YYYY-MM-DD' -date_picker = DateTimePicker( - format=date_format, - options={ - 'pickTime': False - }) datetime_format = 'YYYY-MM-DD HH:mm:ss' -datetime_picker = DateTimePicker( - format=datetime_format, - options={ - 'pickTime': True, - 'pickSeconds': True, - }) - - -class FilterForm(forms.Form): - start_date = forms.DateField(widget=date_picker, label=_('Start Date')) - end_date = forms.DateField(widget=date_picker, label=_('End Date')) - minimum_magnitude = forms.IntegerField( - min_value=0, - max_value=10, - label=_('Minimum Magnitude')) - maximum_magnitude = forms.IntegerField( - min_value=0, - max_value=10, - label=_('Maximum Magnitude')) - # hidden field for felt shakes - felt = forms.BooleanField() class AshUploadForm(forms.ModelForm): @@ -70,7 +28,12 @@ class Meta: event_time = forms.DateTimeField( # initial=datetime.datetime.now(), - widget=datetime_picker) + widget=DateTimePicker( + format=datetime_format, + options={ + 'pickTime': True, + 'pickSeconds': True, + })) alert_level = forms.ChoiceField( choices=[ ('normal', 'Normal'), diff --git a/django_project/realtime/forms/earthquake.py b/django_project/realtime/forms/earthquake.py new file mode 100644 index 00000000..12f41d42 --- /dev/null +++ b/django_project/realtime/forms/earthquake.py @@ -0,0 +1,51 @@ +# coding=utf-8 +"""Forms for realtime app.""" +from bootstrap3_datetime.widgets import DateTimePicker +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from realtime.models.earthquake import Earthquake + + +class EarthquakeForm(forms.ModelForm): + class Meta: + model = Earthquake + fields = [ + 'shake_id', + 'magnitude', + 'time', + 'depth', + 'location', + 'location_description' + ] + + +date_format = 'YYYY-MM-DD' +datetime_format = 'YYYY-MM-DD HH:mm:ss' + + +class FilterForm(forms.Form): + start_date = forms.DateField( + widget=DateTimePicker( + format=date_format, + options={ + 'pickTime': False + }), + label=_('Start Date')) + end_date = forms.DateField( + widget=DateTimePicker( + format=date_format, + options={ + 'pickTime': False + }), + label=_('End Date')) + minimum_magnitude = forms.IntegerField( + min_value=0, + max_value=10, + label=_('Minimum Magnitude')) + maximum_magnitude = forms.IntegerField( + min_value=0, + max_value=10, + label=_('Maximum Magnitude')) + # hidden field for felt shakes + felt = forms.BooleanField() diff --git a/django_project/realtime/forms/flood.py b/django_project/realtime/forms/flood.py new file mode 100644 index 00000000..efc43761 --- /dev/null +++ b/django_project/realtime/forms/flood.py @@ -0,0 +1,37 @@ +# coding=utf-8 +"""Forms for realtime app.""" +from bootstrap3_datetime.widgets import DateTimePicker +from django import forms +from django.utils.translation import ugettext_lazy as _ + +date_format = 'YYYY-MM-DD' +datetime_format = 'YYYY-MM-DD HH:mm:ss' + + +class FilterForm(forms.Form): + start_date = forms.DateField( + widget=DateTimePicker( + format=date_format, + options={ + 'pickTime': False + }), + label=_('Start Date')) + end_date = forms.DateField( + widget=DateTimePicker( + format=date_format, + options={ + 'pickTime': False + }), + label=_('End Date')) + min_people_affected = forms.IntegerField( + min_value=0, + label=_('Minimum')) + max_people_affected = forms.IntegerField( + min_value=0, + label=_('Maximum')) + min_boundary_flooded = forms.IntegerField( + min_value=0, + label=_('Minimum')) + max_boundary_flooded = forms.IntegerField( + min_value=0, + label=_('Maximum')) diff --git a/django_project/realtime/management/commands/recalculateimpactdata.py b/django_project/realtime/management/commands/recalculateimpactdata.py new file mode 100644 index 00000000..b3dd551a --- /dev/null +++ b/django_project/realtime/management/commands/recalculateimpactdata.py @@ -0,0 +1,76 @@ +# coding=utf-8 +import datetime + +import pytz +from django.core.management.base import BaseCommand +from realtime.tasks.flood import recalculate_impact_info + +from realtime.models.flood import Flood + +__author__ = 'Rizky Maulana Nugraha "lucernae" ' +__date__ = '07/02/17' + + +class Command(BaseCommand): + """Script to recalculate impact data. + + """ + help = ( + 'Command to re-calculate total affected and boundary flooded in ' + 'flood.') + + def handle(self, *args, **options): + using_range = False + if len(args) == 3: + if args[0] == 'range': + using_range = True + Command.recalculate_flood_from_range(args[1], args[2]) + + if len(args) > 0 and not using_range: + for a in args: + print 'Process flood : %s' % a + flood = Flood.objects.get(event_id=a) + try: + recalculate_impact_info(flood) + except Exception as e: + print e + + elif not using_range: + floods = Flood.objects.all().order_by('-time') + print 'Process flood (%s)' % len(floods) + for flood in floods: + try: + recalculate_impact_info(flood) + except Exception as e: + print e + + @staticmethod + def recalculate_flood_from_range(start_event_id, end_event_id): + format_str = '%Y%m%d%H-6-rw' + start_time = datetime.datetime.strptime(start_event_id, format_str) + if not end_event_id == 'now': + end_time = datetime.datetime.strptime(end_event_id, format_str) + else: + end_time = datetime.datetime.utcnow() + # convert to UTC + start_time = start_time.replace(tzinfo=pytz.UTC) + end_time = end_time.replace(tzinfo=pytz.UTC) + time_diff = end_time - start_time + total_hours = int(time_diff.total_seconds() / 3600) + success = 0 + failed = 0 + for i in range(0, total_hours): + hour_diff = datetime.timedelta(hours=i + 1) + target_time = start_time + hour_diff + event_id = target_time.strftime(format_str) + try: + print 'Processing flood: %s' % event_id + flood = Flood.objects.get(event_id=event_id) + recalculate_impact_info(flood) + success += 1 + except Exception as e: + failed += 1 + print e + + print 'Recalculate process done' + print 'Success: %s. Failed: %s' % (success, failed) diff --git a/django_project/realtime/migrations/0024_auto_20170206_2044.py b/django_project/realtime/migrations/0024_auto_20170206_2044.py new file mode 100644 index 00000000..85fe80db --- /dev/null +++ b/django_project/realtime/migrations/0024_auto_20170206_2044.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('realtime', '0023_auto_20161022_0957'), + ] + + operations = [ + migrations.AlterField( + model_name='ash', + name='hazard_file', + field=models.FileField(help_text=b'Hazard file formatted as GeoTIFF (*.tif) in EPSG:4326.', upload_to=b'ash/hazard_file/%Y/%m/%d', verbose_name=b'Hazard File'), + preserve_default=True, + ), + ] diff --git a/django_project/realtime/migrations/0025_auto_20170206_2046.py b/django_project/realtime/migrations/0025_auto_20170206_2046.py new file mode 100644 index 00000000..289491d3 --- /dev/null +++ b/django_project/realtime/migrations/0025_auto_20170206_2046.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('realtime', '0024_auto_20170206_2044'), + ] + + operations = [ + migrations.AddField( + model_name='flood', + name='boundary_flooded', + field=models.IntegerField(default=0, help_text=b'Total boundary affected by flood', verbose_name=b'Total boundary flooded'), + preserve_default=True, + ), + migrations.AddField( + model_name='flood', + name='total_affected', + field=models.IntegerField(default=0, help_text=b'Total affected people by flood', verbose_name=b'Total affected people by flood'), + preserve_default=True, + ), + ] diff --git a/django_project/realtime/migrations/0026_auto_20170209_1905.py b/django_project/realtime/migrations/0026_auto_20170209_1905.py new file mode 100644 index 00000000..eb282059 --- /dev/null +++ b/django_project/realtime/migrations/0026_auto_20170209_1905.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('realtime', '0025_auto_20170206_2046'), + ] + + operations = [ + migrations.AddField( + model_name='ash', + name='impact_files', + field=models.FileField(help_text=b'Impact files processed zipped', upload_to=b'ash/impact_files/%Y/%m/%d', null=True, verbose_name=b'Impact Files', blank=True), + preserve_default=True, + ), + migrations.AddField( + model_name='earthquake', + name='mmi_output', + field=models.FileField(help_text=b'MMI related file, layers, and data, zipped.', upload_to=b'earthquake/mmi_output', null=True, verbose_name=b'MMI related file zipped', blank=True), + preserve_default=True, + ), + migrations.AlterField( + model_name='ash', + name='eruption_height', + field=models.IntegerField(default=0, verbose_name=b'Eruption height in metres'), + preserve_default=True, + ), + ] diff --git a/django_project/realtime/migrations/0027_flood_data_source.py b/django_project/realtime/migrations/0027_flood_data_source.py new file mode 100644 index 00000000..ab094f98 --- /dev/null +++ b/django_project/realtime/migrations/0027_flood_data_source.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('realtime', '0026_auto_20170209_1905'), + ] + + operations = [ + migrations.AddField( + model_name='flood', + name='data_source', + field=models.CharField(default=None, max_length=255, blank=True, help_text=b'The source of the hazard data used for analysis', null=True, verbose_name=b'The source of hazard data'), + preserve_default=True, + ), + ] diff --git a/django_project/realtime/models/ash.py b/django_project/realtime/models/ash.py index a64c91f6..082fbc13 100644 --- a/django_project/realtime/models/ash.py +++ b/django_project/realtime/models/ash.py @@ -31,16 +31,23 @@ class Meta: ) hazard_file = models.FileField( verbose_name='Hazard File', - help_text='Collection of hazard file in zip.', + help_text='Hazard file formatted as GeoTIFF (*.tif) in EPSG:4326.', upload_to='ash/hazard_file/%Y/%m/%d', blank=False ) + impact_files = models.FileField( + verbose_name='Impact Files', + help_text='Impact files processed zipped', + upload_to='ash/impact_files/%Y/%m/%d', + blank=True, + null=True + ) event_time = models.DateTimeField( verbose_name='Event Date and Time', help_text='The time the ash happened.', blank=False) eruption_height = models.IntegerField( - verbose_name='Eruption height in meter', + verbose_name='Eruption height in metres', blank=False, default=0) task_id = models.CharField( @@ -67,6 +74,8 @@ def delete(self, using=None): # delete all report if self.hazard_file: self.hazard_file.delete() + if self.impact_files: + self.impact_files.delete() return super(Ash, self).delete(using=using) diff --git a/django_project/realtime/models/earthquake.py b/django_project/realtime/models/earthquake.py index 6a0eeab4..28876eea 100644 --- a/django_project/realtime/models/earthquake.py +++ b/django_project/realtime/models/earthquake.py @@ -22,6 +22,12 @@ class Meta: upload_to='earthquake/grid', blank=True, null=True) + mmi_output = models.FileField( + verbose_name='MMI related file zipped', + help_text='MMI related file, layers, and data, zipped.', + upload_to='earthquake/mmi_output', + blank=True, + null=True) magnitude = models.FloatField( verbose_name='The magnitude', help_text='The magnitude of the event.') @@ -61,6 +67,8 @@ def delete(self, using=None): # delete all report if self.shake_grid: self.shake_grid.delete() + if self.mmi_output: + self.mmi_output.delete() for report in self.reports.all(): report.delete(using=using) super(Earthquake, self).delete(using=using) diff --git a/django_project/realtime/models/flood.py b/django_project/realtime/models/flood.py index 71b9f1ef..348964a7 100644 --- a/django_project/realtime/models/flood.py +++ b/django_project/realtime/models/flood.py @@ -81,6 +81,13 @@ class Meta: max_length=20, unique=True, blank=False) + data_source = models.CharField( + verbose_name='The source of hazard data', + help_text='The source of the hazard data used for analysis', + max_length=255, + blank=True, + null=True, + default=None) time = models.DateTimeField( verbose_name='Date and Time', help_text='The time the flood reported.', @@ -112,6 +119,14 @@ class Meta: through='FloodEventBoundary', verbose_name='Flooded Boundaries', help_text='The linked boundaries flooded by this event') + total_affected = models.IntegerField( + verbose_name='Total affected people by flood', + help_text='Total affected people by flood', + default=0) + boundary_flooded = models.IntegerField( + verbose_name='Total boundary flooded', + help_text='Total boundary affected by flood', + default=0) objects = models.GeoManager() diff --git a/django_project/realtime/scripts/check_indicators.py b/django_project/realtime/scripts/check_indicators.py index e82c97b2..c3e817f8 100644 --- a/django_project/realtime/scripts/check_indicators.py +++ b/django_project/realtime/scripts/check_indicators.py @@ -32,5 +32,6 @@ def check_indicator_status(): ind.label, ind.value_humanize) + if __name__ == '__main__': check_indicator_status() diff --git a/django_project/realtime/serializers/ash_serializer.py b/django_project/realtime/serializers/ash_serializer.py index 6b1c875b..4705f86e 100644 --- a/django_project/realtime/serializers/ash_serializer.py +++ b/django_project/realtime/serializers/ash_serializer.py @@ -117,6 +117,8 @@ class Meta: 'url', 'id', 'volcano', + 'hazard_file', + 'impact_files', 'reports', 'event_time', 'task_status', @@ -132,13 +134,32 @@ class AshGeoJsonSerializer(GeoFeatureModelSerializer): def get_location(self, obj): return obj.volcano.location + def get_event_id_formatted(self, serializer_field, obj): + """ + :param serializer_field: + :type serializer_field: CustomSerializerMethodField + :param obj: + :type obj: Ash + :return: + """ + dateformat = '%Y%m%d%H%M%S%z' + return '%s-%s' % ( + obj.event_time.strftime(dateformat), + obj.volcano.volcano_name) + + # auto bind to get_url method + event_id_formatted = CustomSerializerMethodField() + class Meta: model = Ash geo_field = 'location' id = 'id', fields = ( 'id', + 'event_id_formatted', 'volcano', + 'hazard_file', + 'impact_files', 'event_time', 'alert_level', 'task_status', diff --git a/django_project/realtime/serializers/earthquake_serializer.py b/django_project/realtime/serializers/earthquake_serializer.py index ee2d910d..9adad04a 100644 --- a/django_project/realtime/serializers/earthquake_serializer.py +++ b/django_project/realtime/serializers/earthquake_serializer.py @@ -106,6 +106,7 @@ class Meta: 'url', 'shake_id', 'shake_grid', + 'mmi_output', 'magnitude', 'time', 'depth', @@ -125,6 +126,7 @@ class Meta: fields = ( 'shake_id', 'shake_grid', + 'mmi_output', 'magnitude', 'time', 'depth', diff --git a/django_project/realtime/serializers/flood_serializer.py b/django_project/realtime/serializers/flood_serializer.py index ebed2092..edce937a 100644 --- a/django_project/realtime/serializers/flood_serializer.py +++ b/django_project/realtime/serializers/flood_serializer.py @@ -1,5 +1,5 @@ # coding=utf-8 - +import pytz from django.core.urlresolvers import reverse from rest_framework import serializers from realtime.models.flood import Flood, FloodReport @@ -96,12 +96,55 @@ def get_url(self, serializer_field, obj): # auto bind to get_url method url = CustomSerializerMethodField() + def get_time_description(self, serializer_field, obj): + """ + :param serializer_field: + :type serializer_field: CustomSerializerMethodField + + :param obj: + :type obj: Flood + + :return: + """ + utc_time = obj.time.replace(tzinfo=pytz.utc) + jakarta_time = utc_time.astimezone(tz=pytz.timezone('Asia/Jakarta')) + description_format = ( + '{interval} hour report for %d %B %Y at %H:%M:%S').format( + interval=obj.interval) + return jakarta_time.strftime(description_format) + + # auto bind to get_time_description method + time_description = CustomSerializerMethodField() + + def get_event_id_formatted(self, serializer_field, obj): + """ + :param serializer_field: + :type serializer_field: CustomSerializerMethodField + + :param obj: + :type obj: Flood + + :return: + """ + utc_time = obj.time.replace(tzinfo=pytz.utc) + jakarta_time = utc_time.astimezone(tz=pytz.timezone('Asia/Jakarta')) + event_id_format = '%Y%m%d%H%M%S' + return jakarta_time.strftime(event_id_format) + + # auto bind to get_event_id_formatted method + event_id_formatted = CustomSerializerMethodField() + class Meta: model = Flood fields = ( 'url', 'event_id', + 'data_source', + 'event_id_formatted', 'time', + 'time_description', + 'total_affected', + 'boundary_flooded', 'interval', 'source', 'region', diff --git a/django_project/realtime/signals/flood.py b/django_project/realtime/signals/flood.py index c78fc2de..34fc3c73 100644 --- a/django_project/realtime/signals/flood.py +++ b/django_project/realtime/signals/flood.py @@ -7,7 +7,10 @@ from realtime.app_settings import LOGGER_NAME from realtime.models.flood import Flood -from realtime.tasks.flood import process_hazard_layer, process_impact_layer +from realtime.tasks.flood import ( + process_hazard_layer, + process_impact_layer, + recalculate_impact_info) __author__ = 'Rizky Maulana Nugraha ' __date__ = '12/4/15' @@ -20,12 +23,21 @@ @receiver(post_save, sender=Flood) -def flood_post_save(sender, **kwargs): +def flood_post_save( + sender, instance, created=None, update_fields=None, **kwargs): """Extract impact layer of the flood""" try: - instance = kwargs.get('instance') - chain( - process_hazard_layer.si(instance), - process_impact_layer.si(instance))() + fields = ['total_affected', 'boundary_flooded'] + update_fields = update_fields or [] + for field in fields: + # if total_affected or boundary_flooded is updated, + # do not recalculate + if field in update_fields: + break + else: + chain( + process_hazard_layer.si(instance), + process_impact_layer.si(instance), + recalculate_impact_info.si(instance))() except Exception as e: LOGGER.exception(e) diff --git a/django_project/realtime/static/realtime/css/realtime.css b/django_project/realtime/static/realtime/css/realtime.css index 47017d54..0044d339 100644 --- a/django_project/realtime/static/realtime/css/realtime.css +++ b/django_project/realtime/static/realtime/css/realtime.css @@ -263,4 +263,12 @@ th.dynatable-head a, th.dynatable-head a:hover { /* ******************** */ .timepicker-picker td.separator{ line-height: 54px; -} \ No newline at end of file +} + + +/* ******************** */ +/* Dropdown in table */ +/* ******************** */ +.btn-group .dropdown-menu{ + position: absolute; +} diff --git a/django_project/realtime/static/realtime/js/ash/ash.js b/django_project/realtime/static/realtime/js/ash/ash.js index bece2b3d..93c38b47 100644 --- a/django_project/realtime/static/realtime/js/ash/ash.js +++ b/django_project/realtime/static/realtime/js/ash/ash.js @@ -106,15 +106,15 @@ function createShowEventHandler(map, markers, map_events) { /** * Closure to create handler for showReport - * use magic number 000 for url placeholder + * use magic number for url placeholder * - * @param {string} report_url A report url that contains shake_id placeholder - * @return {function} Open the report based on shake_id in a new tab + * @param {string} report_url A report url that contains event_id placeholder + * @return {function} Open the report based on event_id in a new tab */ function createShowReportHandler(report_url) { var showReportHandler = function (id) { var url = report_url; - // replace magic number 000 with shake_id + // replace magic number function createFindWithId(id){ return function (event) { return event.properties.id == id; @@ -171,6 +171,54 @@ function createShowReportHandler(report_url) { return showReportHandler; } +/** + * Closure to create handler for downloadReport + * use magic number for url placeholder + * + * @param {string} report_url A report url that contains event_id placeholder + * @return {function} Open the report based on event_id in a new tab + */ +function createDownloadReportHandler(report_url) { + var downloadReportHandler = function (id) { + var url = report_url; + // replace magic number 000 with shake_id + function createFindWithId(id){ + return function (event) { + return event.properties.id == id; + } + } + var findWithId = createFindWithId(id); + var feature = event_json.features.find(findWithId); + var volcano_name = feature.properties.volcano.volcano_name; + var event_time = feature.properties.event_time; + var event_time_string = moment(event_time).format('YYYYMMDDHHmmssZZ'); + var event_id_formatted = feature.properties.event_id_formatted; + var task_status = feature.properties.task_status; + if(task_status == 'PENDING'){ + alert("Report is currently being generated. Refresh this page later."); + return; + } + else if(task_status == 'FAILED'){ + alert("Report failed to generate."); + return; + } + url = url.replace('VOLCANOTEMPLATENAME', volcano_name) + .replace('1234567890123456789', event_time_string); + $.get(url, function (data) { + if (data && data.report_map) { + var pdf_url = data.report_map; + SaveToDisk(pdf_url, event_id_formatted+'-'+data.language+'.pdf'); + } + }).fail(function(e){ + console.log(e); + if(e.status == 404){ + alert("No Report recorded for this event."); + } + }); + }; + return downloadReportHandler; +} + /** * Create Action Writer based on button_templates @@ -200,17 +248,64 @@ function createActionRowWriter(button_templates, date_format) { var $span = $(''); for (var i = 0; i < button_templates.length; i++) { var button = button_templates[i]; - var $inner_button = $(''); - $inner_button.addClass('row-action-icon') - $inner_button.addClass(button.css_class); - $inner_button.attr('title', button.name); - $inner_button.text(button.label); - var $button = $(''); - $button.addClass('btn btn-primary row-action-container'); - $button.attr('title', button.name); - $button.attr('onclick', button.handler + "('" + record.id + "')"); - $button.append($inner_button); - $span.append($button); + if(button.type == 'simple-button') { + var $inner_button = $(''); + $inner_button.addClass('row-action-icon'); + $inner_button.addClass(button.css_class); + $inner_button.attr('title', button.name); + var $button = $(''); + $button.addClass('btn btn-primary row-action-container'); + $button.attr('title', button.name); + $button.attr('onclick', button.handler + "('" + record.id + "')"); + $button.append($inner_button); + $span.append($button); + } + else if(button.type == 'dropdown'){ + var $button = $(''); + $button.addClass('btn btn-primary dropdown-toggle row-action-container'); + $button.attr('title', button.name); + $button.attr('data-toggle', 'dropdown'); + $button.attr('aria-haspopup', 'true'); + $button.attr('aria-expanded', 'false'); + var $inner_button = $(''); + $inner_button.addClass('row-action-icon'); + $inner_button.addClass(button.css_class); + $inner_button.attr(button.name); + $button.append($inner_button); + var $menu = $(''); + $menu.addClass('dropdown-menu'); + for(var j=0;j < button.actions.length;j++){ + var action = button.actions[j]; + if(action.active && $.isFunction(action.active) && !action.active(record)){ + continue; + } + var $li = $('
  • '); + var $action = $(''); + if(action.href == undefined){ + $action.attr('href', '#'); + } + else if($.isFunction(action.href)){ + $action.attr('href', action.href(record)); + } + + if(action.download && $.isFunction(action.download)){ + $action.attr('download', action.download(record)); + } + + if(action.handler){ + $action.attr('onclick', action.handler + "('" + record.id + "')"); + } + + $action.text(action.text); + $li.append($action); + $menu.append($li); + } + var $group = $('
    '); + $group.addClass('btn-group'); + $group.append($button); + $group.append($menu); + $span.append($group); + } } tr += '' + $span.html() + ''; diff --git a/django_project/realtime/static/realtime/js/earthquake/shake.js b/django_project/realtime/static/realtime/js/earthquake/shake.js index 2e3f9465..a82a6c80 100644 --- a/django_project/realtime/static/realtime/js/earthquake/shake.js +++ b/django_project/realtime/static/realtime/js/earthquake/shake.js @@ -464,17 +464,64 @@ function createActionRowWriter(button_templates, date_format) { var $span = $(''); for (var i = 0; i < button_templates.length; i++) { var button = button_templates[i]; - var $inner_button = $(''); - $inner_button.addClass('row-action-icon') - $inner_button.addClass(button.css_class); - $inner_button.attr('title', button.name); - $inner_button.text(button.label); - var $button = $(''); - $button.addClass('btn btn-primary row-action-container'); - $button.attr('title', button.name); - $button.attr('onclick', button.handler + "('" + record.shake_id + "')"); - $button.append($inner_button); - $span.append($button); + if(button.type == 'simple-button') { + var $inner_button = $(''); + $inner_button.addClass('row-action-icon'); + $inner_button.addClass(button.css_class); + $inner_button.attr('title', button.name); + var $button = $(''); + $button.addClass('btn btn-primary row-action-container'); + $button.attr('title', button.name); + $button.attr('onclick', button.handler + "('" + record.shake_id + "')"); + $button.append($inner_button); + $span.append($button); + } + else if(button.type == 'dropdown'){ + var $button = $(''); + $button.addClass('btn btn-primary dropdown-toggle row-action-container'); + $button.attr('title', button.name); + $button.attr('data-toggle', 'dropdown'); + $button.attr('aria-haspopup', 'true'); + $button.attr('aria-expanded', 'false'); + var $inner_button = $(''); + $inner_button.addClass('row-action-icon'); + $inner_button.addClass(button.css_class); + $inner_button.attr(button.name); + $button.append($inner_button); + var $menu = $(''); + $menu.addClass('dropdown-menu'); + for(var j=0;j < button.actions.length;j++){ + var action = button.actions[j]; + if(action.active && $.isFunction(action.active) && !action.active(record)){ + continue; + } + var $li = $('
  • '); + var $action = $(''); + if(action.href == undefined){ + $action.attr('href', '#'); + } + else if($.isFunction(action.href)){ + $action.attr('href', action.href(record)); + } + + if(action.download && $.isFunction(action.download)){ + $action.attr('download', action.download(record)); + } + + if(action.handler){ + $action.attr('onclick', action.handler + "('" + record.shake_id + "')"); + } + + $action.text(action.text); + $li.append($action); + $menu.append($li); + } + var $group = $('
    '); + $group.addClass('btn-group'); + $group.append($button); + $group.append($menu); + $span.append($group); + } } tr += '' + $span.html() + ''; diff --git a/django_project/realtime/static/realtime/js/flood/flood.js b/django_project/realtime/static/realtime/js/flood/flood.js index 0a18349a..b912adb9 100644 --- a/django_project/realtime/static/realtime/js/flood/flood.js +++ b/django_project/realtime/static/realtime/js/flood/flood.js @@ -91,22 +91,30 @@ function createShowFeaturesHandler(event_features_url){ } var style_classes = { 1: { - color: "#bdc5f7", + stroke: true, + color: "#000", + weight: 2, fillColor: "#bdc5f7", fillOpacity: 0.8 }, 2: { - color: "#fffe73", + stroke: true, + color: "#000", + weight: 2, fillColor: "#fffe73", fillOpacity: 0.8 }, 3: { - color: "#fea865", + stroke: true, + color: "#000", + weight: 2, fillColor: "#fea865", fillOpacity: 0.8 }, 4: { - color: "#da6c7b", + stroke: true, + color: "#000", + weight: 2, fillColor: "#da6c7b", fillOpacity: 0.8 } @@ -115,6 +123,16 @@ function createShowFeaturesHandler(event_features_url){ style: function(feature){ return style_classes[feature.properties.hazard_data]; }, + filter: function(feature){ + if(feature.properties){ + var props = feature.properties; + var hazard_data = props.hazard_data; + return hazard_data >= 1; + } + else { + return false; + } + }, onEachFeature: function(feature, layer) { // Set popup content if (feature.properties) { @@ -134,7 +152,7 @@ function createShowFeaturesHandler(event_features_url){ scrollTop: $("#"+map_id).offset().top }, 500); var fitBoundsOption = { - maxZoom: 10, + maxZoom: 15, pan: { animate: true, duration: 0.5 @@ -148,7 +166,6 @@ function createShowFeaturesHandler(event_features_url){ // Deselect row in table var $table = $("#realtime-table"); - console.log($table); $table.find("tr.success").removeClass('success'); // Select row $table.find("td:contains("+event_id+")").closest('tr').addClass('success'); @@ -234,17 +251,17 @@ function createShowImpactMapHandler(report_url) { /** * Closure to create handler for downloadReport * @param {string} report_url A report url that contains shake_id placeholder - * @return {function} Download the report based on shake_id + * @return {function} Download the impact map based on event_id */ -function createDownloadReportHandler(report_url) { - var downloadReportHandler = function (shake_id) { +function createDownloadImpactMapHandler(report_url) { + var downloadImpactMapHandler = function (event_id) { var url = report_url; - // replace magic number 000 with shake_id - url = url.replace('000', shake_id); + // replace magic number 0000000000-6-rw with event_id + url = url.replace('0000000000-6-rw', event_id); $.get(url, function (data) { - if (data && data.report_pdf) { - var pdf_url = data.report_pdf; - SaveToDisk(pdf_url, data.shake_id+'-'+data.language+'.pdf'); + if (data && data.impact_map) { + var pdf_url = data.impact_map; + SaveToDisk(pdf_url, data.event_id+'-'+data.language+'.pdf'); } }).fail(function(e){ console.log(e); @@ -253,7 +270,32 @@ function createDownloadReportHandler(report_url) { } }); }; - return downloadReportHandler; + return downloadImpactMapHandler; +} + +/** + * Closure to create handler for downloadHazardLayer + * @return {function} Download the hazard layer based on event_id + */ +function createDownloadHazardLayerHandler() { + var downloadHazardLayerHandler = function (event_id) { + var event = undefined; + for(var i=0;i < event_json.length;i++){ + event = event_json[i]; + if(event_id == event.event_id){ + break; + } + } + + if(event == undefined){ + alert("Event not found"); + return; + } + + var url = event.hazard_layer; + SaveToDisk(url, event.event_id + '-hazard.zip'); + }; + return downloadHazardLayerHandler; } /** @@ -262,17 +304,13 @@ function createDownloadReportHandler(report_url) { * @param {function} dataHandler Function to handle retrieved data * @return {Function} Update handler function */ -function createUpdateFilterHandler(url, form_filter, location_filter, dataHandler) { +function createUpdateFilterHandler(url, form_filter, dataHandler) { var updateFilterHandler = function (e, reset) { if (e.preventDefault) { e.preventDefault(); } var form_filter_query = form_filter.serialize(); - var location_filter_query = ""; - if (location_filter.isEnabled() && reset!==true) { - location_filter_query += "&in_bbox=" + location_filter.getBounds().toBBoxString(); - } - $.get(url + "?" + form_filter_query + location_filter_query, dataHandler); + $.get(url + "?" + form_filter_query, dataHandler); }; return updateFilterHandler; } @@ -282,18 +320,18 @@ function createUpdateFilterHandler(url, form_filter, location_filter, dataHandle * @param data_input {{}} a geo json collections * @param min_date {string} date formattable string * @param max_date {string} date formattable string - * @param min_magnitude {string} float string - * @param max_magnitude {string} float string + * @param min_people_affected {string} float string + * @param max_people_affected {string} float string * @return {{features: Array, type: *}} */ -function clientFilter(data_input, min_date, max_date, min_magnitude, max_magnitude){ +function clientFilter(data_input, min_date, max_date, min_people_affected, max_people_affected, min_boundary_flooded, max_boundary_flooded){ var filtered_features = []; - var features = data_input.features; + var features = data_input; for(var i=0;i parseFloat(max_magnitude)){ + if(max_people_affected && people_affected > parseFloat(max_people_affected)){ + continue + } + + // boundary flooded + var boundary_flooded = feature.boundary_flooded; + if(min_boundary_flooded && boundary_flooded < parseFloat(min_boundary_flooded)){ + continue + } + + if(max_boundary_flooded && boundary_flooded > parseFloat(max_boundary_flooded)){ continue } // filtered filtered_features.push(feature); } - return { - features: filtered_features, - type: data_input.type - }; + return filtered_features; } /** @@ -360,22 +405,21 @@ function clientExtentFilter(data_input, bounds){ * @param {function} dataHandler Function to handle retrieved data * @return {Function} Update handler function */ -function createClientUpdateFilterHandler(url, form_filter, location_filter, dataHandler) { +function createClientUpdateFilterHandler(url, form_filter, dataHandler) { var updateFilterHandler = function (e, reset) { if (e.preventDefault) { e.preventDefault(); } // filter by bounds var filtered = event_json; - if(reset !== true){ - filtered = clientExtentFilter(event_json, location_filter.getBounds()); - } filtered = clientFilter( filtered, $("#id_start_date", form_filter).val(), $("#id_end_date", form_filter).val(), - $("#id_minimum_magnitude", form_filter).val(), - $("#id_maximum_magnitude", form_filter).val() + $("#id_min_people_affected", form_filter).val(), + $("#id_max_people_affected", form_filter).val(), + $("#id_min_boundary_flooded", form_filter).val(), + $("#id_max_boundary_flooded", form_filter).val() ); dataHandler(filtered); }; @@ -389,17 +433,30 @@ function createClientUpdateFilterHandler(url, form_filter, location_filter, data function modifyMapDescriptions(target){ var $target = $(target); - var magnitude_string = ""; - var min_magnitude = $("#id_minimum_magnitude").val(); - var max_magnitude = $("#id_maximum_magnitude").val(); - if(min_magnitude && max_magnitude){ - magnitude_string = 'with magnitudes between '+min_magnitude+' and '+max_magnitude; + var people_affected_string = ""; + var min_people_affected = $("#id_min_people_affected").val(); + var max_people_affected = $("#id_max_people_affected").val(); + var boundary_flooded_string = ""; + var min_boundary_flooded = $("#id_min_boundary_flooded").val(); + var max_boundary_flooded = $("#id_max_boundary_flooded").val(); + if(min_people_affected && max_people_affected){ + people_affected_string = 'with people affected between '+min_people_affected+' and '+max_people_affected; } - else if(min_magnitude){ - magnitude_string = 'with magnitudes greater or equal than '+min_magnitude; + else if(min_people_affected){ + people_affected_string = 'with people affected greater or equal than '+min_people_affected; } - else if(max_magnitude){ - magnitude_string = 'with magnitudes less or equal than '+max_magnitude; + else if(max_people_affected){ + people_affected_string = 'with people affected less or equal than '+max_people_affected; + } + + if(min_boundary_flooded && max_boundary_flooded){ + boundary_flooded_string = 'with RW flooded between '+min_boundary_flooded+' and '+max_boundary_flooded; + } + else if(min_boundary_flooded){ + boundary_flooded_string = 'with RW flooded greater or equal than '+min_boundary_flooded; + } + else if(max_boundary_flooded){ + boundary_flooded_string = 'with RW flooded less or equal than '+max_boundary_flooded; } var date_string = ''; @@ -418,9 +475,12 @@ function modifyMapDescriptions(target){ var end_moment = moment(end_date); date_string = 'before '+end_moment.format('LL'); } - var description = 'Earthquake events'; - if(magnitude_string){ - description+=' '+magnitude_string; + var description = 'Flood events'; + if(people_affected_string){ + description+=' '+people_affected_string; + } + if(boundary_flooded_string){ + description+=' '+boundary_flooded_string; } if(date_string){ description+=' '+date_string; @@ -428,23 +488,6 @@ function modifyMapDescriptions(target){ $target.text(description); } -/** - * Modify location filter plugin styles. - * This function is used to overrides original styles - */ -function modifyLocationFilterStyle(){ - var $location_filter_container = $(".location-filter"); - $location_filter_container.removeClass('button-container').addClass('leaflet-bar'); - var $enable_button = $(".enable-button", $location_filter_container); - $enable_button.text(""); - $enable_button.click(function(){ - // disable text - $("a", $location_filter_container).text(""); - var $toggle_button = $(".enable-button", $location_filter_container); - $toggle_button.toggleClass("remove-button"); - }); -} - /** * Modify default search and show labels of dynatable * Removes ':' from the text @@ -490,16 +533,64 @@ function createActionRowWriter(button_templates, date_format) { var $span = $(''); for (var i = 0; i < button_templates.length; i++) { var button = button_templates[i]; - var $inner_button = $(''); - $inner_button.addClass('row-action-icon') - $inner_button.addClass(button.css_class); - $inner_button.attr('title', button.name); - var $button = $(''); - $button.addClass('btn btn-primary row-action-container'); - $button.attr('title', button.name); - $button.attr('onclick', button.handler + "('" + record.event_id + "')"); - $button.append($inner_button); - $span.append($button); + if(button.type == 'simple-button') { + var $inner_button = $(''); + $inner_button.addClass('row-action-icon'); + $inner_button.addClass(button.css_class); + $inner_button.attr('title', button.name); + var $button = $(''); + $button.addClass('btn btn-primary row-action-container'); + $button.attr('title', button.name); + $button.attr('onclick', button.handler + "('" + record.event_id + "')"); + $button.append($inner_button); + $span.append($button); + } + else if(button.type == 'dropdown'){ + var $button = $(''); + $button.addClass('btn btn-primary dropdown-toggle row-action-container'); + $button.attr('title', button.name); + $button.attr('data-toggle', 'dropdown'); + $button.attr('aria-haspopup', 'true'); + $button.attr('aria-expanded', 'false'); + var $inner_button = $(''); + $inner_button.addClass('row-action-icon'); + $inner_button.addClass(button.css_class); + $inner_button.attr('title', button.name); + $button.append($inner_button); + var $menu = $('
      '); + $menu.addClass('dropdown-menu'); + for(var j=0;j < button.actions.length;j++){ + var action = button.actions[j]; + if(action.active && $.isFunction(action.active) && !action.active(record)){ + continue; + } + var $li = $('
    • '); + var $action = $(''); + if(action.href == undefined){ + $action.attr('href', '#'); + } + else if($.isFunction(action.href)){ + $action.attr('href', action.href(record)); + } + + if(action.download && $.isFunction(action.download)){ + $action.attr('download', action.download(record)); + } + + if(action.handler){ + $action.attr('onclick', action.handler + "('" + record.event_id + "')"); + } + + $action.text(action.text); + $li.append($action); + $menu.append($li); + } + var $group = $('
      '); + $group.addClass('btn-group'); + $group.append($button); + $group.append($menu); + $span.append($group); + } } tr += '' + $span.html() + ''; diff --git a/django_project/realtime/static/realtime/js/realtime.js b/django_project/realtime/static/realtime/js/realtime.js index cfdfa7bd..db2d05f5 100644 --- a/django_project/realtime/static/realtime/js/realtime.js +++ b/django_project/realtime/static/realtime/js/realtime.js @@ -55,6 +55,7 @@ function browser_identity(){ */ function SaveToDisk(fileURL, fileName) { if (!window.ActiveXObject) { + // emulate button click on a element var save = document.createElement('a'); save.href = fileURL; if(!browser_identity().is_safari){ @@ -70,7 +71,7 @@ function SaveToDisk(fileURL, fileName) { }); save.dispatchEvent(evt); - (window.URL || window.webkitURL).revokeObjectURL(save.href); + // (window.URL || window.webkitURL).revokeObjectURL(save.href); } // for IE < 11 diff --git a/django_project/realtime/static/realtime/js/templates/ash/popup_content.jst b/django_project/realtime/static/realtime/js/templates/ash/popup_content.jst index 074c0d5e..3c175390 100644 --- a/django_project/realtime/static/realtime/js/templates/ash/popup_content.jst +++ b/django_project/realtime/static/realtime/js/templates/ash/popup_content.jst @@ -42,13 +42,37 @@ onclick="showReportHandler('<%= id %>')"> - +
      + + +
      diff --git a/django_project/realtime/static/realtime/js/templates/earthquake/popup_content.jst b/django_project/realtime/static/realtime/js/templates/earthquake/popup_content.jst index b0717bc5..2c8230a7 100644 --- a/django_project/realtime/static/realtime/js/templates/earthquake/popup_content.jst +++ b/django_project/realtime/static/realtime/js/templates/earthquake/popup_content.jst @@ -30,19 +30,37 @@ onclick="showReportHandler('<%= shake_id %>')"> - - +
      + + +
      diff --git a/django_project/realtime/tasks/flood.py b/django_project/realtime/tasks/flood.py index bfc6c05c..e5df09f6 100644 --- a/django_project/realtime/tasks/flood.py +++ b/django_project/realtime/tasks/flood.py @@ -7,6 +7,8 @@ import tempfile from zipfile import ZipFile +from django.contrib.gis.gdal.error import OGRIndexError + from realtime.app_settings import OSM_LEVEL_7_NAME, OSM_LEVEL_8_NAME from core.celery_app import app from django.conf import settings @@ -14,6 +16,7 @@ from django.contrib.gis.geos.collections import MultiPolygon from django.contrib.gis.geos.geometry import GEOSGeometry from django.contrib.gis.geos.polygon import Polygon +from django.db.models import Sum from realtime.app_settings import LOGGER_NAME from realtime.models.flood import ( @@ -65,13 +68,21 @@ def process_hazard_layer(flood): FloodEventBoundary.objects.filter(flood=flood).delete() + kelurahan = BoundaryAlias.objects.get(alias=OSM_LEVEL_7_NAME) + rw = BoundaryAlias.objects.get(alias=OSM_LEVEL_8_NAME) + for feat in layer: - pkey = feat.get('pkey') - level_name = feat.get('level_name') - # flooded = feat.get('flooded') - # count = feat.get('count') - parent_name = feat.get('parent_nam') - state = feat.get('state') + if not flood.data_source or flood.data_source == 'petajakarta': + upstream_id = feat.get('pkey') + level_name = feat.get('level_name') + parent_name = feat.get('parent_nam') + state = feat.get('state') + elif flood.data_source == 'petabencana': + upstream_id = feat.get('area_id') + level_name = feat.get('area_name') + parent_name = feat.get('parent_nam') + state = feat.get('state') + geometry = feat.geom geos_geometry = GEOSGeometry(geometry.geojson) @@ -81,29 +92,27 @@ def process_hazard_layer(flood): geos_geometry = MultiPolygon(geos_geometry) # check parent exists - kelurahan = BoundaryAlias.objects.get(alias=OSM_LEVEL_7_NAME) try: boundary_kelurahan = Boundary.objects.get( name__iexact=parent_name.strip(), boundary_alias=kelurahan) except Boundary.DoesNotExist: boundary_kelurahan = Boundary.objects.create( - upstream_id=pkey, + upstream_id=upstream_id, geometry=geos_geometry, name=parent_name, boundary_alias=kelurahan) boundary_kelurahan.save() - rw = BoundaryAlias.objects.get(alias=OSM_LEVEL_8_NAME) try: boundary_rw = Boundary.objects.get( - upstream_id=pkey, boundary_alias=rw) + upstream_id=upstream_id, boundary_alias=rw) boundary_rw.geometry = geos_geometry boundary_rw.name = level_name boundary_rw.parent = boundary_kelurahan except Boundary.DoesNotExist: boundary_rw = Boundary.objects.create( - upstream_id=pkey, + upstream_id=upstream_id, geometry=geos_geometry, name=level_name, parent=boundary_kelurahan, @@ -161,9 +170,15 @@ def process_impact_layer(flood): layer = source[0] ImpactEventBoundary.objects.filter(flood=flood).delete() + + kelurahan = BoundaryAlias.objects.get(alias=OSM_LEVEL_7_NAME) + for feat in layer: level_7_name = feat.get('NAMA_KELUR').strip() - hazard_class = feat.get('affected') + try: + hazard_class = feat.get('affected') + except OGRIndexError: + hazard_class = feat.get('safe_ag') population_affected = feat.get('Pop_Total') geometry = feat.geom geos_geometry = GEOSGeometry(geometry.geojson) @@ -175,8 +190,6 @@ def process_impact_layer(flood): if hazard_class <= 1: continue - kelurahan = BoundaryAlias.objects.get(alias=OSM_LEVEL_7_NAME) - try: boundary_kelurahan = Boundary.objects.get( name__iexact=level_7_name, @@ -204,6 +217,38 @@ def process_impact_layer(flood): return True +@app.task(queue='inasafe-django') +def recalculate_impact_info(flood): + """Recalculate flood impact data. + + :param flood: Flood object + :type flood: realtime.models.flood.Flood + """ + # calculate total boundary flooded in RW level + rw = BoundaryAlias.objects.get(alias=OSM_LEVEL_8_NAME) + boundary_flooded = FloodEventBoundary.objects.filter( + flood=flood, + boundary__boundary_alias=rw).count() + flood.boundary_flooded = boundary_flooded or 0 + + # calculate total population affected in kelurahan level + kelurahan = BoundaryAlias.objects.get(alias=OSM_LEVEL_7_NAME) + total_population_affected = ImpactEventBoundary.objects.filter( + flood=flood, + parent_boundary__boundary_alias=kelurahan + ).aggregate( + total_population_affected=Sum('population_affected')) + try: + flood.total_affected = total_population_affected[ + 'total_population_affected'] or 0 + except KeyError: + flood.total_affected = 0 + pass + + # prevent infinite recursive save + flood.save(update_fields=['total_affected', 'boundary_flooded']) + + @app.task(queue='inasafe-django') def create_flood_report(): process_flood.delay() diff --git a/django_project/realtime/tasks/test/test_flood_task.py b/django_project/realtime/tasks/test/test_flood_task.py index bf2107d6..7eaef635 100644 --- a/django_project/realtime/tasks/test/test_flood_task.py +++ b/django_project/realtime/tasks/test/test_flood_task.py @@ -9,6 +9,7 @@ from django.conf import settings from django.contrib.gis.geos.point import Point from django.core.files.base import File +from django.apps import apps as django_apps from rest_framework.test import APITestCase from core.settings.utils import ABS_PATH @@ -34,6 +35,9 @@ def setUp(self): self.default_media_path = settings.MEDIA_ROOT settings.MEDIA_ROOT = ABS_PATH('media_test') + app_config = django_apps.get_app_config('realtime') + app_config.ready() + Flood.objects.create( event_id=u'2015112518-3-rw', time=datetime.datetime( diff --git a/django_project/realtime/templates/realtime/ash/index.html b/django_project/realtime/templates/realtime/ash/index.html index 00e338e3..33eefa26 100644 --- a/django_project/realtime/templates/realtime/ash/index.html +++ b/django_project/realtime/templates/realtime/ash/index.html @@ -168,16 +168,42 @@

      {% trans "Ash Fall" %}

      {# Use magic number 000 for placeholders #} var report_url = '{% url "realtime:ash_report_detail" volcano_name='VOLCANOTEMPLATENAME' event_time='1234567890123456789' language=language.selected_language.id %}'; var showReportHandler = createShowReportHandler(report_url); + var downloadReportHandler = createDownloadReportHandler(report_url); var button_templates = [ { - name: 'Zoom', + type: 'simple-button', + name: '{% trans "Zoom" %}', css_class: 'glyphicon glyphicon-search', handler: 'showEventHandler' }, { - name: 'Report', + type: 'simple-button', + name: '{% trans "Report" %}', css_class: 'glyphicon glyphicon-file', handler: 'showReportHandler' + }, + { + type: 'dropdown', + name: '{% trans "Download" %}', + css_class: 'glyphicon glyphicon-download', + actions: [ + { + active: function(event){return event.hazard_file != undefined;}, + text: '{% trans "Hazard File" %}', + href: function(event){return event.hazard_file;}, + download: function(event){return event.event_id_formatted+'-hazard.tif';} + }, + { + active: function(event){return event.impact_files != undefined;}, + text: '{% trans "Impact Files" %}', + href: function(event){return event.impact_files;}, + download: function(event){return event.event_id_formatted+'-impact.zip';} + }, + { + text: '{% trans "Report" %}', + handler: 'downloadReportHandler' + } + ] } ]; @@ -199,6 +225,11 @@

      {% trans "Ash Fall" %}

      dynaTable.paginationPerPage.set(10); dynaTable.sorts.add('event_time', -1); dynaTable.process(); + + if(jsonTableContents.length > 0){ + var event_id = jsonTableContents[0].id + showEventHandler(event_id); + } } function onEachFeature(feature, layer) { @@ -237,7 +268,6 @@

      {% trans "Ash Fall" %}

      $.get(get_events_url, function (data) { event_json = data; getAshEventsJson(data); - mapFitAll(map, markers); }); }); diff --git a/django_project/realtime/templates/realtime/earthquake/index.html b/django_project/realtime/templates/realtime/earthquake/index.html index 191c32c4..800c7657 100644 --- a/django_project/realtime/templates/realtime/earthquake/index.html +++ b/django_project/realtime/templates/realtime/earthquake/index.html @@ -299,8 +299,7 @@

      InaSAFE

      // create dynatable var jsonTableContents = []; - var showEventHandler = createShowEventHandler(map, markers, - map_events); + var showEventHandler = createShowEventHandler(map, markers, map_events); {# Use magic number 000 for placeholder#} var report_url = '{% url "realtime:earthquake_report_detail" shake_id='000' language=language.selected_language.id %}'; var report_download_url = '{% url "realtime_report:report_pdf" shake_id='000' language=language.selected_language.id language2=language.selected_language.id %}'; @@ -310,25 +309,38 @@

      InaSAFE

      var downloadGridHandler = createDownloadGridHandler(grid_url); var button_templates = [ { + type: 'simple-button', name: '{% trans "Zoom" %}', css_class: 'glyphicon glyphicon-search', handler: 'showEventHandler' }, { + type: 'simple-button', name: '{% trans "Report" %}', css_class: 'glyphicon glyphicon-file', handler: 'showReportHandler' }, { + type: 'dropdown', name: '{% trans "Download" %}', css_class: 'glyphicon glyphicon-download', - handler: 'downloadReportHandler' - }, - { - name: '{% trans "Grid" %}', - css_class: 'glyphicon glyphicon-download', - handler: 'downloadGridHandler', - label: 'XML' + actions: [ + { + text: '{% trans "Report" %}', + handler: 'downloadReportHandler' + }, + { + active: function(event){return event.shake_grid != undefined;}, + text: '{% trans "Grid XML" %}', + handler: 'downloadGridHandler' + }, + { + active: function(event){return event.mmi_output != undefined;}, + text: '{% trans "MMI output" %}', + href: function(event){return event.mmi_output;}, + download: function(event){return event.shake_id+'-mmi.zip'} + } + ] } ]; @@ -351,6 +363,11 @@

      InaSAFE

      dynaTable.paginationPerPage.set(20); dynaTable.sorts.add('time', -1); dynaTable.process(); + + if(jsonTableContents.length > 0){ + var shake_id = jsonTableContents[0].shake_id; + showEventHandler(shake_id); + } } function onEachFeature(feature, layer) { @@ -417,7 +434,6 @@

      InaSAFE

      event_json = data; getEarthquakeEventsJson(data); modifyMapDescriptions(".map-title"); - mapFitAll(map, markers); }); // add content filter handler diff --git a/django_project/realtime/templates/realtime/flood/index.html b/django_project/realtime/templates/realtime/flood/index.html index 211cc42f..80c686ad 100644 --- a/django_project/realtime/templates/realtime/flood/index.html +++ b/django_project/realtime/templates/realtime/flood/index.html @@ -55,16 +55,84 @@

      {% trans "Map" %}

      {% endblock main_content %} {% block filter_content %} -{#
      #} -{#
      #} -{#
      #} -{#

      {% trans "Filters" %}

      #} -{#
      #} -{#
      #} -{# Lorem Ip-sunni...#} -{#
      #} -{#
      #} -{#
      #} +
      +
      +
      +

      {% trans "Filters" %}

      +
      +
      +
      + + + + + + {{ form.start_date|bootstrap }} +
      +
      + + {{ form.end_date|bootstrap }} +
      +
      +
      +
      + + + + + {{ form.min_people_affected|bootstrap }} +
      +
      + + {{ form.max_people_affected|bootstrap }} +
      +
      +
      +
      + + + + + {{ form.min_boundary_flooded|bootstrap }} +
      +
      + + {{ form.max_boundary_flooded|bootstrap }} +
      +
      +
      +
      + + + +
      + + + + + + + + +
      +
      +
      +
      {% endblock filter_content %} {% block table_content %} @@ -79,7 +147,10 @@

      {% trans "Flood" %}

      {% trans "Event ID" %} - {% trans "Time" %} + {% trans "Event ID" %} + {% trans "Time" %} + {% trans "Population Affected" %} + {% trans "RW Flooded" %} @@ -149,9 +220,15 @@

      {% trans "Flood" %}

      jsonTableContents = event_json; dynatable.settings.dataset.originalRecords = jsonTableContents; - dynatable.paginationPerPage.set(100); - dynatable.sorts.add('time', -1); + dynatable.paginationPerPage.set(10); + dynatable.sorts.add('event_id_formatted', -1); dynatable.process(); + + // programatically show first flood in row + if(jsonTableContents.length > 0) { + var flood_id = jsonTableContents[0].event_id; + showFeaturesHandler(flood_id); + } } // Create handler for table @@ -160,24 +237,44 @@

      {% trans "Flood" %}

      var report_url = '{% url "realtime:flood_report_detail" event_id='0000000000-6-rw' language=language.selected_language.id %}'; var showReportHandler = createShowReportHandler(report_url); var showImpactMapHandler = createShowImpactMapHandler(report_url); -{# var downloadReportHandler = createDownloadReportHandler(report_url);#} + var downloadImpactMapHandler = createDownloadImpactMapHandler(report_url); var button_templates = [ { - name: 'Show', + type: 'simple-button', + name: '{% trans "Show" %}', css_class: 'glyphicon glyphicon-search', handler: 'showFeaturesHandler' }, { - name: 'Impact Map', - css_class: 'glyphicon glyphicon-globe', + type: 'simple-button', + name: '{% trans "Report" %}', + css_class: 'glyphicon glyphicon-file', handler: 'showImpactMapHandler' }, -{# {#} -{# name: 'Download',#} -{# css_class: 'glyphicon glyphicon-download',#} -{# handler: 'downloadReportHandler'#} -{# }#} + { + type: 'dropdown', + name: '{% trans "Download" %}', + css_class: 'glyphicon glyphicon-download', + actions: [ + { + active: function(event){return event.hazard_layer !== undefined;}, + text: '{% trans "Hazard Layer" %}', + href: function(event){return event.hazard_layer;}, + download: function(event){return event.event_id+'-hazard.zip';} + }, + { + active: function(event){return event.impact_layer !== undefined;}, + text: '{% trans "Impact Layer" %}', + href: function(event){return event.impact_layer;}, + download: function(event){return event.event_id+'-impact.zip';} + }, + { + text: '{% trans "Impact Map" %}', + handler: 'downloadImpactMapHandler' + } + ] + } ]; $(document).ready(function(){ @@ -191,6 +288,9 @@

      {% trans "Flood" %}

      dataset: { records: jsonTableContents, sorts: {'magnitude': -1} + }, + features: { + search: false } }).data('dynatable'); @@ -198,9 +298,69 @@

      {% trans "Flood" %}

      addActionColumn('#realtime-table', 'Action'); $.get(get_events_url, function(data){ - var event_json = data; + event_json = data; getEventsJson(event_json); }); + + // add content filter handler + // update via ajax request + var $event_filter = $("#event-filter"); + {% if server_side_filter %} + var _updateFilterHandler = createUpdateFilterHandler(get_events_url, + $event_filter, + getEventsJson); + {% else %} + // update via client filters + var _updateFilterHandler = createClientUpdateFilterHandler(get_events_url, + $event_filter, + getEventsJson); + {% endif %} + var updateFilterHandler = function (e, reset) { + _updateFilterHandler(e, reset); + modifyMapDescriptions(".map-title"); + }; + + {# Modify location filter styles#} + modifySearchAndShowLabels(); + + $(".submit-filter").click(updateFilterHandler); + var clearFilterHandler = function (e) { + e.preventDefault(); + var $event_filter = $("#event-filter"); + $event_filter.find("input[type=text].form-control").val(''); + $event_filter.find("input[type=number].form-control").val(''); + $event_filter.find("input[type=checkbox]").prop("checked", false); + updateFilterHandler(e); + $(".submit-filter, .clear-filter").hide(); + }; + $(".clear-filter").click(clearFilterHandler); + + {# Disable date input text#} + $event_filter.find(".date-filter input").attr("readonly", "readonly"); + {# Disable filter button if no filter exists#} + + var show_filter_button = function (e) { + var is_show_filter = false; + $event_filter.find("input[type=text],input[type=number]").each(function (idx, el) { + if ($(el).val()) { + is_show_filter = true; + } + }); + $event_filter.find("input[type=checkbox]").each(function (idx, el) { + if($(el).prop("checked") == true){ + is_show_filter = true; + } + }); + if (is_show_filter) { + $event_filter.find("button.btn").show(); + } + else { + $event_filter.find("button.btn").hide(); + } + }; + + show_filter_button(); + $event_filter.bind('mousemove click keyup focus blur', show_filter_button); }); {% endblock js_container %} diff --git a/django_project/realtime/views/ash.py b/django_project/realtime/views/ash.py index 6af14b0e..82af22ab 100644 --- a/django_project/realtime/views/ash.py +++ b/django_project/realtime/views/ash.py @@ -25,7 +25,7 @@ from realtime.app_settings import LEAFLET_TILES, LANGUAGE_LIST, \ MAPQUEST_MAP_KEY from realtime.models.ash import Ash, AshReport -from realtime.forms import AshUploadForm +from realtime.forms.ash import AshUploadForm from realtime.models.volcano import Volcano from realtime.serializers.ash_serializer import AshSerializer, \ AshReportSerializer, AshGeoJsonSerializer @@ -165,7 +165,7 @@ class AshDetail(mixins.RetrieveModelMixin, mixins.UpdateModelMixin, queryset = Ash.objects.all() serializer_class = AshSerializer lookup_field = 'id' - parser_classes = [JSONParser, FormParser] + parser_classes = [JSONParser, FormParser, MultiPartParser] permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) def get(self, request, volcano_name=None, event_time=None): @@ -192,12 +192,18 @@ def get(self, request, volcano_name=None, event_time=None): serializer = self.get_serializer(instance) return Response(serializer.data) - def put(self, request, *args, **kwargs): + def put(self, request, volcano_name=None, event_time=None, + *args, **kwargs): try: - event_id = request.data['id'] - if event_id: - event = Ash.objects.get(id=event_id) - event.hazard_file.delete() + if volcano_name and event_time: + instance = Ash.objects.get( + volcano__volcano_name__iexact=volcano_name, + event_time=parse(event_time)) + self.kwargs.update(id=instance.id) + if 'hazard_file' in request.FILES and instance.hazard_file: + instance.hazard_file.delete() + if 'impact_files' in request.FILES and instance.impact_files: + instance.impact_files.delete() retval = self.update(request, partial=True, *args, **kwargs) return retval else: diff --git a/django_project/realtime/views/earthquake.py b/django_project/realtime/views/earthquake.py index ccc6ea52..1034eaa5 100644 --- a/django_project/realtime/views/earthquake.py +++ b/django_project/realtime/views/earthquake.py @@ -21,7 +21,7 @@ from rest_framework.response import Response from realtime.app_settings import ( LEAFLET_TILES, LANGUAGE_LIST, MAPQUEST_MAP_KEY) -from realtime.forms import FilterForm +from realtime.forms.earthquake import FilterForm from realtime.filters.earthquake_filter import EarthquakeFilter from realtime.models.earthquake import Earthquake, EarthquakeReport from realtime.serializers.earthquake_serializer import EarthquakeSerializer, \ @@ -182,21 +182,13 @@ def put(self, request, *args, **kwargs): data = request.data shake_id = kwargs.get('shake_id') or data.get('shake_id') instance = Earthquake.objects.get(shake_id=shake_id) - if instance.shake_grid: + if 'shake_grid' in request.FILES and instance.shake_grid: instance.shake_grid.delete() - if 'shake_grid' in request.data: - # posting shake grid means only updating its shake_grid - # properties - request.data['shake_id'] = shake_id - request.data['location'] = instance.location - request.data['location_description'] = \ - instance.location_description - request.data['time'] = instance.time - request.data['magnitude'] = instance.magnitude - request.data['depth'] = instance.depth + if 'mmi_output' in request.FILES and instance.mmi_output: + instance.mmi_output.delete() except Earthquake.DoesNotExist: pass - retval = self.update(request, *args, **kwargs) + retval = self.update(request, partial=True, *args, **kwargs) track_rest_push(request) return retval diff --git a/django_project/realtime/views/flood.py b/django_project/realtime/views/flood.py index 467b311e..0ae0903e 100644 --- a/django_project/realtime/views/flood.py +++ b/django_project/realtime/views/flood.py @@ -5,6 +5,7 @@ from datetime import datetime, timedelta from django.core.exceptions import ValidationError, MultipleObjectsReturned from django.db.models.aggregates import Count +from django.db.models import Q from django.db.utils import IntegrityError from django.http.response import JsonResponse, HttpResponseServerError from django.shortcuts import render_to_response @@ -21,7 +22,7 @@ from realtime.app_settings import ( LEAFLET_TILES, LANGUAGE_LIST, MAPQUEST_MAP_KEY) -from realtime.forms import FilterForm +from realtime.forms.flood import FilterForm from realtime.models.flood import Flood, FloodReport, Boundary, \ FloodEventBoundary from realtime.serializers.flood_serializer import FloodSerializer, \ @@ -114,7 +115,6 @@ class FloodList(mixins.ListModelMixin, mixins.CreateModelMixin, search_fields = ('event_id', ) ordering = ('event_id', ) permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) - # pagination_class = Pagina def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) @@ -140,8 +140,10 @@ def put(self, request, *args, **kwargs): event_id = request.data['event_id'] if event_id: event = Flood.objects.get(event_id=event_id) - event.hazard_layer.delete() - event.impact_layer.delete() + if 'hazard_layer' in request.FILES and event.hazard_layer: + event.hazard_layer.delete() + if 'impact_layer' in request.FILES and event.impact_layer: + event.impact_layer.delete() retval = self.update(request, partial=True, *args, **kwargs) return retval else: @@ -286,9 +288,22 @@ def delete(self, request, event_id, language): class FloodEventList(FloodList): + # only retrieve for 6 interval hours serializer_class = FloodSerializer pagination_class = None + def get_queryset(self): + """Return only 6-interval hours. + + :return: + """ + # this interval is specific for Jakarta (GMT+07) + # it will show up as 6 hourly flood data from 00:00 + query = ( + Q(time__hour=23) | Q(time__hour=5) | + Q(time__hour=11) | Q(time__hour=17)) + return Flood.objects.filter(query) + def flood_event_features(request, event_id): try: diff --git a/django_project/user_map/tests/test_settings.py b/django_project/user_map/tests/test_settings.py index 710775b8..0ce2890b 100644 --- a/django_project/user_map/tests/test_settings.py +++ b/django_project/user_map/tests/test_settings.py @@ -4,6 +4,7 @@ def local_path(path): return os.path.join(os.path.dirname(__file__), path) + SITE_ID = 1 DATABASES = {