Skip to content
This repository has been archived by the owner on Jul 29, 2022. It is now read-only.

Commit

Permalink
Merge branch 'release/3.2'
Browse files Browse the repository at this point in the history
* release/3.2:
  updates
  lint code
  updates
  pep8
  fix warnings
  updates
  • Loading branch information
saxix committed Feb 12, 2021
2 parents 2edc79c + 6d65ef4 commit 0a8c02e
Show file tree
Hide file tree
Showing 16 changed files with 182 additions and 45 deletions.
8 changes: 8 additions & 0 deletions CHANGES
@@ -1,3 +1,11 @@
Release 3.2
-----------
* Code refactoring
* New Feature: disable buttons when form values are changed
* add ability to customize urls and add extra paramenters
* new `action_page.html` to be used as is or as template for multi-step actions


Release 3.1
-----------
* ButtonLink splitted in ChangeFormButton, ChangeListButton
Expand Down
4 changes: 4 additions & 0 deletions Makefile
Expand Up @@ -17,6 +17,10 @@ docs:
rm -fr ~build/docs/
sphinx-build -n docs/ ~build/docs/

lint:
@flake8 src/
@isort src/


.PHONY: build docs

Expand Down
24 changes: 13 additions & 11 deletions README.rst
Expand Up @@ -10,8 +10,8 @@ pluggable django application that offers one single mixin class ``ExtraUrlMixin`
to easily add new url (and related buttons on the screen) to any ModelAdmin.

- ``action()`` decorator It will produce a button in the change form view.
- ``ChangeFormButton()`` to add button that pont to external urls.
- ``ChangeListButton()`` to add button that pont to external urls.
- ``ChangeFormButton()`` to add button that point to external urls.
- ``ChangeListButton()`` to add button that point to external urls.



Expand Down Expand Up @@ -45,25 +45,23 @@ How to use it
actions = ['smart_action']
@extras.action() # /admin/myapp/mymodel/update_all/
def update_all(self, request):
def consolidate(self, request):
...
...
@extras.action() # /admin/myapp/mymodel/update/10/
def update(self, request, pk):
# if `pk` exists the button will be in change_form
# if we use `pk` in the args, the button will be in change_form
obj = self.get_object(pk=pk)
...
@extras.action() # /admin/myapp/mymodel/
@extras.try_catch
def smart_action(self, request, queryset=None):
# apply actionnto the whole data without
# check all
if not queryset:
queryset = self.model.objects.all()
@action(urls=[r'^aaa/(?P<pk>.*)/(?P<state>.*)/$',
r'^bbb/(?P<pk>.*)/$'])
def revert(self, request, pk, state=None):
obj = self.get_object(pk=pk)
...
@extras.action(label='Truncate', permission=lambda request, obj: request.user.is_superuser)
def truncate(self, request):
Expand All @@ -74,6 +72,8 @@ How to use it
'Continuing will erase the entire content of the table.',
'Successfully executed', )
You don't need to return a HttpResponse. The default behavior is:

- if the method contains the `pk` argument button will be displayed in the 'update' view and the browser will be redirected to ``change_view``
Expand All @@ -97,6 +97,8 @@ action() options
+------------+----------------------+----------------------------------------------------------------------------------------+
| visible | lambda o: o and o.pk | callable or bool. By default do not display "action" button if in `add` mode |
+------------+----------------------+----------------------------------------------------------------------------------------+
| urls | None | list of urls to be linked to the action. |
+------------+----------------------+----------------------------------------------------------------------------------------+



Expand Down
9 changes: 8 additions & 1 deletion docs/api.rst
Expand Up @@ -4,5 +4,12 @@
API
===

.. autofunction:: admin_extra_urls.extras.action
.. autofunction:: admin_extra_urls.api.action


=========
Utilities
=========

.. autofunction:: admin_extra_urls.mixins.ExtraUrlMixin.get_common_context

1 change: 1 addition & 0 deletions setup.py
Expand Up @@ -76,6 +76,7 @@ def run_tests(self):
'Framework :: Django',
'Framework :: Django :: 2.2',
'Framework :: Django :: 3.0',
'Framework :: Django :: 3.1',
'License :: OSI Approved :: BSD License',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.6',
Expand Down
2 changes: 1 addition & 1 deletion src/admin_extra_urls/__init__.py
@@ -1,3 +1,3 @@
NAME = "django-admin-extra-urls"
VERSION = __version__ = "3.1"
VERSION = __version__ = "3.2"
__author__ = 'sax'
6 changes: 3 additions & 3 deletions src/admin_extra_urls/api.py
@@ -1,3 +1,3 @@
from .config import ButtonAction, ChangeFormButton, ChangeListButton
from .decorators import action, link, try_catch
from .mixins import ExtraUrlMixin
from .config import ButtonAction, ChangeFormButton, ChangeListButton # noqa: F401
from .decorators import action, link, try_catch # noqa: F401
from .mixins import ExtraUrlMixin # noqa: F401
7 changes: 3 additions & 4 deletions src/admin_extra_urls/config.py
Expand Up @@ -3,14 +3,12 @@

from admin_extra_urls.templatetags.extra_urls import get_preserved_filters

from .utils import labelize

empty = object()


class Button:
def __init__(self, path, *, label=None, icon='', permission=None,
css_class="btn btn-success", order=999, visible=empty, details=True):
css_class="btn btn-success", order=999, visible=empty, details=True, urls=None):
self.path = path
self.label = label or path

Expand All @@ -21,6 +19,7 @@ def __init__(self, path, *, label=None, icon='', permission=None,
self._visible = visible
self._bound = False
self.details = details
self.urls = urls

def bind(self, context):
self.context = context
Expand Down Expand Up @@ -68,7 +67,7 @@ def __init__(self, func, **kwargs):
self.func = func
super().__init__(**kwargs)
self.path = self.path or func.__name__
self.label = self.label or labelize(func.__name__)
# self.label = self.label or labelize(func.__name__)
self.method = func.__name__

def url(self):
Expand Down
13 changes: 5 additions & 8 deletions src/admin_extra_urls/decorators.py
Expand Up @@ -8,7 +8,7 @@
from django.utils.http import urlencode

from .config import ButtonAction, empty
from .utils import check_permission, encapsulate
from .utils import check_permission, encapsulate, labelize


def try_catch(f):
Expand All @@ -26,7 +26,8 @@ def _inner(modeladmin, request, *args, **kwargs):


def action(path=None, label=None, icon='', permission=None,
css_class="btn btn-success", order=999, visible=empty, wraps=False):
css_class="btn btn-success auto-disable", order=999, visible=empty,
urls=None):
"""
decorator to mark ModelAdmin method.
Expand All @@ -47,8 +48,6 @@ def action(path=None, label=None, icon='', permission=None,
:type order: int
:param visible: button visibility. Can be a callable
:type visible: Any
:param wraps:
:type wraps: bool
"""

if callable(permission):
Expand Down Expand Up @@ -82,9 +81,6 @@ def _inner(modeladmin, request, *args, **kwargs):
url = reverse(admin_urlname(modeladmin.model._meta, 'changelist'))
if permission:
check_permission(permission, request)
# if wraps:
# ret = try_catch(func, modeladmin, request, *args, **kwargs)
# else:
ret = func(modeladmin, request, *args, **kwargs)

if not isinstance(ret, HttpResponse):
Expand All @@ -95,12 +91,13 @@ def _inner(modeladmin, request, *args, **kwargs):

_inner.action = ButtonAction(func=func,
path=path,
label=label,
label=label or labelize(func.__name__),
icon=icon,
permission=permission,
order=order,
css_class=css_class,
visible=visibility,
urls=urls,
details=details)

return _inner
Expand Down
4 changes: 3 additions & 1 deletion src/admin_extra_urls/extras.py
@@ -1,3 +1,5 @@
import warnings

from .api import * # noqa: F403, F401

warnings.warn("extras is deprecated. Please use `from admon_extra_urls import api`", DeprecationWarning)
from .api import *
78 changes: 67 additions & 11 deletions src/admin_extra_urls/mixins.py
Expand Up @@ -4,12 +4,11 @@
from functools import update_wrapper

from django.conf import settings
from django.conf.urls import url
from django.contrib import messages
from django.contrib.admin.templatetags.admin_urls import admin_urlname
from django.http import HttpResponseRedirect
from django.template.response import TemplateResponse
from django.urls import reverse
from django.urls import re_path, reverse

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -64,6 +63,15 @@ class ExtraUrlConfigException(RuntimeError):
pass


class DummyAdminform:
def __init__(self, **kwargs):
self.prepopulated_fields = []
self.__dict__.update(**kwargs)

def __iter__(self):
yield


class ExtraUrlMixin:
"""
Allow to add new 'url' to the standard ModelAdmin
Expand All @@ -82,6 +90,42 @@ def __init__(self, model, admin_site):
# self.extra_detail_actions = []
super().__init__(model, admin_site)

def get_common_context(self, request, pk=None, **kwargs):
""" returns a general context that can be used in custom actions
es.
>>> from admin_extra_urls.api import ExtraUrlMixin, action
>>> @action()
... def revert(self, request, pk):
... context = self.get_common_context(request, pk, MONITORED_FIELDS=MONITORED_FIELDS)
"""
opts = self.model._meta
app_label = opts.app_label
self.object = None

context = {
**self.admin_site.each_context(request),
**kwargs,
"opts": opts,
"add": False,
"change": True,
"save_as": False,
"has_delete_permission": self.has_delete_permission(request, pk),
"has_editable_inline_admin_formsets": False,
"has_view_permission": self.has_view_permission(request, pk),
"has_change_permission": self.has_change_permission(request, pk),
"has_add_permission": self.has_add_permission(request),
"app_label": app_label,
"adminform": DummyAdminform(model_admin=self),
}
context.setdefault("title", "")
# context.update(**kwargs)
if pk:
self.object = self.get_object(request, pk)
context["original"] = self.object
return context

def get_urls(self):
extra_actions = []
# extra_detail_actions = []
Expand All @@ -105,16 +149,28 @@ def wrapper(*args, **kwargs):
for __, options in extra_urls.items():
# isdetail, method_name, options = entry
info[2] = options.method
if options.details:
extra_actions.append(options)
uri = r'^%s/(?P<pk>.*)/$' % options.path
signature = inspect.signature(options.func)
arguments = {
k: v.default
for k, v in signature.parameters.items()
if v.default is not inspect.Parameter.empty
}
if options.urls:
for uri in options.urls:
options.details = 'pk' in uri
extras.append(re_path(uri,
wrap(getattr(self, options.method)),
name='{}_{}_{}'.format(*info)))
else:
uri = r'^%s/$' % options.path
extra_actions.append(options)

extras.append(url(uri,
wrap(getattr(self, options.method)),
name='{}_{}_{}'.format(*info)))
if options.details:
extra_actions.append(options)
uri = r'^%s/(?P<pk>.*)/$' % options.path
else:
uri = r'^%s/$' % options.path
extra_actions.append(options)
extras.append(re_path(uri,
wrap(getattr(self, options.method)),
name='{}_{}_{}'.format(*info)))

for href in self.extra_buttons:
extra_actions.append(href)
Expand Down
31 changes: 31 additions & 0 deletions src/admin_extra_urls/templates/admin_extra_urls/action_page.html
@@ -0,0 +1,31 @@
{% extends "admin_extra_urls/change_form.html" %}{% load i18n static extra_urls admin_list admin_urls %}
{% block breadcrumbs %}
<div class="breadcrumbs">
{% block breadcrumbs-items %}
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'change' original.pk %}">{{ original }}</a>
{% block breadcrumbs-active %}
&rsaquo; {{ action|default_if_empty:title }}
{% endblock %}
{% endblock %}
</div>
{% endblock %}
{% block content_title %}<h1>{{ title|default_if_empty:"&nbsp;" }}</h1>{% endblock %}
{% block content %}<div id="content-main">
{% block object-tools %}
<ul class="object-tools">
{% block object-tools-items %}
{% include "admin_extra_urls/includes/change_form_buttons.html" %}
{% endblock %}
</ul>
{% endblock %}{% block action-content %}{% endblock %}
{% block document_ready %}{% endblock %}
{% endblock %}

{#{% block object-tools-items %}#}
{# {{ block.super }}#}
{# {% include "admin_extra_urls/includes/change_form_buttons.html" %}#}
{#{% endblock %}#}

28 changes: 26 additions & 2 deletions src/admin_extra_urls/templates/admin_extra_urls/change_form.html
@@ -1,7 +1,31 @@
{% extends "admin/change_form.html" %}
{% load i18n static admin_list admin_urls %}
{% extends "admin/change_form.html" %}{% load i18n static admin_list admin_urls %}
{% block extrastyle %}{{ block.super }}<style>
.object-tools a.extra-link.disabled {
background: #ece8e8;
cursor: not-allowed;
}
</style>{% endblock %}

{% block object-tools-items %}
{{ block.super }}
{% include "admin_extra_urls/includes/change_form_buttons.html" %}
{% endblock %}
{% block admin_change_form_document_ready %}{{ block.super }}

<script>
(function ($) {
window.AdminExtraUrl = {update: function(){
var changes = $mainForm.serialize()
if (changes !== $mainForm.data('serialized')) {
$('.object-tools').find('a.auto-disable').addClass('disabled');
} else {
$('.object-tools').find('a.auto-disable').removeClass('disabled');
}
}
}
var $mainForm = $('#{{ opts.model_name }}_form');
$mainForm.data('serialized', $mainForm.serialize());
$mainForm.on('change input', AdminExtraUrl.update);
})(django.jQuery)
</script>
{% endblock %}

0 comments on commit 0a8c02e

Please sign in to comment.