[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:
parent
b9dcc94bdb
commit
be145be254
12 changed files with 522 additions and 313 deletions
gen
|
@ -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):
|
||||
|
|
119
gen/migrator.py
119
gen/migrator.py
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 ' \
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue