mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Initial push to public repo
This commit is contained in:
0
netbox/utilities/__init__.py
Normal file
0
netbox/utilities/__init__.py
Normal file
6
netbox/utilities/api.py
Normal file
6
netbox/utilities/api.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
|
||||
class ServiceUnavailable(APIException):
|
||||
status_code = 503
|
||||
default_detail = "Service temporarily unavailable, please try again later."
|
7
netbox/utilities/context_processors.py
Normal file
7
netbox/utilities/context_processors.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.conf import settings as django_settings
|
||||
|
||||
|
||||
def settings(request):
|
||||
return {
|
||||
'settings': django_settings,
|
||||
}
|
29
netbox/utilities/error_handlers.py
Normal file
29
netbox/utilities/error_handlers.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from django.contrib import messages
|
||||
|
||||
|
||||
def handle_protectederror(obj, request, e):
|
||||
"""
|
||||
Generate a user-friendly error message in response to a ProtectedError exception.
|
||||
"""
|
||||
dependent_objects = e[1]
|
||||
try:
|
||||
dep_class = dependent_objects[0]._meta.verbose_name_plural
|
||||
except IndexError:
|
||||
raise e
|
||||
|
||||
# Handle multiple triggering objects
|
||||
if type(obj) in (list, tuple):
|
||||
messages.error(request, "Unable to delete the requested {}. The following dependent {} were found: {}".format(
|
||||
obj[0]._meta.verbose_name_plural,
|
||||
dep_class,
|
||||
', '.join([str(o) for o in dependent_objects])
|
||||
))
|
||||
|
||||
# Handle a single triggering object
|
||||
else:
|
||||
messages.error(request, "Unable to delete {} {}. The following dependent {} were found: {}".format(
|
||||
obj._meta.verbose_name,
|
||||
obj,
|
||||
dep_class,
|
||||
', '.join([str(o) for o in dependent_objects])
|
||||
))
|
14
netbox/utilities/fields.py
Normal file
14
netbox/utilities/fields.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class NullableCharField(models.CharField):
|
||||
description = "Stores empty values as NULL rather than ''"
|
||||
#__metaclass__ = models.SubfieldBase
|
||||
|
||||
def to_python(self, value):
|
||||
if isinstance(value, models.CharField):
|
||||
return value
|
||||
return value or ''
|
||||
|
||||
def get_prep_value(self, value):
|
||||
return value or None
|
248
netbox/utilities/forms.py
Normal file
248
netbox/utilities/forms.py
Normal file
@@ -0,0 +1,248 @@
|
||||
import re
|
||||
|
||||
from django import forms
|
||||
from django.core.urlresolvers import reverse_lazy
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
|
||||
EXPANSION_PATTERN = '\[(\d+-\d+)\]'
|
||||
|
||||
|
||||
def expand_pattern(string):
|
||||
"""
|
||||
Expand a numeric pattern into a list of strings. Examples:
|
||||
'ge-0/0/[0-3]' => ['ge-0/0/0', 'ge-0/0/1', 'ge-0/0/2', 'ge-0/0/3']
|
||||
'xe-0/[0-3]/[0-7]' => ['xe-0/0/0', 'xe-0/0/1', 'xe-0/0/2', ... 'xe-0/3/5', 'xe-0/3/6', 'xe-0/3/7']
|
||||
"""
|
||||
lead, pattern, remnant = re.split(EXPANSION_PATTERN, string, maxsplit=1)
|
||||
x, y = pattern.split('-')
|
||||
for i in range(int(x), int(y) + 1):
|
||||
if remnant:
|
||||
for string in expand_pattern(remnant):
|
||||
yield "{}{}{}".format(lead, i, string)
|
||||
else:
|
||||
yield "{}{}".format(lead, i)
|
||||
|
||||
|
||||
#
|
||||
# Widgets
|
||||
#
|
||||
|
||||
class SmallTextarea(forms.Textarea):
|
||||
pass
|
||||
|
||||
|
||||
class SelectWithDisabled(forms.Select):
|
||||
"""
|
||||
Modified the stock Select widget to accept choices using a dict() for a label. The dict for each option must include
|
||||
'label' (string) and 'disabled' (boolean).
|
||||
"""
|
||||
|
||||
def render_option(self, selected_choices, option_value, option_label):
|
||||
|
||||
# Determine if option has been selected
|
||||
option_value = force_text(option_value)
|
||||
if option_value in selected_choices:
|
||||
selected_html = mark_safe(' selected="selected"')
|
||||
if not self.allow_multiple_selected:
|
||||
# Only allow for a single selection.
|
||||
selected_choices.remove(option_value)
|
||||
else:
|
||||
selected_html = ''
|
||||
|
||||
# Determine if option has been disabled
|
||||
option_disabled = False
|
||||
exempt_value = force_text(self.attrs.get('exempt', None))
|
||||
if isinstance(option_label, dict):
|
||||
option_disabled = option_label['disabled'] if option_value != exempt_value else False
|
||||
option_label = option_label['label']
|
||||
disabled_html = ' disabled="disabled"' if option_disabled else ''
|
||||
|
||||
return format_html('<option value="{}"{}{}>{}</option>',
|
||||
option_value,
|
||||
selected_html,
|
||||
disabled_html,
|
||||
force_text(option_label))
|
||||
|
||||
|
||||
class APISelect(SelectWithDisabled):
|
||||
"""
|
||||
A select widget populated via an API call
|
||||
|
||||
:param api_url: API URL
|
||||
:param display_field: (Optional) Field to display for child in selection list. Defaults to `name`.
|
||||
:param disabled_indicator: (Optional) Mark option as disabled if this field equates true.
|
||||
"""
|
||||
|
||||
def __init__(self, api_url, display_field=None, disabled_indicator=None, *args, **kwargs):
|
||||
|
||||
super(APISelect, self).__init__(*args, **kwargs)
|
||||
|
||||
self.attrs['class'] = 'api-select'
|
||||
self.attrs['api-url'] = api_url
|
||||
if display_field:
|
||||
self.attrs['display-field'] = display_field
|
||||
if disabled_indicator:
|
||||
self.attrs['disabled-indicator'] = disabled_indicator
|
||||
|
||||
|
||||
class Livesearch(forms.TextInput):
|
||||
"""
|
||||
A text widget that carries a few extra bits of data for use in AJAX-powered autocomplete search
|
||||
|
||||
:param query_key: The name of the parameter to query against
|
||||
:param query_url: The name of the API URL to query
|
||||
:param field_to_update: The name of the "real" form field whose value is being set
|
||||
:param obj_label: The field to use as the option label (optional)
|
||||
"""
|
||||
|
||||
def __init__(self, query_key, query_url, field_to_update, obj_label=None, *args, **kwargs):
|
||||
|
||||
super(Livesearch, self).__init__(*args, **kwargs)
|
||||
|
||||
self.attrs = {
|
||||
'data-key': query_key,
|
||||
'data-source': reverse_lazy(query_url),
|
||||
'data-field': field_to_update,
|
||||
}
|
||||
|
||||
if obj_label:
|
||||
self.attrs['data-label'] = obj_label
|
||||
|
||||
|
||||
#
|
||||
# Form fields
|
||||
#
|
||||
|
||||
class CSVDataField(forms.CharField):
|
||||
"""
|
||||
A field for comma-separated values (CSV)
|
||||
"""
|
||||
csv_form = None
|
||||
|
||||
def __init__(self, csv_form, *args, **kwargs):
|
||||
self.csv_form = csv_form
|
||||
self.columns = self.csv_form().fields.keys()
|
||||
self.widget = forms.Textarea
|
||||
super(CSVDataField, self).__init__(*args, **kwargs)
|
||||
self.strip = False
|
||||
if not self.label:
|
||||
self.label = 'CSV Data'
|
||||
if not self.help_text:
|
||||
self.help_text = 'Enter one line per record in CSV format.'
|
||||
|
||||
def to_python(self, value):
|
||||
# Return a list of dictionaries, each representing an individual record
|
||||
records = []
|
||||
for i, row in enumerate(value.split('\n'), start=1):
|
||||
if row.strip():
|
||||
values = row.strip().split(',')
|
||||
if len(values) < len(self.columns):
|
||||
raise forms.ValidationError("Line {}: Field(s) missing (found {}; expected {})"
|
||||
.format(i, len(values), len(self.columns)))
|
||||
elif len(values) > len(self.columns):
|
||||
raise forms.ValidationError("Line {}: Too many fields (found {}; expected {})"
|
||||
.format(i, len(values), len(self.columns)))
|
||||
record = dict(zip(self.columns, values))
|
||||
records.append(record)
|
||||
return records
|
||||
|
||||
|
||||
class ExpandableNameField(forms.CharField):
|
||||
"""
|
||||
A field which allows for numeric range expansion
|
||||
Example: 'Gi0/[1-3]' => ['Gi0/1', 'Gi0/2', 'Gi0/3']
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ExpandableNameField, self).__init__(*args, **kwargs)
|
||||
if not self.help_text:
|
||||
self.help_text = 'Numeric ranges are supported for bulk creation.'
|
||||
|
||||
def to_python(self, value):
|
||||
if re.search(EXPANSION_PATTERN, value):
|
||||
return list(expand_pattern(value))
|
||||
return [value]
|
||||
|
||||
|
||||
class CommentField(forms.CharField):
|
||||
"""
|
||||
A textarea with support for GitHub-Flavored Markdown. Exists mostly just to add a standard help_text.
|
||||
"""
|
||||
widget = forms.Textarea
|
||||
# TODO: Port GFM syntax cheat sheet to internal documentation
|
||||
default_helptext = '<i class="fa fa-info-circle"></i> '\
|
||||
'<a href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet" target="_blank">'\
|
||||
'GitHub-Flavored Markdown</a> syntax is supported'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
required = kwargs.pop('required', False)
|
||||
help_text = kwargs.pop('help_text', self.default_helptext)
|
||||
super(CommentField, self).__init__(required=required, help_text=help_text, *args, **kwargs)
|
||||
|
||||
|
||||
class FlexibleModelChoiceField(forms.ModelChoiceField):
|
||||
"""
|
||||
Allow a model to be reference by either '{ID}' or the field specified by `to_field_name`.
|
||||
"""
|
||||
def to_python(self, value):
|
||||
if value in self.empty_values:
|
||||
return None
|
||||
try:
|
||||
if not self.to_field_name:
|
||||
key = 'pk'
|
||||
elif re.match('^\{\d+\}$', value):
|
||||
key = 'pk'
|
||||
value = value.strip('{}')
|
||||
else:
|
||||
key = self.to_field_name
|
||||
value = self.queryset.get(**{key: value})
|
||||
except (ValueError, TypeError, self.queryset.model.DoesNotExist):
|
||||
raise forms.ValidationError(self.error_messages['invalid_choice'], code='invalid_choice')
|
||||
return value
|
||||
|
||||
|
||||
#
|
||||
# Forms
|
||||
#
|
||||
|
||||
class BootstrapMixin(forms.BaseForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(BootstrapMixin, self).__init__(*args, **kwargs)
|
||||
for field_name, field in self.fields.items():
|
||||
if type(field.widget) not in [type(forms.CheckboxInput()), type(forms.RadioSelect())]:
|
||||
try:
|
||||
field.widget.attrs['class'] += ' form-control'
|
||||
except KeyError:
|
||||
field.widget.attrs['class'] = 'form-control'
|
||||
if field.required:
|
||||
field.widget.attrs['required'] = 'required'
|
||||
field.widget.attrs['placeholder'] = field.label
|
||||
|
||||
|
||||
class ConfirmationForm(forms.Form, BootstrapMixin):
|
||||
confirm = forms.BooleanField(required=True)
|
||||
|
||||
|
||||
class BulkImportForm(forms.Form):
|
||||
|
||||
def clean(self):
|
||||
records = self.cleaned_data.get('csv')
|
||||
if not records:
|
||||
return
|
||||
|
||||
obj_list = []
|
||||
|
||||
for i, record in enumerate(records, start=1):
|
||||
obj_form = self.fields['csv'].csv_form(data=record)
|
||||
if obj_form.is_valid():
|
||||
obj = obj_form.save(commit=False)
|
||||
obj_list.append(obj)
|
||||
else:
|
||||
for field, errors in obj_form.errors.items():
|
||||
for e in errors:
|
||||
self.add_error('csv', "Record {} ({}): {}".format(i, field, e))
|
||||
|
||||
self.cleaned_data['csv'] = obj_list
|
15
netbox/utilities/middleware.py
Normal file
15
netbox/utilities/middleware.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
LOGIN_REQUIRED = getattr(settings, 'LOGIN_REQUIRED', False)
|
||||
|
||||
|
||||
class LoginRequiredMiddleware:
|
||||
"""
|
||||
If LOGIN_REQUIRED is True, redirect all non-authenticated users to the login page.
|
||||
"""
|
||||
def process_request(self, request):
|
||||
if LOGIN_REQUIRED and not request.user.is_authenticated():
|
||||
if request.path_info != settings.LOGIN_URL:
|
||||
return HttpResponseRedirect(settings.LOGIN_URL)
|
14
netbox/utilities/migrations/0001_initial.py
Normal file
14
netbox/utilities/migrations/0001_initial.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.1 on 2016-02-29 18:50
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
0
netbox/utilities/migrations/__init__.py
Normal file
0
netbox/utilities/migrations/__init__.py
Normal file
3
netbox/utilities/models.py
Normal file
3
netbox/utilities/models.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
30
netbox/utilities/paginator.py
Normal file
30
netbox/utilities/paginator.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from django.core.paginator import Paginator, Page
|
||||
|
||||
|
||||
class EnhancedPaginator(Paginator):
|
||||
|
||||
def _get_page(self, *args, **kwargs):
|
||||
return EnhancedPage(*args, **kwargs)
|
||||
|
||||
|
||||
class EnhancedPage(Page):
|
||||
|
||||
def smart_pages(self):
|
||||
"""
|
||||
Instead of every page, return only first, last, and nearby pages (taken from
|
||||
https://www.technovelty.org/web/skipping-pages-with-djangocorepaginator.html).
|
||||
"""
|
||||
n = self.number
|
||||
last_page = self.paginator.num_pages
|
||||
|
||||
# Determine the page numbers to display
|
||||
pages_wanted = [1, 2, n-2, n-1, n, n+1, n+2, last_page-1, last_page]
|
||||
pages_to_show = set(self.paginator.page_range).intersection(pages_wanted)
|
||||
pages_to_show = sorted(pages_to_show)
|
||||
|
||||
# Insert skip markers
|
||||
skip_pages = [x[1] for x in zip(pages_to_show[:-1], pages_to_show[1:]) if (x[1] - x[0] != 1)]
|
||||
for i in skip_pages:
|
||||
pages_to_show.insert(pages_to_show.index(i), False)
|
||||
|
||||
return pages_to_show
|
0
netbox/utilities/templatetags/__init__.py
Normal file
0
netbox/utilities/templatetags/__init__.py
Normal file
35
netbox/utilities/templatetags/form_helpers.py
Normal file
35
netbox/utilities/templatetags/form_helpers.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from django import template
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.inclusion_tag('utilities/render_field.html')
|
||||
def render_field(field):
|
||||
"""
|
||||
Render a single form field from template
|
||||
"""
|
||||
return {
|
||||
'field': field,
|
||||
}
|
||||
|
||||
|
||||
@register.inclusion_tag('utilities/render_form.html')
|
||||
def render_form(form):
|
||||
"""
|
||||
Render an entire form from template
|
||||
"""
|
||||
return {
|
||||
'form': form,
|
||||
}
|
||||
|
||||
|
||||
@register.filter(name='widget_type')
|
||||
def widget_type(field):
|
||||
"""
|
||||
Return the widget type
|
||||
"""
|
||||
try:
|
||||
return field.field.widget.__class__.__name__.lower()
|
||||
except AttributeError:
|
||||
return None
|
71
netbox/utilities/templatetags/helpers.py
Normal file
71
netbox/utilities/templatetags/helpers.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from markdown import markdown
|
||||
|
||||
from django import template
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
#
|
||||
# Filters
|
||||
#
|
||||
|
||||
@register.filter(name='oneline')
|
||||
def oneline(value):
|
||||
"""
|
||||
Replace each line break with a single space
|
||||
"""
|
||||
return value.replace('\n', ' ')
|
||||
|
||||
|
||||
@register.filter(name='getlist')
|
||||
def getlist(value, arg):
|
||||
"""
|
||||
Return all values of a QueryDict key
|
||||
"""
|
||||
return value.getlist(arg)
|
||||
|
||||
|
||||
@register.filter(name='gfm', is_safe=True)
|
||||
def gfm(value):
|
||||
"""
|
||||
Render text as GitHub-Flavored Markdown
|
||||
"""
|
||||
html = markdown(value, extensions=['mdx_gfm'])
|
||||
return mark_safe(html)
|
||||
|
||||
|
||||
#
|
||||
# Tags
|
||||
#
|
||||
|
||||
@register.simple_tag(name='querystring_toggle')
|
||||
def querystring_toggle(request, multi=True, page_key='page', **kwargs):
|
||||
"""
|
||||
Add or remove a parameter in the HTTP GET query string
|
||||
"""
|
||||
new_querydict = request.GET.copy()
|
||||
|
||||
# Remove page number from querystring
|
||||
try:
|
||||
new_querydict.pop(page_key)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# Add/toggle parameters
|
||||
for k, v in kwargs.items():
|
||||
values = new_querydict.getlist(k)
|
||||
if k in new_querydict and v in values:
|
||||
values.remove(v)
|
||||
new_querydict.setlist(k, values)
|
||||
elif not multi:
|
||||
new_querydict[k] = v
|
||||
else:
|
||||
new_querydict.update({k: v})
|
||||
|
||||
querystring = new_querydict.urlencode()
|
||||
if querystring:
|
||||
return '?' + querystring
|
||||
else:
|
||||
return ''
|
178
netbox/utilities/views.py
Normal file
178
netbox/utilities/views.py
Normal file
@@ -0,0 +1,178 @@
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.admin.views.decorators import staff_member_required
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import transaction, IntegrityError
|
||||
from django.db.models import ProtectedError
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.generic import View
|
||||
|
||||
from django_tables2 import RequestConfig
|
||||
|
||||
from .error_handlers import handle_protectederror
|
||||
from .paginator import EnhancedPaginator
|
||||
from extras.models import ExportTemplate
|
||||
|
||||
|
||||
class ObjectListView(View):
|
||||
queryset = None
|
||||
filter = None
|
||||
filter_form = None
|
||||
table = None
|
||||
template_name = None
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
object_ct = ContentType.objects.get_for_model(self.queryset.model)
|
||||
|
||||
if self.filter:
|
||||
self.queryset = self.filter(request.GET, self.queryset).qs
|
||||
|
||||
# Export
|
||||
if request.GET.get('export'):
|
||||
et = get_object_or_404(ExportTemplate, content_type=object_ct, name=request.GET.get('export'))
|
||||
response = et.to_response(context_dict={'queryset': self.queryset},
|
||||
filename='netbox_r{}'.format(self.queryset.model._meta.verbose_name_plural))
|
||||
return response
|
||||
|
||||
table = self.table(self.queryset)
|
||||
RequestConfig(request, paginate={'per_page': settings.PAGINATE_COUNT, 'klass': EnhancedPaginator})\
|
||||
.configure(table)
|
||||
|
||||
export_templates = ExportTemplate.objects.filter(content_type=object_ct)
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'table': table,
|
||||
'filter_form': self.filter_form(request.GET, label_suffix='') if self.filter_form else None,
|
||||
'export_templates': export_templates,
|
||||
})
|
||||
|
||||
|
||||
class BulkImportView(View):
|
||||
form = None
|
||||
table = None
|
||||
template_name = None
|
||||
obj_list_url = None
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'form': self.form(),
|
||||
'obj_list_url': self.obj_list_url,
|
||||
})
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
form = self.form(request.POST)
|
||||
if form.is_valid():
|
||||
new_objs = []
|
||||
try:
|
||||
with transaction.atomic():
|
||||
for obj in form.cleaned_data['csv']:
|
||||
self.save_obj(obj)
|
||||
new_objs.append(obj)
|
||||
|
||||
obj_table = self.table(new_objs)
|
||||
messages.success(request, "Imported {} objects".format(len(new_objs)))
|
||||
|
||||
return render(request, "import_success.html", {
|
||||
'table': obj_table,
|
||||
})
|
||||
|
||||
except IntegrityError as e:
|
||||
form.add_error('csv', "Record {}: {}".format(len(new_objs) + 1, e.__cause__))
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'form': form,
|
||||
'obj_list_url': self.obj_list_url,
|
||||
})
|
||||
|
||||
def save_obj(self, obj):
|
||||
obj.save()
|
||||
|
||||
|
||||
class BulkEditView(View):
|
||||
cls = None
|
||||
form = None
|
||||
template_name = None
|
||||
redirect_url = None
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return redirect(self.redirect_url)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
if '_apply' in request.POST:
|
||||
form = self.form(request.POST)
|
||||
if form.is_valid():
|
||||
pk_list = [obj.pk for obj in form.cleaned_data['pk']]
|
||||
self.update_objects(pk_list, form)
|
||||
if not form.errors:
|
||||
return redirect(self.redirect_url)
|
||||
|
||||
else:
|
||||
form = self.form(initial={'pk': request.POST.getlist('pk')})
|
||||
|
||||
selected_objects = self.cls.objects.filter(pk__in=request.POST.getlist('pk'))
|
||||
if not selected_objects:
|
||||
messages.warning(request, "No {} were selected.".format(self.cls._meta.verbose_name_plural))
|
||||
return redirect(self.redirect_url)
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'form': form,
|
||||
'selected_objects': selected_objects,
|
||||
'cancel_url': reverse(self.redirect_url),
|
||||
})
|
||||
|
||||
def update_objects(self, obj_list, form):
|
||||
"""
|
||||
This method provides the update logic (must be overridden by subclasses).
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class BulkDeleteView(View):
|
||||
cls = None
|
||||
form = None
|
||||
template_name = None
|
||||
redirect_url = None
|
||||
|
||||
@method_decorator(staff_member_required)
|
||||
def dispatch(self, *args, **kwargs):
|
||||
return super(BulkDeleteView, self).dispatch(*args, **kwargs)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return redirect(self.redirect_url)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if '_confirm' in request.POST:
|
||||
form = self.form(request.POST)
|
||||
if form.is_valid():
|
||||
|
||||
# Delete objects
|
||||
objects_to_delete = self.cls.objects.filter(pk__in=[v.id for v in form.cleaned_data['pk']])
|
||||
try:
|
||||
deleted_count = objects_to_delete.count()
|
||||
objects_to_delete.delete()
|
||||
except ProtectedError, e:
|
||||
handle_protectederror(list(objects_to_delete), request, e)
|
||||
return redirect(self.redirect_url)
|
||||
|
||||
messages.success(request, "Deleted {} {}".format(deleted_count, self.cls._meta.verbose_name_plural))
|
||||
return redirect(self.redirect_url)
|
||||
|
||||
else:
|
||||
form = self.form(initial={'pk': request.POST.getlist('pk')})
|
||||
|
||||
selected_objects = self.cls.objects.filter(pk__in=form.initial.get('pk'))
|
||||
if not selected_objects:
|
||||
messages.warning(request, "No {} were selected for deletion.".format(self.cls._meta.verbose_name_plural))
|
||||
return redirect(self.redirect_url)
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'form': form,
|
||||
'selected_objects': selected_objects,
|
||||
'cancel_url': reverse(self.redirect_url),
|
||||
})
|
Reference in New Issue
Block a user