Skip to content

Commit

Permalink
Merge pull request #3319 from WikiWatershed/tt/custom-weather-usage-a…
Browse files Browse the repository at this point in the history
…cross-scenarios

Connects #3309
  • Loading branch information
rajadain committed May 19, 2020
2 parents 9f4b1a2 + 6ceff3b commit f9c2bbc
Show file tree
Hide file tree
Showing 12 changed files with 178 additions and 77 deletions.
3 changes: 3 additions & 0 deletions src/mmw/apps/modeling/urls.py
Expand Up @@ -21,6 +21,9 @@
url(r'projects/(?P<proj_id>[0-9]+)$', views.project, name='project'),
url(r'scenarios/$', views.scenarios, name='scenarios'),
url(r'scenarios/(?P<scen_id>[0-9]+)$', views.scenario, name='scenario'),
url(r'scenarios/(?P<scen_id>[0-9]+)/duplicate/?$',
views.scenario_duplicate,
name='scenario_duplicate'),
url(r'scenarios/(?P<scen_id>[0-9]+)/custom-weather-data/?$',
views.scenario_custom_weather_data,
name='scenario_custom_weather_data'),
Expand Down
61 changes: 60 additions & 1 deletion src/mmw/apps/modeling/views.py
Expand Up @@ -176,6 +176,36 @@ def scenario(request, scen_id):
return Response(status=status.HTTP_404_NOT_FOUND)


@decorators.api_view(['POST'])
@decorators.permission_classes((IsAuthenticatedOrReadOnly, ))
def scenario_duplicate(request, scen_id):
"""Duplicate a scenario."""
scenario = get_object_or_404(Scenario, id=scen_id)

if scenario.project.user != request.user:
return Response(status=status.HTTP_404_NOT_FOUND)

scenario.pk = None
scenario.is_current_conditions = False

# Give the scenario a new name. Same logic as in
# modeling/models.js:makeNewScenarioName.
names = scenario.project.scenarios.values_list('name', flat=True)
copy_name = 'Copy of {}'.format(scenario.name)
copy_counter = 1

while copy_name in names:
copy_name = 'Copy of {} {}'.format(scenario.name, copy_counter)
copy_counter += 1

scenario.name = copy_name
scenario.save()

serializer = ScenarioSerializer(scenario)

return Response(serializer.data, status=status.HTTP_201_CREATED)


@decorators.api_view(['GET', 'POST', 'DELETE'])
@decorators.permission_classes((IsAuthenticatedOrReadOnly, ))
def scenario_custom_weather_data(request, scen_id):
Expand Down Expand Up @@ -206,8 +236,9 @@ def scenario_custom_weather_data(request, scen_id):
'file_name': scenario.weather_custom.name})

elif project.user.id == request.user.id:
errors = []

if request.method == 'POST':
errors = []

if not request.FILES or 'weather' not in request.FILES:
errors.append('Must specify file in `weather` field.')
Expand All @@ -231,6 +262,34 @@ def scenario_custom_weather_data(request, scen_id):

elif request.method == 'DELETE':
if scenario.weather_custom.name:
# Check if any other scenarios use the same weather file
others = (scenario.project.scenarios
.exclude(id=scenario.id)
.filter(weather_type=WeatherType.CUSTOM,
weather_custom=scenario.weather_custom)
.values_list('name', flat=True))

if others.count() > 0:
errors.append('Cannot delete weather file.'
' It is also used in: "{}".'
' Either delete those scenarios, or set them'
' to use Available Data before deleting'
' the custom weather file.'.format(
'", "'.join(others)))
return Response({'errors': errors},
status=status.HTTP_400_BAD_REQUEST)

# Delete file from all scenarios not actively using it
passives = (scenario.project.scenarios
.exclude(id=scenario.id,
weather_type=WeatherType.CUSTOM)
.filter(weather_custom=scenario.weather_custom))

for s in passives:
s.weather_custom.delete()
s.save()

# Delete file from given scenario
scenario.weather_custom.delete()

scenario.weather_type = WeatherType.DEFAULT
Expand Down
2 changes: 1 addition & 1 deletion src/mmw/js/src/modeling/gwlfe/quality/views.js
Expand Up @@ -42,7 +42,7 @@ var ResultView = Marionette.LayoutView.extend({

return {
weather_type: weather_type,
weather_custom: modelingUtils.getFileName(weather_custom),
weather_custom: modelingUtils.getFileName(weather_custom, '.csv'),
years: gis_data.WxYrs,
showSubbasinModelingButton: coreUtils
.isWKAoIValidForSubbasinModeling(App.currentProject.get('wkaoi')),
Expand Down
2 changes: 1 addition & 1 deletion src/mmw/js/src/modeling/gwlfe/runoff/views.js
Expand Up @@ -59,7 +59,7 @@ var ResultView = Marionette.LayoutView.extend({
return {
lengthUnit: lengthUnit,
weather_type: weather_type,
weather_custom: modelingUtils.getFileName(weather_custom),
weather_custom: modelingUtils.getFileName(weather_custom, '.csv'),
years: gis_data.WxYrs,
};
},
Expand Down
34 changes: 34 additions & 0 deletions src/mmw/js/src/modeling/gwlfe/weather/models.js
Expand Up @@ -15,6 +15,18 @@ var WindowModel = Backbone.Model.extend({
custom_weather_file_name: null,
},

validate: function(attrs) {
if (attrs.weather_type === WeatherType.CUSTOM) {
if (attrs.custom_weather_output === null) {
return 'Custom Weather cannot have empty output';
}

if (attrs.custom_weather_errors.length > 0) {
return 'Custom Weather has errors';
}
}
},

postCustomWeather: function(formData) {
var self = this,
scenario_id = this.get('scenario_id'),
Expand Down Expand Up @@ -56,6 +68,22 @@ var WindowModel = Backbone.Model.extend({
custom_weather_file_name: data.file_name,
custom_weather_errors: data.errors || [],
});
}).catch(function(err) {
var errors = err && err.responseJSON && err.responseJSON.errors;

if (err.status === 404) {
if (errors) {
errors.push('Custom weather file not found.');
} else {
errors = ['Custom weather file not found.'];
}
}

self.set({
custom_weather_output: null,
custom_weather_errors: errors || ['Unknown server error.'],
custom_weather_file_name: null,
});
});
},

Expand Down Expand Up @@ -92,6 +120,12 @@ var WindowModel = Backbone.Model.extend({
custom_weather_errors: [], // Array of String
custom_weather_file_name: null,
});
}).catch(function(err) {
var errors = err && err.responseJSON && err.responseJSON.errors;

self.set({
custom_weather_errors: errors || ['Unknown server error.'],
});
});
},

Expand Down
4 changes: 4 additions & 0 deletions src/mmw/js/src/modeling/gwlfe/weather/templates/existing.html
Expand Up @@ -19,3 +19,7 @@
<p>
{{ custom_weather_output.WxYrs }} years, {{ custom_weather_output.WxYrBeg }}&ndash;{{ custom_weather_output.WxYrEnd }}
</p>
<div class="alert alert-danger hidden delete-error-box">
<strong>Delete failed:</strong>
<ul class="delete-error-list"></ul>
</div>
5 changes: 2 additions & 3 deletions src/mmw/js/src/modeling/gwlfe/weather/templates/modal.html
Expand Up @@ -51,10 +51,9 @@ <h2 class="weather-col-title">
</div>
<div class="modal-footer" style="text-align: right">
<div class="footer-content">
<button class="btn btn-md btn-default" data-dismiss="modal">
Cancel
<button class="btn btn-md btn-active" data-dismiss="modal">
Done
</button>
<button class="btn btn-md btn-active save">Save</button>
</div>
</div>
</div>
Expand Down
71 changes: 36 additions & 35 deletions src/mmw/js/src/modeling/gwlfe/weather/views.js
Expand Up @@ -23,17 +23,16 @@ var WeatherDataModal = modalViews.ModalBaseView.extend({

ui: {
weatherTypeRadio: 'input[name="weather-type"]',
saveButton: 'button.save',
},

events: _.defaults({
'change @ui.weatherTypeRadio': 'onWeatherTypeDOMChange',
'click @ui.saveButton': 'saveAndClose',
}, modalViews.ModalBaseView.prototype.events),

modelEvents: {
'change:custom_weather_file_name': 'showWeatherDataView',
'change:weather_type': 'onWeatherTypeModelChange',
'change:weather_type change:custom_weather_output change:custom_weather_file_name': 'validateAndSave',
},

initialize: function(options) {
Expand All @@ -44,10 +43,12 @@ var WeatherDataModal = modalViews.ModalBaseView.extend({
onRender: function() {
var self = this;

this.model.fetchCustomWeatherIfNeeded().then(function() {
self.showWeatherDataView();
self.$el.modal('show');
});
this.model
.fetchCustomWeatherIfNeeded()
.always(function() {
self.showWeatherDataView();
self.$el.modal('show');
});
},

showWeatherDataView: function() {
Expand All @@ -58,31 +59,10 @@ var WeatherDataModal = modalViews.ModalBaseView.extend({
this.customWeatherRegion.show(new WeatherDataView({
model: this.model,
}));

this.validateModal();
},

validateModal: function() {
var weather_type = this.model.get('weather_type'),
custom_weather_output = this.model.get('custom_weather_output'),
custom_weather_errors = this.model.get('custom_weather_errors'),

valid_default = weather_type === WeatherType.DEFAULT,
valid_simulation = weather_type === WeatherType.SIMULATION,
valid_custom = weather_type === WeatherType.CUSTOM &&
custom_weather_output !== null &&
custom_weather_errors.length === 0,

disabled = !(valid_default || valid_simulation || valid_custom);


this.ui.saveButton.prop('disabled', disabled);
},

onWeatherTypeDOMChange: function(e) {
this.model.set('weather_type', e.target.value);

this.validateModal();
},

onWeatherTypeModelChange: function() {
Expand All @@ -93,16 +73,18 @@ var WeatherDataModal = modalViews.ModalBaseView.extend({
this.$('input[name="weather-type"][value="' + weather_type + '"]');

radio.prop('checked', true);

this.validateModal();
},

saveAndClose: function() {
this.addModification(this.model.getOutput());
this.scenario.set('weather_type', this.model.get('weather_type'));
validateAndSave: function() {
var oldWeather = this.scenario.get('weather_type'),
newWeather = this.model.get('weather_type');

this.hide();
}
if (this.model.isValid() && oldWeather !== newWeather) {
// Set weather type silently so it doesn't trigger it's own save
this.scenario.set('weather_type', newWeather, { silent: true });
this.addModification(this.model.getOutput());
}
},
});

var UploadWeatherDataView = Marionette.ItemView.extend({
Expand Down Expand Up @@ -177,16 +159,22 @@ var ExistingWeatherDataView = Marionette.ItemView.extend({

ui: {
delete: '.delete',
deleteErrorBox: '.delete-error-box',
deleteErrorList: '.delete-error-list',
},

events: {
'click @ui.delete': 'onDeleteClick',
},

modelEvents: {
'change:custom_weather_errors': 'onServerValidation',
},

templateHelpers: function() {
return {
custom_weather_file_name:
utils.getFileName(this.model.get('custom_weather_file_name')),
utils.getFileName(this.model.get('custom_weather_file_name'), '.csv'),
};
},

Expand All @@ -206,6 +194,19 @@ var ExistingWeatherDataView = Marionette.ItemView.extend({
self.model.deleteCustomWeather();
});
},

onServerValidation: function() {
var custom_weather_errors = this.model.get('custom_weather_errors');

if (custom_weather_errors.length > 0) {
this.ui.deleteErrorBox.removeClass('hidden');
this.ui.deleteErrorList.html(
custom_weather_errors
.map(function(err) { return '<li>' + err + '</li>'; })
.join('')
);
}
},
});

function showWeatherDataModal(scenario, addModification) {
Expand Down
24 changes: 6 additions & 18 deletions src/mmw/js/src/modeling/models.js
Expand Up @@ -1787,25 +1787,13 @@ var ScenariosCollection = Backbone.Collection.extend({
},

duplicateScenario: function(cid) {
var source = this.get(cid),
newModel = new ScenarioModel({
is_current_conditions: false,
name: this.makeNewScenarioName('Copy of ' + source.get('name')),
user_id: source.get('user_id'),
inputs: source.get('inputs').toJSON(),
inputmod_hash: source.get('inputmod_hash'),
modifications: source.get('modifications').toJSON(),
modification_hash: source.get('modification_hash'),
job_id: source.get('job_id'),
poll_error: source.get('poll_error'),
results: source.get('results').toJSON(),
aoi_census: source.get('aoi_census'),
modification_censuses: source.get('modification_censuses'),
allow_save: source.get('allow_save'),
});
var self = this;

this.add(newModel);
this.setActiveScenarioByCid(newModel.cid);
$.post('/mmw/modeling/scenarios/' + this.get(cid).id + '/duplicate/')
.then(function(data) {
self.add(new ScenarioModel(data));
self.setActiveScenarioById(data.id);
});
},

// Generate a unique scenario name based off baseName.
Expand Down

0 comments on commit f9c2bbc

Please sign in to comment.