Implement datagrid field using djblets datagrid and floppyforms.
Change-Id: I88fefb1da1f3c1c70bfc6805c15f4e3c04fa0b77
This commit is contained in:
parent
28cbf0dd2f
commit
74f5da49f4
131
muranodashboard/datagrids.py
Normal file
131
muranodashboard/datagrids.py
Normal file
@ -0,0 +1,131 @@
|
||||
from djblets.datagrid.grids import Column, DataGrid
|
||||
from models import Node, FakeQuerySet
|
||||
import floppyforms as forms
|
||||
import json
|
||||
import re
|
||||
from django.contrib.auth.models import SiteProfileNotAvailable
|
||||
|
||||
|
||||
class PK(object):
|
||||
def __init__(self, value=0):
|
||||
self.value = value
|
||||
|
||||
def next(self):
|
||||
self.value += 1
|
||||
return self.value
|
||||
|
||||
def current(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class CheckColumn(Column):
|
||||
def render_data(self, item):
|
||||
checked = getattr(item, self.field_name)
|
||||
checked = 'checked="checked"' if checked else ''
|
||||
return '<input type="checkbox" %s/>' % (checked,)
|
||||
|
||||
|
||||
class RadioColumn(Column):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.name = kwargs.get('name', 'default')
|
||||
super(RadioColumn, self).__init__(*args, **kwargs)
|
||||
|
||||
def render_data(self, item):
|
||||
checked = getattr(item, self.field_name)
|
||||
checked = 'checked="checked"' if checked else ''
|
||||
name = 'name="%s"' % (self.name,)
|
||||
return '<input type="radio" %s %s/>' % (name, checked)
|
||||
|
||||
|
||||
class NodeDataGrid(DataGrid):
|
||||
name = Column('Node', sortable=False)
|
||||
is_sync = CheckColumn('Sync')
|
||||
is_primary = RadioColumn('Primary')
|
||||
|
||||
def __init__(self, request, data):
|
||||
self.pk = PK()
|
||||
items = []
|
||||
if type(data) in (str, unicode):
|
||||
# BEWARE, UGLY HACK!!! Data should be list there already!
|
||||
data = json.loads(data)
|
||||
for kwargs in data:
|
||||
items.append(Node(**dict(kwargs.items() +
|
||||
[('id', self.pk.next())])))
|
||||
super(NodeDataGrid, self).__init__(request, FakeQuerySet(
|
||||
Node, items=items), optimize_sorts=False)
|
||||
self.default_sort = []
|
||||
self.default_columns = ['name', 'is_sync', 'is_primary']
|
||||
|
||||
# hack
|
||||
def load_state(self, render_context=None):
|
||||
if self.request.user.is_authenticated():
|
||||
def get_profile():
|
||||
raise SiteProfileNotAvailable
|
||||
setattr(self.request.user, 'get_profile', get_profile)
|
||||
super(NodeDataGrid, self).load_state(render_context)
|
||||
|
||||
|
||||
# def add(self):
|
||||
# id = self.pk.next()
|
||||
# self.queryset.add({'name': 'node%s' % (id,), 'is_sync': False,
|
||||
# 'is_async': False, 'is_primary': False, 'id': id})
|
||||
#
|
||||
# def remove(self):
|
||||
# pass
|
||||
|
||||
|
||||
class DataGridWidget(forms.widgets.Input):
|
||||
template_name = 'data_grid_field.html'
|
||||
|
||||
def get_context(self, name, value, attrs=None):
|
||||
ctx = super(DataGridWidget, self).get_context_data()
|
||||
ctx['data_grid'] = NodeDataGrid(self.request, data=value)
|
||||
return ctx
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
base, match = None, re.match('(.*)_[0-9]+', name)
|
||||
if match:
|
||||
base = match.group(1)
|
||||
if base:
|
||||
pattern = re.compile(base + '_[0-9]+')
|
||||
for key in data:
|
||||
if re.match(pattern, key):
|
||||
return data[key]
|
||||
return super(DataGridWidget, self).value_from_datadict(
|
||||
data, files, name)
|
||||
|
||||
class Media:
|
||||
css = {'all': ('css/datagrid.css',
|
||||
'muranodashboard/css/datagridfield.css')}
|
||||
js = ('js/jquery.gravy.js',
|
||||
'js/datagrid.js',
|
||||
'muranodashboard/js/datagridfield.js')
|
||||
|
||||
|
||||
class DataGridCompound(forms.MultiWidget):
|
||||
def __init__(self, attrs=None):
|
||||
_widgets = (DataGridWidget(),
|
||||
forms.HiddenInput(attrs={'class': 'gridfield-hidden'}))
|
||||
super(DataGridCompound, self).__init__(_widgets, attrs)
|
||||
|
||||
def update_request(self, request):
|
||||
self.widgets[0].request = request
|
||||
|
||||
def decompress(self, value):
|
||||
if value != '':
|
||||
return [json.loads(value), value]
|
||||
else:
|
||||
return [None, None]
|
||||
|
||||
|
||||
class DataGridField(forms.MultiValueField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DataGridField, self).__init__(
|
||||
(forms.CharField(required=False), forms.CharField()),
|
||||
*args, widget=DataGridCompound, **kwargs)
|
||||
|
||||
def update_request(self, request):
|
||||
self.widget.update_request(request)
|
||||
|
||||
def compress(self, data_list):
|
||||
return data_list[1]
|
@ -1,3 +1,78 @@
|
||||
"""
|
||||
Stub file to work around django bug: https://code.djangoproject.com/ticket/7198
|
||||
"""
|
||||
from django.db import models
|
||||
from django.db.models.query import EmptyQuerySet
|
||||
import copy
|
||||
|
||||
|
||||
class FakeQuerySet(EmptyQuerySet):
|
||||
"""Turn a list into a Django QuerySet... kind of."""
|
||||
def __init__(self, model=None, query=None, using=None, items=[]):
|
||||
super(FakeQuerySet, self).__init__(model, query, using)
|
||||
self._result_cache = items
|
||||
|
||||
def __getitem__(self, k):
|
||||
if isinstance(k, slice):
|
||||
obj = self._clone()
|
||||
obj._result_cache = super(FakeQuerySet, self).__getitem__(k)
|
||||
return obj
|
||||
else:
|
||||
return super(FakeQuerySet, self).__getitem__(k)
|
||||
|
||||
def count(self):
|
||||
return len(self)
|
||||
|
||||
def _clone(self, klass=None, setup=False, **kwargs):
|
||||
c = super(FakeQuerySet, self)._clone(klass, setup=setup, **kwargs)
|
||||
c._result_cache = copy.copy(self._result_cache)
|
||||
return c
|
||||
|
||||
def iterator(self):
|
||||
# This slightly odd construction is because we need an empty generator
|
||||
# (it raises StopIteration immediately).
|
||||
yield iter(self._result_cache).next()
|
||||
|
||||
def order_by(self, *fields):
|
||||
obj = self._clone()
|
||||
cache = obj._result_cache
|
||||
for field in fields:
|
||||
reverse = False
|
||||
if field[0] == '-':
|
||||
reverse = True
|
||||
field = field[1:]
|
||||
cache = sorted(cache, None, lambda item: getattr(item, field),
|
||||
reverse=reverse)
|
||||
obj._result_cache = cache
|
||||
return obj
|
||||
|
||||
def distinct(self, *fields):
|
||||
obj = self._clone()
|
||||
return obj
|
||||
|
||||
def values_list(self, *fields, **kwargs):
|
||||
obj = self._clone()
|
||||
cache = []
|
||||
for item in self._result_cache:
|
||||
value = []
|
||||
for field in fields:
|
||||
value.append(getattr(item, field))
|
||||
cache.append(tuple(value))
|
||||
if kwargs.get('flat', False) and len(fields) == 1:
|
||||
cache = [item[0] for item in cache]
|
||||
obj._result_cache = cache
|
||||
return obj
|
||||
|
||||
def add(self, item):
|
||||
self._result_cache.append(item)
|
||||
|
||||
def remove(self):
|
||||
self._result_cache.pop()
|
||||
|
||||
|
||||
class Node(models.Model):
|
||||
id = models.IntegerField(primary_key=True)
|
||||
name = models.CharField(max_length=20)
|
||||
is_primary = models.BooleanField(default=False)
|
||||
is_sync = models.BooleanField(default=False)
|
||||
is_async = models.BooleanField(default=False)
|
||||
|
@ -41,7 +41,7 @@ def get_endpoint(request):
|
||||
def muranoclient(request):
|
||||
endpoint = get_endpoint(request)
|
||||
|
||||
token_id = request.user.token.token['id']
|
||||
token_id = request.user.token.id
|
||||
log.debug('Murano::Client <Url: {0}, '
|
||||
'TokenId: {1}>'.format(endpoint, token_id))
|
||||
|
||||
|
@ -16,6 +16,7 @@ import logging
|
||||
|
||||
import re
|
||||
import ast
|
||||
import json
|
||||
from netaddr import all_matching_cidrs
|
||||
from django import forms
|
||||
from django.core.validators import RegexValidator, validate_ipv4_address
|
||||
@ -26,6 +27,8 @@ from horizon import exceptions, messages
|
||||
from openstack_dashboard.api.nova import novaclient
|
||||
from muranodashboard.panel import api
|
||||
from consts import *
|
||||
from muranodashboard.datagrids import DataGridField
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
CONFIRM_ERR_DICT = {'required': _('Please confirm your password')}
|
||||
@ -354,6 +357,7 @@ class WizardFormMSSQLClusterConfiguration(WizardFormMSSQLConfiguration):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(WizardFormMSSQLClusterConfiguration, self).__init__(*args,
|
||||
**kwargs)
|
||||
# CommonPropertiesExtension.__init__(self)
|
||||
self.fields.insert(3, 'external_ad', forms.BooleanField(
|
||||
label=_('Active Directory is configured '
|
||||
'by the System Administrator'),
|
||||
@ -450,9 +454,26 @@ class WizardMSSQLConfigureAG(forms.Form):
|
||||
help_text=_('Enter an integer value between 1 and 100'))
|
||||
|
||||
|
||||
class WizardMSSQDatagrid(forms.Form):
|
||||
pass
|
||||
class WizardMSSQLDatagrid(forms.Form):
|
||||
nodes = DataGridField(
|
||||
initial=json.dumps(
|
||||
[{'name': 'node1', 'is_sync': True, 'is_primary': True}]
|
||||
))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(WizardMSSQLDatagrid, self).__init__(*args, **kwargs)
|
||||
self.fields['nodes'].update_request(self.initial.get('request'))
|
||||
|
||||
|
||||
# def user_list(request, mode, template_name='sandbox.html'):
|
||||
# if request.method == 'POST':
|
||||
# print request.POST
|
||||
# form = SampleForm(request.POST, request=request)
|
||||
# else:
|
||||
# form = SampleForm(request=request)
|
||||
# ctx = RequestContext(request)
|
||||
# ctx.update({'sampleform': form})
|
||||
# return render_to_response(template_name, ctx)
|
||||
|
||||
class WizardInstanceConfiguration(forms.Form):
|
||||
flavor = forms.ChoiceField(label=_('Instance flavor'))
|
||||
@ -539,5 +560,5 @@ FORMS = [('service_choice', WizardFormServiceType),
|
||||
(MSSQL_NAME, WizardFormMSSQLConfiguration),
|
||||
(MSSQL_CLUSTER_NAME, WizardFormMSSQLClusterConfiguration),
|
||||
('mssql_ag_configuration', WizardMSSQLConfigureAG),
|
||||
('mssql_datagrid', WizardMSSQDatagrid),
|
||||
('mssql_datagrid', WizardMSSQLDatagrid),
|
||||
('instance_configuration', WizardInstanceConfiguration)]
|
||||
|
@ -352,6 +352,7 @@ class DeploymentsView(tables.DataTableView):
|
||||
|
||||
try:
|
||||
environment_name = api.get_environment_name(
|
||||
|
||||
self.request,
|
||||
self.environment_id)
|
||||
context['environment_name'] = environment_name
|
||||
|
@ -14,6 +14,13 @@ if ROOT_PATH not in sys.path:
|
||||
DEBUG = False
|
||||
TEMPLATE_DEBUG = DEBUG
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.abspath(os.path.join(ROOT_PATH, 'dashboard.sqlite'))
|
||||
}
|
||||
}
|
||||
|
||||
SITE_BRANDING = 'OpenStack Dashboard'
|
||||
|
||||
LOGIN_URL = '/auth/login/'
|
||||
@ -122,6 +129,10 @@ INSTALLED_APPS = (
|
||||
'openstack_dashboard.dashboards.admin',
|
||||
'openstack_dashboard.dashboards.settings',
|
||||
'openstack_auth',
|
||||
'djblets',
|
||||
'djblets.datagrid',
|
||||
'djblets.util',
|
||||
'floppyforms',
|
||||
'muranodashboard'
|
||||
)
|
||||
|
||||
|
@ -0,0 +1,3 @@
|
||||
.datagrid-wrapper {
|
||||
width: 100px;
|
||||
}
|
86
muranodashboard/static/muranodashboard/js/datagridfield.js
Normal file
86
muranodashboard/static/muranodashboard/js/datagridfield.js
Normal file
@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Created with PyCharm.
|
||||
* User: tsufiev
|
||||
* Date: 30.07.13
|
||||
* Time: 20:27
|
||||
* To change this template use File | Settings | File Templates.
|
||||
*/
|
||||
$(function() {
|
||||
var MAX_NODES = 5,
|
||||
MIN_NODES = 1;
|
||||
|
||||
function trimLabel(label) {
|
||||
return $.trim(label.replace(/(\r\n|\r|\n|↵)/gm, ''));
|
||||
}
|
||||
|
||||
function getMaxLabel() {
|
||||
var labels = [],
|
||||
max = 0,
|
||||
base = '';
|
||||
$('table.datagrid tbody tr td:first-child').each(function(i, td) {
|
||||
labels.push(trimLabel($(td).text()));
|
||||
});
|
||||
labels.forEach(function(label) {
|
||||
var match = /([a-zA-z]+)([0-9]+)/.exec(label),
|
||||
n = +match[2];
|
||||
base = match[1];
|
||||
if ( n > max )
|
||||
max = n;
|
||||
});
|
||||
return [base, max]
|
||||
}
|
||||
|
||||
function getNextLabel() {
|
||||
var baseMax = getMaxLabel();
|
||||
return baseMax[0]+(+baseMax[1]+1);
|
||||
}
|
||||
|
||||
$('button#node-add').click(function() {
|
||||
//debugger;
|
||||
if ( $('table.datagrid tbody tr').length >= MAX_NODES ) {
|
||||
alert('Maximum number of nodes ('+MAX_NODES+') already reached.');
|
||||
return;
|
||||
}
|
||||
var lastRow = $('table.datagrid tbody tr:last-child'),
|
||||
clone = lastRow.clone();
|
||||
clone.toggleClass('even').toggleClass('odd');
|
||||
clone.find('td:first-child').text(getNextLabel());
|
||||
lastRow.after(clone);
|
||||
});
|
||||
|
||||
$('button#node-remove').click(function() {
|
||||
if ( $('table.datagrid tbody tr').length <= MIN_NODES ) {
|
||||
alert('Cannot remove the only node');
|
||||
return;
|
||||
}
|
||||
var labelNum = getMaxLabel(),
|
||||
label = labelNum.join(''),
|
||||
rowRef = 'table.datagrid tbody tr:contains('+label+')';
|
||||
if ( rowRef + ' :radio[checked="checked"]' ) {
|
||||
label = labelNum[0] + (labelNum[1] - 1);
|
||||
$('table.datagrid tbody tr:contains('+label+') :radio').attr(
|
||||
'checked', 'checked');
|
||||
}
|
||||
$(rowRef).remove();
|
||||
});
|
||||
|
||||
$('.modal-footer input.btn-primary').click(function() {
|
||||
//debugger;
|
||||
var data = [];
|
||||
$('table.datagrid tbody tr').each(function(i, tr) {
|
||||
function getInputVal(td) {
|
||||
return td.find('input').attr('checked') == 'checked';
|
||||
}
|
||||
data.push({
|
||||
name: trimLabel($(tr).children().eq(0).text()),
|
||||
is_sync: getInputVal($(tr).children().eq(1)),
|
||||
is_primary: getInputVal($(tr).children().eq(3))
|
||||
})
|
||||
});
|
||||
$('input.gridfield-hidden').val(JSON.stringify(data));
|
||||
})
|
||||
|
||||
// temporarily disable all controls which are bugged
|
||||
$('table.datagrid th.edit-columns').remove();
|
||||
$('div.datagrid-titlebox').remove();
|
||||
});
|
6
muranodashboard/templates/data_grid_field.html
Normal file
6
muranodashboard/templates/data_grid_field.html
Normal file
@ -0,0 +1,6 @@
|
||||
<form action="." class="gridfield" method="post">{% csrf_token %}
|
||||
{{ data_grid.render_listview }}
|
||||
<button type="button" id="node-add"/>Add</button>
|
||||
<button type="button" id="node-remove"/>Remove</button>
|
||||
{# <button type="submit" id="node-submit"/>Submit</button>#}
|
||||
</form>
|
@ -13,7 +13,7 @@
|
||||
<br><br><br>
|
||||
{% endif %}
|
||||
|
||||
<table>
|
||||
<table><tr><td>
|
||||
{{ wizard.management_form }}
|
||||
{% if wizard.form.forms %}
|
||||
{{ wizard.form.management_form }}
|
||||
@ -25,7 +25,7 @@
|
||||
{% include "services/_horizon_form_fields.html" %}
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
</table>
|
||||
</td></tr></table>
|
||||
</div>
|
||||
<div class="right">
|
||||
{% if wizard.steps.index == 0 %}
|
||||
@ -253,7 +253,7 @@
|
||||
|
||||
{% elif type == '' %}
|
||||
<p>{% blocktrans %} Please try again {% endblocktrans %}</p>
|
||||
|
||||
|
||||
{% endif %}
|
||||
<p>
|
||||
<strong>{% blocktrans %} Hostname template {% endblocktrans %}</strong>
|
||||
@ -396,6 +396,8 @@
|
||||
$(".checkbox").css({'float':'left', 'width': 'auto', 'margin-right': '10px'})
|
||||
})
|
||||
</script>
|
||||
{{ wizard.form.media }}
|
||||
|
||||
<input type="hidden" name="wizard_goto_step" id="wizard_goto_step"/>
|
||||
{% if wizard.steps.next %}
|
||||
{% trans "Next" as next %}
|
||||
|
@ -6,3 +6,5 @@ requests==0.14.2
|
||||
bunch
|
||||
iso8601>=0.1.4
|
||||
six
|
||||
django-pipeline
|
||||
django-floppyforms==1.1
|
||||
|
Loading…
Reference in New Issue
Block a user