From e8cb74edf8515c4bd5980224d794638ccdaec5b6 Mon Sep 17 00:00:00 2001 From: mutantsan Date: Tue, 12 Sep 2023 16:25:39 +0300 Subject: [PATCH] feature: fix the upload, implement scope for upload widget --- ckanext/tour/assets/js/tour-htmx.js | 2 +- ckanext/tour/assets/js/tour-image-upload.js | 305 +++++++++++++++++- ckanext/tour/assets/js/tour-steps.js | 1 - ckanext/tour/helpers.py | 5 + ckanext/tour/logic/action.py | 68 ++-- ckanext/tour/logic/schema.py | 10 +- ckanext/tour/logic/validators.py | 2 +- ...e58_create_tour_tourstep_tourstepimage_.py | 1 + ckanext/tour/model.py | 4 +- .../tour/snippets/tour_image_upload.html | 5 +- .../templates/tour/snippets/tour_step.html | 8 +- ckanext/tour/templates/tour/tour_add.html | 7 + ckanext/tour/templates/tour/tour_edit.html | 31 +- ckanext/tour/tests/logic/test_action.py | 1 + ckanext/tour/views/tour_update.py | 4 +- 15 files changed, 380 insertions(+), 74 deletions(-) diff --git a/ckanext/tour/assets/js/tour-htmx.js b/ckanext/tour/assets/js/tour-htmx.js index 57d9369..ea7156f 100644 --- a/ckanext/tour/assets/js/tour-htmx.js +++ b/ckanext/tour/assets/js/tour-htmx.js @@ -5,7 +5,7 @@ ckan.module("tour-htmx", function ($) { return { initialize: function () { $.proxyAll(this, /_on/); - console.log('loaded'); + document.addEventListener('htmx:beforeRequest', this._onHTMXbeforeRequest); document.addEventListener('htmx:afterSettle', this._onHTMXafterSettle); document.addEventListener('htmx:pushedIntoHistory', this._onHTMXpushedIntoHistory); diff --git a/ckanext/tour/assets/js/tour-image-upload.js b/ckanext/tour/assets/js/tour-image-upload.js index 3b4ccf6..7a822fd 100644 --- a/ckanext/tour/assets/js/tour-image-upload.js +++ b/ckanext/tour/assets/js/tour-image-upload.js @@ -1,17 +1,300 @@ /** -* Extends core image-upload.js to prevent uploaded URL change. -* -* @param {Event} e -*/ + * It's an updated version of core image-upload.js, with a scope support, because + * we need to have multiple uploads on the same page + */ +this.ckan.module('tour-image-upload', function ($) { + return { + /* options object can be extended using data-module-* attributes */ + options: { + is_url: false, + is_upload: false, + field_upload: 'image_upload', + field_url: 'image_url', + field_clear: 'clear_upload', + field_name: 'name', + scope: null, + upload_label: '', + previous_upload: false + }, -var extendedModule = $.extend({}, ckan.module.registry["image-upload"].prototype); + /* Should be changed to true if user modifies resource's name + * + * @type {Boolean} + */ + _nameIsDirty: false, -extendedModule._fileNameFromUpload = function (url) { - return url; -} + /* Initialises the module setting up elements and event listeners. + * + * Returns nothing. + */ + initialize: function () { + $.proxyAll(this, /_on/); + var options = this.options; -ckan.module("tour-image-upload", function ($, _) { - "use strict"; + // firstly setup the fields + var field_upload = 'input[name="' + options.field_upload + '"]'; + var field_url = 'input[name="' + options.field_url + '"]'; + var field_clear = 'input[name="' + options.field_clear + '"]'; + var field_name = 'input[name="' + options.field_name + '"]'; - return extendedModule; + this.scope = $(options.scope); + + this.input = this.scope.find(field_upload, this.el); + this.field_url = this.scope.find(field_url, this.el).parents('.form-group'); + this.field_image = this.input.parents('.form-group'); + this.field_url_input = $('input', this.field_url); + this.field_name = this.scope.find(field_name); + + // this is the location for the upload/link data/image label + this.label_location = this.scope.find('label[for="field-image-url"]'); + // determines if the resource is a data resource + this.is_data_resource = (this.options.field_url === 'url') && (this.options.field_upload === 'upload'); + this.previousUpload = this.options.previous_upload; + + // Is there a clear checkbox on the form already? + var checkbox = this.scope.find(field_clear, this.el); + if (checkbox.length > 0) { + checkbox.parents('.form-group').remove(); + } + + // Adds the hidden clear input to the form + this.field_clear = $('') + .appendTo(this.el); + + // Button to set the field to be a URL + this.button_url = $('' + + '' + + this._('Link') + '') + .prop('title', this._('Link to a URL on the internet (you can also link to an API)')) + .on('click', this._onFromWeb) + .insertAfter(this.input); + + // Button to attach local file to the form + this.button_upload = $('' + + '' + + this._('Upload') + '') + .insertAfter(this.input); + + if (this.previousUpload) { + $('
' + + this._('Please select the file to upload again') + '
').appendTo(this.field_image); + } + + // Button for resetting the form when there is a URL set + var removeText = this._('Remove'); + $('' + + removeText + '') + .prop('title', removeText) + .on('click', this._onRemove) + .insertBefore(this.field_url_input); + + // Update the main label (this is displayed when no data/image has been uploaded/linked) + $('label[for="field-image-upload"]').text(options.upload_label || this._('Image')); + + // Setup the file input + this.input + .on('mouseover', this._onInputMouseOver) + .on('mouseout', this._onInputMouseOut) + .on('change', this._onInputChange) + .prop('title', this._('Upload a file on your computer')) + .css('width', this.button_upload.outerWidth()); + + // Fields storage. Used in this.changeState + this.fields = $('') + .add(this.button_upload) + .add(this.button_url) + .add(this.input) + .add(this.field_url) + .add(this.field_image); + + // Disables autoName if user modifies name field + this.field_name + .on('change', this._onModifyName); + // Disables autoName if resource name already has value, + // i.e. we on edit page + if (this.field_name.val()) { + this._nameIsDirty = true; + } + + if (options.is_url) { + this._showOnlyFieldUrl(); + + this._updateUrlLabel(this._('URL')); + } else if (options.is_upload) { + this._showOnlyFieldUrl(); + + this.field_url_input.prop('readonly', true); + this.field_url_input.val(this.field_url_input.val()); + + this._updateUrlLabel(this._('File')); + } else { + this._showOnlyButtons(); + } + }, + + /* Quick way of getting just the filename from the uri of the resource data + * + * url - The url of the uploaded data file + * + * Returns String. + */ + _fileNameFromUpload: function (url) { + // If it's a local CKAN image return the entire URL. + if (/^\/base\/images/.test(url)) { + return url; + } + + // remove fragment (#) + url = url.substring(0, (url.indexOf("#") === -1) ? url.length : url.indexOf("#")); + // remove query string + url = url.substring(0, (url.indexOf("?") === -1) ? url.length : url.indexOf("?")); + // extract the filename + url = url.substring(url.lastIndexOf("/") + 1, url.length); + + return url; // filename + }, + + /* Update the `this.label_location` text + * + * If the upload/link is for a data resource, rather than an image, + * the text for label[for="field-image-url"] will be updated. + * + * label_text - The text for the label of an uploaded/linked resource + * + * Returns nothing. + */ + _updateUrlLabel: function (label_text) { + if (!this.is_data_resource) { + return; + } + + this.label_location.text(label_text); + }, + + /* Event listener for when someone sets the field to URL mode + * + * Returns nothing. + */ + _onFromWeb: function () { + this._showOnlyFieldUrl(); + + this.field_url_input.focus() + .on('blur', this._onFromWebBlur); + + if (this.options.is_upload) { + this.field_clear.val('true'); + } + + this._updateUrlLabel(this._('URL')); + }, + + /* Event listener for resetting the field back to the blank state + * + * Returns nothing. + */ + _onRemove: function () { + this._showOnlyButtons(); + + this.field_url_input.val(''); + this.field_url_input.prop('readonly', false); + + this.field_clear.val('true'); + }, + + /* Event listener for when someone chooses a file to upload + * + * Returns nothing. + */ + _onInputChange: function () { + var file_name = this.input.val().split(/^C:\\fakepath\\/).pop(); + + // Internet Explorer 6-11 and Edge 20+ + var isIE = !!document.documentMode; + var isEdge = !isIE && !!window.StyleMedia; + // for IE/Edge when 'include filepath option' is enabled + if (isIE || isEdge) { + var fName = file_name.match(/[^\\\/]+$/); + file_name = fName ? fName[0] : file_name; + } + + this.field_url_input.val(file_name); + this.field_url_input.prop('readonly', true); + + this.field_clear.val(''); + + this._showOnlyFieldUrl(); + + this._autoName(file_name); + + this._updateUrlLabel(this._('File')); + }, + + /* Show only the buttons, hiding all others + * + * Returns nothing. + */ + _showOnlyButtons: function () { + this.fields.hide(); + this.button_upload + .add(this.field_image) + .add(this.button_url) + .add(this.input) + .show(); + }, + + /* Show only the URL field, hiding all others + * + * Returns nothing. + */ + _showOnlyFieldUrl: function () { + this.fields.hide(); + this.field_url.show(); + }, + + /* Event listener for when a user mouseovers the hidden file input + * + * Returns nothing. + */ + _onInputMouseOver: function () { + this.button_upload.addClass('hover'); + }, + + /* Event listener for when a user mouseouts the hidden file input + * + * Returns nothing. + */ + _onInputMouseOut: function () { + this.button_upload.removeClass('hover'); + }, + + /* Event listener for changes in resource's name by direct input from user + * + * Returns nothing + */ + _onModifyName: function () { + this._nameIsDirty = true; + }, + + /* Event listener for when someone loses focus of URL field + * + * Returns nothing + */ + _onFromWebBlur: function () { + var url = this.field_url_input.val().match(/([^\/]+)\/?$/) + if (url) { + this._autoName(url.pop()); + } + }, + + /* Automatically add file name into field Name + * + * Select by attribute [name] to be on the safe side and allow to change field id + * Returns nothing + */ + _autoName: function (name) { + if (!this._nameIsDirty) { + this.field_name.val(name); + } + } + }; }); diff --git a/ckanext/tour/assets/js/tour-steps.js b/ckanext/tour/assets/js/tour-steps.js index ead36e5..e9ad7e1 100644 --- a/ckanext/tour/assets/js/tour-steps.js +++ b/ckanext/tour/assets/js/tour-steps.js @@ -76,7 +76,6 @@ ckan.module("tour-steps", function ($) { var steps = $(".tour-steps__steps .tour-accordion") .not(".draggable-source--is-dragging") .not(".draggable--original"); - console.log(steps.length); steps.each((idx, step) => this._updateStepIndexes(idx + 1, step)); }, diff --git a/ckanext/tour/helpers.py b/ckanext/tour/helpers.py index ea98960..2f63426 100644 --- a/ckanext/tour/helpers.py +++ b/ckanext/tour/helpers.py @@ -1,3 +1,5 @@ +import uuid + from ckanext.tour.model import TourStep @@ -11,3 +13,6 @@ def tour_get_position_options(): TourStep.Position.left, ) ] + +def tour_random_step_id() -> str: + return str(uuid.uuid4()) diff --git a/ckanext/tour/logic/action.py b/ckanext/tour/logic/action.py index 4bb98d6..e961c5c 100644 --- a/ckanext/tour/logic/action.py +++ b/ckanext/tour/logic/action.py @@ -10,6 +10,7 @@ import ckanext.tour.logic.schema as schema import ckanext.tour.model as tour_model from ckanext.tour.exception import TourStepFileError +from ckanext.tour.model import Tour, TourStep, TourStepImage @tk.side_effect_free @@ -17,7 +18,7 @@ def tour_show(context, data_dict): tk.check_access("tour_show", context, data_dict) - return tour_model.Tour.get(data_dict["id"]).dictize(context) # type: ignore + return Tour.get(data_dict["id"]).dictize(context) # type: ignore @tk.side_effect_free @@ -26,12 +27,12 @@ def tour_list(context, data_dict): """Return a list of tours from database""" tk.check_access("tour_list", context, data_dict) - query = model.Session.query(tour_model.Tour) + query = model.Session.query(Tour) if data_dict.get("state"): - query = query.filter(tour_model.Tour.state == data_dict["state"]) + query = query.filter(Tour.state == data_dict["state"]) - query = query.order_by(tour_model.Tour.created_at.desc()) + query = query.order_by(Tour.created_at.desc()) return [tour.dictize(context) for tour in query.all()] @@ -41,7 +42,7 @@ def tour_create(context, data_dict): tk.check_access("tour_create", context, data_dict) steps: list[dict[str, Any]] = data_dict.pop("steps", []) - tour = tour_model.Tour.create(data_dict) + tour = Tour.create(data_dict) for step in steps: step["tour_id"] = tour.id @@ -58,7 +59,7 @@ def tour_create(context, data_dict): def tour_remove(context, data_dict): tk.check_access("tour_remove", context, data_dict) - tour = cast(tour_model.Tour, tour_model.Tour.get(data_dict["id"])) + tour = cast(Tour, Tour.get(data_dict["id"])) for step in tour.steps: step.delete() @@ -79,7 +80,9 @@ def tour_step_create(context, data_dict): if len(images) > 1: raise tk.ValidationError({"image": "only 1 image for step allowed"}) - tour_step = tour_model.TourStep.create(data_dict) + tour = cast(Tour, Tour.get(data_dict["tour_id"])) + data_dict["index"] = len(tour.steps) + 1 + tour_step = TourStep.create(data_dict) for image in images: try: @@ -92,7 +95,9 @@ def tour_step_create(context, data_dict): }, ) except TourStepFileError as e: - raise tk.ValidationError(f"Error while uploading step image: {e}") + raise tk.ValidationError( + {"image": [f"Error while uploading step image: {e}"]} + ) return tour_step.dictize(context) @@ -101,7 +106,7 @@ def tour_step_create(context, data_dict): def tour_update(context, data_dict): tk.check_access("tour_update", context, data_dict) - tour = cast(tour_model.Tour, tour_model.Tour.get(data_dict["id"])) + tour = cast(Tour, Tour.get(data_dict["id"])) tour.title = data_dict["title"] tour.anchor = data_dict["anchor"] @@ -122,19 +127,13 @@ def tour_update(context, data_dict): # ) for step in steps: - if step.get("id"): - tk.get_action("tour_step_update")( - {"ignore_auth": True}, - step, - ) - else: - step["tour_id"] = tour.id - - tk.get_action("tour_step_create")( - {"ignore_auth": True}, - step, - ) + action = "tour_step_update" if step.get("id") else "tour_step_create" + step["tour_id"] = tour.id + try: + tk.get_action(action)({"ignore_auth": True}, step) + except tk.ValidationError as e: + raise tk.ValidationError(e.error_dict) return tour.dictize(context) @@ -142,8 +141,9 @@ def tour_update(context, data_dict): def tour_step_update(context, data_dict): tk.check_access("tour_step_update", context, data_dict) - tour_step = cast(tour_model.TourStep, tour_model.TourStep.get(data_dict["id"])) + tour_step = cast(TourStep, TourStep.get(data_dict["id"])) + tour_step.index = int(data_dict.get("index")) or tour_step.index tour_step.title = data_dict["title"] tour_step.element = data_dict["element"] tour_step.intro = data_dict["intro"] @@ -162,7 +162,9 @@ def tour_step_update(context, data_dict): try: tk.get_action(action)({"ignore_auth": True}, data_dict["image"][0]) except TourStepFileError as e: - raise tk.ValidationError(f"Error while uploading step image: {e}") + raise tk.ValidationError( + {"image": [f"Error while uploading step image: {e}"]} + ) model.Session.commit() @@ -171,7 +173,7 @@ def tour_step_update(context, data_dict): @validate(schema.tour_step_remove) def tour_step_remove(context, data_dict): - tour_step = cast(tour_model.TourStep, tour_model.TourStep.get(data_dict["id"])) + tour_step = cast(TourStep, TourStep.get(data_dict["id"])) tour_step.delete() model.Session.commit() @@ -192,7 +194,7 @@ def tour_step_image_upload(context, data_dict): ) if not data_dict.get("upload"): - return tour_model.TourStepImage.create( + return TourStepImage.create( {"url": data_dict["url"], "tour_step_id": tour_step_id} ).dictize(context) @@ -204,12 +206,14 @@ def tour_step_image_upload(context, data_dict): "upload": data_dict["upload"], }, ) - except (tk.ValidationError, OSError) as e: - raise TourStepFileError(str(e)) + except tk.ValidationError as e: + raise TourStepFileError(e.error_summary) + except OSError as e: + raise TourStepFileError(e) data_dict["file_id"] = result["id"] - return tour_model.TourStepImage.create( + return TourStepImage.create( {"file_id": result["id"], "tour_step_id": tour_step_id} ).dictize(context) @@ -227,8 +231,8 @@ def tour_step_image_update(context, data_dict): ) tour_step_image = cast( - tour_model.TourStepImage, - tour_model.TourStepImage.get_by_step(data_dict["tour_step_id"]), + TourStepImage, + TourStepImage.get_by_step(data_dict["tour_step_id"]), ) if not data_dict.get("upload"): @@ -256,9 +260,7 @@ def tour_step_image_update(context, data_dict): @validate(schema.tour_step_image_remove_schema) def tour_step_image_remove(context, data_dict): - tour_step_image = cast( - tour_model.TourStepImage, tour_model.TourStepImage.get(data_dict["id"]) - ) + tour_step_image = cast(TourStepImage, TourStepImage.get(data_dict["id"])) tour_step_image.delete(with_file=bool(tour_step_image.file_id)) model.Session.commit() diff --git a/ckanext/tour/logic/schema.py b/ckanext/tour/logic/schema.py index 0d614e7..cca3276 100644 --- a/ckanext/tour/logic/schema.py +++ b/ckanext/tour/logic/schema.py @@ -1,8 +1,8 @@ from __future__ import annotations from typing import Any, Dict -from ckan.lib.navl.validators import ignore_empty +from ckan.lib.navl.validators import ignore_empty from ckan.logic.schema import validator_args from ckanext.tour.model import Tour, TourStep @@ -89,12 +89,18 @@ def tour_step_schema( @validator_args def tour_step_update( - ignore_empty, unicode_safe, tour_tour_step_exist, default, boolean_validator + ignore_empty, + unicode_safe, + tour_tour_step_exist, + default, + boolean_validator, + int_validator, ) -> Schema: step_schema = tour_step_schema() step_schema.pop("tour_id") step_schema["id"] = [ignore_empty, unicode_safe, tour_tour_step_exist] step_schema["clear"] = [default("false"), boolean_validator] + step_schema["index"] = [ignore_empty, int_validator] return step_schema diff --git a/ckanext/tour/logic/validators.py b/ckanext/tour/logic/validators.py index 672e027..c0129f9 100644 --- a/ckanext/tour/logic/validators.py +++ b/ckanext/tour/logic/validators.py @@ -1,7 +1,7 @@ from __future__ import annotations -from typing import Any import string +from typing import Any from urllib.parse import urlparse import ckan.plugins.toolkit as tk diff --git a/ckanext/tour/migration/tour/versions/c9e5d4235e58_create_tour_tourstep_tourstepimage_.py b/ckanext/tour/migration/tour/versions/c9e5d4235e58_create_tour_tourstep_tourstepimage_.py index 014fade..9a17b56 100644 --- a/ckanext/tour/migration/tour/versions/c9e5d4235e58_create_tour_tourstep_tourstepimage_.py +++ b/ckanext/tour/migration/tour/versions/c9e5d4235e58_create_tour_tourstep_tourstepimage_.py @@ -46,6 +46,7 @@ def upgrade(): op.create_table( "tour_step", sa.Column("id", sa.Text, primary_key=True, unique=True), + sa.Column("index", sa.Integer), sa.Column("title", sa.Text), sa.Column("element", sa.Text), sa.Column("intro", sa.Text), diff --git a/ckanext/tour/model.py b/ckanext/tour/model.py index e3ca329..fb194f9 100644 --- a/ckanext/tour/model.py +++ b/ckanext/tour/model.py @@ -7,7 +7,7 @@ from ckan import model from ckan.model.types import make_uuid from ckan.plugins import toolkit as tk -from sqlalchemy import Column, DateTime, ForeignKey, Text +from sqlalchemy import Column, DateTime, ForeignKey, Integer, Text from sqlalchemy.orm import Query, relationship from typing_extensions import Self @@ -93,6 +93,7 @@ class Position: id = Column(Text, primary_key=True, default=make_uuid) + index = Column(Integer) title = Column(Text, nullable=True) element = Column(Text) intro = Column(Text, nullable=True) @@ -135,6 +136,7 @@ def image(self) -> TourStepImage: def dictize(self, context): return { "id": self.id, + "index": self.index, "title": self.title, "element": self.element, "intro": self.intro, diff --git a/ckanext/tour/templates/tour/snippets/tour_image_upload.html b/ckanext/tour/templates/tour/snippets/tour_image_upload.html index 1d5ac73..48b567d 100644 --- a/ckanext/tour/templates/tour/snippets/tour_image_upload.html +++ b/ckanext/tour/templates/tour/snippets/tour_image_upload.html @@ -4,7 +4,7 @@ {% macro image_upload(data, errors={}, field_url='step_url', field_upload='step_upload', field_clear='step_clear', is_url=false, is_upload=false, placeholder=false, - url_label='', upload_label='', field_name='step_url') %} + url_label='', upload_label='', field_name='step_url', scope='') %} {% set placeholder = placeholder if placeholder else _('http://example.com/my-image.jpg') %} {% set url_label = url_label or _('Image URL') %} {% set upload_label = upload_label or _('Image') %} @@ -21,7 +21,8 @@ data-module-field_clear="{{ field_clear }}" data-module-upload_label="{{ upload_label }}" data-module-field_name="{{ field_name }}" - data-module-previous_upload="{{ 'true' if previous_upload else 'false' }}"> + data-module-previous_upload="{{ 'true' if previous_upload else 'false' }}" + data-module-scope="{{ scope or '.image-upload' }}"> {{ input(field_url, label=url_label, id='field-image-url', type='url', placeholder=placeholder, value=data.image.url if data else "") }} {{ input(field_upload, label=upload_label, id='field-image-upload', type='file', placeholder='', value='', error='', attrs={"accept": "image/*", "class": "form-control"}) }} diff --git a/ckanext/tour/templates/tour/snippets/tour_step.html b/ckanext/tour/templates/tour/snippets/tour_step.html index fc4b542..4e3c81c 100644 --- a/ckanext/tour/templates/tour/snippets/tour_step.html +++ b/ckanext/tour/templates/tour/snippets/tour_step.html @@ -1,7 +1,7 @@ {% import 'macros/form.html' as form %} {% from "tour/snippets/tour_image_upload.html" import image_upload %} -{% set step_id = step.step_id or step.id %} +{% set step_id = step.step_id or step.id or h.tour_random_step_id() %}
@@ -12,12 +12,12 @@

{% snippet 'snippets/svg/drag.svg' %} -

-
@@ -36,7 +36,7 @@

{{ form.select('step_position', label=_('Position'), options=h.tour_get_position_options(), selected=step.step_position or step.position, error=error or {}) }} - {{ image_upload(step) }} + {{ image_upload(step, scope="#step-" + step_id) }}

diff --git a/ckanext/tour/templates/tour/tour_add.html b/ckanext/tour/templates/tour/tour_add.html index 6c56aac..672ba94 100644 --- a/ckanext/tour/templates/tour/tour_add.html +++ b/ckanext/tour/templates/tour/tour_add.html @@ -22,6 +22,13 @@

{{ _("Tour steps:") }}

+ {#} it's a temporary solution {#} + {% if errors %} +
+ {{ errors }} +
+ {% endif %} +
diff --git a/ckanext/tour/templates/tour/tour_edit.html b/ckanext/tour/templates/tour/tour_edit.html index bff0cf2..dadf244 100644 --- a/ckanext/tour/templates/tour/tour_edit.html +++ b/ckanext/tour/templates/tour/tour_edit.html @@ -21,36 +21,35 @@ {{ form.info(_('Optional. A path to a page, where tour will be applied. Note, that a page tour will be triggered automatically.', classes=['info-help-tight'])) }} {% endcall %} +
+ +
+

{{ _("Tour steps:") }}

-
+ {#} it's a temporary solution {#} + {% if errors %} +
+ {{ errors }} +
+ {% endif %} + +
{% for step in tour.steps %} - {% snippet 'tour/snippets/tour_step.html', step=step, step_index=loop.index %} + {% snippet 'tour/snippets/tour_step.html', step=step, step_index=loop.index %} {% endfor %}
-
- -
- -
{% endblock ap_content %} diff --git a/ckanext/tour/tests/logic/test_action.py b/ckanext/tour/tests/logic/test_action.py index c2d08bd..e161eff 100644 --- a/ckanext/tour/tests/logic/test_action.py +++ b/ckanext/tour/tests/logic/test_action.py @@ -36,6 +36,7 @@ def test_basic_create(self, tour_factory, tour_step_factory): tour = call_action("tour_show", id=tour["id"]) assert tour["steps"][0]["id"] == tour_step["id"] + assert tour["steps"][0]["index"] == 1 def test_wrong_position(self, tour_factory, tour_step_factory): tour = tour_factory(steps=[]) diff --git a/ckanext/tour/views/tour_update.py b/ckanext/tour/views/tour_update.py index 18cbed2..ce9ebc5 100644 --- a/ckanext/tour/views/tour_update.py +++ b/ckanext/tour/views/tour_update.py @@ -29,7 +29,7 @@ def post(self, tour_id: str) -> Response | str: data_dict = self._prepare_payload(tour_id) try: - tk.get_action("tour_update")(self._build_context(),data_dict) + tk.get_action("tour_update")(self._build_context(), data_dict) except tk.ValidationError as e: return tk.render( "tour/tour_edit.html", @@ -58,6 +58,7 @@ def _prepare_payload(self, tour_id: str): "step_intro", "step_position", "step_clear", + "step_index" ) steps = {} @@ -85,7 +86,6 @@ def _prepare_payload(self, tour_id: str): steps[idx].setdefault("image", [{}]) steps[idx]["image"][0].update({"upload": file or None}) - return { "id": tour_id, "title": tk.request.form.get("title"),