diff --git a/calliope_app/api/models/configuration.py b/calliope_app/api/models/configuration.py index bfee4a0e..288b6ded 100644 --- a/calliope_app/api/models/configuration.py +++ b/calliope_app/api/models/configuration.py @@ -833,7 +833,7 @@ def update(self, form_data): METHODS = ['essentials', 'add', 'edit', 'delete'] for method in METHODS: if method in form_data.keys(): - data = form_data[method] + data = form_data[method] getattr(Tech_Param, '_' + method)(self, data) @@ -937,6 +937,7 @@ def _add(cls, technology, data): if (('year' in value_dict) & ('value' in value_dict)): years = value_dict['year'] values = value_dict['value'] + build_year_offsets = value_dict['build_year_offset'] num_records = np.min([len(years), len(values)]) new_objects = [] for i in range(num_records): @@ -945,6 +946,7 @@ def _add(cls, technology, data): model_id=technology.model_id, technology_id=technology.id, year=years[i], + build_year_offset=build_year_offsets[i], parameter_id=key, value=ParamsManager.clean_str_val(vals[0]), raw_value=vals[1] if len(vals) > 1 else vals[0])) @@ -992,6 +994,8 @@ def _edit(cls, technology, data): raw_value=vals[1] if len(vals) > 1 else vals[0]) if 'year' in value_dict: parameter_instance.update(year=value_dict['year']) + if 'build_year_offset' in value_dict: + parameter_instance.update(build_year_offset=value_dict['build_year_offset']) @classmethod def _delete(cls, technology, data): @@ -1009,7 +1013,6 @@ def _delete(cls, technology, data): model_id=technology.model_id, id=key).hard_delete() - class Location(models.Model): class Meta: db_table = "location" @@ -1068,7 +1071,6 @@ def __str__(self): else: return '%s | %s [%s]' % (self.location_1, self.technology, self.technology.pretty_tag) - def update(self, form_data): """ Update the Location Technology parameters stored in Loc_Tech_Param """ @@ -1109,80 +1111,68 @@ class Meta: def _add(cls, loc_tech, data): """ Add a new parameter to a location technology """ for key, value_dict in data.items(): - if (('year' in value_dict) & ('value' in value_dict)): + if all(field in value_dict for field in ['year', 'value', 'build_year_offset']): years = value_dict['year'] values = value_dict['value'] - num_records = np.min([len(years), len(values)]) + build_year_offsets = value_dict['build_year_offset'] + num_records = min(len(years), len(values), len(build_year_offsets)) new_objects = [] for i in range(num_records): - vals = str(values[i]).split('||') + value = str(values[i]) + if '||' in value: + clean_value, raw_value = value.split('||') + else: + clean_value = raw_value = value new_objects.append(cls( - model_id=loc_tech.model_id, - loc_tech_id=loc_tech.id, + model_id=loc_tech.model.id, + loc_tech=loc_tech, year=years[i], + build_year_offset=build_year_offsets[i], parameter_id=key, - value=ParamsManager.clean_str_val(vals[0]), - raw_value=vals[1] if len(vals) > 1 else vals[0])) + value=ParamsManager.clean_str_val(clean_value.strip()), + raw_value=raw_value.strip() + )) cls.objects.bulk_create(new_objects) - @classmethod def _edit(cls, loc_tech, data): - """ Edit a location technology parameter """ - if 'parameter' in data: + if 'parameter_instance' in data: + for param_id, param_data in data['parameter_instance'].items(): + loc_tech_param = cls.objects.filter(id=param_id, loc_tech=loc_tech).first() + if loc_tech_param: + if 'build_year_offset' in param_data: + loc_tech_param.build_year_offset = param_data['build_year_offset'] + if 'year' in param_data: + loc_tech_param.year = param_data['year'] + if 'value' in param_data: + value = param_data['value'] + if '||' in value: + clean_value, raw_value = value.split('||') + else: + clean_value = raw_value = value + loc_tech_param.value = ParamsManager.clean_str_val(clean_value.strip()) + loc_tech_param.raw_value = raw_value.strip() + loc_tech_param.save() + elif 'parameter' in data: for key, value in data['parameter'].items(): - vals = str(value).split('||') - cls.objects.filter( - model_id=loc_tech.model_id, - loc_tech_id=loc_tech.id, - parameter_id=key).hard_delete() - cls.objects.create( - model_id=loc_tech.model_id, - loc_tech_id=loc_tech.id, - parameter_id=key, - value=ParamsManager.clean_str_val(vals[0]), - raw_value=vals[1] if len(vals) > 1 else vals[0]) - if 'timeseries' in data: - for key, value in data['timeseries'].items(): - cls.objects.filter( - model_id=loc_tech.model_id, - loc_tech_id=loc_tech.id, - parameter_id=key).hard_delete() - cls.objects.create( - model_id=loc_tech.model_id, - loc_tech_id=loc_tech.id, + if '||' in value: + clean_value, raw_value = value.split('||') + else: + clean_value = raw_value = value + cls.objects.update_or_create( + loc_tech=loc_tech, parameter_id=key, - value=ParamsManager.clean_str_val(value), - timeseries_meta_id=value, - timeseries=True) - if 'parameter_instance' in data: - instance_items = data['parameter_instance'].items() - for key, value_dict in instance_items: - parameter_instance = cls.objects.filter( - model_id=loc_tech.model_id, - id=key) - if 'value' in value_dict: - vals = str(value_dict['value']).split('||') - parameter_instance.update( - value=ParamsManager.clean_str_val(vals[0]), - raw_value=vals[1] if len(vals) > 1 else vals[0]) - if 'year' in value_dict: - parameter_instance.update(year=value_dict['year']) - + defaults={ + 'value': ParamsManager.clean_str_val(clean_value.strip()), + 'raw_value': raw_value.strip(), + 'model': loc_tech.model + } + ) @classmethod def _delete(cls, loc_tech, data): - """ Delete a location technology parameter """ - if 'parameter' in data: - for key, value in data['parameter'].items(): - cls.objects.filter( - model_id=loc_tech.model_id, - loc_tech_id=loc_tech.id, - parameter_id=key).hard_delete() - elif 'parameter_instance' in data: - instance_items = data['parameter_instance'].items() - for key, value in instance_items: - cls.objects.filter( - model_id=loc_tech.model_id, - id=key).hard_delete() + param_ids = data.get('parameter_instance', []) + for param_id in param_ids: + cls.objects.filter(id=param_id, loc_tech=loc_tech).delete() + class Scenario(models.Model): @@ -1451,7 +1441,7 @@ def get_tech_params_dict(level, id, excl_ids=None, systemwide=True): if level in ['1_tech', '2_loc_tech']: values += ["year", "timeseries", "timeseries_meta_id", - "raw_value", "value"] + "raw_value", "value", "build_year_offset"] # System-Wide Handling if systemwide is False: @@ -1467,6 +1457,7 @@ def get_tech_params_dict(level, id, excl_ids=None, systemwide=True): 'id': param["id"] if 'id' in param.keys() else 0, 'level': level, 'year': param["year"] if 'year' in param.keys() else 0, + 'build_year_offset': param["build_year_offset"] if "build_year_offset" in param.keys() else 0, 'technology_id': technology.id, 'parameter_root': param["parameter__root"], 'parameter_category': param[parameter__category], diff --git a/calliope_app/api/views/configuration.py b/calliope_app/api/views/configuration.py index 53172c7a..ce8425fe 100644 --- a/calliope_app/api/views/configuration.py +++ b/calliope_app/api/views/configuration.py @@ -586,6 +586,35 @@ def delete_technology(request): return HttpResponse(json.dumps(payload), content_type="application/json") +def format_comment(full_name, data): + actions = [] + + # Process 'add' actions + if 'add' in data: + for key, value in data['add'].items(): + years = value['year'] + build_year_offsets = value['build_year_offset'] + values = value['value'] + for year, offset, val in zip(years, build_year_offsets, values): + formatted_val = val.replace("||", " or ") + actions.append(f"Added technology with build_year_offset {offset}, value {formatted_val}, year {year}") + if 'edit' in data: + if 'parameter_instance' in data['edit']: + edit_count = len(data['edit']['parameter_instance']) + if edit_count > 0: + actions.append(f"Edited {edit_count} parameter instance{'s' if edit_count > 1 else ''}") + if 'delete' in data: + if 'parameter_instance' in data['delete']: + delete_count = len(data['delete']['parameter_instance']) + if delete_count > 0: + actions.append(f"Deleted {delete_count} parameter instance{'s' if delete_count > 1 else ''}") + result = f"{full_name} performed the following actions:\n" + for action in actions: + result += f"* {action}\n" + + return result.strip() + + @csrf_protect def update_tech_params(request): """ @@ -607,19 +636,14 @@ def update_tech_params(request): technology_id = escape(request.POST["technology_id"]) form_data = json.loads(request.POST["form_data"]) escaped_form_data = recursive_escape(form_data) - model = Model.by_uuid(model_uuid) model.handle_edit_access(request.user) technology = model.technologies.filter(id=technology_id) - if len(technology) > 0: technology.first().update(escaped_form_data) - # Log Activity - comment = "{} updated the technology: {}.".format( - request.user.get_full_name(), - technology.first().pretty_name, - ) + comment = format_comment(request.user.get_full_name(), escaped_form_data) + # Change this: from what params to this... Move to _add request.user Model_Comment.objects.create(model=model, comment=comment, type="edit") model.notify_collaborators(request.user) model.deprecate_runs(technology_id=technology_id) @@ -862,6 +886,7 @@ def update_loc_tech_params(request): if len(loc_tech) > 0: loc_tech.first().update(form_data) # Log Activity + comment = format_comment(request.user.get_full_name(), form_data) comment = "{} updated the node: {} ({}) @ {}.".format( request.user.get_full_name(), loc_tech.first().technology.pretty_name, diff --git a/calliope_app/client/component_views/configuration.py b/calliope_app/client/component_views/configuration.py index 4057b59f..f650c6b9 100644 --- a/calliope_app/client/component_views/configuration.py +++ b/calliope_app/client/component_views/configuration.py @@ -13,7 +13,7 @@ from django.utils.timezone import make_aware from api.models.configuration import Scenario_Param, Scenario, Scenario_Loc_Tech, \ - Timeseries_Meta, ParamsManager, Model, User_File, Carrier, Tech_Param + Timeseries_Meta, ParamsManager, Model, User_File, Carrier, Tech_Param, Loc_Tech_Param from api.utils import get_cols_from_csv @@ -158,7 +158,6 @@ def all_tech_params(request): for param in parameters: param['raw_units'] = param['units'] - timeseries = Timeseries_Meta.objects.filter(model=model, failure=False, is_uploading=False) diff --git a/calliope_app/client/static/js/main.js b/calliope_app/client/static/js/main.js index 82e27754..1f2ec73a 100644 --- a/calliope_app/client/static/js/main.js +++ b/calliope_app/client/static/js/main.js @@ -13,67 +13,81 @@ $(function() { $(this).slideUp(500); }); }); - function activate_table() { + $('.parameter-value.float-value').each(function() { + autocomplete_units(this); + }); + + // Detection of unsaved changes + $('.parameter-value-new, .parameter-value-existing, .parameter-year-existing, .parameter-build-year-offset-existing').unbind(); + $('.parameter-value-new, .parameter-value-existing, .parameter-year-existing, .parameter-build-year-offset-existing').on('focusout', function() { + if ($(this).val() == '') { $(this).val( $(this).data('value') ) }; + }); + $('.parameter-value-new, .parameter-value-existing, .parameter-year-existing, .parameter-build-year-offset-existing').on('change keyup paste', function() { + var row = $(this).parents('tr'), + year = row.find('.parameter-year').val(), + old_year = row.find('.parameter-year').data('value'), + build_year_offset = row.find('.parameter-build-year-offset').val(), + old_build_year_offset = row.find('.parameter-build-year-offset').data('value'), + value = row.find('.parameter-value').val(), + old_value = row.find('.parameter-value').data('value'), + param_id = $(this).parents('tr').data('param_id'), + ts_id = row.find('.parameter-value.timeseries').val(); + + // Convert to number if possible + if (+value) { value = +value }; + if (+old_value) { old_value = +old_value }; + if (+build_year_offset) { build_year_offset = +build_year_offset }; + if (+old_build_year_offset) { old_build_year_offset = +old_build_year_offset }; + + // If it is a timeseries: render the charts + if (ts_id) { + activate_charts(param_id, ts_id) + }; + + // Reset the formatting of row + row.find('.parameter-reset').addClass('hide') + $('.parameter-delete').on('click', function() { + var row = $(this).parents('tr'); + // Deletion logic here + }); + + + row.removeClass('table-warning'); + $(this).removeClass('invalid-value'); + + // Update Row based on Input + var update_val = (value != '') && (value != old_value), + update_year = (year != '') && (year != old_year), + update_build_year_offset = (build_year_offset != '') && (build_year_offset != old_build_year_offset); + + if (update_val && ($(this).hasClass('float-value') == true)) { + var units = row.find('.parameter-units').attr('data-value'), + val = convert_units(value, units); + if (typeof(val) == 'number') { + $(this).attr('data-target_value', formatNumber(val, false)); + row.find('.parameter-target-value').html(formatNumber(val, true)); + row.find('.parameter-reset').removeClass('hide') + row.find('.parameter-delete, .parameter-value-delete').addClass('hide') + row.addClass('table-warning'); + } else { + $(this).addClass('invalid-value'); + row.find('.parameter-target-value').html(row.find('.parameter-target-value').data('value')); + } + } else if (update_val || update_year || update_build_year_offset) { + row.find('.parameter-reset').removeClass('hide') + row.find('.parameter-delete, .parameter-value-delete').addClass('hide') + row.addClass('table-warning'); + } else { + row.find('.parameter-target-value').html(row.find('.parameter-target-value').data('value')); + } + check_unsaved(); + }); - $('.parameter-value.float-value').each(function() { - autocomplete_units(this); - }); - - // Detection of unsaved changes - $('.parameter-value-new, .parameter-value-existing, .parameter-year-existing').unbind(); - $('.parameter-value-new, .parameter-value-existing, .parameter-year-existing').on('focusout', function() { - if ($(this).val() == '') { $(this).val( $(this).data('value') ) }; - }); - $('.parameter-value-new, .parameter-value-existing, .parameter-year-existing').on('change keyup paste', function() { - var row = $(this).parents('tr'), - year = row.find('.parameter-year').val(), - old_year = row.find('.parameter-year').data('value'), - value = row.find('.parameter-value').val(), - old_value = row.find('.parameter-value').data('value'), - param_id = $(this).parents('tr').data('param_id'), - ts_id = row.find('.parameter-value.timeseries').val(); - // Convert to number if possible - if (+value) { value = +value }; - if (+old_value) { old_value = +old_value }; - // If it is a timeseries: render the charts - if (ts_id) { - activate_charts(param_id, ts_id) - }; - // Reset the formatting of row - row.find('.parameter-reset').addClass('hide') - row.find('.parameter-delete, .parameter-value-delete').removeClass('hide') - row.removeClass('table-warning'); - $(this).removeClass('invalid-value'); - - // Update Row based on Input - var update_val = (value != '') & (value != old_value), - update_year = (year != '') & (year != old_year); - if (update_val & ($(this).hasClass('float-value') == true)) { - var units = row.find('.parameter-units').attr('data-value'), - val = convert_units(value, units); - if (typeof(val) == 'number') { - $(this).attr('data-target_value', formatNumber(val, false)); - row.find('.parameter-target-value').html(formatNumber(val, true)); - row.find('.parameter-reset').removeClass('hide') - row.find('.parameter-delete, .parameter-value-delete').addClass('hide') - row.addClass('table-warning'); - } else { - $(this).addClass('invalid-value'); - row.find('.parameter-target-value').html(row.find('.parameter-target-value').data('value')); - } - } else if (update_val || update_year) { - row.find('.parameter-reset').removeClass('hide') - row.find('.parameter-delete, .parameter-value-delete').addClass('hide') - row.addClass('table-warning'); - } else { - row.find('.parameter-target-value').html(row.find('.parameter-target-value').data('value')); - } - check_unsaved(); - }); // Paste multiple values - activate_paste('.dynamic_value_input'); - activate_paste('.dynamic_year_input'); + activate_paste('.dynamic_value_input'); + activate_paste('.dynamic_year_input'); + activate_paste('.dynamic_build_year_offset_input'); // Reset parameter to saved value in database $('.parameter-reset').unbind(); @@ -104,21 +118,27 @@ function activate_table() { $('.parameter-delete').on('click', function() { var row = $(this).parents('tr'), param_id = row.data('param_id'), - rows = $('tr[data-param_id='+param_id+']'); + rows = $('tr[data-param_id=' + param_id + ']'); + if (row.hasClass('table-danger')) { - rows.find('.check_delete').prop("checked", false) + // Unmark for deletion + rows.find('.check_delete').prop("checked", false); rows.removeClass('table-danger'); + rows.addClass('table-warning'); // Add warning class to indicate unsaved change rows.find('.parameter-value, .parameter-year').prop('disabled', false); change_timeseries_color(param_id, true); } else { - rows.find('.check_delete').prop("checked", true) + // Mark for deletion + rows.find('.check_delete').prop("checked", true); rows.addClass('table-danger'); - rows.find('.parameter-value, .parameter-year').prop('disabled', true) + rows.addClass('table-warning'); // Add warning class to indicate unsaved change + rows.find('.parameter-value, .parameter-year').prop('disabled', true); change_timeseries_color(param_id, false); } + check_unsaved(); }); - $('.parameter-value-delete').unbind(); + $('.parameter-value-delete').unbind(); $('.parameter-value-delete').on('click', function() { var row = $(this).parents('tr'); if (row.hasClass('table-danger')) { @@ -168,9 +188,10 @@ function activate_table() { }); // Allow 'return' key to tab through input cells - activate_return('.static_inputs'); - activate_return('.dynamic_year_input'); - activate_return('.dynamic_value_input'); + activate_return('.static_inputs'); + activate_return('.dynamic_year_input'); + activate_return('.dynamic_build_year_offset_input'); + activate_return('.dynamic_value_input'); // Show and Hide the parameter rows $('.param_row_toggle').unbind(); @@ -1206,6 +1227,8 @@ function add_row($this) { row.find('.param_row_toggle').find('.view_rows').addClass('hide'); var add_row = $('.add_param_row_'+param_id).last().clone(); add_row.find('.parameter-value-new').addClass('dynamic_value_input'); + add_row.find('.parameter-build-year-offset-new').addClass('dynamic_build_year_offset_input'); + add_row.find('.parameter-year-new').addClass('dynamic_year_input'); add_row.removeClass('add_param_row_min').addClass('table-warning'); add_row.insertBefore($('.add_param_row_'+param_id).last()); diff --git a/calliope_app/client/templates/technology_parameters.html b/calliope_app/client/templates/technology_parameters.html index 26fd6b59..32bc8184 100644 --- a/calliope_app/client/templates/technology_parameters.html +++ b/calliope_app/client/templates/technology_parameters.html @@ -140,6 +140,7 @@