Skip to content

Commit

Permalink
implement user chart builder view , part2
Browse files Browse the repository at this point in the history
  • Loading branch information
mutantsan committed Jun 17, 2024
1 parent 99355cf commit 7f9e086
Show file tree
Hide file tree
Showing 13 changed files with 259 additions and 77 deletions.
2 changes: 1 addition & 1 deletion ckanext/charts/assets/css/charts.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 16 additions & 6 deletions ckanext/charts/chart_builders/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import ckan.types as types
import ckan.plugins.toolkit as tk

import ckanext.charts.const as const
from ckanext.charts.exception import ChartTypeNotImplementedError
from ckanext.charts import fetchers

Expand Down Expand Up @@ -75,13 +76,19 @@ def __init__(
if self.settings.pop("sort_y", False):
self.df.sort_values(by=self.settings["y"], inplace=True)

if limit := self.settings.pop("limit", 0):
self.df = self.df.head(int(limit))
self.df = self.df.head(self.get_limit())

self.settings.pop("query", None)

self.settings = self.drop_view_fields(self.drop_empty_values(self.settings))

def get_limit(self) -> int:
"""Get the limit of rows to show in the chart."""
if "limit" not in self.settings:
return const.CHART_DEFAULT_ROW_LIMIT

return int(self.settings.pop("limit"))

@classmethod
@abstractmethod
def get_supported_forms(cls) -> list[type[BaseChartForm]]:
Expand Down Expand Up @@ -182,7 +189,7 @@ def get_form_fields(self) -> list[dict[str, Any]]:
"""The list for a specific chart could be defined similar to a scheming
dataset schema fields."""

def get_form_tabs(self) -> list[str]:
def get_form_tabs(self, exclude_tabs: list[str] | None = None) -> list[str]:
result = []

for field in self.get_form_fields():
Expand All @@ -194,6 +201,9 @@ def get_form_tabs(self) -> list[str]:

result.append(field["group"])

if exclude_tabs:
result = [tab for tab in result if tab not in exclude_tabs]

return result

def get_expanded_form_fields(self):
Expand Down Expand Up @@ -522,17 +532,17 @@ def opacity_field(self) -> dict[str, Any]:
],
}

def limit_field(self) -> dict[str, Any]:
def limit_field(self, default: int = 100, maximum: int = 10000) -> dict[str, Any]:
"""The limit field represent an amount of rows to show in the chart."""
return {
"field_name": "limit",
"label": "Limit",
"form_snippet": "chart_text.html",
"input_type": "number",
"validators": [
self.get_validator("default")(100),
self.get_validator("default")(default),
self.get_validator("int_validator"),
self.get_validator("limit_to_configured_maximum")("", 10000),
self.get_validator("limit_to_configured_maximum")("", maximum),
],
"group": "Data",
}
Expand Down
2 changes: 2 additions & 0 deletions ckanext/charts/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
CACHE_REDIS = "redis"

REDIS_PREFIX = "ckanext-charts:*"

CHART_DEFAULT_ROW_LIMIT = 100
43 changes: 20 additions & 23 deletions ckanext/charts/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from ckan.common import CKANConfig

import ckanext.charts.config as conf
import ckanext.charts.utils as utils
import ckanext.charts.const as const
from ckanext.charts import cache, exception, fetchers, utils
from ckanext.charts.logic.schema import settings_schema
from ckanext.charts.chart_builders import DEFAULT_CHART_FORM
Expand Down Expand Up @@ -56,19 +58,10 @@ def info(self) -> dict[str, Any]:
}

def can_view(self, data_dict: dict[str, Any]) -> bool:
if data_dict["resource"].get("datastore_active"):
return True

# TODO: Add support for XML, XLS, XLSX, and other formats tabular data?
# if data_dict["resource"]["format"].lower() == "xml":
# return True

return False
return utils.can_view_be_viewed(data_dict)

def setup_template_variables(
self,
context: types.Context,
data_dict: dict[str, Any],
self, context: types.Context, data_dict: dict[str, Any]
) -> dict[str, Any]:
"""
The ``data_dict`` contains the following keys:
Expand All @@ -85,7 +78,7 @@ def setup_template_variables(
}

data_dict["resource_view"]["resource_id"] = data_dict["resource"]["id"]
context["_for_show"] = True
context["_for_show"] = True # type: ignore

try:
settings, _ = tk.navl_validate(
Expand Down Expand Up @@ -204,7 +197,7 @@ def info(self) -> dict[str, Any]:
return {
"name": "charts_builder_view",
"title": tk._("Chart Builder"),
"schema": settings_schema(),
"schema": {},
"icon": "chart-line",
"iframed": False,
"filterable": False,
Expand All @@ -213,23 +206,27 @@ def info(self) -> dict[str, Any]:
}

def can_view(self, data_dict: dict[str, Any]) -> bool:
if data_dict["resource"].get("datastore_active"):
return True

return False
return utils.can_view_be_viewed(data_dict)

def setup_template_variables(
self,
context: types.Context,
data_dict: dict[str, Any],
self, context: types.Context, data_dict: dict[str, Any]
) -> dict[str, Any]:
return {
"settings": {},
form_builder = DEFAULT_CHART_FORM

data = {
"resource_id": data_dict["resource"]["id"],
"settings": {
"engine": "plotly",
"type": "line",
"limit": const.CHART_DEFAULT_ROW_LIMIT,
},
"form_builder": form_builder,
}

return data

def view_template(self, context: types.Context, data_dict: dict[str, Any]) -> str:
return "charts/charts_view.html"
return "charts/charts_builder_view.html"

def form_template(self, context: types.Context, data_dict: dict[str, Any]) -> str:
return "charts/charts_builder_form.html"
9 changes: 9 additions & 0 deletions ckanext/charts/templates/charts/charts_builder_view.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<form method="POST">
{% snippet "charts/snippets/chart_create_form.html",
resource_id=resource_id,
settings=settings,
form_builder=form_builder(resource_id),
errors={},
exclude_tabs=["General"]
%}
</form>
26 changes: 6 additions & 20 deletions ckanext/charts/templates/charts/charts_form.html
Original file line number Diff line number Diff line change
@@ -1,20 +1,6 @@
{% import "macros/form.html" as form %}

{% asset "charts/plotly" %}
{% asset "charts/chartjs" %}
{% asset "charts/observable" %}

{# Global styles for all charts #}
{% asset "charts/charts-css" %}
{% asset "charts/charts-js" %}

{# Custom select library #}
{% asset "charts/tom-select-js" %}
{% asset "charts/tom-select-css" %}

<div class="charts-view--form" data-module="charts-global">
{% snippet "charts/snippets/charts_form_fields.html", resource_id=resource_id, data=settings,
builder=form_builder(resource_id), errors=errors %}
</div>

<div class="charts-view--preview" id="charts-view--preview" style="min-height: 450px;"></div>
{% snippet "charts/snippets/chart_create_form.html",
resource_id=resource_id,
settings=settings,
form_builder=form_builder(resource_id),
errors=errors
%}
20 changes: 20 additions & 0 deletions ckanext/charts/templates/charts/snippets/chart_create_form.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{% import "macros/form.html" as form %}

{% asset "charts/plotly" %}
{% asset "charts/chartjs" %}
{% asset "charts/observable" %}

{# Global styles for all charts #}
{% asset "charts/charts-css" %}
{% asset "charts/charts-js" %}

{# Custom select library #}
{% asset "charts/tom-select-js" %}
{% asset "charts/tom-select-css" %}

<div class="charts-view--form" data-module="charts-global">
{% snippet "charts/snippets/charts_form_fields.html", resource_id=resource_id, data=settings,
builder=form_builder, errors=errors, exclude_tabs=exclude_tabs or false, user_chart_builder=1 %}
</div>

<div class="charts-view--preview" id="charts-view--preview" style="min-height: 450px;"></div>
33 changes: 23 additions & 10 deletions ckanext/charts/templates/charts/snippets/charts_form_fields.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
{% set active_tab = active_tab or "General" %}
{% set form_tabs = builder.get_form_tabs() %}

<div class="charts-view--form">
{% set active_tab = active_tab or "General" %}
{% set form_tabs = builder.get_form_tabs(exclude_tabs=exclude_tabs) %}

{% if "General" not in form_tabs %}
{% set active_tab = form_tabs[0] %}
{% endif %}

<ul class="nav nav-pills mb-3" id="pills-tab" role="tablist">
{% for tab in form_tabs %}
{% set tab_fields = builder.get_fields_by_tab(tab) %}
Expand All @@ -13,15 +17,20 @@
</li>
{% endif %}
{% endfor %}
</ul>

<button class="btn btn-default" id="chart-clear" type="button" style="position: absolute; right: 0; top: 0;"
hx-get="{{ h.url_for('charts_view.clear_chart', resource_id=resource_id) }}" hx-trigger="click"
hx-include="closest form" hx-target=".charts-view--form" hx-swap="outerHTML">
{{ _("Clear") }}
</button>
<button class="btn btn-default" id="chart-clear" type="button"
{% if user_chart_builder %}
hx-get="{{ h.url_for('charts_view.clear_user_builder_chart', resource_id=resource_id) }}"
{% else %}
hx-get="{{ h.url_for('charts_view.clear_chart', resource_id=resource_id) }}"
{% endif %}
hx-trigger="click"
hx-include="closest form" hx-target=".charts-view--form" hx-swap="outerHTML">
{{ _("Clear") }}
</button>
</ul>

<div class="tab-content" id="pills-tabContent" style="display: block;"
<div class="tab-content" id="pills-tabContent"
hx-get="{{ h.url_for('charts_view.update_chart', resource_id=resource_id) }}" hx-trigger="load, submit, change"
hx-include="closest form" hx-target="#charts-view--preview">

Expand All @@ -38,6 +47,10 @@
{% endfor %}
</div>

{% if user_chart_builder %}
<input type="hidden" name="user_chart_builder" value="1">
{% endif %}

<input type="hidden" name="resource_id" value="{{ resource_id }}">
<input type="hidden" name="{{ g.csrf_field_name }}" value="{{ csrf_token() }}"
hx-swap-oob='outerHTML:[name="{{ g.csrf_field_name }}"]' />
Expand Down
94 changes: 94 additions & 0 deletions ckanext/charts/templates/package/snippets/resource_view.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
{# alter: adding resource-view-{{ resource_view.view_type }} #}

{% import 'macros/form.html' as form %}

{% block resource_view %}
<div id="view-{{ resource_view['id'] }}" class="resource-view resource-view-{{ resource_view.view_type }}" data-id="{{ resource_view['id'] }}" data-title="{{ resource_view['title'] }}" data-description="{{ resource_view['descripion'] }}">
<div class="actions">
<a class="btn btn-default"
target="_blank"
rel="noreferrer"
href="{{ h.url_for(package['type'] ~ '_resource.view', id=package['name'], resource_id=resource['id'], view_id=resource_view['id'], qualified=True) }}">
<i class="fa fa-arrows-alt"></i>
{{ _("Fullscreen") }}
</a>
<a class="btn btn-default"
href="#embed-{{ resource_view['id'] }}"
data-module="resource-view-embed"
data-module-id="{{ resource_view['id'] }}"
data-module-url="{{ h.url_for(package['type'] ~ '_resource.view', id=package['name'], resource_id=resource['id'], view_id=resource_view['id'], qualified=True) }}">
<i class="fa fa-object-group"></i>
{{ _("Embed") }}
</a>
</div>
<p class="desc">{{ h.render_markdown(resource_view['description']) }}</p>
<div class="m-top ckanext-datapreview">
{% if not to_preview and h.resource_view_is_filterable(resource_view) %}
{% snippet 'package/snippets/resource_view_filters.html', resource=resource %}
{% endif %}
{% if not h.resource_view_is_iframed(resource_view) %}
{{ h.rendered_resource_view(resource_view, resource, package) }}
{% else %}
<div class="data-viewer-error js-hide">
<p class="text-danger">
<i class="fa fa-info-circle"></i>
{{ _('This resource view is not available at the moment.') }}
<a href="#" data-bs-toggle="collapse" data-bs-target="#data-view-error">
{{ _('Click here for more information.') }}
</a>
</p>
<p id="data-view-error" class="collapse"></p>
<p>
<a href="{{ resource.url }}" class="btn btn-default btn-lg resource-url-analytics" target="_blank" rel="noreferrer">
<i class="fa fa-lg fa-arrow-circle-down"></i>
{{ _('Download resource') }}
</a>
</p>
</div>
{% if not to_preview %}
{% set current_filters = request.args.get('filters') %}
{% if current_filters %}
{% set src = h.url_for(package['type'] ~ '_resource.view', id=package['name'],
resource_id=resource['id'],
view_id=resource_view['id'],
filters=current_filters, qualified=true) %}
{% else %}
{% set src = h.url_for(package['type'] ~ '_resource.view', id=package['name'],
resource_id=resource['id'],
view_id=resource_view['id'], qualified=true) %}
{% endif %}
{% else %}
{# When previewing we need to stick the whole resource_view as a param as there is no other way to pass to information on to the iframe #}
{% set src = h.url_for(package['type'] ~ '_resource.view', id=package['name'], resource_id=resource['id'], qualified=true) + '?' + h.urlencode({'resource_view': h.dump_json(resource_view)}) %}
{% endif %}
<iframe title="Data viewer" src="{{ src }}" frameborder="0" width="100%" data-module="data-viewer">
<p>{{ _('Your browser does not support iframes.') }}</p>
</iframe>
{% endif %}
</div>
<div id="embed-{{ resource_view['id'] }}" class="modal fade resource-view-embed">
<div class="modal-dialog">
<div class="modal-content">

<div class="modal-header">
<h3 class="modal-title">{{ _("Embed resource view") }}</h3>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ _('Close') }}"></button>

</div>
<div class="modal-body">
<p class="embed-content">{{ _("You can copy and paste the embed code into a CMS or blog software that supports raw HTML") }}</p>
<div class="row">
<div class="col-md-6">
{{ form.input("width", label=_("Width"), value=700, classes=["control-full"]) }}
</div>
<div class="col-md-6">
{{ form.input("height", label=_("Height"), value=400, classes=["control-full"]) }}
</div>
</div>
{{ form.textarea("code", label=_("Code"), value="", classes=["pre"], rows=3) }}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
Loading

0 comments on commit 7f9e086

Please sign in to comment.