Skip to content

Commit

Permalink
feature: basic implementation of uploading image by url/file
Browse files Browse the repository at this point in the history
  • Loading branch information
mutantsan committed Sep 11, 2023
1 parent 660d89e commit 55d94d5
Show file tree
Hide file tree
Showing 15 changed files with 268 additions and 70 deletions.
17 changes: 17 additions & 0 deletions ckanext/tour/assets/js/tour-image-upload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Extends core image-upload.js to prevent uploaded URL change.
*
* @param {Event} e
*/

var extendedModule = $.extend({}, ckan.module.registry["image-upload"].prototype);

extendedModule._fileNameFromUpload = function (url) {
return url;
}

ckan.module("tour-image-upload", function ($, _) {
"use strict";

return extendedModule;
});
4 changes: 2 additions & 2 deletions ckanext/tour/assets/js/tour-init.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ this.ckan.module('tour-init', function (jQuery) {

// if (shouldStart) {
// // for development
// // localStorage.setItem('intro-' + introData.id, 1);
// intro.start();
// localStorage.setItem('intro-' + introData.id, 1);
// this.intro.start();
// }
},

Expand Down
1 change: 1 addition & 0 deletions ckanext/tour/assets/webassets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ tour-js:
- js/tour-init.js
- js/tour-steps.js
- js/tour-htmx.js
- js/tour-image-upload.js

tour-htmx:
filter: rjsmin
Expand Down
139 changes: 107 additions & 32 deletions ckanext/tour/logic/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ def tour_step_create(context, data_dict):
tk.get_action("tour_step_image_upload")(
{"ignore_auth": True},
{
"name": f"Tour step image <{dt.utcnow().isoformat()}>",
"upload": image.get("upload"),
"url": image.get("url"),
"tour_step_id": tour_step.id,
Expand All @@ -98,25 +97,6 @@ def tour_step_create(context, data_dict):
return tour_step.dictize(context)


@validate(schema.tour_step_image_schema)
def tour_step_image_upload(context, data_dict):
tour_step_id = data_dict.pop("tour_step_id", None)

try:
result = tk.get_action("files_file_create")(
{"ignore_auth": True},
{"name": data_dict["name"], "upload": data_dict["upload"]},
)
except (tk.ValidationError, OSError) as e:
raise TourStepFileError(str(e))

data_dict["file_id"] = result["id"]

return tour_model.TourStepImage.create(
{"file_id": result["id"], "tour_step_id": tour_step_id}
).dictize(context)


@validate(schema.tour_update)
def tour_update(context, data_dict):
tk.check_access("tour_update", context, data_dict)
Expand All @@ -131,20 +111,29 @@ def tour_update(context, data_dict):

steps: list[dict[str, Any]] = data_dict.pop("steps", [])

form_steps: set[str] = {step["id"] for step in steps}
tour_steps: set[str] = {step.id for step in tour.steps}
# TODO: how to delete steps?... Probably, with JS, add ID to some field
# form_steps: set[str] = {step["id"] for step in steps}
# tour_steps: set[str] = {step.id for step in tour.steps}

for step_id in tour_steps - form_steps:
tk.get_action("tour_step_remove")(
{"ignore_auth": True},
{"id": step_id},
)
# for step_id in tour_steps - form_steps:
# tk.get_action("tour_step_remove")(
# {"ignore_auth": True},
# {"id": step_id},
# )

for step in steps:
tk.get_action("tour_step_update")(
{"ignore_auth": True},
step,
)
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,
)

return tour.dictize(context)

Expand All @@ -153,13 +142,24 @@ 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.Tour, tour_model.TourStep.get(data_dict["id"]))
tour_step = cast(tour_model.TourStep, tour_model.TourStep.get(data_dict["id"]))

tour_step.title = data_dict["title"]
tour_step.element = data_dict["element"]
tour_step.intro = data_dict["intro"]
tour_step.position = data_dict["position"]

if data_dict.get("image"):
data_dict["image"][0]["tour_step_id"] = tour_step.id
action = "tour_step_image_update" if tour_step.image else "tour_step_image_upload"

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}")

model.Session.commit()

return tour_step.dictize(context)
Expand All @@ -173,3 +173,78 @@ def tour_step_remove(context, data_dict):
model.Session.commit()

return True


@validate(schema.tour_step_image_schema)
def tour_step_image_upload(context, data_dict):
tour_step_id = data_dict.pop("tour_step_id", None)

if not any([data_dict.get("upload"), data_dict.get("url")]):
raise TourStepFileError(tk._("You have to provide either file or URL"))

if all([data_dict.get("upload"), data_dict.get("url")]):
raise TourStepFileError(
tk._("You cannot use a file and a URL at the same time")
)

if not data_dict.get("upload"):
return tour_model.TourStepImage.create(
{"url": data_dict["url"], "tour_step_id": tour_step_id}
).dictize(context)

try:
result = tk.get_action("files_file_create")(
{"ignore_auth": True},
{
"name": f"Tour step image <{dt.utcnow().isoformat()}>",
"upload": data_dict["upload"],
},
)
except (tk.ValidationError, OSError) as e:
raise TourStepFileError(str(e))

data_dict["file_id"] = result["id"]

return tour_model.TourStepImage.create(
{"file_id": result["id"], "tour_step_id": tour_step_id}
).dictize(context)


@validate(schema.tour_step_image_update_schema)
def tour_step_image_update(context, data_dict):
tk.check_access("tour_step_update", context, data_dict)

if not any([data_dict.get("upload"), data_dict.get("url")]):
raise TourStepFileError(tk._("You have to provide either file or URL"))

if all([data_dict.get("upload"), data_dict.get("url")]):
raise TourStepFileError(
tk._("You cannot use a file and a URL at the same time")
)

tour_step_image = cast(
tour_model.TourStepImage,
tour_model.TourStepImage.get_by_step(data_dict["tour_step_id"]),
)

if not data_dict.get("upload"):
tour_step_image.url = data_dict["url"]
model.Session.commit()
return tour_step_image.dictize(context)

try:
result = tk.get_action("files_file_create")(
{"ignore_auth": True},
{
"id": tour_step_image.file_id,
"upload": data_dict["upload"],
},
)
except (tk.ValidationError, OSError) as e:
raise TourStepFileError(str(e))

tour_step_image.url = result["url"]

model.Session.commit()

return tour_step_image.dictize(context)
4 changes: 2 additions & 2 deletions ckanext/tour/logic/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ def tour_remove(context, data_dict):


def tour_list(context, data_dict):
return {"success": False}
return {"success": True}


def tour_show(context, data_dict):
return {"success": False}
return {"success": True}


def tour_step_update(context, data_dict):
Expand Down
23 changes: 17 additions & 6 deletions ckanext/tour/logic/schema.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

from typing import Any, Dict
from ckan.lib.navl.validators import ignore_empty

from ckan.logic.schema import validator_args

Expand Down Expand Up @@ -50,6 +51,10 @@ def tour_update(
tour_schema = tour_create()
tour_schema["id"] = [not_empty, unicode_safe, tour_tour_exist]
tour_schema["steps"] = tour_step_update()

# we shouldn't be able to change an author_id
tour_schema.pop("author_id")

return tour_schema


Expand All @@ -76,7 +81,6 @@ def tour_step_schema(
]
),
],
"url": [ignore_missing, unicode_safe],
"image": image_schema,
"tour_id": [not_empty, unicode_safe, tour_tour_exist],
"__extras": [ignore],
Expand All @@ -85,22 +89,24 @@ def tour_step_schema(

@validator_args
def tour_step_update(
not_empty,
ignore_empty,
unicode_safe,
tour_tour_step_exist,
) -> Schema:
step_schema = tour_step_schema()
step_schema.pop("tour_id")
step_schema["id"] = [not_empty, unicode_safe, tour_tour_step_exist]
step_schema["id"] = [ignore_empty, unicode_safe, tour_tour_step_exist]

return step_schema


@validator_args
def tour_step_image_schema(not_missing, unicode_safe, tour_tour_step_exist) -> Schema:
def tour_step_image_schema(
ignore_empty, not_missing, unicode_safe, tour_tour_step_exist, tour_url_validator
) -> Schema:
return {
"name": [not_missing, unicode_safe],
"upload": [not_missing],
"upload": [ignore_empty],
"url": [ignore_empty, unicode_safe, tour_url_validator],
"tour_step_id": [not_missing, unicode_safe, tour_tour_step_exist],
}

Expand All @@ -120,3 +126,8 @@ def tour_remove(not_empty, unicode_safe, tour_tour_exist) -> Schema:
@validator_args
def tour_step_remove(not_empty, unicode_safe, tour_tour_step_exist) -> Schema:
return {"id": [not_empty, unicode_safe, tour_tour_step_exist]}


@validator_args
def tour_step_image_update_schema() -> Schema:
return tour_step_image_schema()
41 changes: 41 additions & 0 deletions ckanext/tour/logic/validators.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from __future__ import annotations

from typing import Any
import string
from urllib.parse import urlparse

import ckan.plugins.toolkit as tk
import ckan.types as types

import ckanext.tour.model as tour_model

Expand All @@ -27,3 +30,41 @@ def tour_tour_step_exist(v: str, context) -> Any:
raise tk.Invalid(f"The tour with an id {v} doesn't exist.")

return v


def tour_tour_step_image_exist(v: str, context) -> Any:
"""Ensures that the tour step image exists for a specific tour step"""

result = tour_model.TourStepImage.get_by_step(v)

if not result:
raise tk.Invalid(f"The tour image for tour step {v} doesn't exists.")

return v


def tour_url_validator(
key: types.FlattenKey,
data: types.FlattenDataDict,
errors: types.FlattenErrorDict,
context: types.Context,
) -> Any:
"""Checks that the provided value (if it is present) is a valid URL"""

url = data.get(key, None)
if not url:
return

try:
pieces = urlparse(url)
if (
all([pieces.scheme, pieces.netloc])
and set(pieces.netloc) <= set(string.ascii_letters + string.digits + "-.:")
and pieces.scheme in ["http", "https"]
):
return
except ValueError:
# url is invalid
pass

errors[key].append(tk._("Please provide a valid URL"))
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ def upgrade():
op.create_table(
"tour_step_image",
sa.Column("id", sa.Text, primary_key=True, unique=True),
sa.Column("file_id", sa.Text, unique=True),
sa.Column("file_id", sa.Text, unique=True, nullable=True),
sa.Column("url", sa.Text, nullable=True),
sa.Column(
"uploaded_at",
sa.DateTime,
Expand Down
7 changes: 4 additions & 3 deletions ckanext/tour/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,8 @@ class TourStepImage(tk.BaseModel):

id = Column(Text, primary_key=True, default=make_uuid)

file_id = Column(Text, unique=True)
file_id = Column(Text, unique=True, nullable=True)
url = Column(Text, nullable=True)
uploaded_at = Column(DateTime, nullable=False, default=datetime.utcnow)
tour_step_id = Column(Text, ForeignKey("tour_step.id", ondelete="CASCADE"))

Expand Down Expand Up @@ -178,14 +179,14 @@ def get_by_step(cls, tour_step_id: str) -> Self | None:
return query.one_or_none() # type: ignore

def dictize(self, context):
uploaded_file = self.get_file_data(self.file_id)
uploaded_file = self.get_file_data(self.file_id) if self.file_id else None

return {
"id": self.id,
"file_id": self.file_id,
"tour_step_id": self.tour_step_id,
"uploaded_at": self.uploaded_at.isoformat(),
"url": uploaded_file["url"],
"url": uploaded_file["url"] if uploaded_file else self.url,
}

def delete(self) -> None:
Expand Down
Loading

0 comments on commit 55d94d5

Please sign in to comment.