acdh-django-browsing
Advanced tools
| import django_filters | ||
| def get_generic_filter(model_class): | ||
| class GenericFilter(django_filters.FilterSet): | ||
| class Meta: | ||
| model = model_class | ||
| fields = "__all__" | ||
| return GenericFilter |
| <div class="pt-2 ps-5"> | ||
| <nav style="--bs-breadcrumb-divider: '>';" aria-label="breadcrumb"> | ||
| <ol class="breadcrumb"> | ||
| <li class="breadcrumb-item"><a href="/">Home</a></li> | ||
| <li class="breadcrumb-item"><a href="{{ object.get_listview_url }}">{{ verbose_name_plural }}</a></li> | ||
| <li class="breadcrumb-item active" aria-current="page">{{ object }}</li> | ||
| </ol> | ||
| </nav> | ||
| </div> |
| <div class="row"> | ||
| <div class="col-md-2"> | ||
| {% if object.get_prev %} | ||
| <p class="text-start"> | ||
| <a href="{{ object.get_prev }}"> | ||
| <i class="fs-3 bi bi-chevron-left" title="previous entry" aria-hidden="true"></i> | ||
| <span class="visually-hidden">previous entry</span> | ||
| </a> | ||
| </p> | ||
| {% endif %} | ||
| </div> | ||
| <div class="col-md-8 text-center"> | ||
| <h1 class="text-center"> | ||
| {{ object }} | ||
| </h1> | ||
| {% if user.is_authenticated %} | ||
| <a href="{{ object.get_edit_url }}"> | ||
| <i class="fs-3 bi bi-pencil-square" title="edit entry" aria-hidden="true"></i> | ||
| <span class="visually-hidden">edit entry</span> | ||
| </a> | ||
| {% endif %} | ||
| </div> | ||
| <div class="col-md-2"> | ||
| {% if object.get_next %} | ||
| <p class="text-end"> | ||
| <a href="{{ object.get_next }}"> | ||
| <i class="fs-3 bi bi-chevron-right" title="next entry" aria-hidden="true"></i> | ||
| <span class="visually-hidden">next entry</span> | ||
| </a> | ||
| </p> | ||
| {% endif %} | ||
| </div> | ||
| </div> |
| <div class="pt-2 ps-5"> | ||
| <nav style="--bs-breadcrumb-divider: '>';" aria-label="breadcrumb"> | ||
| <ol class="breadcrumb"> | ||
| <li class="breadcrumb-item"><a href="/">Home</a></li> | ||
| <li class="breadcrumb-item active">{{ verbose_name_plural }}</li> | ||
| </ol> | ||
| </nav> | ||
| </div> |
| import django_tables2 | ||
| from django.utils.safestring import mark_safe | ||
| from django.views.generic.detail import DetailView | ||
| from django.views.generic.edit import CreateView, UpdateView | ||
| from crispy_forms.helper import FormHelper | ||
| from django_tables2.export.views import ExportMixin | ||
| from browsing.filters import get_generic_filter | ||
| input_form = """ | ||
| <input type="checkbox" name="keep" value="{}" title="keep this"/> | | ||
| <input type="checkbox" name="remove" value="{}" title="remove this"/> | ||
| """ | ||
| class MergeColumn(django_tables2.Column): | ||
| """renders a column with to checkbox - used to select objects for merging""" | ||
| def __init__(self, *args, **kwargs): | ||
| super(MergeColumn, self).__init__(*args, **kwargs) | ||
| def render(self, value): | ||
| return mark_safe(input_form.format(value, value)) | ||
| def get_entities_table(model_class): | ||
| class GenericEntitiesTable(django_tables2.Table): | ||
| id = django_tables2.LinkColumn(verbose_name="ID") | ||
| merge = MergeColumn(verbose_name="keep | remove", accessor="pk") | ||
| class Meta: | ||
| model = model_class | ||
| attrs = {"class": "table table-hover table-striped table-condensed"} | ||
| return GenericEntitiesTable | ||
| class GenericFilterFormHelper(FormHelper): | ||
| def __init__(self, *args, **kwargs): | ||
| super(GenericFilterFormHelper, self).__init__(*args, **kwargs) | ||
| self.helper = FormHelper() | ||
| self.form_class = "genericFilterForm" | ||
| self.form_method = "GET" | ||
| self.form_tag = False | ||
| class GenericListView(ExportMixin, django_tables2.SingleTableView): | ||
| filter_class = None | ||
| formhelper_class = GenericFilterFormHelper | ||
| context_filter_name = "filter" | ||
| paginate_by = 50 | ||
| template_name = "browsing/generic_list.html" | ||
| init_columns = [ | ||
| "id", | ||
| ] | ||
| enable_merge = False | ||
| excluded_cols = [] | ||
| h1 = "" | ||
| create_button_text = "Create new item" | ||
| introduction = "" | ||
| def get_filterset_class(self): | ||
| if self.filter_class: | ||
| return self.filter_class | ||
| else: | ||
| filter_class = get_generic_filter(self.model) | ||
| return filter_class | ||
| def get_table_class(self): | ||
| if self.table_class: | ||
| return self.table_class | ||
| else: | ||
| return get_entities_table(self.model) | ||
| def get_all_cols(self): | ||
| all_cols = { | ||
| key: value.header for key, value in self.get_table().base_columns.items() | ||
| } | ||
| return all_cols | ||
| def get_queryset(self, **kwargs): | ||
| qs = super(GenericListView, self).get_queryset() | ||
| filter_class = self.get_filterset_class() | ||
| self.filter = filter_class(self.request.GET, queryset=qs) | ||
| self.filter.form.helper = self.formhelper_class() | ||
| return self.filter.qs.distinct() | ||
| def get_table(self, **kwargs): | ||
| table = super(GenericListView, self).get_table() | ||
| default_cols = self.init_columns | ||
| all_cols = table.base_columns.keys() | ||
| selected_cols = self.request.GET.getlist("columns") + default_cols | ||
| exclude_vals = [x for x in all_cols if x not in selected_cols] | ||
| table.exclude = exclude_vals | ||
| return table | ||
| def get_context_data(self, **kwargs): | ||
| context = super(GenericListView, self).get_context_data() | ||
| context["enable_merge"] = self.enable_merge | ||
| togglable_colums = { | ||
| key: value | ||
| for key, value in self.get_all_cols().items() | ||
| if key not in self.init_columns and key not in self.exclude_columns | ||
| } | ||
| context["togglable_colums"] = togglable_colums | ||
| context[self.context_filter_name] = self.filter | ||
| context["docstring"] = f"{self.model.__doc__}" | ||
| try: | ||
| context["create_view_link"] = self.model.get_createview_url() | ||
| except AttributeError: | ||
| context["create_view_link"] = None | ||
| model_name = self.model.__name__.lower() | ||
| context["entity"] = model_name | ||
| context["app_name"] = self.model._meta.app_label | ||
| if self.h1: | ||
| context["h1"] = self.h1 | ||
| else: | ||
| context["h1"] = f"Browse {self.model._meta.verbose_name_plural}" | ||
| context["create_button_text"] = self.create_button_text | ||
| context["verbose_name"] = self.model._meta.verbose_name | ||
| context["verbose_name_plural"] = self.model._meta.verbose_name_plural | ||
| return context | ||
| class BaseDetailView(DetailView): | ||
| model = None | ||
| template_name = "browsing/generic_detail.html" | ||
| def get_context_data(self, **kwargs): | ||
| context = super().get_context_data() | ||
| context["docstring"] = f"{self.model.__doc__}" | ||
| context["class_name"] = f"{self.model.__name__}" | ||
| context["app_name"] = f"{self.model._meta.app_label}" | ||
| context["verbose_name"] = self.model._meta.verbose_name | ||
| context["verbose_name_plural"] = self.model._meta.verbose_name_plural | ||
| return context | ||
| class BaseCreateView(CreateView): | ||
| model = None | ||
| form_class = None | ||
| template_name = "browsing/generic_create.html" | ||
| def get_context_data(self, **kwargs): | ||
| context = super(BaseCreateView, self).get_context_data() | ||
| context["docstring"] = f"{self.model.__doc__}" | ||
| context["class_name"] = f"{self.model.__name__}" | ||
| return context | ||
| class BaseUpdateView(UpdateView): | ||
| model = None | ||
| form_class = None | ||
| template_name = "browsing/generic_create.html" | ||
| def get_context_data(self, **kwargs): | ||
| context = super(BaseUpdateView, self).get_context_data() | ||
| context["docstring"] = f"{self.model.__doc__}" | ||
| context["class_name"] = f"{self.model.__name__}" | ||
| return context | ||
| def model_to_dict(instance): | ||
| """ | ||
| serializes a model.object to dict, including non editable fields as well as | ||
| ManyToManyField fields | ||
| """ | ||
| field_dicts = [] | ||
| for x in instance._meta.get_fields(): | ||
| f_type = "{}".format(type(x)) | ||
| field_dict = { | ||
| "name": x.name, | ||
| "help_text": getattr(x, "help_text", ""), | ||
| } | ||
| try: | ||
| field_dict["extra_fields"] = x.extra | ||
| except AttributeError: | ||
| field_dict["extra_fields"] = None | ||
| if "reverse_related" in f_type: | ||
| values = getattr(instance, x.name, None) | ||
| if values is not None: | ||
| field_dict["value"] = values.all() | ||
| else: | ||
| field_dict["value"] = [] | ||
| if getattr(x, "related_name", None) is not None: | ||
| field_dict["verbose_name"] = getattr(x, "related_name", x.name) | ||
| else: | ||
| field_dict["verbose_name"] = getattr(x, "verbose_name", x.name) | ||
| field_dict["f_type"] = "REVRESE_RELATION" | ||
| elif "related.ForeignKey" in f_type: | ||
| field_dict["verbose_name"] = getattr(x, "verbose_name", x.name) | ||
| field_dict["value"] = getattr(instance, x.name, "") | ||
| field_dict["f_type"] = "FK" | ||
| elif "TreeForeignKey" in f_type: | ||
| field_dict["verbose_name"] = getattr(x, "verbose_name", x.name) | ||
| field_dict["value"] = getattr(instance, x.name, "") | ||
| field_dict["f_type"] = "FK" | ||
| elif "related.ManyToManyField" in f_type: | ||
| values = getattr(instance, x.name, None) | ||
| if values is not None: | ||
| field_dict["value"] = values.all() | ||
| else: | ||
| field_dict["value"] = [] | ||
| field_dict["verbose_name"] = getattr(x, "verbose_name", x.name) | ||
| field_dict["f_type"] = "M2M" | ||
| elif "fields.DateTimeField" in f_type: | ||
| field_value = getattr(instance, x.name, "") | ||
| field_dict["verbose_name"] = getattr(x, "verbose_name", x.name) | ||
| field_dict["f_type"] = "DateTime" | ||
| if field_value: | ||
| field_dict["value"] = field_value.strftime("%Y-%m-%d %H:%M:%S") | ||
| else: | ||
| field_dict["verbose_name"] = getattr(x, "verbose_name", x.name) | ||
| field_dict["value"] = f"{getattr(instance, x.name, '')}" | ||
| field_dict["f_type"] = "SIMPLE" | ||
| field_dicts.append(field_dict) | ||
| return field_dicts |
| [build-system] | ||
| requires = ["setuptools"] | ||
| build-backend = "setuptools.build_meta" | ||
| [project] | ||
| name = "acdh-django-browsing" | ||
| version = "3.0" | ||
| description = "Django-App providing some useful things to create browsing views" | ||
| readme = "README.md" | ||
| license = { file = "LICENSE" } | ||
| authors = [ | ||
| {name = "Peter Andorfer", email = "peter.andorfer@oeaw.ac.at"} | ||
| ] | ||
| requires-python = ">=3.10" | ||
| keywords = ["django"] | ||
| dependencies = [ | ||
| "django-crispy-forms", | ||
| "django-filter", | ||
| "django-super-deduper", | ||
| "django-tables2", | ||
| "tablib", | ||
| ] | ||
| [project.optional-dependencies] | ||
| # Example for dev dependencies, add as needed | ||
| tests = ["pytest", "coverage"] | ||
| linting = ["flake8"] | ||
| build = ["build"] | ||
| [project.urls] | ||
| Homepage = "https://github.com/acdh-oeaw/acdh-django-browsing" | ||
| [tool.setuptools] | ||
| packages = ["browsing",] |
| Metadata-Version: 2.1 | ||
| Name: acdh-django-browsing | ||
| Version: 2.0 | ||
| Version: 3.0 | ||
| Summary: Django-App providing some useful things to create browsing views | ||
| Home-page: https://github.com/acdh-oeaw/acdh-django-browsing | ||
| Author: Peter Andorfer | ||
| Author-email: peter.andorfer@oeaw.ac.at | ||
| License: MIT License | ||
| Platform: UNKNOWN | ||
| Classifier: Environment :: Web Environment | ||
| Classifier: Framework :: Django | ||
| Classifier: Framework :: Django :: 3.0 | ||
| Classifier: Intended Audience :: Developers | ||
| Classifier: License :: OSI Approved :: MIT License | ||
| Classifier: Operating System :: OS Independent | ||
| Classifier: Programming Language :: Python | ||
| Classifier: Programming Language :: Python :: 3.6 | ||
| Author-email: Peter Andorfer <peter.andorfer@oeaw.ac.at> | ||
| Project-URL: Homepage, https://github.com/acdh-oeaw/acdh-django-browsing | ||
| Keywords: django | ||
| Requires-Python: >=3.10 | ||
| Description-Content-Type: text/markdown | ||
| License-File: LICENSE.txt | ||
| Requires-Dist: django-crispy-forms | ||
| Requires-Dist: django-filter | ||
| Requires-Dist: django-super-deduper | ||
| Requires-Dist: django-tables2 | ||
| Requires-Dist: tablib | ||
| Provides-Extra: tests | ||
| Requires-Dist: pytest; extra == "tests" | ||
| Requires-Dist: coverage; extra == "tests" | ||
| Provides-Extra: linting | ||
| Requires-Dist: flake8; extra == "linting" | ||
| Provides-Extra: build | ||
| Requires-Dist: build; extra == "build" | ||
| [](https://github.com/acdh-oeaw/acdh-django-browsing/actions/workflows/lint.yml) | ||
| [](https://github.com/acdh-oeaw/acdh-django-browsing/actions/workflows/test.yml) | ||
@@ -44,1 +48,24 @@ # acdh-django-browsing | ||
| ## Develop | ||
| ### install dev-dependencies | ||
| ```bash | ||
| pip install build pytest coverage flake8 black django django-next-prev | ||
| ``` | ||
| ### install package | ||
| ```bash | ||
| pip install . | ||
| ``` | ||
| ### run migrations and start dev server | ||
| ``` | ||
| python manage.py migrate | ||
| python manage.py runserver | ||
| ``` | ||
| ### build the package | ||
| ``` | ||
| python -m build | ||
| ``` |
@@ -1,8 +0,15 @@ | ||
| acdh-django-charts==0.5.3 | ||
| crispy-bootstrap5<1,>=0.7 | ||
| django-autocomplete-light<4,>=3.8.1 | ||
| django-crispy-forms<3,>=2.0 | ||
| django-filter<24,>=23.1 | ||
| django-super-deduper>=0.1.4 | ||
| django-tables2<3,>=2.5 | ||
| tablib<4,>=3.5.0 | ||
| django-crispy-forms | ||
| django-filter | ||
| django-super-deduper | ||
| django-tables2 | ||
| tablib | ||
| [build] | ||
| build | ||
| [linting] | ||
| flake8 | ||
| [tests] | ||
| pytest | ||
| coverage |
| LICENSE.txt | ||
| MANIFEST.in | ||
| README.md | ||
| setup.py | ||
| pyproject.toml | ||
| acdh_django_browsing.egg-info/PKG-INFO | ||
@@ -12,10 +12,7 @@ acdh_django_browsing.egg-info/SOURCES.txt | ||
| browsing/apps.py | ||
| browsing/browsing_utils.py | ||
| browsing/models.py | ||
| browsing/filters.py | ||
| browsing/tests.py | ||
| browsing/urls.py | ||
| browsing/utils.py | ||
| browsing/views.py | ||
| browsing/migrations/0001_initial.py | ||
| browsing/migrations/0002_delete_browsconf.py | ||
| browsing/migrations/__init__.py | ||
| browsing/static/browsing/js/filter-for-blank-fields.js | ||
@@ -27,10 +24,10 @@ browsing/static/browsing/js/set-form-attributes.js | ||
| browsing/templates/browsing/generic_list.html | ||
| browsing/templates/browsing/partials/chart_form.html | ||
| browsing/templates/browsing/partials/column_selector.html | ||
| browsing/templates/browsing/partials/detailview_breadcrumb.html | ||
| browsing/templates/browsing/partials/detailview_title.html | ||
| browsing/templates/browsing/partials/download_menu.html | ||
| browsing/templates/browsing/partials/listview_breadcrumb.html | ||
| browsing/templates/browsing/partials/pagination.html | ||
| browsing/templates/browsing/partials/table.html | ||
| browsing/templates/browsing/tags/class_definition.html | ||
| browsing/templates/browsing/tags/column_selector.html | ||
| browsing/templatetags/__init__.py | ||
| browsing/templatetags/browsing_extras.py | ||
| browsing/templates/browsing/tags/column_selector.html |
@@ -1,15 +0,15 @@ | ||
| $(function() | ||
| { | ||
| var form = $( 'form' )[0]; | ||
| $( form ).submit(function() | ||
| { | ||
| $('input, select').each(function() | ||
| { | ||
| if ($(this).val().length === 0) | ||
| { | ||
| $(this).prop('disabled', true); | ||
| $(this).next().prop('disabled', true); | ||
| } | ||
| }); | ||
| document.addEventListener("DOMContentLoaded", function () { | ||
| var form = document.querySelector("form"); | ||
| form.addEventListener("submit", function () { | ||
| var inputs = form.querySelectorAll("input, select"); | ||
| inputs.forEach(function (input) { | ||
| if (input.value.length === 0) { | ||
| input.disabled = true; | ||
| var nextElement = input.nextElementSibling; | ||
| if (nextElement) { | ||
| nextElement.disabled = true; | ||
| } | ||
| } | ||
| }); | ||
| }); | ||
| }); | ||
| }); |
| (function set_form_attributes() { | ||
| var textinput = document.getElementsByClassName('textinput'); | ||
| for (var i = 0; i < textinput.length; i++) { | ||
| textinput[i].classList.add('form-control'); | ||
| } | ||
| var select = document.getElementsByClassName('select'); | ||
| for (var i = 0; i < select.length; i++) { | ||
| select[i].classList.add('custom-select'); | ||
| } | ||
| })(); | ||
| var textinput = document.getElementsByClassName("textinput"); | ||
| for (var i = 0; i < textinput.length; i++) { | ||
| textinput[i].classList.add("form-control"); | ||
| } | ||
| var select = document.getElementsByClassName("select"); | ||
| for (var i = 0; i < select.length; i++) { | ||
| select[i].classList.add("custom-select"); | ||
| } | ||
| })(); |
@@ -1,2 +0,2 @@ | ||
| {% extends "webpage/base.html" %} | ||
| {% extends "base.html" %} | ||
| {% block title %}{% endblock %} | ||
@@ -3,0 +3,0 @@ {% load crispy_forms_tags %} |
@@ -1,63 +0,24 @@ | ||
| {% extends "webpage/base.html" %} | ||
| {% extends "base.html" %} | ||
| {% load static %} | ||
| {% load webpage_extras %} | ||
| {% block title %}{{ object.name }}{% endblock %} | ||
| {% block title %}{{ object }}{% endblock %} | ||
| {% block content %} | ||
| <!-- <div class="container"> --> | ||
| <div class="card"> | ||
| <div class="card-header"> | ||
| <div class="row"> | ||
| <div class="col-md-2"> | ||
| {% if object.get_prev %} | ||
| <h2 class="text-start"> | ||
| <a href="{{ object.get_prev }}" > | ||
| <i class="bi bi-chevron-left" title="previous entry"></i> | ||
| </a> | ||
| </h2> | ||
| {% endif %} | ||
| </div> | ||
| <div class="col-md-8"> | ||
| <h2 class="text-center"> | ||
| <small><a href="{{ object.get_listview_url }}"><i class="bi bi-list" | ||
| title="back to list view"></i></a></small> | ||
| {{ object }} | ||
| {% if user.is_authenticated %} | ||
| <small> | ||
| <a href="{{ object.get_edit_url }}"> | ||
| <i class="bi bi-pencil-square" title="edit entry"></i> | ||
| </a> | ||
| </small> | ||
| {% endif %} | ||
| </h2> | ||
| </div> | ||
| <div class="col-md-2"> | ||
| <h2 class="text-end"> | ||
| {% if object.get_next %} | ||
| <a href="{{ object.get_next }}" > | ||
| <i class="bi bi-chevron-right" title="next entry"></i> | ||
| </a> | ||
| </h2> | ||
| {% endif %} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <div class="card-body"> | ||
| {% include 'browsing/partials/detailview_breadcrumb.html' %} | ||
| <div class="container"> | ||
| {% include 'browsing/partials/detailview_title.html' %} | ||
| {% block custom %} | ||
| <legend>Basic Information</legend> | ||
| <table class="table table-bordered table-hover"> | ||
| <h2>Basic Information</h2> | ||
| <dl> | ||
| {% for x in object.field_dict %} | ||
| <tr> | ||
| <th> | ||
| <dt> | ||
| <abbr title="{{x.help_text}}">{{ x.verbose_name }}</abbr> | ||
| </th> | ||
| <td> | ||
| </dt> | ||
| <dd> | ||
| {{ x.value }} | ||
| </td> | ||
| </tr> | ||
| </dd> | ||
| {% endfor %} | ||
| </table> | ||
| </div> | ||
| </dl> | ||
| {% if user.is_authenticated %} | ||
| <div class="card-footer"> | ||
| <div class="float-end"> | ||
@@ -68,3 +29,2 @@ <a href="{{ object.get_delete_url }}"> | ||
| </div> | ||
| </div> | ||
| {% endif %} | ||
@@ -71,0 +31,0 @@ </div> |
@@ -1,8 +0,7 @@ | ||
| {% extends "webpage/base.html" %} | ||
| {% extends "base.html" %} | ||
| {% load static %} | ||
| {% load django_tables2 %} | ||
| {% load browsing_extras %} | ||
| {% load crispy_forms_field %} | ||
| {% load crispy_forms_tags %} | ||
| {% block title %} Browse {{ class_name }} {% endblock %} | ||
| {% block title %} Browse {{ verbose_name_plural }} {% endblock %} | ||
| {% block scriptHeader %} | ||
@@ -12,58 +11,43 @@ | ||
| {% block content %} | ||
| {% include 'browsing/partials/listview_breadcrumb.html' %} | ||
| <div class="card mt-3"> | ||
| <div class="card-header text-center"> | ||
| <h1 > | ||
| Browse {% class_definition %} {% block list_title %}{% endblock %} | ||
| </h1> | ||
| {% if user.is_authenticated %} | ||
| {% if create_view_link %} | ||
| <div class="d-grid gap-2"> | ||
| <a class="btn btn-primary float-center ms-5 me-5" href="{{ create_view_link }}">Create new {{ class_name }}</a> | ||
| <div class="container"> | ||
| <h1 class="display-5 text-center">{{ h1 }}</h1> | ||
| {% if user.is_authenticated and create_view_link %} | ||
| <div class="d-grid gap-2 pb-2"> | ||
| <a class="btn btn-primary float-center ms-5 me-5" href="{{ create_view_link }}"> | ||
| Create new {{ verbose_name }}</a> | ||
| </div> | ||
| {% endif %} | ||
| {% endif %} | ||
| </div> | ||
| <div class="card-body"> | ||
| <div class="row"> | ||
| <div class="col-md-4"> | ||
| <div class="card"> | ||
| <div class="card-body"> | ||
| <h2 class="text-center">Search</h2> | ||
| {% block customView %}{% endblock %} | ||
| {% block create_button %}{% endblock %} | ||
| <!--Search mask--> | ||
| {% load django_tables2 crispy_forms_tags %} | ||
| <form action="." class="uniForm" method="get"> | ||
| {% crispy filter.form filter.form.helper %} | ||
| {% include 'browsing/partials/column_selector.html' %} | ||
| <a class="btn btn-primary" href=".">Reset</a> | ||
| <button type="submit" class="btn btn-primary">Search</button> | ||
| </form> | ||
| {% include 'browsing/partials/chart_form.html' %} | ||
| </div> | ||
| <div class="row"> | ||
| <div class="col-md-4"> | ||
| <!--Search mask--> | ||
| <h2 class="text-center pb-4 pt-3">Refine Results</h2> | ||
| <form action="." class="uniForm" method="get"> | ||
| {% crispy filter.form filter.form.helper %} | ||
| {% include 'browsing/partials/column_selector.html' %} | ||
| <div class="d-grid gap-2 pt-2" aria-label="Search or reset search"> | ||
| <button type="submit" class="btn btn-primary">Submit</button> | ||
| <a class="btn btn-primary" href=".">Reset</a> | ||
| </div> | ||
| </form> | ||
| </div> | ||
| <div class="col-md-8" id="results"> | ||
| {% with table.paginator.count as total %} | ||
| <h2 class="text-center pb-4 pt-3">{{ total }} Result(s)</h2> | ||
| {% endwith %} | ||
| {% block table %} | ||
| {% include 'browsing/partials/table.html' %} | ||
| {% endblock table %} | ||
| {% block pagination.allpages %} | ||
| {% include 'browsing/partials/pagination.html' %} | ||
| {% endblock pagination.allpages %} | ||
| <div class="float-end"> | ||
| {% include "browsing/partials/download_menu.html" %} | ||
| </div> | ||
| <div class="col-md-8" id="results"> | ||
| <div class="card"> | ||
| {% with table.paginator.count as total %} | ||
| <h2 class="text-center">{{ total }} Result(s)</h2> | ||
| {% endwith %} | ||
| <div class="card-body table-responsive"> | ||
| {% block table %} | ||
| {% include 'browsing/partials/table.html' %} | ||
| {% endblock table %} | ||
| {% block pagination.allpages %} | ||
| {% include 'browsing/partials/pagination.html' %} | ||
| {% endblock pagination.allpages %} | ||
| <div class="float-end"> | ||
| {% include "browsing/partials/download_menu.html" %} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
@@ -70,0 +54,0 @@ {% endblock %} |
@@ -16,6 +16,4 @@ from django.contrib.auth.decorators import login_required | ||
| app_name = request.POST.get("app_name", None) | ||
| print("##############################") | ||
| print(keep, remove, model_name, app_name) | ||
| if keep and remove and model_name and app_name: | ||
| print("all good") | ||
| try: | ||
@@ -38,5 +36,5 @@ ct = ContentType.objects.get( | ||
| x.delete() | ||
| print("merged {} into {}".format(x, keep_obj)) | ||
| print(f"merged {x} into {keep_obj}") | ||
| return HttpResponseRedirect(go_back) | ||
| else: | ||
| return HttpResponseRedirect(go_back) |
+41
-14
| Metadata-Version: 2.1 | ||
| Name: acdh-django-browsing | ||
| Version: 2.0 | ||
| Version: 3.0 | ||
| Summary: Django-App providing some useful things to create browsing views | ||
| Home-page: https://github.com/acdh-oeaw/acdh-django-browsing | ||
| Author: Peter Andorfer | ||
| Author-email: peter.andorfer@oeaw.ac.at | ||
| License: MIT License | ||
| Platform: UNKNOWN | ||
| Classifier: Environment :: Web Environment | ||
| Classifier: Framework :: Django | ||
| Classifier: Framework :: Django :: 3.0 | ||
| Classifier: Intended Audience :: Developers | ||
| Classifier: License :: OSI Approved :: MIT License | ||
| Classifier: Operating System :: OS Independent | ||
| Classifier: Programming Language :: Python | ||
| Classifier: Programming Language :: Python :: 3.6 | ||
| Author-email: Peter Andorfer <peter.andorfer@oeaw.ac.at> | ||
| Project-URL: Homepage, https://github.com/acdh-oeaw/acdh-django-browsing | ||
| Keywords: django | ||
| Requires-Python: >=3.10 | ||
| Description-Content-Type: text/markdown | ||
| License-File: LICENSE.txt | ||
| Requires-Dist: django-crispy-forms | ||
| Requires-Dist: django-filter | ||
| Requires-Dist: django-super-deduper | ||
| Requires-Dist: django-tables2 | ||
| Requires-Dist: tablib | ||
| Provides-Extra: tests | ||
| Requires-Dist: pytest; extra == "tests" | ||
| Requires-Dist: coverage; extra == "tests" | ||
| Provides-Extra: linting | ||
| Requires-Dist: flake8; extra == "linting" | ||
| Provides-Extra: build | ||
| Requires-Dist: build; extra == "build" | ||
| [](https://github.com/acdh-oeaw/acdh-django-browsing/actions/workflows/lint.yml) | ||
| [](https://github.com/acdh-oeaw/acdh-django-browsing/actions/workflows/test.yml) | ||
@@ -44,1 +48,24 @@ # acdh-django-browsing | ||
| ## Develop | ||
| ### install dev-dependencies | ||
| ```bash | ||
| pip install build pytest coverage flake8 black django django-next-prev | ||
| ``` | ||
| ### install package | ||
| ```bash | ||
| pip install . | ||
| ``` | ||
| ### run migrations and start dev server | ||
| ``` | ||
| python manage.py migrate | ||
| python manage.py runserver | ||
| ``` | ||
| ### build the package | ||
| ``` | ||
| python -m build | ||
| ``` |
+26
-0
| [](https://github.com/acdh-oeaw/acdh-django-browsing/actions/workflows/lint.yml) | ||
| [](https://github.com/acdh-oeaw/acdh-django-browsing/actions/workflows/test.yml) | ||
@@ -22,1 +23,26 @@ # acdh-django-browsing | ||
| ``` | ||
| ## Develop | ||
| ### install dev-dependencies | ||
| ```bash | ||
| pip install build pytest coverage flake8 black django django-next-prev | ||
| ``` | ||
| ### install package | ||
| ```bash | ||
| pip install . | ||
| ``` | ||
| ### run migrations and start dev server | ||
| ``` | ||
| python manage.py migrate | ||
| python manage.py runserver | ||
| ``` | ||
| ### build the package | ||
| ``` | ||
| python -m build | ||
| ``` |
| import django_tables2 | ||
| from django.conf import settings | ||
| from django.utils.safestring import mark_safe | ||
| from django.views.generic.detail import DetailView | ||
| from django.views.generic.edit import CreateView, UpdateView | ||
| from crispy_forms.helper import FormHelper | ||
| from crispy_forms.layout import Submit | ||
| from django_tables2.export.views import ExportMixin | ||
| if "charts" in settings.INSTALLED_APPS: | ||
| from charts.models import ChartConfig | ||
| from charts.views import create_payload | ||
| input_form = """ | ||
| <input type="checkbox" name="keep" value="{}" title="keep this"/> | | ||
| <input type="checkbox" name="remove" value="{}" title="remove this"/> | ||
| """ | ||
| class MergeColumn(django_tables2.Column): | ||
| """renders a column with to checkbox - used to select objects for merging""" | ||
| def __init__(self, *args, **kwargs): | ||
| super(MergeColumn, self).__init__(*args, **kwargs) | ||
| def render(self, value): | ||
| return mark_safe(input_form.format(value, value)) | ||
| def get_entities_table(model_class): | ||
| class GenericEntitiesTable(django_tables2.Table): | ||
| id = django_tables2.LinkColumn() | ||
| class Meta: | ||
| model = model_class | ||
| attrs = {"class": "table table-hover table-striped table-condensed"} | ||
| return GenericEntitiesTable | ||
| class GenericFilterFormHelper(FormHelper): | ||
| def __init__(self, *args, **kwargs): | ||
| super(GenericFilterFormHelper, self).__init__(*args, **kwargs) | ||
| self.helper = FormHelper() | ||
| self.form_class = "genericFilterForm" | ||
| self.form_method = "GET" | ||
| self.helper.form_tag = False | ||
| self.add_input(Submit("Filter", "Search")) | ||
| class GenericListView(ExportMixin, django_tables2.SingleTableView): | ||
| filter_class = None | ||
| formhelper_class = None | ||
| context_filter_name = "filter" | ||
| paginate_by = 50 | ||
| template_name = "browsing/generic_list.html" | ||
| init_columns = [] | ||
| enable_merge = False | ||
| excluded_cols = [] | ||
| def get_table_class(self): | ||
| if self.table_class: | ||
| return self.table_class | ||
| else: | ||
| return get_entities_table(self.model) | ||
| def get_all_cols(self): | ||
| all_cols = { | ||
| key: value.header for key, value in self.get_table().base_columns.items() | ||
| } | ||
| return all_cols | ||
| def get_queryset(self, **kwargs): | ||
| qs = super(GenericListView, self).get_queryset() | ||
| self.filter = self.filter_class(self.request.GET, queryset=qs) | ||
| self.filter.form.helper = self.formhelper_class() | ||
| return self.filter.qs.distinct() | ||
| def get_table(self, **kwargs): | ||
| table = super(GenericListView, self).get_table() | ||
| default_cols = self.init_columns | ||
| all_cols = table.base_columns.keys() | ||
| selected_cols = self.request.GET.getlist("columns") + default_cols | ||
| exclude_vals = [x for x in all_cols if x not in selected_cols] | ||
| table.exclude = exclude_vals | ||
| return table | ||
| def get_context_data(self, **kwargs): | ||
| context = super(GenericListView, self).get_context_data() | ||
| context["enable_merge"] = self.enable_merge | ||
| togglable_colums = { | ||
| key: value | ||
| for key, value in self.get_all_cols().items() | ||
| if key not in self.init_columns and key not in self.exclude_columns | ||
| } | ||
| context["togglable_colums"] = togglable_colums | ||
| context[self.context_filter_name] = self.filter | ||
| context["docstring"] = "{}".format(self.model.__doc__) | ||
| if self.model._meta.verbose_name_plural: | ||
| context["class_name"] = "{}".format(self.model._meta.verbose_name.title()) | ||
| else: | ||
| if self.model.__name__.endswith("s"): | ||
| context["class_name"] = "{}".format(self.model.__name__) | ||
| else: | ||
| context["class_name"] = "{}s".format(self.model.__name__) | ||
| try: | ||
| context["create_view_link"] = self.model.get_createview_url() | ||
| except AttributeError: | ||
| context["create_view_link"] = None | ||
| model_name = self.model.__name__.lower() | ||
| context["entity"] = model_name | ||
| context["app_name"] = self.model._meta.app_label | ||
| if "charts" in settings.INSTALLED_APPS: | ||
| model = self.model | ||
| app_label = model._meta.app_label | ||
| filtered_objs = ChartConfig.objects.filter( | ||
| model_name=model.__name__.lower(), app_name=app_label | ||
| ) | ||
| context["vis_list"] = filtered_objs | ||
| context["property_name"] = self.request.GET.get("property") | ||
| context["charttype"] = self.request.GET.get("charttype") | ||
| if context["charttype"] and context["property_name"]: | ||
| qs = self.get_queryset() | ||
| chartdata = create_payload( | ||
| context["entity"], | ||
| context["property_name"], | ||
| context["charttype"], | ||
| qs, | ||
| app_label=app_label, | ||
| ) | ||
| context = dict(context, **chartdata) | ||
| return context | ||
| class BaseDetailView(DetailView): | ||
| model = None | ||
| template_name = "browsing/generic_detail.html" | ||
| def get_context_data(self, **kwargs): | ||
| context = super().get_context_data() | ||
| context["docstring"] = "{}".format(self.model.__doc__) | ||
| context["class_name"] = "{}".format(self.model.__name__) | ||
| context["app_name"] = "{}".format(self.model._meta.app_label) | ||
| return context | ||
| class BaseCreateView(CreateView): | ||
| model = None | ||
| form_class = None | ||
| template_name = "browsing/generic_create.html" | ||
| def get_context_data(self, **kwargs): | ||
| context = super(BaseCreateView, self).get_context_data() | ||
| context["docstring"] = "{}".format(self.model.__doc__) | ||
| context["class_name"] = "{}".format(self.model.__name__) | ||
| return context | ||
| class BaseUpdateView(UpdateView): | ||
| model = None | ||
| form_class = None | ||
| template_name = "browsing/generic_create.html" | ||
| def get_context_data(self, **kwargs): | ||
| context = super(BaseUpdateView, self).get_context_data() | ||
| context["docstring"] = "{}".format(self.model.__doc__) | ||
| context["class_name"] = "{}".format(self.model.__name__) | ||
| # if self.model.__name__.endswith('s'): | ||
| # context['class_name'] = "{}".format(self.model.__name__) | ||
| # else: | ||
| # context['class_name'] = "{}s".format(self.model.__name__) | ||
| return context | ||
| def model_to_dict(instance): | ||
| """ | ||
| serializes a model.object to dict, including non editable fields as well as | ||
| ManyToManyField fields | ||
| """ | ||
| field_dicts = [] | ||
| for x in instance._meta.get_fields(): | ||
| f_type = "{}".format(type(x)) | ||
| field_dict = { | ||
| "name": x.name, | ||
| "help_text": getattr(x, "help_text", ""), | ||
| } | ||
| try: | ||
| field_dict["extra_fields"] = x.extra | ||
| except AttributeError: | ||
| field_dict["extra_fields"] = None | ||
| if "reverse_related" in f_type: | ||
| values = getattr(instance, x.name, None) | ||
| if values is not None: | ||
| field_dict["value"] = values.all() | ||
| else: | ||
| field_dict["value"] = [] | ||
| if getattr(x, "related_name", None) is not None: | ||
| field_dict["verbose_name"] = getattr(x, "related_name", x.name) | ||
| else: | ||
| field_dict["verbose_name"] = getattr(x, "verbose_name", x.name) | ||
| field_dict["f_type"] = "REVRESE_RELATION" | ||
| elif "related.ForeignKey" in f_type: | ||
| field_dict["verbose_name"] = getattr(x, "verbose_name", x.name) | ||
| field_dict["value"] = getattr(instance, x.name, "") | ||
| field_dict["f_type"] = "FK" | ||
| elif "TreeForeignKey" in f_type: | ||
| field_dict["verbose_name"] = getattr(x, "verbose_name", x.name) | ||
| field_dict["value"] = getattr(instance, x.name, "") | ||
| field_dict["f_type"] = "FK" | ||
| elif "related.ManyToManyField" in f_type: | ||
| values = getattr(instance, x.name, None) | ||
| if values is not None: | ||
| field_dict["value"] = values.all() | ||
| else: | ||
| field_dict["value"] = [] | ||
| field_dict["verbose_name"] = getattr(x, "verbose_name", x.name) | ||
| field_dict["f_type"] = "M2M" | ||
| elif "fields.DateTimeField" in f_type: | ||
| field_value = getattr(instance, x.name, "") | ||
| field_dict["verbose_name"] = getattr(x, "verbose_name", x.name) | ||
| field_dict["f_type"] = "DateTime" | ||
| if field_value: | ||
| field_dict["value"] = field_value.strftime("%Y-%m-%d %H:%M:%S") | ||
| else: | ||
| field_dict["verbose_name"] = getattr(x, "verbose_name", x.name) | ||
| field_dict["value"] = f"{getattr(instance, x.name, '')}" | ||
| field_dict["f_type"] = "SIMPLE" | ||
| field_dicts.append(field_dict) | ||
| return field_dicts |
| # Generated by Django 2.2.6 on 2019-12-11 13:34 | ||
| from django.db import migrations, models | ||
| class Migration(migrations.Migration): | ||
| initial = True | ||
| dependencies = [] | ||
| operations = [ | ||
| migrations.CreateModel( | ||
| name="BrowsConf", | ||
| fields=[ | ||
| ( | ||
| "id", | ||
| models.AutoField( | ||
| auto_created=True, | ||
| primary_key=True, | ||
| serialize=False, | ||
| verbose_name="ID", | ||
| ), | ||
| ), | ||
| ( | ||
| "model_name", | ||
| models.CharField( | ||
| blank=True, | ||
| help_text="The name of the model class you like to analyse.", | ||
| max_length=255, | ||
| ), | ||
| ), | ||
| ( | ||
| "label", | ||
| models.CharField( | ||
| blank=True, | ||
| help_text="The label of the value of interest.", | ||
| max_length=255, | ||
| ), | ||
| ), | ||
| ( | ||
| "field_path", | ||
| models.CharField( | ||
| blank=True, | ||
| help_text="The constructor of to the value of interest.", | ||
| max_length=255, | ||
| ), | ||
| ), | ||
| ], | ||
| ), | ||
| ] |
| # Generated by Django 3.2 on 2021-07-21 09:48 | ||
| from django.db import migrations | ||
| class Migration(migrations.Migration): | ||
| dependencies = [ | ||
| ("browsing", "0001_initial"), | ||
| ] | ||
| operations = [ | ||
| migrations.DeleteModel( | ||
| name="BrowsConf", | ||
| ), | ||
| ] |
| # from django.db import models |
| {% if vis_list %} | ||
| <div class="btn-group"> | ||
| <button class="btn btn-default dropdown-toggle dropdown-custom" type="button" id="dropdownMenuVis" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true"> | ||
| Visualize <span class="caret"></span> | ||
| </button> | ||
| <div class="dropdown-menu" role="menu"> | ||
| {% for x in vis_list %} | ||
| <li> | ||
| {% for y in x.chart_types.all %} | ||
| <a class="dropdown-item" title="{{ x.help_text }}" href="?{{ request.GET.urlencode }}&charttype={{ y }}&property={{ x.field_path }}">{{ x.label }} {{ y.icon|safe }}</a> | ||
| {% endfor %} | ||
| </li> | ||
| <hr/> | ||
| {% endfor %} | ||
| </div> | ||
| </div> | ||
| {% endif %} | ||
| {% if vis_list %} | ||
| {% if data %} | ||
| <div class="card"> | ||
| <div class="card-heading"> | ||
| <h1>{{ data.title }}</h1> | ||
| <legend>{{ data.items }}</legend> | ||
| <button type="button" class="btn btn-default" data-toggle="collapse" data-target="#howtocite" id="howtocite-btn">JSON data</button> | ||
| <div id="howtocite" class="collapse"> | ||
| <p>{{data}}</p> | ||
| </div> | ||
| </div> | ||
| <div class="card-body"> | ||
| <div id="bar"></div> | ||
| </div> | ||
| {% if error %} | ||
| <h4>{{ error_msg|safe }}</h4> | ||
| {% endif %} | ||
| </div> | ||
| {% endif %} | ||
| {% endif %} |
| from django import template | ||
| from django.contrib.contenttypes.models import ContentType | ||
| register = template.Library() | ||
| @register.simple_tag | ||
| def nav_menu(app=None): | ||
| """creates links to all class of the passed in application | ||
| for which get_listview_url() methods have been registered""" | ||
| if app: | ||
| models = ContentType.objects.filter(app_label=app) | ||
| result = [] | ||
| for x in models: | ||
| modelname = x.name | ||
| modelname = modelname.replace(" ", "").lower() | ||
| try: | ||
| fetched_model = ContentType.objects.get( | ||
| app_label=app, model=modelname | ||
| ).model_class() | ||
| item = { | ||
| "name": modelname.title(), | ||
| } | ||
| except Exception as e: | ||
| print(e) | ||
| item = {"name": None} | ||
| try: | ||
| item["link"] = fetched_model.get_listview_url() | ||
| result.append(item) | ||
| except AttributeError: | ||
| item["link"] = None | ||
| return result | ||
| @register.inclusion_tag("browsing/tags/class_definition.html", takes_context=True) | ||
| def class_definition(context): | ||
| values = {} | ||
| try: | ||
| values["class_name"] = context["class_name"] | ||
| values["docstring"] = context["docstring"] | ||
| except Exception as e: | ||
| print(e) | ||
| pass | ||
| return values | ||
| @register.inclusion_tag("browsing/tags/column_selector.html", takes_context=True) | ||
| def column_selector(context): | ||
| return context |
-38
| from setuptools import find_packages, setup | ||
| with open("README.md") as readme_file: | ||
| readme = readme_file.read() | ||
| setup( | ||
| name="acdh-django-browsing", | ||
| version="2.0", | ||
| packages=find_packages(), | ||
| include_package_data=True, | ||
| license="MIT License", | ||
| description="Django-App providing some useful things to create browsing views", | ||
| long_description=readme, | ||
| long_description_content_type="text/markdown", | ||
| url="https://github.com/acdh-oeaw/acdh-django-browsing", | ||
| author="Peter Andorfer", | ||
| author_email="peter.andorfer@oeaw.ac.at", | ||
| classifiers=[ | ||
| "Environment :: Web Environment", | ||
| "Framework :: Django", | ||
| "Framework :: Django :: 3.0", | ||
| "Intended Audience :: Developers", | ||
| "License :: OSI Approved :: MIT License", # example license | ||
| "Operating System :: OS Independent", | ||
| "Programming Language :: Python", | ||
| "Programming Language :: Python :: 3.6", | ||
| ], | ||
| install_requires=[ | ||
| "acdh-django-charts==0.5.3", | ||
| "django-autocomplete-light>=3.8.1,<4", | ||
| "django-crispy-forms>=2.0,<3", | ||
| "crispy-bootstrap5>=0.7,<1", | ||
| "django-filter>=23.1,<24", | ||
| "django-super-deduper>=0.1.4", | ||
| "django-tables2>=2.5,<3", | ||
| "tablib>=3.5.0,<4", | ||
| ], | ||
| ) |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
30821
-13.02%33
-8.33%265
-34.73%