[gen] Binary files stored in fields appy.fields.File are now stored outside the ZODB, on the filesystem; Ref fields can now also be rendered as dropdown menus: every menu represents a coherent group of link

ed objects. The main menu entry can be textual or an icon; computed fields are by default rendered in view and cell layouts.
This commit is contained in:
Gaetan Delannay 2014-02-26 10:40:27 +01:00
parent b9dcc94bdb
commit be145be254
12 changed files with 522 additions and 313 deletions

View file

@ -169,6 +169,9 @@ class ZopeInstaller:
appyTool = tool.appy()
appyTool.log('Appy version is "%s".' % appy.version.short)
# Execute custom pre-installation code if any.
if hasattr(appyTool, 'beforeInstall'): appyTool.beforeInstall()
# Create the default users if they do not exist.
for login, roles in self.defaultUsers.iteritems():
if not appyTool.count('User', noSecurity=True, login=login):

View file

@ -9,123 +9,18 @@ class Migrator:
self.installer = installer
self.logger = installer.logger
self.app = installer.app
self.tool = self.app.config.appy()
bypassRoles = ('Authenticated', 'Member')
bypassGroups = ('Administrators', 'Reviewers')
def migrateUsers(self, ploneSite):
'''Migrate users from Plone's acl_users to Zope acl_users with
corresponding Appy objects.'''
# First of all, remove the Plone-patched root acl_users by a standard
# (hum, Appy-patched) Zope UserFolder.
tool = self.app.config.appy()
from AccessControl.User import manage_addUserFolder
self.app.manage_delObjects(ids=['acl_users'])
manage_addUserFolder(self.app)
# Put an admin user into it
newUsersDb = self.app.acl_users
newUsersDb._doAddUser('admin', 'admin', ['Manager'], ())
# Copy users from Plone acl_users to Zope acl_users
for user in ploneSite.acl_users.getUsers():
id = user.getId()
userRoles = user.getRoles()
for br in self.bypassRoles:
if br in userRoles: userRoles.remove(br)
userInfo = ploneSite.portal_membership.getMemberById(id)
userName = userInfo.getProperty('fullname') or id
userEmail = userInfo.getProperty('email') or ''
appyUser = tool.create('users', login=id,
password1='fake', password2='fake', roles=userRoles,
name=userName, firstName=' ', email=userEmail)
appyUser.title = appyUser.title.strip()
# Set the correct password
password = ploneSite.acl_users.source_users._user_passwords[id]
newUsersDb.data[id].__ = password
# Manage groups. Exclude not-used default Plone groups.
for groupId in user.getGroups():
if groupId in self.bypassGroups: continue
if tool.count('Group', noSecurity=True, login=groupId):
# The Appy group already exists, get it
appyGroup = tool.search('Group', noSecurity=True,
login=groupId)[0]
else:
# Create the group. Todo: get Plone group roles and title
appyGroup = tool.create('groups', login=groupId,
title=groupId)
appyGroup.addUser(appyUser)
def reindexObject(self, obj):
obj.reindex()
i = 1
for subObj in obj.objectValues():
i += self.reindexObject(subObj)
return i # The number of reindexed (sub-)object(s)
def migrateTo_0_8_0(self):
'''Migrates a Plone-based (<= 0.7.1) Appy app to a Ploneless (0.8.0)
Appy app.'''
self.logger.info('Migrating to Appy 0.8.0...')
# Find the Plone site. It must be at the root of the Zope tree.
ploneSite = None
for obj in self.app.objectValues():
if obj.__class__.__name__ == 'PloneSite':
ploneSite = obj
break
# As a preamble: delete translation objects from self.app.config: they
# will be copied from the old tool.
self.app.config.manage_delObjects(ids=self.app.config.objectIds())
# Migrate data objects:
# - from oldDataFolder to self.app.data
# - from oldTool to self.app.config (excepted translation
# objects that were re-created from i18n files).
appName = self.app.config.getAppName()
for oldFolderName in (appName, 'portal_%s' % appName.lower()):
oldFolder = getattr(ploneSite, oldFolderName)
objectIds = [id for id in oldFolder.objectIds()]
cutted = oldFolder.manage_cutObjects(ids=objectIds)
if oldFolderName == appName:
destFolder = self.app.data
else:
destFolder = self.app.config
destFolder.manage_pasteObjects(cutted)
i = 0
for obj in destFolder.objectValues():
i += self.reindexObject(obj)
self.logger.info('%d objects imported into %s.' % \
(i, destFolder.getId()))
if oldFolderName != appName:
# Re-link objects copied into the self.app.config with the Tool
# through Ref fields.
tool = self.app.config.appy()
pList = tool.o.getProductConfig().PersistentList
for field in tool.fields:
if field.type != 'Ref': continue
n = field.name
if n in ('users', 'groups'): continue
uids = getattr(oldFolder, n)
if uids:
# Update the forward reference
setattr(tool.o, n, pList(uids))
# Update the back reference
for obj in getattr(tool, n):
backList = getattr(obj.o, field.back.name)
backList.remove(oldFolder._at_uid)
backList.append(tool.uid)
self.logger.info('config.%s: linked %d object(s)' % \
(n, len(uids)))
else:
self.logger.info('config.%s: no object to link.' % n)
self.migrateUsers(ploneSite)
self.logger.info('Migration done.')
def migrateTo_0_9_0(self):
'''Migrates this DB to Appy 0.9.x.'''
pass
def run(self):
if self.app.acl_users.__class__.__name__ == 'UserFolder':
return # Already Ploneless
tool = self.app.config.appy()
appyVersion = tool.appyVersion
if not appyVersion or (appyVersion < '0.8.0'):
appyVersion = self.tool.appyVersion
if not appyVersion or (appyVersion < '0.9.0'):
# Migration is required.
startTime = time.time()
self.migrateTo_0_8_0()
self.migrateTo_0_9_0()
stopTime = time.time()
elapsed = (stopTime-startTime) / 60.0
self.logger.info('Migration done in %d minute(s).' % elapsed)

View file

@ -3,7 +3,7 @@
- mixins/ToolMixin is mixed in with the generated application Tool class.'''
# ------------------------------------------------------------------------------
import os, os.path, sys, types, urllib, cgi
import os, os.path, re, sys, types, urllib, cgi
from appy import Object
from appy.px import Px
from appy.fields.workflow import UiTransition
@ -11,11 +11,14 @@ import appy.gen as gen
from appy.gen.utils import *
from appy.gen.layout import Table, defaultPageLayouts
from appy.gen.descriptors import WorkflowDescriptor, ClassDescriptor
from appy.shared.utils import sequenceTypes,normalizeText,Traceback,getMimeType
from appy.shared import utils as sutils
from appy.shared.data import rtlLanguages
from appy.shared.xml_parser import XmlMarshaller
from appy.shared.diff import HtmlDiff
# ------------------------------------------------------------------------------
NUMBERED_ID = re.compile('.+\d{4}$')
# ------------------------------------------------------------------------------
class BaseMixin:
'''Every Zope class generated by appy.gen inherits from this class or a
@ -135,6 +138,11 @@ class BaseMixin:
field.back.unlinkObject(obj, self, back=True)
# Uncatalog the object
self.reindex(unindex=True)
# Delete the filesystem folder corresponding to this object
folder = os.path.join(*self.getFsFolder())
if os.path.exists(folder):
sutils.FolderDeleter.delete(folder)
sutils.FolderDeleter.deleteEmpty(os.path.dirname(folder))
# Delete the object
self.getParentNode().manage_delObjects([self.id])
@ -210,6 +218,39 @@ class BaseMixin:
obj = createObject(tool.getPath('/temp_folder'), id, className, appName)
return self.goto(obj.getUrl(**urlParams))
def getDbFolder(self):
'''Gets the folder, on the filesystem, where the database (Data.fs and
sub-folders) lies.'''
return os.path.dirname(self.getTool().getApp()._p_jar.db().getName())
def getFsFolder(self, create=False):
'''Gets the folder where binary files tied to this object will be stored
on the filesystem. If p_create is True and the folder does not exist,
it is created (together with potentially missing parent folders).
This folder is returned as a tuple (s_baseDbFolder, s_subPath).'''
objId = self.id
# Get the root folder where Data.fs lies.
dbFolder = self.getDbFolder()
# Build the list of path elements within this db folder.
path = []
inConfig = False
for elem in self.getPhysicalPath():
if not elem: continue
if elem == 'data': continue
if elem == 'config': inConfig = True
if not path or ((len(path) == 1) and inConfig):
# This object is at the root of the filesystem.
if NUMBERED_ID.match(elem):
path.append(elem[-4:])
path.append(elem)
# We are done if elem corresponds to the object id.
if elem == objId: break
path = os.sep.join(path)
if create:
fullPath = os.path.join(dbFolder, path)
if not os.path.exists(fullPath): os.makedirs(fullPath)
return dbFolder, path
def view(self):
'''Returns the view PX.'''
obj = self.appy()
@ -504,7 +545,7 @@ class BaseMixin:
else:
res = XmlMarshaller().marshall(methodRes, objectType='appy')
except Exception, e:
tb = Traceback.get()
tb = sutils.Traceback.get()
res = XmlMarshaller().marshall(tb, objectType='appy')
return res
@ -1132,7 +1173,7 @@ class BaseMixin:
elif resultType.startswith('file'):
# msg does not contain a message, but a file instance.
response = self.REQUEST.RESPONSE
response.setHeader('Content-Type', getMimeType(msg.name))
response.setHeader('Content-Type', sutils.getMimeType(msg.name))
response.setHeader('Content-Disposition', 'inline;filename="%s"' %\
os.path.basename(msg.name))
response.write(msg.read())
@ -1218,7 +1259,7 @@ class BaseMixin:
def SortableTitle(self):
'''Returns the title as must be stored in index "SortableTitle".'''
return normalizeText(self.Title())
return sutils.normalizeText(self.Title())
def SearchableText(self):
'''This method concatenates the content of every field with
@ -1522,17 +1563,10 @@ class BaseMixin:
(not appyType.isShowable(self, 'result')):
from zExceptions import NotFound
raise NotFound()
theFile = getattr(self.aq_base, name, None)
if theFile:
response = self.REQUEST.RESPONSE
response.setHeader('Content-Disposition', 'inline;filename="%s"' % \
theFile.filename)
# Define content type
if theFile.content_type:
response.setHeader('Content-Type', theFile.content_type)
response.setHeader('Cachecontrol', 'no-cache')
response.setHeader('Expires', 'Thu, 11 Dec 1975 12:05:05 GMT')
return theFile.index_html(self.REQUEST, self.REQUEST.RESPONSE)
info = getattr(self.aq_base, name, None)
if info:
# Write the file in the HTTP response.
info.writeResponse(self.REQUEST.RESPONSE, self.getDbFolder())
def upload(self):
'''Receives an image uploaded by the user via ckeditor and stores it in

View file

@ -100,6 +100,11 @@ img { border: 0; vertical-align: middle }
.popup { display: none; position: absolute; top: 30%; left: 35%;
width: 350px; z-index : 100; background: white; padding: 8px;
border: 1px solid grey }
.dropdown { display:none; position: absolute; border: 1px solid #cccccc;
background-color: white; padding-top: 4px }
.dropdownMenu { cursor: pointer; padding-right: 4px }
.dropdown a { padding: 0 0.5em }
.dropdown a:hover { text-decoration: underline }
.list { margin-bottom: 3px }
.list td, .list th { border: 1px solid grey;
padding-left: 5px; padding-right: 5px; padding-top: 3px }

View file

@ -244,6 +244,18 @@ function toggleCheckbox(visibleCheckbox, hiddenBoolean) {
else hidden.value = 'False';
}
// Shows/hides a dropdown menu
function toggleDropdown(dropdownId, forcedValue){
var dropdown = document.getElementById(dropdownId);
// Force to p_forcedValue if specified
if (forcedValue) {dropdown.style.display = forcedValue}
else {
var displayValue = dropdown.style.display;
if (displayValue == 'block') dropdown.style.display = 'none';
else dropdown.style.display = 'block';
}
}
// Function that sets a value for showing/hiding sub-titles.
function setSubTitles(value, tag) {
createCookie('showSubTitles', value);

View file

@ -14,9 +14,6 @@ from appy.shared.xml_parser import XmlMarshaller
from appy.shared.csv_parser import CsvMarshaller
# Some error messages ----------------------------------------------------------
WRONG_FILE_TUPLE = 'This is not the way to set a file. You can specify a ' \
'2-tuple (fileName, fileContent) or a 3-tuple (fileName, fileContent, ' \
'mimeType).'
FREEZE_ERROR = 'Error while trying to freeze a "%s" file in POD field ' \
'"%s" (%s).'
FREEZE_FATAL_ERROR = 'A server error occurred. Please contact the system ' \