Skip to content

Commit 973b1d0

Browse files
committed
Merge pull request #13 from tahajahangir/bootstrap3
Compatible with Bootstrap 3. Thx!
2 parents 65ef4af + d60764a commit 973b1d0

File tree

24 files changed

+379
-313
lines changed

24 files changed

+379
-313
lines changed

fa/bootstrap/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
# -*- coding: utf-8 -*-
22
from pyramid_formalchemy import events
33
from fa.bootstrap import fanstatic_resources
4+
from fa.bootstrap.forms import BootstrapFieldSet
45

56

67
#object listening rendering events
78
@events.subscriber([object, events.IBeforeListingRenderEvent])
89
def before_object_listing_render(context, event):
910
fanstatic_resources.listing.need()
1011

12+
1113
def includeme(config):
1214
config.add_translation_dirs('fa.bootstrap:locale/')
1315
config.include('fa.jquery')

fa/bootstrap/actions.py

Lines changed: 73 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,53 @@
11
from pyramid_formalchemy.i18n import TranslationStringFactory
22
from pyramid_formalchemy import actions
33
from pyramid_formalchemy.actions import Actions
4-
from pyramid_formalchemy.actions import action; action
54
from webhelpers.html import literal
65

76
_ = TranslationStringFactory('fa_bootstrap')
87

98

9+
class RealButton(actions.Action):
10+
body = '<button class="${_class}" tal:attributes="%(attributes)s">' \
11+
'<span tal:condition="action.icon" class="${action.icon}"></span> ${content}</button>'
12+
13+
@property
14+
def icon(self):
15+
return self.rcontext.get('icon', None)
16+
17+
1018
class UIButton(actions.UIButton):
1119
"""Overwrite default pyramid_formalchemy action to support bootstrap style."""
1220

13-
body = '''<a class="${_class}" tal:attributes="%(attributes)s"><i tal:condition="action.icon" class="${action.icon}"></i> ${content}</a>'''
21+
body = '<a class="${_class}" tal:attributes="%(attributes)s">' \
22+
'<span tal:condition="action.icon" class="${action.icon}"></span> ${content}</a>'
1423

1524
@property
1625
def icon(self):
1726
return self.rcontext.get('icon', None)
1827

1928

20-
2129
class TabAction(actions.Action):
2230
"""New action type - comaptible with boostrap style."""
23-
body = u'<li tal:attributes="class action.isActive(request) and \'active\' or \'\'"><a tal:attributes="%(attributes)s">${content}</a></li>'
31+
body = u"<li tal:attributes=\"class action.is_active(request) and 'active' or ''\">" \
32+
u'<a tal:attributes="%(attributes)s">${content}</a></li>'
2433

25-
def isActive(self, request):
34+
def is_active(self, request):
2635
for _id in self.rcontext.get('children', ()):
2736
if _id in request.matchdict.get('traverse'):
2837
return True
2938
return request.path_url.strip('/') == eval(self.attrs['href']).strip('/')
3039

3140

3241
class BreadcrumbAction(TabAction):
33-
body = u'''<li tal:attributes="class action.isActive(request) and \'active\' or \'\'">
34-
<a tal:condition="not action.isActive(request)" tal:attributes="%(attributes)s">${content}</a>
35-
<span tal:condition="action.isActive(request)" tal:omit-tag="">${content}</span>
36-
<span tal:condition="not action.isActive(request)" class="divider">/</span>
42+
body = u'''<li tal:attributes="class action.is_active(request) and \'active\' or \'\'">
43+
<a tal:condition="not action.is_active(request)" tal:attributes="%(attributes)s">${content}</a>
44+
<span tal:condition="action.is_active(request)" tal:omit-tag="">${content}</span>
45+
<span tal:condition="not action.is_active(request)" class="divider">/</span>
3746
</li>'''
3847

3948

4049
class TabsActions(Actions, actions.Action):
41-
body='''<ul class="nav nav-tabs">${items}</ul>'''
50+
body = '''<ul class="nav nav-tabs">${items}</ul>'''
4251

4352
def __init__(self, *args, **kwargs):
4453
Actions.__init__(self, *args, **kwargs)
@@ -51,7 +60,7 @@ def render(self, request, **kwargs):
5160

5261

5362
class PillsActions(TabsActions):
54-
body='''<ul class="nav nav-pills">${items}</ul>'''
63+
body = '''<ul class="nav nav-pills">${items}</ul>'''
5564

5665

5766
class DropdownActions(Actions, actions.Action):
@@ -74,70 +83,69 @@ def render(self, request, **kwargs):
7483

7584

7685
new = UIButton(
77-
id='new',
78-
content=_('New ${model_label}'),
79-
permission='new',
80-
_class='btn btn-primary',
81-
icon='icon-white icon-plus',
82-
attrs=dict(href="request.fa_url(request.model_name, 'new')"),
83-
)
84-
85-
86-
save = UIButton(
87-
id='save',
88-
content=_('Save'),
89-
permission='edit',
90-
_class='btn btn-success',
91-
icon='icon-white icon-ok',
92-
attrs=dict(onclick="jQuery(this).parents('form').submit();"),
93-
)
86+
id='new',
87+
content=_('New ${model_label}'),
88+
permission='new',
89+
_class='btn btn-primary',
90+
icon='glyphicon glyphicon-plus',
91+
attrs=dict(href="request.fa_url(request.model_name, 'new')"),
92+
)
93+
94+
save = RealButton(
95+
id='save',
96+
type='submit',
97+
content=_('Save'),
98+
permission='edit',
99+
_class='btn btn-success',
100+
icon='glyphicon glyphicon-ok',
101+
)
94102

95103
save_and_add_another = UIButton(
96-
id='save_and_add_another',
97-
content=_('Save and add another'),
98-
permission='edit',
99-
_class='btn btn-success',
100-
icon='icon-white icon-plus',
101-
attrs=dict(onclick=("var f = jQuery(this).parents('form');"
102-
"jQuery('#next', f).val(window.location.href);"
103-
"f.submit();")),
104-
)
104+
id='save_and_add_another',
105+
content=_('Save and add another'),
106+
permission='edit',
107+
_class='btn btn-success',
108+
icon='glyphicon glyphicon-plus',
109+
attrs=dict(onclick=("var f = jQuery(this).parents('form');"
110+
"jQuery('#next', f).val(window.location.href);"
111+
"f.submit();")),
112+
)
105113

106114
edit = UIButton(
107-
id='edit',
108-
content=_('Edit'),
109-
permission='edit',
110-
_class='btn btn-info',
111-
icon='icon-white icon-edit',
112-
attrs=dict(href="request.fa_url(request.model_name, request.model_id, 'edit')"),
113-
)
115+
id='edit',
116+
content=_('Edit'),
117+
permission='edit',
118+
_class='btn btn-info',
119+
icon='glyphicon glyphicon-edit',
120+
attrs=dict(href="request.fa_url(request.model_name, request.model_id, 'edit')"),
121+
)
114122

115123
back = UIButton(
116-
id='back',
117-
content=_('Back'),
118-
_class='btn',
119-
attrs=dict(href="request.fa_url(request.model_name)"),
120-
)
124+
id='back',
125+
content=_('Back'),
126+
_class='btn btn-default',
127+
attrs=dict(href="request.fa_url(request.model_name)"),
128+
)
121129

122130
delete = UIButton(
123-
id='delete',
124-
content=_('Delete'),
125-
permission='delete',
126-
_class='btn btn-danger',
127-
icon='icon-white icon-trash',
128-
attrs=dict(onclick=("var f = jQuery(this).parents('form');"
129-
"f.attr('action', window.location.href.replace('/edit', '/delete'));"
130-
"f.submit();")),
131-
)
131+
id='delete',
132+
content=_('Delete'),
133+
permission='delete',
134+
_class='btn btn-danger',
135+
icon='glyphicon glyphicon-trash',
136+
attrs=dict(onclick=("var f = jQuery(this).parents('form');"
137+
"f.attr('action', window.location.href.replace('/edit', '/delete'));"
138+
"f.submit();")),
139+
)
132140

133141
cancel = UIButton(
134-
id='cancel',
135-
content=_('Cancel'),
136-
permission='view',
137-
_class='btn',
138-
icon='icon-remove',
139-
attrs=dict(href="request.fa_url(request.model_name)"),
140-
)
142+
id='cancel',
143+
content=_('Cancel'),
144+
permission='view',
145+
_class='btn btn-default',
146+
icon='glyphicon glyphicon-remove',
147+
attrs=dict(href="request.fa_url(request.model_name)"),
148+
)
141149

142150
defaults_actions = actions.defaults_actions.copy()
143151
defaults_actions['listing_buttons'] = Actions(new)

fa/bootstrap/forms.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from formalchemy import FieldSet, fatypes
2+
from fa.bootstrap import renderers
3+
4+
5+
class BootstrapFieldSet(FieldSet):
6+
default_renderers = dict(FieldSet.default_renderers)
7+
default_renderers.update({
8+
fatypes.String: renderers.BootstrapTextFieldRenderer,
9+
fatypes.Unicode: renderers.BootstrapTextFieldRenderer,
10+
fatypes.Text: renderers.BootstrapTextFieldRenderer,
11+
fatypes.Integer: renderers.BootstrapIntegerFieldRenderer,
12+
fatypes.Float: renderers.BootstrapFloatFieldRenderer,
13+
fatypes.Numeric: renderers.BootstrapFloatFieldRenderer,
14+
fatypes.Interval: renderers.BootstrapIntervalFieldRenderer,
15+
fatypes.Boolean: renderers.BootstrapBooleanFieldRenderer,
16+
})

fa/bootstrap/renderers.py

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ def __init__(self, *args, **kwargs):
1717

1818
def update_options(self, options, kwargs):
1919
kwargs['source'] = self.request.fa_url(
20-
self.field.relation_type().__name__, 'autocomplete')
20+
self.field.relation_type().__name__, 'autocomplete')
2121

2222
def render(self, **kwargs):
2323
fanstatic_resources.autocomplete.need()
24-
filter_by = self.jq_options.get('filter_by')
24+
filter_value = self.jq_options.get('filter_by')
2525
if self.raw_value:
26-
label = getattr(self.raw_value, filter_by, u'Not selected')
26+
label = getattr(self.raw_value, filter_value, u'Not selected')
2727
else:
2828
label = u''
2929

@@ -36,13 +36,51 @@ def render(self, **kwargs):
3636

3737
def _serialized_value(self):
3838
try:
39-
return super(Renderer,self)._serialized_value()
39+
return super(Renderer, self)._serialized_value()
4040
except FieldNotFoundError:
4141
return None
4242

4343
jq_options.update(filter_by=filter_by, show_input=True)
4444

4545
return renderers.jQueryFieldRenderer('bootstrap_autocomplete_relation', renderer=Renderer, **jq_options)
4646

47+
4748
@renderers.alias(AutocompleteRelationRenderer)
48-
def autocomplete_relation(): pass
49+
def autocomplete_relation():
50+
pass
51+
52+
53+
class BootstrapFieldMixin(object):
54+
"""
55+
Mixin to add `class="form-control"` to renderers
56+
"""
57+
58+
def render(self, **kwargs):
59+
if 'class' in kwargs:
60+
kwargs['class'] += ' form-control'
61+
else:
62+
kwargs['class'] = ' form-control'
63+
return super(BootstrapFieldMixin, self).render(**kwargs)
64+
65+
66+
class BootstrapBooleanFieldRenderer(fields.CheckBoxFieldRenderer):
67+
def render(self, **kwargs):
68+
checkbox = super(BootstrapBooleanFieldRenderer, self).render(**kwargs)
69+
return '<span class="input-group" style="width:1px"><span class="input-group-addon">%s</span>' \
70+
'<span class="form-control">%s</span></span>' % (checkbox, 'True')
71+
72+
73+
class BootstrapTextFieldRenderer(BootstrapFieldMixin, fields.TextFieldRenderer):
74+
pass
75+
76+
77+
class BootstrapIntegerFieldRenderer(BootstrapFieldMixin, fields.IntegerFieldRenderer):
78+
pass
79+
80+
81+
class BootstrapFloatFieldRenderer(BootstrapFieldMixin, fields.FloatFieldRenderer):
82+
pass
83+
84+
85+
class BootstrapIntervalFieldRenderer(BootstrapFieldMixin, fields.IntervalFieldRenderer):
86+
pass

fa/bootstrap/scaffolds/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from paste.util.template import paste_script_template_renderer
22
from pyramid.scaffolds import PyramidTemplate
33

4+
45
class PyramidFormAlchemyBootstrapTemplate(PyramidTemplate):
56
_template_dir = ('pyramid_fa_bootstrap')
67
summary = "Pyramid SQLAlchemy project using fa.bootstrap"

fa/bootstrap/scaffolds/pyramid_fa_bootstrap/+package+/__init__.py_tmpl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ def main(global_config, **settings):
1616
renderer="templates/home.pt")
1717

1818
config.include('pyramid_formalchemy')
19-
# Adding the jquery libraries
19+
config.include('pyramid_chameleon')
20+
# Adding the jquery/bootstrap libraries
2021
config.include('fa.bootstrap')
2122
# Adding the package specific routes
2223
config.include('{{package}}.routes')

fa/bootstrap/scaffolds/pyramid_fa_bootstrap/+package+/forms.py_tmpl

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
from formalchemy import forms
21
from formalchemy import tables
32
from pyramid.renderers import get_renderer
43
from fa.bootstrap.views import ModelView as Base
5-
from fa.bootstrap import fanstatic_resources
4+
from fa.bootstrap import fanstatic_resources, BootstrapFieldSet
65

76

8-
class FieldSet(forms.FieldSet):
7+
class FieldSet(BootstrapFieldSet):
98
pass
109

1110

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
1-
import transaction
1+
from datetime import timedelta
22

3+
import transaction
34
from sqlalchemy.orm import scoped_session
45
from sqlalchemy.orm import sessionmaker
5-
66
from sqlalchemy.ext.declarative import declarative_base
7-
87
from sqlalchemy.exc import IntegrityError
9-
10-
from sqlalchemy import Integer
11-
from sqlalchemy import Unicode
12-
from sqlalchemy import Column
13-
8+
from sqlalchemy import Integer, Unicode, Column, Float, Interval, Boolean
149
from zope.sqlalchemy import ZopeTransactionExtension
1510

1611
DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension()))
@@ -21,18 +16,23 @@ class MyModel(Base):
2116
id = Column(Integer, primary_key=True)
2217
name = Column(Unicode(255), unique=True)
2318
value = Column(Integer)
19+
float_value = Column(Float)
20+
boolean = Column(Boolean)
21+
interval = Column(Interval)
22+
23+
def __init__(self, **kwargs):
24+
for key, value in kwargs.items():
25+
setattr(self, key, value)
2426

25-
def __init__(self, name='', value=''):
26-
self.name = name
27-
self.value = value
2827

2928
def populate():
3029
session = DBSession()
31-
model = MyModel(name=u'test name', value=55)
30+
model = MyModel(name=u'test name', value=55, float_value=3.5, interval=timedelta(4), boolean=True)
3231
session.add(model)
3332
session.flush()
3433
transaction.commit()
3534

35+
3636
def initialize_sql(engine):
3737
DBSession.configure(bind=engine)
3838
Base.metadata.bind = engine
@@ -43,5 +43,6 @@ def initialize_sql(engine):
4343
transaction.abort()
4444
return DBSession
4545

46+
4647
def appmaker(engine):
4748
initialize_sql(engine)

0 commit comments

Comments
 (0)