Improved gran permissions support for systemusers
This commit is contained in:
parent
5c10c39157
commit
c5140ccbae
8
TODO.md
8
TODO.md
|
@ -346,10 +346,12 @@ TODO mount the filesystem with "nosuid" option
|
||||||
|
|
||||||
# Deprecate restart/start/stop services (do touch wsgi.py and fuck celery)
|
# Deprecate restart/start/stop services (do touch wsgi.py and fuck celery)
|
||||||
# orchestrate async stdout stderr (inspired on pangea managemengt commands)
|
# orchestrate async stdout stderr (inspired on pangea managemengt commands)
|
||||||
# orchestra-beat support for uwsgi cron
|
orchestra-beat support for uwsgi cron
|
||||||
|
|
||||||
# message.log if len() == 1: return changeform
|
|
||||||
|
|
||||||
make django admin taskstate uncollapse fucking traceback, ( if exists ?)
|
make django admin taskstate uncollapse fucking traceback, ( if exists ?)
|
||||||
|
|
||||||
# form for custom message on admin save "comment & save"?
|
# form for custom message on admin save "comment & save"?
|
||||||
|
|
||||||
|
# backend.context and backned.instance provided when an action is called? like forms.cleaned_data: do it on manager.generation(backend.context = backend.get_context()) or in backend.__getattr__ ? also backend.head,tail,content switching on manager.generate()?
|
||||||
|
|
||||||
|
# replace return_code by exit_code everywhere
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
This is a simlified clone of [django-mailer](https://github.com/pinax/django-mailer).
|
This is a simplified clone of [django-mailer](https://github.com/pinax/django-mailer).
|
||||||
|
|
||||||
Using `orchestra.contrib.mailer.backends.EmailBackend` as your email backend will have the following effects:
|
Using `orchestra.contrib.mailer.backends.EmailBackend` as your email backend will have the following effects:
|
||||||
* E-mails sent with Django's `send_mass_mail()` will be queued and sent by an out-of-band perioic task.
|
* E-mails sent with Django's `send_mass_mail()` will be queued and sent by an out-of-band perioic task.
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import collections
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
from .backends import ServiceBackend, ServiceController, replace
|
from .backends import ServiceBackend, ServiceController, replace
|
||||||
|
@ -35,19 +36,31 @@ class Operation():
|
||||||
@classmethod
|
@classmethod
|
||||||
def execute(cls, operations, serialize=False, async=None):
|
def execute(cls, operations, serialize=False, async=None):
|
||||||
from . import manager
|
from . import manager
|
||||||
scripts, oserialize = manager.generate(operations)
|
scripts, backend_serialize = manager.generate(operations)
|
||||||
return manager.execute(scripts, serialize=(serialize or oserialize), async=async)
|
return manager.execute(scripts, serialize=(serialize or backend_serialize), async=async)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def execute_action(cls, instance, action):
|
def create_for_action(cls, instances, action):
|
||||||
backends = ServiceBackend.get_backends(instance=instance, action=action)
|
if not isinstance(instances, collections.Iterable):
|
||||||
operations = [cls(backend_cls, instance, action) for backend_cls in backends]
|
instances = [instances]
|
||||||
|
operations = []
|
||||||
|
for instance in instances:
|
||||||
|
backends = ServiceBackend.get_backends(instance=instance, action=action)
|
||||||
|
for backend_cls in backends:
|
||||||
|
operations.append(
|
||||||
|
cls(backend_cls, instance, action)
|
||||||
|
)
|
||||||
|
return operations
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def execute_action(cls, instances, action):
|
||||||
|
""" instances can be an object or an iterable for batch processing """
|
||||||
|
operations = cls.create_for_action(instances, action)
|
||||||
return cls.execute(operations)
|
return cls.execute(operations)
|
||||||
|
|
||||||
def preload_context(self):
|
def preload_context(self):
|
||||||
"""
|
"""
|
||||||
Heuristic
|
Heuristic: Running get_context will prevent most of related objects do not exist errors
|
||||||
Running get_context will prevent most of related objects do not exist errors
|
|
||||||
"""
|
"""
|
||||||
if self.action == self.DELETE:
|
if self.action == self.DELETE:
|
||||||
if hasattr(self.backend, 'get_context'):
|
if hasattr(self.backend, 'get_context'):
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import time
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
|
||||||
|
@ -96,9 +97,22 @@ class Command(BaseCommand):
|
||||||
return
|
return
|
||||||
break
|
break
|
||||||
if not dry:
|
if not dry:
|
||||||
logs = manager.execute(scripts, serialize=serialize)
|
logs = manager.execute(scripts, serialize=serialize, async=True)
|
||||||
for log in logs:
|
running = list(logs)
|
||||||
self.stdout.write(log.stdout)
|
stdout = 0
|
||||||
self.stderr.write(log.stderr)
|
stderr = 0
|
||||||
|
while running:
|
||||||
|
for log in running:
|
||||||
|
cstdout = len(log.stdout)
|
||||||
|
cstderr = len(log.stderr)
|
||||||
|
if cstdout > stdout:
|
||||||
|
self.stdout.write(log.stdout[stdout:])
|
||||||
|
stdout = cstdout
|
||||||
|
if cstderr > stderr:
|
||||||
|
self.stderr.write(log.stderr[stderr:])
|
||||||
|
stderr = cstderr
|
||||||
|
if log.has_finished:
|
||||||
|
running.remove(log)
|
||||||
|
time.sleep(0.1)
|
||||||
for log in logs:
|
for log in logs:
|
||||||
self.stdout.write(' '.join((log.backend, log.state)))
|
self.stdout.write(' '.join((log.backend, log.state)))
|
||||||
|
|
|
@ -123,9 +123,14 @@ def execute(scripts, serialize=False, async=None):
|
||||||
'async': async,
|
'async': async,
|
||||||
}
|
}
|
||||||
log = backend.create_log(*args, **kwargs)
|
log = backend.create_log(*args, **kwargs)
|
||||||
|
# TODO Perform this shit outside of the current transaction in a non-hacky way
|
||||||
|
#t = threading.Thread(target=backend.create_log, args=args, kwargs=kwargs)
|
||||||
|
#t.start()
|
||||||
|
#log = t.join()
|
||||||
|
# End of hack
|
||||||
kwargs['log'] = log
|
kwargs['log'] = log
|
||||||
task = keep_log(backend.execute, log, operations)
|
task = keep_log(backend.execute, log, operations)
|
||||||
logger.debug('%s is going to be executed on %s' % (backend, route.host))
|
logger.debug('%s is going to be executed on %s.' % (backend, route.host))
|
||||||
if serialize:
|
if serialize:
|
||||||
# Execute one backend at a time, no need for threads
|
# Execute one backend at a time, no need for threads
|
||||||
task(*args, **kwargs)
|
task(*args, **kwargs)
|
||||||
|
@ -181,7 +186,7 @@ def collect(instance, action, **kwargs):
|
||||||
if update_fields is not None:
|
if update_fields is not None:
|
||||||
# TODO remove this, django does not execute post_save if update_fields=[]...
|
# TODO remove this, django does not execute post_save if update_fields=[]...
|
||||||
# Maybe open a ticket at Djangoproject ?
|
# Maybe open a ticket at Djangoproject ?
|
||||||
# "update_fileds=[]" is a convention for explicitly executing backend
|
# INITIAL INTENTION: "update_fileds=[]" is a convention for explicitly executing backend
|
||||||
# i.e. account.disable()
|
# i.e. account.disable()
|
||||||
if update_fields != []:
|
if update_fields != []:
|
||||||
execute = False
|
execute = False
|
||||||
|
|
|
@ -92,10 +92,15 @@ class BackendLog(models.Model):
|
||||||
def execution_time(self):
|
def execution_time(self):
|
||||||
return (self.updated_at-self.created_at).total_seconds()
|
return (self.updated_at-self.created_at).total_seconds()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_finished(self):
|
||||||
|
return self.state not in (self.STARTED, self.RECEIVED)
|
||||||
|
|
||||||
def backend_class(self):
|
def backend_class(self):
|
||||||
return ServiceBackend.get_backend(self.backend)
|
return ServiceBackend.get_backend(self.backend)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class BackendOperation(models.Model):
|
class BackendOperation(models.Model):
|
||||||
"""
|
"""
|
||||||
Encapsulates an operation, storing its related object, the action and the backend.
|
Encapsulates an operation, storing its related object, the action and the backend.
|
||||||
|
|
|
@ -28,7 +28,7 @@ class ContractedPlanAdmin(AccountAdminMixin, admin.ModelAdmin):
|
||||||
list_display = ('plan', 'account_link')
|
list_display = ('plan', 'account_link')
|
||||||
list_filter = ('plan__name',)
|
list_filter = ('plan__name',)
|
||||||
list_select_related = ('plan', 'account')
|
list_select_related = ('plan', 'account')
|
||||||
|
search_fields = ('account__username', 'plan__name', 'id')
|
||||||
|
|
||||||
admin.site.register(Plan, PlanAdmin)
|
admin.site.register(Plan, PlanAdmin)
|
||||||
admin.site.register(ContractedPlan, ContractedPlanAdmin)
|
admin.site.register(ContractedPlan, ContractedPlanAdmin)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
from orchestra.core import administration, accounts
|
from orchestra.core import administration, accounts, services
|
||||||
from orchestra.core.translations import ModelTranslation
|
from orchestra.core.translations import ModelTranslation
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,5 +11,6 @@ class PlansConfig(AppConfig):
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from .models import Plan, ContractedPlan
|
from .models import Plan, ContractedPlan
|
||||||
accounts.register(ContractedPlan, icon='ContractedPack.png')
|
accounts.register(ContractedPlan, icon='ContractedPack.png')
|
||||||
|
services.register(ContractedPlan, menu=False, dashboard=False)
|
||||||
administration.register(Plan, icon='Pack.png')
|
administration.register(Plan, icon='Pack.png')
|
||||||
ModelTranslation.register(Plan, ('verbose_name',))
|
ModelTranslation.register(Plan, ('verbose_name',))
|
||||||
|
|
|
@ -68,9 +68,11 @@ class RateQuerySet(models.QuerySet):
|
||||||
class Rate(models.Model):
|
class Rate(models.Model):
|
||||||
STEP_PRICE = 'STEP_PRICE'
|
STEP_PRICE = 'STEP_PRICE'
|
||||||
MATCH_PRICE = 'MATCH_PRICE'
|
MATCH_PRICE = 'MATCH_PRICE'
|
||||||
|
BEST_PRICE = 'BEST_PRICE'
|
||||||
RATE_METHODS = {
|
RATE_METHODS = {
|
||||||
STEP_PRICE: rating.step_price,
|
STEP_PRICE: rating.step_price,
|
||||||
MATCH_PRICE: rating.match_price,
|
MATCH_PRICE: rating.match_price,
|
||||||
|
BEST_PRICE: rating.best_price,
|
||||||
}
|
}
|
||||||
|
|
||||||
service = models.ForeignKey('services.Service', verbose_name=_("service"),
|
service = models.ForeignKey('services.Service', verbose_name=_("service"),
|
||||||
|
|
|
@ -152,3 +152,9 @@ def match_price(rates, metric):
|
||||||
match_price.verbose_name = _("Match price")
|
match_price.verbose_name = _("Match price")
|
||||||
match_price.help_text = _("Only <b>the rate</b> with a) inmediate inferior metric and b) lower price is applied. "
|
match_price.help_text = _("Only <b>the rate</b> with a) inmediate inferior metric and b) lower price is applied. "
|
||||||
"Nominal price will be used when initial block is missing.")
|
"Nominal price will be used when initial block is missing.")
|
||||||
|
|
||||||
|
|
||||||
|
def best_price(rates, metric):
|
||||||
|
pass
|
||||||
|
best_price.verbose_name = _("Best price")
|
||||||
|
best_price.help_text = _("Produces the best possible price given all active rating lines.")
|
||||||
|
|
|
@ -49,7 +49,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
bool(getattr(self, method)(obj))
|
bool(getattr(self, method)(obj))
|
||||||
except Exception as exception:
|
except Exception as exc:
|
||||||
raise ValidationError(format_exception(exc))
|
raise ValidationError(format_exception(exc))
|
||||||
|
|
||||||
def validate_match(self, service):
|
def validate_match(self, service):
|
||||||
|
@ -124,8 +124,8 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
safe_locals = self.get_expression_context(instance)
|
safe_locals = self.get_expression_context(instance)
|
||||||
try:
|
try:
|
||||||
return eval(self.metric, safe_locals)
|
return eval(self.metric, safe_locals)
|
||||||
except Exception as error:
|
except Exception as exc:
|
||||||
raise type(error)("%s on '%s'" %(error, self.service))
|
raise type(exc)("%s on '%s'" %(exc, self.service))
|
||||||
|
|
||||||
def get_order_description(self, instance):
|
def get_order_description(self, instance):
|
||||||
safe_locals = self.get_expression_context(instance)
|
safe_locals = self.get_expression_context(instance)
|
||||||
|
|
|
@ -1,25 +1,56 @@
|
||||||
|
import os
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import messages, admin
|
from django.contrib import messages, admin
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.template.response import TemplateResponse
|
||||||
from django.utils.translation import ungettext, ugettext_lazy as _
|
from django.utils.translation import ungettext, ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin.decorators import action_with_confirmation
|
from orchestra.admin.decorators import action_with_confirmation
|
||||||
from orchestra.contrib.orchestration import Operation
|
from orchestra.contrib.orchestration.middlewares import OperationsMiddleware
|
||||||
|
|
||||||
|
from .forms import GrantPermissionForm
|
||||||
|
|
||||||
|
|
||||||
class GrantPermissionForm(forms.Form):
|
|
||||||
base_path = forms.ChoiceField(label=_("Grant access to"), choices=(('hola', 'hola'),),
|
|
||||||
help_text=_("User will be granted access to this directory."))
|
|
||||||
path_extension = forms.CharField(label='', required=False)
|
|
||||||
read_only = forms.BooleanField(label=_("Read only"), initial=False, required=False,
|
|
||||||
help_text=_("Designates whether the permissions granted will be read-only or read/write."))
|
|
||||||
|
|
||||||
|
|
||||||
@action_with_confirmation(extra_context=dict(form=GrantPermissionForm()))
|
|
||||||
def grant_permission(modeladmin, request, queryset):
|
def grant_permission(modeladmin, request, queryset):
|
||||||
user = queryset.get()
|
account_id = None
|
||||||
log = Operation.execute_action(user, 'grant_permission')
|
for user in queryset:
|
||||||
# TODO
|
account_id = account_id or user.account_id
|
||||||
|
if user.account_id != account_id:
|
||||||
|
messages.error("Users from the same account should be selected.")
|
||||||
|
user = queryset[0]
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = GrantPermissionForm(user, request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
cleaned_data = form.cleaned_data
|
||||||
|
to = os.path.join(cleaned_data['base_path'], cleaned_data['path_extension'])
|
||||||
|
ro = cleaned_data['read_only']
|
||||||
|
for user in queryset:
|
||||||
|
user.grant_to = to
|
||||||
|
user.grant_ro = ro
|
||||||
|
OperationsMiddleware.collect('grant_permission', instance=user)
|
||||||
|
context = {
|
||||||
|
'type': _("read-only") if ro else _("read-write"),
|
||||||
|
'to': to,
|
||||||
|
}
|
||||||
|
msg = _("Granted %(type)s permissions on %(to)s") % context
|
||||||
|
modeladmin.log_change(request, user, msg)
|
||||||
|
return
|
||||||
|
opts = modeladmin.model._meta
|
||||||
|
app_label = opts.app_label
|
||||||
|
context = {
|
||||||
|
'title': _("Grant permission"),
|
||||||
|
'action_name': _("Grant permission"),
|
||||||
|
'action_value': 'grant_permission',
|
||||||
|
'queryset': queryset,
|
||||||
|
'opts': opts,
|
||||||
|
'obj': user,
|
||||||
|
'app_label': app_label,
|
||||||
|
'action_checkbox_name': admin.helpers.ACTION_CHECKBOX_NAME,
|
||||||
|
'form': GrantPermissionForm(user),
|
||||||
|
}
|
||||||
|
return TemplateResponse(request, 'admin/systemusers/systemuser/grant_permission.html',
|
||||||
|
context, current_app=modeladmin.admin_site.name)
|
||||||
grant_permission.url_name = 'grant-permission'
|
grant_permission.url_name = 'grant-permission'
|
||||||
grant_permission.verbose_name = _("Grant permission")
|
grant_permission.verbose_name = _("Grant permission")
|
||||||
|
|
||||||
|
|
|
@ -66,8 +66,16 @@ class UNIXUserBackend(ServiceController):
|
||||||
self.append("rm -fr %(base_home)s" % context)
|
self.append("rm -fr %(base_home)s" % context)
|
||||||
|
|
||||||
def grant_permission(self, user):
|
def grant_permission(self, user):
|
||||||
|
# TODO
|
||||||
context = self.get_context(user)
|
context = self.get_context(user)
|
||||||
# TODO setacl
|
context.update({
|
||||||
|
'to': user.grant_to,
|
||||||
|
'ro': user.grant_ro,
|
||||||
|
})
|
||||||
|
if user.ro:
|
||||||
|
self.append('echo "acl add read permissions for %(user)s to %(to)s"' % context)
|
||||||
|
else:
|
||||||
|
self.append('echo "acl add read-write permissions for %(user)s to %(to)s"' % context)
|
||||||
|
|
||||||
def get_groups(self, user):
|
def get_groups(self, user):
|
||||||
if user.is_main:
|
if user.is_main:
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.utils.translation import ngettext, ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.forms import UserCreationForm, UserChangeForm
|
from orchestra.forms import UserCreationForm, UserChangeForm
|
||||||
|
|
||||||
|
@ -34,6 +35,8 @@ class SystemUserFormMixin(object):
|
||||||
self.fields['directory'].widget = forms.HiddenInput()
|
self.fields['directory'].widget = forms.HiddenInput()
|
||||||
elif self.instance.pk and (self.instance.get_base_home() == self.instance.home):
|
elif self.instance.pk and (self.instance.get_base_home() == self.instance.home):
|
||||||
self.fields['directory'].widget = forms.HiddenInput()
|
self.fields['directory'].widget = forms.HiddenInput()
|
||||||
|
else:
|
||||||
|
self.fields['directory'].widget = forms.TextInput(attrs={'size':'70'})
|
||||||
if not self.instance.pk or not self.instance.is_main:
|
if not self.instance.pk or not self.instance.is_main:
|
||||||
# Some javascript for hidde home/directory inputs when convinient
|
# Some javascript for hidde home/directory inputs when convinient
|
||||||
self.fields['shell'].widget.attrs = {
|
self.fields['shell'].widget.attrs = {
|
||||||
|
@ -74,3 +77,23 @@ class SystemUserCreationForm(SystemUserFormMixin, UserCreationForm):
|
||||||
|
|
||||||
class SystemUserChangeForm(SystemUserFormMixin, UserChangeForm):
|
class SystemUserChangeForm(SystemUserFormMixin, UserChangeForm):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class GrantPermissionForm(forms.Form):
|
||||||
|
base_path = forms.ChoiceField(label=_("Grant access to"), choices=(),
|
||||||
|
help_text=_("User will be granted access to this directory."))
|
||||||
|
path_extension = forms.CharField(label=_("Path extension"), required=False, initial='',
|
||||||
|
widget=forms.TextInput(attrs={'size':'70'}), help_text=_("Relative to chosen home."))
|
||||||
|
read_only = forms.BooleanField(label=_("Read only"), initial=False, required=False,
|
||||||
|
help_text=_("Designates whether the permissions granted will be read-only or read/write."))
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
instance = args[0]
|
||||||
|
super_args = []
|
||||||
|
if len(args) > 1:
|
||||||
|
super_args.append(args[1])
|
||||||
|
super(GrantPermissionForm, self).__init__(*super_args, **kwargs)
|
||||||
|
related_users = type(instance).objects.filter(account=instance.account_id)
|
||||||
|
self.fields['base_path'].choices = (
|
||||||
|
(user.get_base_home(), user.get_base_home()) for user in related_users
|
||||||
|
)
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
{% extends "admin/base_site.html" %}
|
||||||
|
{% load i18n l10n %}
|
||||||
|
{% load url from future %}
|
||||||
|
{% load admin_urls static utils %}
|
||||||
|
|
||||||
|
{% block extrastyle %}
|
||||||
|
{{ block.super }}
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static "orchestra/css/hide-inline-id.css" %}" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
<div class="breadcrumbs">
|
||||||
|
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
|
||||||
|
› <a href="{% url 'admin:app_list' app_label=app_label %}">{{ app_label|capfirst|escape }}</a>
|
||||||
|
› <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
|
||||||
|
{% if obj %}
|
||||||
|
› <a href="{% url opts|admin_urlname:'change' obj.pk %}">{{ obj }}</a>
|
||||||
|
› {{ action_name }}
|
||||||
|
{% elif add %}
|
||||||
|
› <a href="../">{% trans "Add" %} {{ opts.verbose_name }}</a>
|
||||||
|
› {{ action_name }}
|
||||||
|
{% else %}
|
||||||
|
› {{ action_name }} multiple objects
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div>
|
||||||
|
<div style="margin:20px;">
|
||||||
|
Grant permissions to these system users: {% for user in queryset %}{{ user.username }}{% if not forloop.last %},{% endif %}{% endfor %}.
|
||||||
|
<ul>{{ display_objects | unordered_list }}</ul>
|
||||||
|
<form action="" method="post">{% csrf_token %}
|
||||||
|
<fieldset class="module aligned wide">
|
||||||
|
<div class="form-row ">
|
||||||
|
<div class="field-box field-home">
|
||||||
|
{{ form.path_extension.errors }}
|
||||||
|
<label for="{{ form.base_path.id_for_label }}">{{ form.base_path.label }}:</label>
|
||||||
|
{{ form.base_path }}
|
||||||
|
<p class="help">{{ form.base_path.help_text|safe }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="field-box field-home">
|
||||||
|
{{ form.path_extension.errors }}
|
||||||
|
<label for="{{ form.path_extension.id_for_label }}"></label>
|
||||||
|
{{ form.path_extension }}
|
||||||
|
<p class="help">{{ form.path_extension.help_text|safe }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row ">
|
||||||
|
{{ form.read_only }} <label for="{{ form.read_only.id_for_label }}" class="vCheckboxLabel">{{ form.read_only.label }}</label>
|
||||||
|
<p class="help">{{ form.read_only.help_text|safe }}</p>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<div>
|
||||||
|
{% for obj in queryset %}
|
||||||
|
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk|unlocalize }}" />
|
||||||
|
{% endfor %}
|
||||||
|
<input type="hidden" name="action" value="{{ action_value }}" />
|
||||||
|
<input type="hidden" name="post" value="{{ post_value|default:'generic_confirmation' }}" />
|
||||||
|
<input type="submit" value="{{ submit_value|default:_("Save") }}" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
Loading…
Reference in New Issue