[gen] Bugfix: slave groups; bugfix: security check for pod fields; security bugfix for pod fields: write access to the field is required for performing any freeze-related action; migration to Appy 0.9.0: dump frozen pod fields on disk; repaired test system for Appy 0.9.0; more explicit error message when using, on some field, a specific write or read permission that is not used in the workflow.
This commit is contained in:
parent
61598b91f7
commit
b4e6333472
|
@ -393,6 +393,7 @@ class File(Field):
|
||||||
cfg = zobj.getProductConfig()
|
cfg = zobj.getProductConfig()
|
||||||
if isinstance(value, cfg.FileUpload) or isinstance(value, cfg.File):
|
if isinstance(value, cfg.FileUpload) or isinstance(value, cfg.File):
|
||||||
# Cases a, b
|
# Cases a, b
|
||||||
|
value.filename = value.filename.replace('/', '-')
|
||||||
info.writeFile(self.name, value, dbFolder)
|
info.writeFile(self.name, value, dbFolder)
|
||||||
elif isinstance(value, UnmarshalledFile):
|
elif isinstance(value, UnmarshalledFile):
|
||||||
# Case c
|
# Case c
|
||||||
|
|
|
@ -231,12 +231,12 @@ class UiGroup:
|
||||||
|
|
||||||
# PX that renders a group of fields (the group is refered as var "field").
|
# PX that renders a group of fields (the group is refered as var "field").
|
||||||
pxView = Px('''
|
pxView = Px('''
|
||||||
<x var="tagCss=field.master and ('slave_%s_%s' % \
|
<x var="tagCss=field.master and ('slave*%s*%s' % \
|
||||||
(field.masterName, '_'.join(field.masterValue))) or '';
|
(field.masterName, '*'.join(field.masterValue))) or '';
|
||||||
widgetCss=field.css_class;
|
widgetCss=field.css_class;
|
||||||
groupCss=tagCss and ('%s %s' % (tagCss, widgetCss)) or widgetCss;
|
groupCss=tagCss and ('%s %s' % (tagCss, widgetCss)) or widgetCss;
|
||||||
tagName=field.master and 'slave' or '';
|
tagName=field.master and 'slave' or '';
|
||||||
tagId='%s_%s' % (zobj.UID(), field.name)">
|
tagId='%s_%s' % (zobj.id, field.name)">
|
||||||
|
|
||||||
<!-- Render the group as a fieldset if required -->
|
<!-- Render the group as a fieldset if required -->
|
||||||
<fieldset if="field.style == 'fieldset'">
|
<fieldset if="field.style == 'fieldset'">
|
||||||
|
|
|
@ -278,9 +278,9 @@ class Pod(Field):
|
||||||
template = template or self.template[0]
|
template = template or self.template[0]
|
||||||
format = format or 'odt'
|
format = format or 'odt'
|
||||||
# Security check.
|
# Security check.
|
||||||
if not noSecurity and not queryData and \
|
if not noSecurity and not queryData:
|
||||||
not self.showTemplate(obj, template):
|
if self.showTemplate and not self.showTemplate(obj, template):
|
||||||
raise Exception(self.UNAUTHORIZED)
|
raise Exception(self.UNAUTHORIZED)
|
||||||
# Return the possibly frozen document (not applicable for query-related
|
# Return the possibly frozen document (not applicable for query-related
|
||||||
# pods).
|
# pods).
|
||||||
if not queryData:
|
if not queryData:
|
||||||
|
@ -445,7 +445,9 @@ class Pod(Field):
|
||||||
def getFreezeFormats(self, obj, template=None):
|
def getFreezeFormats(self, obj, template=None):
|
||||||
'''What are the formats into which the current user may freeze
|
'''What are the formats into which the current user may freeze
|
||||||
p_template?'''
|
p_template?'''
|
||||||
# Manager can always perform freeze actions.
|
# One may have the right to edit the field to freeze anything in it.
|
||||||
|
if not obj.o.mayEdit(self.writePermission): return ()
|
||||||
|
# Manager can perform all freeze actions.
|
||||||
template = template or self.template[0]
|
template = template or self.template[0]
|
||||||
isManager = obj.user.has_role('Manager')
|
isManager = obj.user.has_role('Manager')
|
||||||
if isManager: return self.getAllFormats(template)
|
if isManager: return self.getAllFormats(template)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
import time
|
import os.path, time
|
||||||
from appy.fields.file import FileInfo
|
from appy.fields.file import FileInfo
|
||||||
|
from appy.shared import utils as sutils
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
class Migrator:
|
class Migrator:
|
||||||
|
@ -13,29 +14,58 @@ class Migrator:
|
||||||
self.tool = self.app.config.appy()
|
self.tool = self.app.config.appy()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def migrateFileFields(obj):
|
def migrateBinaryFields(obj):
|
||||||
'''Ensures all file fields on p_obj are FileInfo instances.'''
|
'''Ensures all file and frozen pod fields on p_obj are FileInfo
|
||||||
|
instances.'''
|
||||||
migrated = 0 # Count the number of migrated fields
|
migrated = 0 # Count the number of migrated fields
|
||||||
for field in obj.fields:
|
for field in obj.fields:
|
||||||
if field.type != 'File': continue
|
if field.type == 'File':
|
||||||
oldValue = getattr(obj, field.name)
|
oldValue = getattr(obj, field.name)
|
||||||
if oldValue and not isinstance(oldValue, FileInfo):
|
if oldValue and not isinstance(oldValue, FileInfo):
|
||||||
# A legacy File object. Convert it to a FileInfo instance and
|
# A legacy File object. Convert it to a FileInfo instance
|
||||||
# extract the binary to the filesystem.
|
# and extract the binary to the filesystem.
|
||||||
setattr(obj, field.name, oldValue)
|
setattr(obj, field.name, oldValue)
|
||||||
migrated += 1
|
migrated += 1
|
||||||
|
elif field.type == 'Pod':
|
||||||
|
frozen = getattr(obj.o, field.name, None)
|
||||||
|
if frozen:
|
||||||
|
# Dump this file on disk.
|
||||||
|
tempFolder = sutils.getOsTempFolder()
|
||||||
|
fmt = os.path.splitext(frozen.filename)[1][1:]
|
||||||
|
fileName = os.path.join(tempFolder,
|
||||||
|
'%f.%s' % (time.time(), fmt))
|
||||||
|
f = file(fileName, 'wb')
|
||||||
|
if frozen.data.__class__.__name__ == 'Pdata':
|
||||||
|
# The file content is splitted in several chunks.
|
||||||
|
f.write(frozen.data.data)
|
||||||
|
nextPart = frozen.data.next
|
||||||
|
while nextPart:
|
||||||
|
f.write(nextPart.data)
|
||||||
|
nextPart = nextPart.next
|
||||||
|
else:
|
||||||
|
# Only one chunk
|
||||||
|
f.write(frozen.data)
|
||||||
|
f.close()
|
||||||
|
f = file(fileName)
|
||||||
|
field.freeze(obj, template=field.template[0], format=fmt,
|
||||||
|
noSecurity=True, upload=f,
|
||||||
|
freezeOdtOnError=False)
|
||||||
|
f.close()
|
||||||
|
# Remove the legacy in-zodb file object
|
||||||
|
setattr(obj.o, field.name, None)
|
||||||
|
migrated += 1
|
||||||
return migrated
|
return migrated
|
||||||
|
|
||||||
def migrateTo_0_9_0(self):
|
def migrateTo_0_9_0(self):
|
||||||
'''Migrates this DB to Appy 0.9.x.'''
|
'''Migrates this DB to Appy 0.9.x.'''
|
||||||
# Put all binaries to the filesystem
|
# Put all binaries to the filesystem
|
||||||
tool = self.tool
|
tool = self.tool
|
||||||
tool.log('Migrating file fields...')
|
tool.log('Migrating binary fields...')
|
||||||
context = {'migrate': self.migrateFileFields, 'nb': 0}
|
context = {'migrate': self.migrateBinaryFields, 'nb': 0}
|
||||||
for className in tool.o.getAllClassNames():
|
for className in tool.o.getAllClassNames():
|
||||||
tool.compute(className, context=context, noSecurity=True,
|
tool.compute(className, context=context, noSecurity=True,
|
||||||
expression="ctx['nb'] += ctx['migrate'](obj)")
|
expression="ctx['nb'] += ctx['migrate'](obj)")
|
||||||
tool.log('Migrated %d File field(s).' % context['nb'])
|
tool.log('Migrated %d binary field(s).' % context['nb'])
|
||||||
|
|
||||||
def run(self, force=False):
|
def run(self, force=False):
|
||||||
'''Executes a migration when relevant, or do it for sure if p_force is
|
'''Executes a migration when relevant, or do it for sure if p_force is
|
||||||
|
|
|
@ -9,11 +9,6 @@ except ImportError:
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
class TestMixin:
|
class TestMixin:
|
||||||
'''This class is mixed in with any ZopeTestCase.'''
|
'''This class is mixed in with any ZopeTestCase.'''
|
||||||
def changeUser(self, userId):
|
|
||||||
'''Logs out currently logged user and logs in p_loginName.'''
|
|
||||||
self.logout()
|
|
||||||
self.login(userId)
|
|
||||||
|
|
||||||
def getNonEmptySubModules(self, moduleName):
|
def getNonEmptySubModules(self, moduleName):
|
||||||
'''Returns the list of sub-modules of p_app that are non-empty.'''
|
'''Returns the list of sub-modules of p_app that are non-empty.'''
|
||||||
res = []
|
res = []
|
||||||
|
@ -53,16 +48,7 @@ class TestMixin:
|
||||||
for arg in sys.argv:
|
for arg in sys.argv:
|
||||||
if arg.startswith('[coverage'):
|
if arg.startswith('[coverage'):
|
||||||
return arg[10:].strip(']')
|
return arg[10:].strip(']')
|
||||||
return None
|
return
|
||||||
|
|
||||||
def login(self, name='admin'):
|
|
||||||
user = self.app.acl_users.getUserById(name)
|
|
||||||
newSecurityManager(None, user)
|
|
||||||
|
|
||||||
def logout(self):
|
|
||||||
'''Logs out.'''
|
|
||||||
noSecurityManager()
|
|
||||||
|
|
||||||
def _setup(self): pass
|
def _setup(self): pass
|
||||||
|
|
||||||
# Functions executed before and after every test -------------------------------
|
# Functions executed before and after every test -------------------------------
|
||||||
|
@ -74,7 +60,6 @@ def beforeTest(test):
|
||||||
g['appFolder'] = cfg.diskFolder
|
g['appFolder'] = cfg.diskFolder
|
||||||
moduleOrClassName = g['test'].name # Not used yet.
|
moduleOrClassName = g['test'].name # Not used yet.
|
||||||
# Initialize the test
|
# Initialize the test
|
||||||
test.login('admin')
|
|
||||||
g['t'] = g['test']
|
g['t'] = g['test']
|
||||||
|
|
||||||
def afterTest(test):
|
def afterTest(test):
|
||||||
|
|
|
@ -1201,6 +1201,11 @@ class BaseMixin:
|
||||||
'''Gets, according to the workflow, the roles that are currently granted
|
'''Gets, according to the workflow, the roles that are currently granted
|
||||||
p_permission on this object.'''
|
p_permission on this object.'''
|
||||||
state = self.State(name=False)
|
state = self.State(name=False)
|
||||||
|
if permission not in state.permissions:
|
||||||
|
wf = self.getWorkflow().__name__
|
||||||
|
raise Exception('Permission "%s" not in permissions dict for ' \
|
||||||
|
'state %s.%s' % \
|
||||||
|
(permission, wf, self.State(name=True)))
|
||||||
roles = state.permissions[permission]
|
roles = state.permissions[permission]
|
||||||
if roles: return [role.name for role in roles]
|
if roles: return [role.name for role in roles]
|
||||||
return ()
|
return ()
|
||||||
|
|
|
@ -57,8 +57,8 @@ img { border: 0; vertical-align: middle }
|
||||||
.userStrip a:visited { color: #e7e7e7 }
|
.userStrip a:visited { color: #e7e7e7 }
|
||||||
.breadcrumb { font-size: 11pt; padding-bottom: 6px }
|
.breadcrumb { font-size: 11pt; padding-bottom: 6px }
|
||||||
.login { margin: 3px; color: black }
|
.login { margin: 3px; color: black }
|
||||||
input.button { color: #666666; height: 20px; width: 130px;
|
input.button { color: #666666; height: 20px; width: 130px; margin-bottom: 5px;
|
||||||
cursor:pointer; font-size: 90%; padding: 1px 0 0 10px;
|
cursor:pointer; font-size: 90%; padding-left: 10px;
|
||||||
background-color: white; background-repeat: no-repeat;
|
background-color: white; background-repeat: no-repeat;
|
||||||
background-position: 5% 25%; box-shadow: 2px 2px 2px #888888}
|
background-position: 5% 25%; box-shadow: 2px 2px 2px #888888}
|
||||||
input.buttonSmall { width: 100px !important; font-size: 85%; height: 18px;
|
input.buttonSmall { width: 100px !important; font-size: 85%; height: 18px;
|
||||||
|
|
|
@ -647,4 +647,8 @@ class ToolWrapper(AbstractWrapper):
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
failed.append(startObject)
|
failed.append(startObject)
|
||||||
return nb, failed
|
return nb, failed
|
||||||
|
|
||||||
|
def _login(self, login):
|
||||||
|
'''Performs a login programmatically. Used by the test system.'''
|
||||||
|
self.request.user = self.search1('User', noSecurity=True, login=login)
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
|
@ -730,7 +730,7 @@ class AbstractWrapper(object):
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<%s appyobj at %s>' % (self.klass.__name__, id(self))
|
return '<%s at %s>' % (self.klass.__name__, id(self))
|
||||||
|
|
||||||
def __cmp__(self, other):
|
def __cmp__(self, other):
|
||||||
if other: return cmp(self.o, other.o)
|
if other: return cmp(self.o, other.o)
|
||||||
|
|
Loading…
Reference in a new issue