Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

merge news

  • Loading branch information...
commit 0da45267a2f0b61eb6bc0822bee8f024733dd39d 2 parents f76b2c8 + 711aee3
Erik Janssens authored
Showing with 386 additions and 159 deletions.
  1. +1 −1  camelot/__init__.py
  2. +0 −8 camelot/bin/camelot_admin.py
  3. +6 −1 camelot/core/orm.py
  4. +7 −0 camelot/core/utils.py
  5. +3 −1 camelot/view/action_steps/__init__.py
  6. +42 −0 camelot/view/action_steps/print_preview.py
  7. +19 −18 camelot/view/controls/delegates/stardelegate.py
  8. +6 −17 camelot/view/controls/editors/charteditor.py
  9. +7 −7 camelot/view/controls/editors/stareditor.py
  10. +6 −7 camelot/view/controls/filter_operator.py
  11. +6 −1 camelot/view/controls/formview.py
  12. +1 −1  camelot/view/controls/tableview.py
  13. +6 −0 camelot/view/field_attributes.py
  14. +1 −1  camelot/view/flowlayout.py
  15. +40 −3 camelot/view/model_thread/signal_slot_model_thread.py
  16. +17 −19 camelot/view/proxy/collection_proxy.py
  17. +11 −15 camelot/view/search.py
  18. +5 −4 camelot_example/model.py
  19. +6 −30 doc/sphinx/source/advanced/deployment.rst
  20. +1 −0  doc/sphinx/source/doc/actions.rst
  21. +19 −13 doc/sphinx/source/doc/charts.rst
  22. +2 −2 doc/sphinx/source/migrate/11-12-30.rst
  23. +8 −6 doc/sphinx/source/tutorial/videostore.rst
  24. +3 −2 install.txt
  25. +17 −1 news.txt
  26. +21 −0 test/test_action.py
  27. +95 −0 test/test_search.py
  28. +28 −0 test/test_source.py
  29. +2 −1  test/test_view.py
View
2  camelot/__init__.py
@@ -26,7 +26,7 @@
the Django admin interface. Start building applications at warp speed, simply
by adding some additional information to you Elixir model."""
-__version__ = 'master'
+__version__ = '12.06.29'
View
8 camelot/bin/camelot_admin.py
@@ -43,9 +43,6 @@
command_description = [
('startproject', """Starts a new project, use startproject project_name.
"""),
- ('makemessages', """Outputs a message file with all field names of all
-entities. This command requires settings.py of the project to be in the
-PYTHONPATH"""),
('apidoc', """Extract API documentation from source code, to be used
with sphinx.
"""),
@@ -231,11 +228,6 @@ def startproject(module):
action = CreateNewProject()
action.start_project( options )
-def makemessages():
- from camelot.core.conf import settings
- LOGGER.error( 'Not yet implemented' )
- settings.setup_model()
-
def meta():
"""launch meta camelot, in a separate function to make sure camelot_admin
does not depend on PyQt, otherwise it is imposible to run to_pyside without
View
7 camelot/core/orm.py
@@ -98,7 +98,7 @@ class ClassMutator( object ):
def __init__( self, *args, **kwargs ):
# jam this mutator into the class's mutator list
class_locals = sys._getframe(1).f_locals
- mutators = class_locals.setdefault(MUTATORS, [])
+ mutators = class_locals.setdefault( MUTATORS, [] )
mutators.append( (self, args, kwargs) )
def process( self, entity_dict, *args, **kwargs ):
@@ -288,6 +288,11 @@ def process( self, entity_dict, tablename = None, order_by = None ):
if order_by:
mapper_args = entity_dict.get('__mapper_args__', {} )
mapper_args['order_by'] = order_by
+
+class has_field( ClassMutator ):
+
+ def process( self, entity_dict, name, *args, **kwargs ):
+ entity_dict[ name ] = Field( *args, **kwargs )
class EntityMeta( DeclarativeMeta ):
"""Subclass of :class:`sqlalchmey.ext.declarative.DeclarativeMeta`. This
View
7 camelot/core/utils.py
@@ -54,6 +54,13 @@ def is_deleted_pyside( qobj ):
else:
pyqt = False
is_deleted = is_deleted_pyside
+ # try to activate the PySide backend of matplotlib
+ # http://www.scipy.org/Cookbook/Matplotlib/PySide
+ try:
+ import matplotlib
+ matplotlib.rcParams['backend.qt4'] = 'PySide'
+ except:
+ pass
def create_constant_function(constant):
return lambda:constant
View
4 camelot/view/action_steps/__init__.py
@@ -30,7 +30,8 @@
from open_file import ( OpenFile, OpenStream,
OpenString, OpenJinjaTemplate, WordJinjaTemplate )
from orm import CreateObject, DeleteObject, FlushSession, UpdateObject
-from print_preview import PrintHtml, PrintPreview, PrintJinjaTemplate
+from print_preview import ( PrintChart, PrintHtml, PrintPreview,
+ PrintJinjaTemplate )
from select_file import SelectFile
from select_object import SelectObject
from text_edit import EditTextDocument
@@ -51,6 +52,7 @@
OpenJinjaTemplate.__name__,
OpenStream.__name__,
OpenString.__name__,
+ PrintChart.__name__,
PrintHtml.__name__,
PrintJinjaTemplate.__name__,
PrintPreview.__name__,
View
42 camelot/view/action_steps/print_preview.py
@@ -85,6 +85,48 @@ def gui_run( self, gui_context ):
dialog = self.render()
dialog.exec_()
+class ChartDocument( QtCore.QObject ):
+ """Helper class to print matplotlib charts
+
+ :param chart: a :class:`camelot.container.chartcontainer.FigureContainer` object
+ or a :class:`camelot.container.chartcontainer.AxesContainer` subclass
+
+ """
+
+ def __init__( self, chart ):
+ from camelot.container.chartcontainer import structure_to_figure_container
+ super( ChartDocument, self ).__init__()
+ self.chart = structure_to_figure_container( chart )
+
+ def print_( self, printer ):
+ from matplotlib.figure import Figure
+ from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
+ rect = printer.pageRect( QtGui.QPrinter.Inch )
+ dpi = printer.resolution()
+ fig = Figure( facecolor='#ffffff')
+ fig.set_size_inches( ( rect.width(), rect.height() ) )
+ fig.set_dpi( dpi )
+ self.chart.plot_on_figure( fig )
+ canvas = FigureCanvas( fig )
+ canvas.render( printer )
+
+class PrintChart( PrintPreview ):
+ """
+ Display a print preview dialog box for a matplotlib chart.
+
+ :param chart: a :class:`camelot.container.chartcontainer.FigureContainer` object
+ or a :class:`camelot.container.chartcontainer.AxesContainer` subclass
+
+ Example use of this action step :
+
+ .. literalinclude:: ../../../test/test_action.py
+ :start-after: begin chart print
+ :end-before: end chart print
+ """
+
+ def __init__( self, chart ):
+ super( PrintChart, self ).__init__( ChartDocument( chart ) )
+
class PrintHtml( PrintPreview ):
"""
Display a print preview dialog box for an html string.
View
37 camelot/view/controls/delegates/stardelegate.py
@@ -29,41 +29,42 @@
from camelot.view.controls import editors
from camelot.view.art import Icon
-class StarDelegate(CustomDelegate):
- """Delegate for integer values from (1 to 5)(Rating Delegate)
-
- """
+class StarDelegate( CustomDelegate ):
+ """Delegate for integer values from ( default from 1 to 5)(Rating Delegate)
+ """
__metaclass__ = DocumentationMetaclass
- editor = editors.StarEditor
+ editor = editors.StarEditor
+ star_icon = Icon('tango/16x16/status/weather-clear.png')
- def __init__(self, parent=None, editable=True, maximum=5, **kwargs):
- CustomDelegate.__init__(self,
- parent=parent,
- editable=editable,
- maximum=maximum,
- **kwargs)
+ def __init__( self, parent = None, editable = True, maximum = 5, **kwargs ):
+ CustomDelegate.__init__( self,
+ parent = parent,
+ editable = editable,
+ maximum = maximum,
+ **kwargs)
self.maximum = maximum
- def paint(self, painter, option, index):
+ def paint( self, painter, option, index ):
painter.save()
self.drawBackground(painter, option, index)
stars = variant_to_pyobject( index.model().data(index, Qt.EditRole) )
rect = option.rect
- rect = QtCore.QRect(rect.left()+3, rect.top()+6, rect.width()-5, rect.height())
+ rect = QtCore.QRect( rect.left()+3, rect.top()+6,
+ rect.width()-5, rect.height() )
if( option.state & QtGui.QStyle.State_Selected ):
painter.fillRect(option.rect, option.palette.highlight())
else:
if not self.editable:
painter.fillRect(option.rect, option.palette.window())
-
- for i in range(5):
+
+ pixmap = self.star_icon.getQPixmap()
+ style = QtGui.QApplication.style()
+ for i in range( self.maximum ):
if i+1<=stars:
- icon = Icon('tango/16x16/status/weather-clear.png').getQPixmap()
- QtGui.QApplication.style().drawItemPixmap(painter, rect, 1, icon)
+ style.drawItemPixmap( painter, rect, 1, pixmap )
rect = QtCore.QRect(rect.left()+20, rect.top(), rect.width(), rect.height())
painter.restore()
-
View
23 camelot/view/controls/editors/charteditor.py
@@ -27,11 +27,12 @@
from PyQt4 import QtGui
from PyQt4 import QtCore
+from camelot.admin.action.list_action import ListActionGuiContext
+from camelot.core.utils import ugettext as _
from camelot.view.controls.editors.customeditor import AbstractCustomEditor
from camelot.view.controls.editors.wideeditor import WideEditor
from camelot.view.proxy import ValueLoading
from camelot.view.art import Icon
-from camelot.core.utils import ugettext as _
PAD_INCHES = 0.1
@@ -107,6 +108,7 @@ def __init__(self, parent=None, width=50, height=40, dpi=50, field_name='chart',
self.show_fullscreen_signal.connect(self.show_fullscreen)
self.canvas.updateGeometry()
self._litebox = None
+ self.gui_context = ListActionGuiContext()
@QtCore.pyqtSlot()
def copy_to_clipboard(self):
@@ -118,22 +120,9 @@ def copy_to_clipboard(self):
@QtCore.pyqtSlot()
def print_preview(self):
"""Popup a print preview dialog for the Chart"""
- dialog = QtGui.QPrintPreviewDialog()
- dialog.paintRequested.connect( self.on_paint_request )
- dialog.exec_()
-
- @QtCore.pyqtSlot( QtGui.QPrinter )
- def on_paint_request(self, printer):
- from matplotlib.figure import Figure
- from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
- rect = printer.pageRect( QtGui.QPrinter.Inch )
- dpi = printer.resolution()
- fig = Figure( facecolor='#ffffff')
- fig.set_figsize_inches( (rect.width(),rect.height()) )
- fig.set_dpi( dpi )
- self._value.plot_on_figure( fig )
- canvas = FigureCanvas(fig)
- canvas.render( printer )
+ from camelot.view.action_steps import PrintChart
+ print_chart = PrintChart( self._value )
+ print_chart.gui_run( self.gui_context )
def set_field_attributes(self, *args, **kwargs):
"""Overwrite set_field attributes because a ChartEditor cannot be disabled
View
14 camelot/view/controls/editors/stareditor.py
@@ -28,7 +28,7 @@
from customeditor import CustomEditor
from camelot.view.art import Icon
-class StarEditor(CustomEditor):
+class StarEditor( CustomEditor ):
star_icon = Icon('tango/16x16/status/weather-clear.png')
no_star_icon = Icon('tango/16x16/status/weather-clear-noStar.png')
@@ -46,9 +46,9 @@ def __init__(self,
layout.setContentsMargins( 0, 0, 0, 0)
layout.setSpacing(0)
- self.starCount = 5
+ self.maximum = maximum
self.buttons = []
- for i in range(self.starCount):
+ for i in range(self.maximum):
button = QtGui.QToolButton(self)
button.setIcon(self.no_star_icon.getQIcon())
button.setFocusPolicy(Qt.ClickFocus)
@@ -63,10 +63,10 @@ def __init__(self,
def createStarClick(i):
return lambda:self.starClick(i+1)
- for i in range(self.starCount):
+ for i in range(self.maximum):
self.buttons[i].clicked.connect(createStarClick(i))
- for i in range(self.starCount):
+ for i in range(self.maximum):
layout.addWidget(self.buttons[i])
layout.addStretch()
self.setLayout(layout)
@@ -85,7 +85,7 @@ def starClick(self, value):
self.stars -= 1
else:
self.stars = int(value)
- for i in range(self.starCount):
+ for i in range(self.maximum):
if i+1 <= self.stars:
self.buttons[i].setIcon(self.star_icon.getQIcon())
else:
@@ -95,7 +95,7 @@ def starClick(self, value):
def set_value(self, value):
value = CustomEditor.set_value(self, value) or 0
self.stars = int(value)
- for i in range(self.starCount):
+ for i in range(self.maximum):
if i+1 <= self.stars:
self.buttons[i].setIcon(self.star_icon.getQIcon())
else:
View
13 camelot/view/controls/filter_operator.py
@@ -24,6 +24,7 @@
from PyQt4 import QtGui, QtCore
+from camelot.view.field_attributes import order_operators
from camelot.core.utils import ugettext
from camelot.view.utils import operator_names
from camelot.view.controls.user_translatable_label import UserTranslatableLabel
@@ -149,13 +150,11 @@ def decorate_query(self, query):
return query.filter(getattr(self._entity, self._field_name)==None)
field = getattr(self._entity, self._field_name)
operator, arity = self.get_operator_and_arity()
- if arity == 1:
- args = field, self._value
- elif arity == 2:
- args = field, self._value, self._value2
- else:
- assert False, 'Unsupported operator arity: %d' % arity
- return query.filter(operator(*args))
+ values = [self._value, self._value2][:arity]
+ none_values = sum( v == None for v in values )
+ if ( operator in order_operators ) and none_values > 0:
+ return query
+ return query.filter( operator( field, *values ) )
def get_operator_and_arity(self):
""":return: the current operator and its arity"""
View
7 camelot/view/controls/formview.py
@@ -266,7 +266,7 @@ def __init__(self, title, admin, model, index, parent = None):
layout = QtGui.QVBoxLayout()
layout.setSpacing( 1 )
- layout.setMargin( 1 )
+ layout.setContentsMargins( 1, 1, 1, 1 )
layout.setObjectName( 'layout' )
form_and_actions_layout = QtGui.QHBoxLayout()
form_and_actions_layout.setObjectName('form_and_actions_layout')
@@ -354,6 +354,11 @@ def set_toolbar_actions(self, actions):
toolbar.addAction( qaction )
toolbar.addWidget( BusyWidget() )
layout.insertWidget( 0, toolbar, 0, Qt.AlignTop )
+ # @todo : this show is needed on OSX or the form window
+ # is hidden after the toolbar is added, maybe this can
+ # be solved using windowflags, since this causes some
+ # flicker
+ self.show()
@QtCore.pyqtSlot( bool )
def action_triggered( self, _checked = False ):
View
2  camelot/view/controls/tableview.py
@@ -390,7 +390,7 @@ def __init__(self, admin, parent=None):
column_groups.setObjectName( 'column_groups' )
layout = QtGui.QVBoxLayout()
layout.setSpacing( 0 )
- layout.setMargin( 0 )
+ layout.setContentsMargins( 0, 0, 0, 0 )
layout.addWidget( table_widget )
layout.addWidget( column_groups )
self.setLayout( layout )
View
6 camelot/view/field_attributes.py
@@ -49,6 +49,12 @@
_numerical_operators = (operator.eq, operator.ne, operator.lt, operator.le, operator.gt, operator.ge, between_op)
_text_operators = (operator.eq, operator.ne, like_op)
+#
+# operators assuming an order in the values they operate on. these operators don't
+# work on None values
+#
+order_operators = (operator.lt, operator.le, operator.gt, operator.ge, between_op, like_op)
+
_sqlalchemy_to_python_type_ = {
sqlalchemy.types.Boolean: lambda f: {
View
2  camelot/view/flowlayout.py
@@ -20,7 +20,7 @@ def __init__( self, parent = None ):
"""
super(FlowLayout, self).__init__(parent)
if parent is not None:
- self.setMargin( 0 )
+ self.setContentsMargins( 0, 0, 0, 0 )
self.setSpacing( -1 )
self.item_list = []
View
43 camelot/view/model_thread/signal_slot_model_thread.py
@@ -32,11 +32,47 @@
from PyQt4 import QtCore
+from camelot.core.utils import pyqt
+from camelot.core.threading import synchronized
from camelot.view.model_thread import ( AbstractModelThread, object_thread,
setup_model )
-from camelot.core.threading import synchronized
from camelot.view.controls.exception import register_exception
+#
+# Wrap and unwrap None passed through signal/slot accross threads to
+# prevent segfaults with PySide
+#
+# https://bugreports.qt-project.org/browse/PYSIDE-17
+#
+
+if pyqt:
+ wrap_none = lambda x:x
+ unwrap_none = lambda x:x
+else:
+ class Null( object ):
+ pass
+
+ null = Null()
+
+ def wrap_none( func ):
+
+ def new_func( *args ):
+ y = func( *args )
+ if y == None:
+ return null
+ return y
+
+ return new_func
+
+ def unwrap_none( func ):
+
+ def new_func( x ):
+ if x == null:
+ x = None
+ return func( x )
+
+ return new_func
+
class Task(QtCore.QObject):
finished = QtCore.pyqtSignal(object)
@@ -186,14 +222,15 @@ def post( self, request, response = None, exception = None, args = () ):
name = '%s -> %s.%s'%(request.__name__, response.im_self.__class__.__name__, response.__name__)
else:
name = request.__name__
- task = Task(request, name=name, args=args)
+ task = Task( wrap_none( request ), name = name, args = args )
# QObject::connect is a thread safe function
if response:
assert response.im_self != None
assert isinstance(response.im_self, QtCore.QObject)
# verify if the response has been defined as a slot
#assert hasattr(response, '__pyqtSignature__')
- task.finished.connect( response, QtCore.Qt.QueuedConnection )
+ task.finished.connect( unwrap_none( response ),
+ QtCore.Qt.QueuedConnection )
if exception:
task.exception.connect( exception, QtCore.Qt.QueuedConnection )
# task.moveToThread(self)
View
36 camelot/view/proxy/collection_proxy.py
@@ -199,6 +199,11 @@ class CollectionProxy( QtGui.QProxyModel ):
row_changed_signal = QtCore.pyqtSignal(int)
exception_signal = QtCore.pyqtSignal(object)
rows_removed_signal = QtCore.pyqtSignal()
+
+ # it looks as QtCore.QModelIndex cannot be serialized for cross
+ # thread signals
+ _rows_about_to_be_inserted_signal = QtCore.pyqtSignal( int, int )
+ _rows_inserted_signal = QtCore.pyqtSignal( int, int )
def __init__( self,
admin,
@@ -281,6 +286,8 @@ def __init__( self,
self.unflushed_rows = set()
self._sort_and_filter = SortingRowMapper()
self.row_changed_signal.connect( self._emit_changes )
+ self._rows_about_to_be_inserted_signal.connect( self._rows_about_to_be_inserted, Qt.QueuedConnection )
+ self._rows_inserted_signal.connect( self._rows_inserted, Qt.QueuedConnection )
self.rsh = get_signal_handler()
self.rsh.connect_signals( self )
@@ -1137,6 +1144,14 @@ def copy_function():
post( create_copy_function( row ) )
return True
+ @QtCore.pyqtSlot( int, int )
+ def _rows_about_to_be_inserted( self, first, last ):
+ self.beginInsertRows( QtCore.QModelIndex(), first, last )
+
+ @QtCore.pyqtSlot( int, int )
+ def _rows_inserted( self, _first, _last ):
+ self.endInsertRows()
+
@model_function
def append_object( self, obj ):
"""Append an object to this collection, set the possible defaults and flush
@@ -1147,7 +1162,7 @@ def append_object( self, obj ):
"""
rows = self._rows
row = max( rows - 1, 0 )
- self.beginInsertRows( QtCore.QModelIndex(), row, row )
+ self._rows_about_to_be_inserted_signal.emit( row, row )
self.append( obj )
# defaults might depend on object being part of a collection
self.admin.set_defaults( obj )
@@ -1166,26 +1181,9 @@ def append_object( self, obj ):
#
columns = self._columns
self._add_data( columns, rows, obj )
- self.endInsertRows()
+ self._rows_inserted_signal.emit( row, row )
return self._rows
- @QtCore.pyqtSlot(object)
- def append_row( self, object_getter ):
- """
- :param object_getter: a function that returns the object to be put in the
- appended row.
- """
- assert object_thread( self )
-
- def create_append_function( getter ):
-
- def append_function():
- return self.append_object( getter() )
-
- return append_function
-
- post( create_append_function( object_getter ), self._refresh_content )
-
@model_function
def getData( self ):
"""Generator for all the data queried by this proxy"""
View
26 camelot/view/search.py
@@ -34,18 +34,17 @@
import camelot.types
-def create_entity_search_query_decorator(admin, text):
+def create_entity_search_query_decorator( admin, text ):
"""create a query decorator to search through a collection of entities
- @param admin: the admin interface of the entity
- @param text: the text to search for
- @return: a function that can be applied to a query to make the query filter
+ :param admin: the admin interface of the entity
+ :param text: the text to search for
+ :return: a function that can be applied to a query to make the query filter
only the objects related to the requested text or None if no such decorator
could be build
"""
from camelot.view import utils
if len(text.strip()):
- from sqlalchemy import Unicode, or_
# arguments for the where clause
args = []
# join conditions : list of join entities
@@ -70,12 +69,12 @@ def append_column( c ):
elif issubclass(c.type.__class__, sqlalchemy.types.Integer):
try:
arg = (c==utils.int_from_string(text))
- except Exception, utils.ParsingError:
+ except ( Exception, utils.ParsingError ):
pass
elif issubclass(c.type.__class__, sqlalchemy.types.Date):
try:
arg = (c==utils.date_from_string(text))
- except Exception, utils.ParsingError:
+ except ( Exception, utils.ParsingError ):
pass
elif issubclass(c.type.__class__, sqlalchemy.types.Float):
try:
@@ -83,13 +82,13 @@ def append_column( c ):
precision = c.type.precision
if isinstance(precision, (tuple)):
precision = precision[1]
- delta = 0.1**precision
+ delta = 0.1**( precision or 0 )
arg = sql.and_(c>=float_value-delta, c<=float_value+delta)
- except Exception, utils.ParsingError:
+ except ( Exception, utils.ParsingError ):
pass
- elif issubclass(c.type.__class__, (Unicode, )) or \
+ elif issubclass(c.type.__class__, (sqlalchemy.types.String, )) or \
(hasattr(c.type, 'impl') and \
- issubclass(c.type.impl.__class__, (Unicode, ))):
+ issubclass(c.type.impl.__class__, (sqlalchemy.types.String, ))):
LOGGER.debug('look in column : %s'%c.name)
arg = sql.operators.ilike_op(c, '%'+text+'%')
@@ -128,7 +127,7 @@ def query_decorator(query):
query = query.outerjoin(join)
if len(args):
if len(args)>1:
- query = query.filter(or_(*args))
+ query = query.filter( sql.or_( *args ) )
else:
query = query.filter(args[0])
return query
@@ -136,6 +135,3 @@ def query_decorator(query):
return query_decorator
return create_query_decorator(joins, args)
-
-
-
View
9 camelot_example/model.py
@@ -91,8 +91,9 @@ class Movie( Entity ):
director_party_id = Column( sqlalchemy.types.Integer,
ForeignKey( 'person.party_id' ) )
director = relationship( Person )
- cast = relationship( 'Cast' )
- visitor_reports = relationship( 'VisitorReport' )
+ # replaced by backref on Cast class
+ #cast = relationship( 'Cast' )
+ visitor_reports = relationship( 'VisitorReport', )
tags = ManyToMany( 'Tag',
tablename = 'tags_movies__movies_tags',
local_colname = 'movies_id',
@@ -163,7 +164,7 @@ class Admin(EntityAdmin):
'genre',
'description',], columns = 2)),
('Cast', WidgetOnlyForm('cast')),
- # ('Visitors', WidgetOnlyForm('visitors_chart')),
+ ('Visitors', WidgetOnlyForm('visitors_chart')),
('Tags', WidgetOnlyForm('tags'))
])
@@ -205,7 +206,7 @@ class Cast( Entity ):
actor_party_id = Column( sqlalchemy.types.Integer,
ForeignKey( 'person.party_id' ),
nullable = False )
- movie = relationship( 'Movie' )
+ movie = relationship( 'Movie', backref = 'cast' )
actor = relationship( Person )
class Admin( EntityAdmin ):
View
36 doc/sphinx/source/advanced/deployment.rst
@@ -4,9 +4,6 @@
Deployment
#############
-:Release: |version|
-:Date: |today|
-
After developing a Camelot application comes the need to deploy the
application, either at a central location or in a distributed setup.
@@ -20,35 +17,14 @@ the lifetime of the application. Resource files (like icons or templates
can be included in this .egg file as well).
Building .egg files is a relatively straightforward process using
-setuptools_
-
-A setup.py file for building an .egg of your application could look
-like this::
-
- from setuptools import setup, find_packages
-
- setup(
- name = 'movie store',
- version = '01.01',
- description = 'Movie Store',
- author = 'Conceptive Engineering',
- author_email = 'project-camelot@conceptive.be',
- url = 'www.conceptive.be',
- include_package_data = True,
- packages = find_packages(),
- py_modules = ['settings'] )
-
-Which is then build using this command::
-
+setuptools.
+
+When a new Camelot project was created with `camelot_admin`, a
+:file:`setup.py` file was made that is able to build eggs using this
+command ::
+
python -O setup.py bdist_egg --exclude-source-files
-The setup.py script above includes settings.py in the .egg file. This
-is prefered if the settings.py file is going to be the same for all
-deployments. (Eg.: the database is on a central server accessible
-for all) In some occasions it might be better not to include the settings.py
-file into the .egg file, and only put it in your PYTHONPATH at deployment
-time.
-
.. note::
The advantage of using .egg files comes when updating the application, simply
View
1  doc/sphinx/source/doc/actions.rst
@@ -91,6 +91,7 @@ an action. Possible Action Steps that can be yielded to the GUI include:
* :class:`camelot.view.action_steps.change_object.ChangeObject`
* :class:`camelot.view.action_steps.change_object.ChangeObjects`
+ * :class:`camelot.view.action_steps.print_preview.PrintChart`
* :class:`camelot.view.action_steps.print_preview.PrintPreview`
* :class:`camelot.view.action_steps.print_preview.PrintHtml`
* :class:`camelot.view.action_steps.print_preview.PrintJinjaTemplate`
View
32 doc/sphinx/source/doc/charts.rst
@@ -38,13 +38,26 @@ The simpel chart containers map to their respective matplotlib command. They in
.. autoclass:: camelot.container.chartcontainer.BarContainer
+Actions
+=======
+
+The `PlotContainer` and `BarContainer` can be used to print or display charts
+as part of an action through the use of the appropriate action steps :
+
+ * :class:`camelot.view.action_steps.print_preview.PrintChart`
+ * :class:`camelot.view.action_steps.gui.ShowChart`
+
+.. literalinclude:: ../../../../test/test_action.py
+ :start-after: begin chart print
+ :end-before: end chart print
+
Advanced Plots
==============
-For more advanced plots, the **AxesContainer** class can be used. The AxesContainer class can be
-used as if it were a matplotlib Axes object. But when a method on the AxesContainer is called it
-will record the method call instead of creating a plot. These method calls will then be replayed
-by the gui to create the actual plot.
+For more advanced plots, the :class:`camelot.container.chartcontainer.AxesContainer` class can be used.
+The `AxesContainer` class can be used as if it were a matplotlib `Axes` object.
+But when a method on the `AxesContainer` is called it will record the method call instead of creating a plot.
+These method calls will then be replayed by the gui to create the actual plot.
.. literalinclude:: ../../../../test/snippet/chart/advanced_plot.py
@@ -53,15 +66,8 @@ by the gui to create the actual plot.
More
====
-For more information on the various types of plots that can be created, have a look at the
-`Matplotlib Gallery <http://matplotlib.sourceforge.net/gallery.html>`_.
+For more information on the various types of plots that can be created, have a look at the `Matplotlib Gallery <http://matplotlib.sourceforge.net/gallery.html>`_.
When the AxesContainer does not provide enough flexibility, for example when the plot needs to
manipulated through its object structure, more customization is possible by subclassing either
-the AxesContainer or the FigureContainer :
-
-.. autoclass:: camelot.container.chartcontainer.AxesContainer
- :members:
-
-.. autoclass:: camelot.container.chartcontainer.FigureContainer
- :members:
+the :class:`camelot.container.chartcontainer.AxesContainer` or the :class:`camelot.container.chartcontainer.FigureContainer` :
View
4 doc/sphinx/source/migrate/11-12-30.rst
@@ -1,7 +1,7 @@
.. _migrate-11.12.30:
-Migrate from Camelot 11.12.30 to master
-=======================================
+Migrate from Camelot 11.12.30 to 12.06.29
+=========================================
The place of the default `metadata` has changed. So the top line at the
model files should change from::
View
14 doc/sphinx/source/tutorial/videostore.rst
@@ -346,7 +346,8 @@ column and use it to attach a ``Director`` object to a ``Movie`` object ::
genre = Column( Unicode( 15 ) )
director_id = Column( Integer, ForeignKey('director.id') )
- director = relationship( 'Director' )
+ director = relationship( 'Director',
+ backref = 'movies' )
class Admin( EntityAdmin ):
verbose_name = 'Movie'
@@ -361,8 +362,8 @@ column and use it to attach a ``Director`` object to a ``Movie`` object ::
We also inserted ``'director'`` in ``list_display``.
-To be able to have the movies accessible from a director, a ``relationship`` is
-defined on the ``Director`` entity as well. This will result in a ``movies``
+To be able to have the movies accessible from a director, a ``backref`` is
+defined in the `director` relationship. This will result in a ``movies``
attribute for each director, containing a list of movie objects.
Our ``Director`` entity needs an administration class as well. We will also
@@ -373,11 +374,11 @@ follows::
__tablename__ = 'director'
name = Column( Unicode(60) )
- movies = relationship( 'Movie' )
class Admin( EntityAdmin ):
verbose_name = 'Director'
list_display = [ 'name' ]
+ form_display = list_display + ['movies']
def __unicode__(self):
return self.name or 'unknown director'
@@ -406,7 +407,8 @@ For completeness the two entities are once again listed below::
genre = Column( Unicode( 15 ) )
director_id = Column( Integer, ForeignKey('director.id') )
- director = relationship( 'Director' )
+ director = relationship( 'Director',
+ backref = 'movies' )
class Admin( EntityAdmin ):
verbose_name = 'Movie'
@@ -423,11 +425,11 @@ For completeness the two entities are once again listed below::
__tablename__ = 'director'
name = Column( Unicode(60) )
- movies = relationship( 'Movie' )
class Admin( EntityAdmin ):
verbose_name = 'Director'
list_display = [ 'name' ]
+ form_display = list_display + ['movies']
def __unicode__(self):
return self.name or 'unknown director'
View
5 install.txt
@@ -43,12 +43,13 @@ to crash, so newer versions of those libraries are required.
* QT 4.7
* PyQt 4.8 `PyQt Website <http://www.riverbankcomputing.co.uk/>`_
- * SQLAlchemy 0.7.2 `SQLAlchemy Website <http://www.sqlalchemy.org>`_
+ * SQLAlchemy >= 0.7.7 and < 0.8.0 `SQLAlchemy Website <http://www.sqlalchemy.org>`_
* sqlalchemy-migrate 0.7.1
* Jinja2 2.5.5
* chardet 1.0.1
* xlwt 0.7.2
- * xlrd 0.5.2
+ * xlrd 0.7.1
+ * Elixir 0.7.1 (only needed for examples and default model)
**Releases**
View
18 news.txt
@@ -1,9 +1,25 @@
+Master
+------
* Add SessionTransaction context manager
-Master
+Stable
------
+ * Add :class:`camelot.view.action_steps.print_preview.PrintChart` action step.
+
+ * Adapt printing of charts to matplotlib 1.0
+
+ * Fix `maximum` field attribute of rating fields in editor and delegate.
+
+ * Workaround for form window hiding on Mac
+
+Release 12.06.29
+----------------
+
+ * ``camelot_manage``has been removed, since it did not contain essential functions
+ for the development of Camelot applications.
+
* Port the ``camelot_example`` application and :ref:`tutorial-videostore` to `Declarative`
* Add a toolbar to the form view, configurable through
View
21 test/test_action.py
@@ -108,6 +108,27 @@ def test_select_file( self ):
dialog = select_file.render()
self.grab_widget( dialog )
+ def test_print_chart( self ):
+
+ # begin chart print
+ class ChartPrint( Action ):
+
+ def model_run( self, model_context ):
+ from camelot.container.chartcontainer import BarContainer
+ from camelot.view.action_steps import PrintChart
+ chart = BarContainer( [1, 2, 3, 4],
+ [5, 1, 7, 2] )
+ print_chart_step = PrintChart( chart )
+ print_chart_step.page_orientation = QtGui.QPrinter.Landscape
+ yield print_chart_step
+ # end chart print
+
+ action = ChartPrint()
+ steps = list( action.model_run( self.context ) )
+ dialog = steps[0].render()
+ dialog.show()
+ self.grab_widget( dialog )
+
def test_print_preview( self ):
# begin webkit print
View
95 test/test_search.py
@@ -0,0 +1,95 @@
+import datetime
+import inspect
+import types
+import unittest
+
+import sqlalchemy.types
+
+from camelot.core.conf import settings
+from camelot.core.orm import Entity, Session, has_field
+from camelot.core.sql import metadata
+
+#
+# build a list of the various column types for which the search functions
+# should be tested
+#
+types_to_test = dict()
+for i, (name, definition) in enumerate( sqlalchemy.types.__dict__.items() ):
+ if not inspect.isclass( definition ):
+ continue
+ if definition == sqlalchemy.types.TypeEngine:
+ continue
+ if issubclass( definition, sqlalchemy.types.TypeEngine ):
+ if not issubclass( definition, ( sqlalchemy.types.TypeDecorator,
+ sqlalchemy.types.UserDefinedType,
+ sqlalchemy.types.NullType,
+ sqlalchemy.types._Binary,
+ sqlalchemy.types.Enum ) ):
+ types_to_test[(i, '%s_%i'%(name, i))] = definition
+
+class T( Entity ):
+ """An entity with for each column type a column"""
+ for (i,name), definition in types_to_test.items():
+ has_field( name, definition )
+
+class TAdmin( object ):
+ search_all_fields = True
+ list_search = []
+ entity = T
+
+class SearchCase( unittest.TestCase ):
+ """Test the creation of search queries"""
+
+ def setUp( self ):
+ metadata.bind = settings.ENGINE()
+ metadata.create_all()
+ self.session = Session()
+ #
+ # insert the value of i in each column of T, that can be searched for
+ #
+ for (i,name), definition in types_to_test.items():
+ value = self.value_for_type( definition, i )
+ t = T()
+ setattr( t, name, value )
+ self.session.flush()
+ self.admin = TAdmin()
+
+ def value_for_type( self, definition, i ):
+ value = i
+ if issubclass( definition, sqlalchemy.types.DateTime ):
+ value = datetime.datetime( year = 2000, month = 1, day = 1, hour = 1, minute = i )
+ elif issubclass( definition, sqlalchemy.types.Date ):
+ value = datetime.date( year = 2000, month = 1, day = i%31 )
+ elif issubclass( definition, sqlalchemy.types.Time ):
+ value = datetime.time( hour = 1, minute = i )
+ elif issubclass( definition, sqlalchemy.types.String ):
+ value = str( i )
+ elif issubclass( definition, sqlalchemy.types.Boolean ):
+ value = True
+ return value
+
+ def test_search_decorator( self ):
+ """Verify it search works for most common types"""
+ from camelot.view.search import create_entity_search_query_decorator
+ for (i,name), definition in types_to_test.items():
+ value = self.value_for_type( definition, i )
+ #
+ # @todo : search for types that need special conversion to string
+ # is skipped for now because the test would become too
+ # convoluted, this should work through a to_string field
+ # attribute.
+ #
+ if isinstance( value, ( datetime.date, datetime.time, bool) ):
+ continue
+ string_value = str( value )
+
+ search_decorator = create_entity_search_query_decorator( self.admin,
+ string_value )
+ query = self.session.query( T )
+ query = search_decorator( query )
+
+ #print query
+
+ self.assertTrue( query.count() > 0 )
+
+
View
28 test/test_source.py
@@ -0,0 +1,28 @@
+"""
+Test the quality of the source code
+"""
+
+import os
+import unittest
+
+source_code = os.path.join( os.path.dirname( __file__ ) , '..', 'camelot' )
+
+class SourceQualityCase( unittest.TestCase ):
+
+ def test_deprecated( self ):
+ # make sure no deprecated functions are used
+
+ deprecated = [
+ 'setMargin',
+ ]
+
+ for (dirpath, _dirnames, filenames) in os.walk( source_code ):
+ for filename in filenames:
+ if os.path.splitext( filename )[-1] == '.py':
+ code = open( os.path.join( dirpath, filename ) ).read()
+ for expr in deprecated:
+ if expr in code:
+ raise Exception( '%s in %s/%s'%( expr,
+ dirpath,
+ filename ) )
+
View
3  test/test_view.py
@@ -88,10 +88,11 @@ def test_ChartEditor(self):
editor.set_value( plot )
editor.setMaximumSize( 400, 200 )
self.grab_widget( editor, 'editable' )
- editor.set_field_attributes(editable=False)
+ editor.set_field_attributes( editable=False )
self.grab_widget( editor, 'disabled' )
self.assert_valid_editor( editor, plot )
+
def test_DateEditor(self):
import datetime
editor = self.editors.DateEditor()
Please sign in to comment.
Something went wrong with that request. Please try again.