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.3'
Browse files Browse the repository at this point in the history
* release/3.3:
  updates
  • Loading branch information
saxix committed Feb 24, 2021
2 parents 0a8c02e + 64b60fb commit 1b452bc
Show file tree
Hide file tree
Showing 22 changed files with 223 additions and 113 deletions.
8 changes: 8 additions & 0 deletions CHANGES
@@ -1,3 +1,11 @@
Release 3.3
-----------
* deprecate action() new `button()` decorator - action() will be removed in future releases
* new `href()` decorator
* removed deprecated `link()` decorator
* new colored buttons


Release 3.2
-----------
* Code refactoring
Expand Down
7 changes: 7 additions & 0 deletions Makefile
Expand Up @@ -21,6 +21,13 @@ lint:
@flake8 src/
@isort src/

release:
tox
rm -fr dist/
./setup.py sdist
#PACKAGE_NAME=django-admin-extra-buttons ./setup.py sdist
twine upload dist/


.PHONY: build docs

Expand Down
19 changes: 16 additions & 3 deletions README.rst
Expand Up @@ -10,8 +10,7 @@ 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 point to external urls.
- ``ChangeListButton()`` to add button that point to external urls.
- ``href()`` to add button that point to external urls.



Expand Down Expand Up @@ -41,9 +40,23 @@ How to use it
from admin_extra_urls import api as extras
class MyModelModelAdmin(extras.ExtraUrlMixin, admin.ModelAdmin):
extra_buttons = [extras.ChangeFormButton('/{original.pk}/'),]
actions = ['smart_action']
@extras.href(label='Search On Google', 'http://www.google.com?q={target}') # /admin/myapp/mymodel/update_all/
def search_on_google(self, button):
# this is called by the template engine just before rendering the button
# `context` is the Context instance in the template
if 'original' in button.context:
obj = button.context['original']
return {'target': obj.name}
else:
button.visible = False
@extras.href()
def search_on_bing(self, button):
return 'http://www.bing.com?q=target'
@extras.action() # /admin/myapp/mymodel/update_all/
def consolidate(self, request):
...
Expand Down
6 changes: 3 additions & 3 deletions setup.py
Expand Up @@ -11,14 +11,15 @@
ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__)))
init = os.path.join(ROOT, 'src', 'admin_extra_urls', '__init__.py')


_version_re = re.compile(r'__version__\s+=\s+(.*)')
_name_re = re.compile(r'NAME\s+=\s+(.*)')

with open(init, 'rb') as f:
content = f.read().decode('utf-8')
version = str(ast.literal_eval(_version_re.search(content).group(1)))
name = str(ast.literal_eval(_name_re.search(content).group(1)))
name = os.getenv('PACKAGE_NAME',
str(ast.literal_eval(_name_re.search(content).group(1))))


def read(*parts):
here = os.path.abspath(os.path.dirname(__file__))
Expand Down Expand Up @@ -49,7 +50,6 @@ def run_tests(self):
if 'test' in sys.argv:
setup_requires += tests_require


setup(
name=name,
version=version,
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.2"
VERSION = __version__ = "3.3"
__author__ = 'sax'
4 changes: 2 additions & 2 deletions src/admin_extra_urls/api.py
@@ -1,3 +1,3 @@
from .config import ButtonAction, ChangeFormButton, ChangeListButton # noqa: F401
from .decorators import action, link, try_catch # noqa: F401
# from .config import ButtonAction # noqa: F401
from .decorators import action, button, href, try_catch # noqa: F401
from .mixins import ExtraUrlMixin # noqa: F401
47 changes: 26 additions & 21 deletions src/admin_extra_urls/config.py
@@ -1,14 +1,16 @@
from django.contrib.admin.templatetags.admin_urls import admin_urlname
from django.urls import NoReverseMatch, reverse
from django.urls import reverse

from admin_extra_urls.templatetags.extra_urls import get_preserved_filters
from admin_extra_urls.utils import get_preserved_filters, safe

empty = object()


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

Expand All @@ -20,6 +22,7 @@ def __init__(self, path, *, label=None, icon='', permission=None,
self._bound = False
self.details = details
self.urls = urls
self.modeladmin = modeladmin

def bind(self, context):
self.context = context
Expand All @@ -28,7 +31,7 @@ def bind(self, context):
user = request.user
self.querystring = get_preserved_filters(request)
if callable(self._visible):
self.visible = self._visible(obj)
self.visible = safe(self._visible, obj)
else:
self.visible = self._visible

Expand All @@ -41,33 +44,35 @@ def bind(self, context):


class ButtonHREF(Button):
def __init__(self, func, *, path=None, label=None, icon='', permission=None,
css_class="btn btn-success", order=999, visible=empty,
modeladmin=None, details=True, html_attrs=None):
self.func = func
self.html_attrs = html_attrs
self.callback_paramenter = None
super().__init__(path=path, label=label, icon=icon, permission=permission,
css_class=css_class, order=order, visible=visible,
modeladmin=modeladmin, details=details)

def url(self):
try:
base_url = reverse(self.path)
except NoReverseMatch:
try:
base_url = self.path.format(**self.context.flatten())
except KeyError as e:
base_url = str(e)
self.label = base_url
return "%s?%s" % (base_url, self.querystring)


class ChangeFormButton(ButtonHREF):
details = True
def __repr__(self):
return f"<ButtonHREF {self.label} {self.func.__name__}>"

def bind(self, context):
super().bind(context)
self.callback_paramenter = self.func(self)

class ChangeListButton(ButtonHREF):
details = False
def url(self):
if isinstance(self.callback_paramenter, dict):
return self.path.format(**self.callback_paramenter)
else:
return self.callback_paramenter


class ButtonAction(Button):
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.method = func.__name__

def url(self):
Expand Down
38 changes: 30 additions & 8 deletions src/admin_extra_urls/decorators.py
Expand Up @@ -7,12 +7,11 @@
from django.urls import reverse
from django.utils.http import urlencode

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


def try_catch(f):

@wraps(f)
def _inner(modeladmin, request, *args, **kwargs):
try:
Expand All @@ -25,8 +24,8 @@ def _inner(modeladmin, request, *args, **kwargs):
return _inner


def action(path=None, label=None, icon='', permission=None,
css_class="btn btn-success auto-disable", order=999, visible=empty,
def button(path=None, label=None, icon='', permission=None,
css_class="btn-action auto-disable", order=999, visible=empty,
urls=None):
"""
decorator to mark ModelAdmin method.
Expand Down Expand Up @@ -55,6 +54,7 @@ def action(path=None, label=None, icon='', permission=None,

def action_decorator(func):
sig = inspect.signature(func)
# modeladmin = list(sig.parameters)[0]
args = list(sig.parameters)[1:2]
if not args == ['request']:
raise ValueError('AdminExtraUrls: error decorating `{0}`. '
Expand Down Expand Up @@ -90,6 +90,7 @@ def _inner(modeladmin, request, *args, **kwargs):
return ret

_inner.action = ButtonAction(func=func,
# modeladmin=modeladmin,
path=path,
label=label or labelize(func.__name__),
icon=icon,
Expand All @@ -105,6 +106,27 @@ def _inner(modeladmin, request, *args, **kwargs):
return action_decorator


def link(**kwargs):
assert 'pk' not in kwargs
return action(**kwargs)
@deprecated(button, "{name}() decorator has been deprecated. Use {updated}() now")
def action(*a, **kw):
return button(*a, **kw)


def href(*, label=None, url=None, icon='', permission=None, html_attrs=None,
css_class="btn-href", order=999, visible=empty):
def action_decorator(func):
def _inner(modeladmin, btn):
return func(modeladmin, btn)

_inner.button = ButtonHREF(func=func,
css_class=css_class,
permission=permission,
visible=visible,
path=url,
html_attrs=html_attrs or {},
icon=icon,
label=label or labelize(func.__name__),
order=order)

return _inner

return action_decorator
25 changes: 13 additions & 12 deletions src/admin_extra_urls/mixins.py
@@ -1,7 +1,7 @@
import inspect
import logging
from collections import namedtuple
from functools import update_wrapper
from functools import partial, update_wrapper

from django.conf import settings
from django.contrib import messages
Expand Down Expand Up @@ -94,8 +94,8 @@ 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()
>>> from admin_extra_urls.api import ExtraUrlMixin, button
>>> @button()
... def revert(self, request, pk):
... context = self.get_common_context(request, pk, MONITORED_FIELDS=MONITORED_FIELDS)
Expand Down Expand Up @@ -128,12 +128,19 @@ def get_common_context(self, request, pk=None, **kwargs):

def get_urls(self):
extra_actions = []
extra_buttons = []
# extra_detail_actions = []
extra_urls = {}
for c in inspect.getmro(self.__class__):
for method_name, method in c.__dict__.items():
if hasattr(method, 'action'):
extra_urls[method_name] = getattr(method, 'action')
if callable(method):
if hasattr(method, 'action'):
extra_urls[method_name] = getattr(method, 'action')
elif hasattr(method, 'button'):
button = getattr(method, 'button')
button.func = partial(method, self)
button.func.__name__ = method_name
extra_buttons.append(button)

original = super().get_urls()

Expand All @@ -149,12 +156,6 @@ def wrapper(*args, **kwargs):
for __, options in extra_urls.items():
# isdetail, method_name, options = entry
info[2] = options.method
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
Expand All @@ -172,7 +173,7 @@ def wrapper(*args, **kwargs):
wrap(getattr(self, options.method)),
name='{}_{}_{}'.format(*info)))

for href in self.extra_buttons:
for href in extra_buttons:
extra_actions.append(href)
self.extra_actions = sorted(extra_actions, key=lambda d: d.order)

Expand Down
29 changes: 17 additions & 12 deletions src/admin_extra_urls/templates/admin_extra_urls/change_form.html
@@ -1,10 +1,6 @@
{% 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 extrastyle %}{{ block.super }}
{% include "admin_extra_urls/includes/styles.html" %}{% endblock %}

{% block object-tools-items %}
{{ block.super }}
Expand All @@ -14,14 +10,23 @@

<script>
(function ($) {
window.AdminExtraUrl = {update: function(){
var changes = $mainForm.serialize()
if (changes !== $mainForm.data('serialized')) {
$('.object-tools').find('a.auto-disable').addClass('disabled');
$('.btn.disable-on-click').click(function (e) {
if ($(this).hasClass("disabled")) {
e.preventDefault();
} else {
$('.object-tools').find('a.auto-disable').removeClass('disabled');
$(this).removeClass('btn-success').addClass('disabled');
}
});

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());
Expand Down
@@ -1,5 +1,6 @@
{% extends "admin/change_list.html" %}
{% load extra_urls i18n static admin_list admin_urls %}
{% extends "admin/change_list.html" %}{% load extra_urls %}
{% block extrastyle %}{{ block.super }}
{% include "admin_extra_urls/includes/styles.html" %}{% endblock %}

{% block object-tools-items %}
{{ block.super }}
Expand Down
@@ -0,0 +1 @@
{% for name, value in options.html_attrs.items %}{% if value is not False %} {{ name }}{% if value is not True %}="{{ value|stringformat:'s' }}"{% endif %}{% endif %}{% endfor %}
@@ -1,4 +1,5 @@
{% spaceless %}<li>
<a id="btn-{{ options.method }}" href="{{ options.url }}{% if is_popup %}&_popup=1{% endif %}"
{% include "admin_extra_urls/includes/attrs.html" %}
class="btn extra-link {{ options.css_class }} {{ options.method_name }}">{% if icon %}<i class="{{ options.icon }}"></i>&nbsp;{% endif %}{{ options.label }}</a>
</li>{% endspaceless %}

0 comments on commit 1b452bc

Please sign in to comment.