Implement datagrid field using djblets datagrid and floppyforms.

Change-Id: I88fefb1da1f3c1c70bfc6805c15f4e3c04fa0b77
This commit is contained in:
Timur Sufiev 2013-08-02 15:06:24 +04:00
parent 28cbf0dd2f
commit 74f5da49f4
11 changed files with 345 additions and 7 deletions

View 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]

View File

@ -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)

View File

@ -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))

View File

@ -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)]

View File

@ -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

View File

@ -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'
)

View File

@ -0,0 +1,3 @@
.datagrid-wrapper {
width: 100px;
}

View 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();
});

View 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>

View File

@ -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 %}

View File

@ -6,3 +6,5 @@ requests==0.14.2
bunch
iso8601>=0.1.4
six
django-pipeline
django-floppyforms==1.1