Skip to content

Commit

Permalink
List view (#10)
Browse files Browse the repository at this point in the history
* adding list view with pygwalker link

* cleaning up list view and generic view

* adding unittests; code quality updates

* updating documentation

* update version history
  • Loading branch information
davidslusser committed Aug 25, 2023
1 parent 0193197 commit a9c0dfe
Show file tree
Hide file tree
Showing 14 changed files with 295 additions and 7 deletions.
45 changes: 44 additions & 1 deletion docs/source/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ class MyPygWalkerView(StaticCsvPygWalkerView):

## DynamicCsvPygWalkerView

The DynamicCsvPygWalkerView view is a 'generic' PyGWalker page that allows users to upload a csv file and creates the PyGWalker interface based on data in the uploaded csv file. To use this view, add the following to your INSTALLED_APPS in settings.py:
The DynamicCsvPygWalkerView view is page that allows users to upload a csv file and creates the PyGWalker interface based on data in the uploaded csv file. To use this view, add the following to your INSTALLED_APPS in settings.py:

```python
djangoaddicts.pygwalker
Expand All @@ -112,4 +112,47 @@ The page can be reached using a link such as:
#### Rendered Visualization
![rendered visualization](images/pyg_chart.png)


<br/>

## GenericPygWalkerView

The GenericPygWalkerView creates a PyGWalker visualization interface from a provided app and model passed as kwargs. If query parameters are present, it includes only filtered data, based on query parameters, in the PyGWalker interface. To use this view, add the following to your INSTALLED_APPS in settings.py:

```python
djangoaddicts.pygwalker
```
and add the following to your project-level urls.py:

```python
path("generic_pyg/<str:app_name>/<str:model_name>/", GenericPygWalkerView.as_view(), name="generic_pyg"),
```

The page can be reached using a link such as:

```python
<a href="/pygwalker/generic_pyg/my_app/my_model">my pygwalker link</a>
```


<br/>

## PygWalkerListView

The PygWalkerListView is a list view that extends the HandyHelperListPlusCreateAndFilterView (from handyhelpers) to add an icon for a PyGWalker visualzation interface. If the list view is filtered, include only filtered data in the PyGWalker interface.

A Bootstrap 5 template is included, but can be overwritten using the template_name parameter.

### Usage Examples

```python
from djangoaddicts.pygwalker.views import PygWalkerListView

class ListProducts(PygWalkerListView):
"""list available MyModel entries"""
queryset = MyModel.objects.all()
title = "MyModel Entries"
table = "myapp/table/mymodels.htm"
```

<br/>
2 changes: 1 addition & 1 deletion docs/source/internals.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ The documentation below details some of the internal workings of django-pygwalke

```{eval-rst}
.. automodule:: djangoaddicts.pygwalker.views
:members: PygWalkerView, StaticCsvPygWalkerView, DynamicCsvPygWalkerView
:members: PygWalkerView, StaticCsvPygWalkerView, DynamicCsvPygWalkerView, GenericPygWalkerView, PygWalkerListView
```

<br/>
1 change: 1 addition & 0 deletions docs/source/version_history.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

| Release | Details |
|----------|--------|
| 0.0.4 | adding PyGWalker list view |
| 0.0.3 | adding view for uploadable csv file |
| 0.0.2 | adding view for static csv file |
| 0.0.1 | initial release |
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ dev = [
"build",
"coveralls",
"django-environ",
"django-handyhelpers",
"isort",
"model-bakery",
"mypy",
Expand Down
11 changes: 11 additions & 0 deletions src/djangoaddicts/pygwalker/templates/pygwalker/bs5/list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{% extends base_template|default:"handyhelpers/handyhelpers_base.htm" %}
{% load static %}
{% load handyhelper_tags %}

{% block local_head %}
{% include "handyhelpers/component/bs5/table_components.htm" %}
{% endblock %}

{% block content %}
{% include "pygwalker/bs5/partials/list_content.htm" %}
{% endblock content %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% extends base_template|default:"handyhelpers/handyhelpers_base.htm" %}
{% load static %}
{% load handyhelper_tags %}

{% block content %}
{% include "pygwalker/bs5/partials/list_content.htm" %}
{% endblock content %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{% load handyhelper_tags %}
<div class="container-fluid animated fadeIn my-5">
<h1><span class="text-primary"><b>{{ title }}:</b></span> {% if subtitle %}<span class="text-secondary"><small><small>{{ subtitle }}</small></small></span>{% endif %}</h1>

<div class="text-end">
{# To include a pygwalker link, include the pygwalker_url in the context of your view #}
{% if pygwalker_url %}
<a href="{{ pygwalker_url|safe }}" class="mx-2"><i class="fa-solid fa-magnifying-glass-chart" title="PyGWalker view"></i></a>
{% endif %}

{# To include a create form, exposed via modal, include create_form (dict) in the context of your view. The create_from must include modal_name and link_title fields #}
{% if create_form %}
{% if allow_create_groups and request.user|in_any_group:allow_create_groups %}
<a href="#" data-bs-toggle="modal" data-bs-target="#modal_{{ create_form.modal_name }}" title="{{ create_form.tool_tip|default_if_none:'create' }}">
<i class="fas fa-plus-circle"></i>
{% if create_form.link_title %} {{ create_form.link_title }} {% endif %}
</a>
{% endif %}
{% endif %}

{# To include a filter form, exposed via modal, include filter_form (dict) in the context of your view. The filter_from must include modal_name and link_title fields #}
{% if filter_form %}
<a href="#" data-bs-toggle="modal" data-bs-target="#modal_{{ filter_form.modal_name }}" title="{{ filter_form.tool_tip|default_if_none:'filter' }}" class="ms-2">
<i class="fas fa-filter"></i>
{% if filter_form.link_title %} {{ filter_form.link_title }} {% endif %}
</a>
{% if filter_form.undo and request.META.QUERY_STRING %}
<a href="{% url 'handyhelpers:show_all_list_view' %}" title="clear filters" class="ms-2">
<i class="fas fa-undo-alt"></i>
</a>
{% endif %}
{% endif %}
</div>

{% include table %}
</div>

{% include 'handyhelpers/component/bs5/modals.htm' %}

{# include generic modal form for the create object form if passed from the view #}
{% with create_form as form_data %}
{% include 'handyhelpers/generic/bs5/generic_modal_form.htm' %}
{% endwith %}

{# include generic modal form for the filter object form if passed from the view #}
{% with filter_form as form_data %}
{% include 'handyhelpers/generic/bs5/generic_modal_form.htm' %}
{% endwith %}

{# include custom modal html/js template if passed in from the view #}
{% if modals %}
{% include modals %}
{% endif %}

{# block for additional static content #}
{% block additional_static %}
{% if add_static %}
{{ add_static|safe }}
{% endif %}
{% endblock additional_static %}

{# block for additional template content #}
{% block additional_template %}
{% if add_template %}
{% include add_template %}
{% endif %}
{% endblock additional_template %}
6 changes: 5 additions & 1 deletion src/djangoaddicts/pygwalker/urls.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from django.urls import path

from djangoaddicts.pygwalker.views import DynamicCsvPygWalkerView
from djangoaddicts.pygwalker.views import DynamicCsvPygWalkerView, GenericPygWalkerView

app_name = "pygwalker"

urlpatterns = [
path("", DynamicCsvPygWalkerView.as_view(), name=""),
path("csv", DynamicCsvPygWalkerView.as_view(), name="csv"),
path("dynamic", DynamicCsvPygWalkerView.as_view(), name="dynamic"),
path("file", DynamicCsvPygWalkerView.as_view(), name="file"),
path("generic_pyg/<str:app_name>/<str:model_name>/", GenericPygWalkerView.as_view(), name="generic_pyg"),
]
90 changes: 88 additions & 2 deletions src/djangoaddicts/pygwalker/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

import pandas as pd
import pygwalker as pyg
from django.apps import apps
from django.conf import settings
from django.contrib import messages
from django.db.models import QuerySet
from django.shortcuts import render
from django.views.generic import View
from handyhelpers.views.gui import HandyHelperListPlusCreateAndFilterView

from djangoaddicts.pygwalker.forms import UploadFileForm

Expand Down Expand Up @@ -35,7 +37,7 @@ class MyPygView(PygWalkerView):
field_list: list = []
queryset: QuerySet = None
template_name: str = "pygwalker/bs5/pygwalker.html"
theme: str = "media"
theme: str = getattr(settings, "PYGWALKER_THEME", "media")
title: str = "Data Analysis"

def get(self, request):
Expand Down Expand Up @@ -65,7 +67,7 @@ class MyPygView(StaticCsvPygWalkerView):

csv_file = None
template_name: str = "pygwalker/bs5/pygwalker.html"
theme: str = "media"
theme: str = getattr(settings, "PYGWALKER_THEME", "media")
title: str = "Data Analysis"

def get(self, request):
Expand Down Expand Up @@ -119,3 +121,87 @@ def post(self, request):
extra_tags="alert-danger",
)
return self.get(request)


class GenericPygWalkerView(View):
"""View to create a PyGWalker visualization interface from an app and model passed as kwargs. If query
parameters are present, include only filtered data, based on query parameters, in the PyGWalker interface."""

field_list: list = []
queryset: QuerySet = None
template_name: str = "pygwalker/bs5/pygwalker.html"
theme: str = getattr(settings, "PYGWALKER_THEME", "media")
title: str = "Data Analysis"

def get(self, request, **kwargs):
"""process GET request"""
referrer = request.META.get("HTTP_REFERER", "")
if "?" in referrer:
query_dict = {
key: value for key, value in (item.split("=") for item in referrer.split("?")[1].split("&") if item)
}
else:
query_dict = {}
model = apps.get_model(kwargs["app_name"], kwargs["model_name"])
self.queryset = model.objects.filter(**query_dict)
pd_data = pd.DataFrame(list(self.queryset.values(*self.field_list)))
context = {"pyg": pyg.walk(pd_data, return_html=True, dark=self.theme), "title": self.title}
return render(request, self.template_name, context)


class PygWalkerListView(HandyHelperListPlusCreateAndFilterView):
"""extend the HandyHelperListPlusCreateAndFilterView (from handyhelpers) to add an icon for a PyGWalker
visualzation interface. If the list view is filtered, include only filtered data in the PyGWalker interface."""

template_name = "pygwalker/bs5/list.html"
pygwalker_url = None

def get(self, request, *args, **kwargs):
if not self.pygwalker_url:
self.pygwalker_url = (
f"/pygwalker/generic_pyg/{self.queryset.model._meta.app_label}/{self.queryset.model._meta.model_name}"
)
context = dict(
base_template=self.base_template,
queryset=self.filter_by_query_params(),
title=self.title,
subtitle=self.page_description,
table=self.table,
modals=self.modals,
add_static=self.add_static,
add_template=self.add_template,
pygwalker_url=self.pygwalker_url,
allow_create_groups=self.allow_create_groups,
args=self.args,
kwargs=self.kwargs,
)
if self.create_form_obj:
self.create_form["form"] = self.create_form_obj(request.POST or None)
self.create_form["action"] = "Add"
self.create_form["action_url"] = self.create_form_url
self.create_form["title"] = self.create_form_title
self.create_form["modal_name"] = self.create_form_modal
self.create_form["modal_backdrop"] = self.create_form_modal_backdrop
self.create_form["modal_scrollable"] = self.create_form_modal_scrollable
self.create_form["modal_size"] = self.create_form_modal_size
self.create_form["link_title"] = self.create_form_link_title
self.create_form["tool_tip"] = self.create_form_tool_tip
self.create_form["autocomplete"] = self.create_form_autocomplete
context["create_form"] = self.create_form

if self.filter_form_obj:
self.filter_form["form"] = self.filter_form_obj(request.POST or None, initial=self.request.GET.dict())
self.filter_form["action"] = "Filter"
self.filter_form["action_url"] = self.filter_form_url
self.filter_form["title"] = self.filter_form_title
self.filter_form["modal_name"] = self.filter_form_modal
self.filter_form["modal_backdrop"] = self.filter_form_modal_backdrop
self.filter_form["modal_scrollable"] = self.filter_form_modal_scrollable
self.filter_form["modal_size"] = self.filter_form_modal_size
self.filter_form["link_title"] = self.filter_form_link_title
self.filter_form["tool_tip"] = self.filter_form_tool_tip
self.filter_form["undo"] = self.filter_form_undo
self.filter_form["autocomplete"] = self.filter_form_autocomplete
context["filter_form"] = self.filter_form

return render(request, self.template_name, context)
1 change: 1 addition & 0 deletions tests/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"django.contrib.auth",
"django.contrib.contenttypes",
"djangoaddicts.pygwalker",
"handyhelpers",
"tests.core.testapp",
]

Expand Down
5 changes: 5 additions & 0 deletions tests/core/testapp/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django import forms


class TestForm(forms.Form):
pass
7 changes: 6 additions & 1 deletion tests/core/testapp/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from djangoaddicts.pygwalker.views import PygWalkerView, StaticCsvPygWalkerView
from djangoaddicts.pygwalker.views import PygWalkerListView, PygWalkerView, StaticCsvPygWalkerView
from tests.core.testapp.models import TestModel


Expand All @@ -25,3 +25,8 @@ class BasicStaticCsvPygWalkerViewView(StaticCsvPygWalkerView):
class CustomTemplateStaticCsvPygWalkerViewView(StaticCsvPygWalkerView):
csv_file = "tests/data/data.csv"
template_name = "testapp/my_custom_template.html"


class TestModelListView(PygWalkerListView):
queryset = TestModel.objects.all()
title = "TestModels"
2 changes: 2 additions & 0 deletions tests/core/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
CustomTemplatePygWalkerView,
CustomTemplateStaticCsvPygWalkerViewView,
ExplicitFieldsPygWalkerView,
TestModelListView,
)

urlpatterns = [
Expand All @@ -14,5 +15,6 @@
path("custom/", CustomTemplatePygWalkerView.as_view(), name="custom"),
path("static_basic/", BasicStaticCsvPygWalkerViewView.as_view(), name="static_basic"),
path("static_custom/", CustomTemplateStaticCsvPygWalkerViewView.as_view(), name="static_custom"),
path("test_model_list_view/", TestModelListView.as_view(), name="test_model_list_view"),
path("pygwalker/", include("djangoaddicts.pygwalker.urls")),
]

0 comments on commit a9c0dfe

Please sign in to comment.