[gen] Ref.select can now hold a Search instance; began implementation of Ref.link == 'popup'.

This commit is contained in:
Gaetan Delannay 2014-07-18 16:54:11 +02:00
parent a466513fb3
commit b2a2aa5210
7 changed files with 102 additions and 37 deletions

View file

@ -18,6 +18,7 @@
import sys, re import sys, re
from appy import Object from appy import Object
from appy.fields import Field from appy.fields import Field
from appy.fields.search import Search
from appy.px import Px from appy.px import Px
from appy.gen.layout import Table from appy.gen.layout import Table
from appy.gen import utils as gutils from appy.gen import utils as gutils
@ -302,6 +303,7 @@ class Ref(Field):
batchSize=info.batchSize; batchSize=info.batchSize;
batchNumber=len(objects); batchNumber=len(objects);
tiedClassName=tiedClassName|ztool.getPortalType(field.klass); tiedClassName=tiedClassName|ztool.getPortalType(field.klass);
target=ztool.getLinksTargetInfo(field.klass);
mayEdit=mayEdit|\ mayEdit=mayEdit|\
not field.isBack and zobj.mayEdit(field.writePermission); not field.isBack and zobj.mayEdit(field.writePermission);
mayUnlink=False; mayUnlink=False;
@ -431,8 +433,8 @@ class Ref(Field):
</x>''') </x>''')
pxCell = pxView pxCell = pxView
pxEdit = Px(''' pxEdit = Px('''<x if="(field.link) and (field.link != 'list')">
<select if="field.link" <select if="field.link != 'popup'"
var2="objects=field.getPossibleValues(zobj); var2="objects=field.getPossibleValues(zobj);
uids=[o.id for o in field.getValue(zobj, appy=False)]" uids=[o.id for o in field.getValue(zobj, appy=False)]"
name=":name" id=":name" size=":isMultiple and field.height or ''" name=":name" id=":name" size=":isMultiple and field.height or ''"
@ -445,7 +447,19 @@ class Ref(Field):
selected=":inRequest and (uid in requestValue) or \ selected=":inRequest and (uid in requestValue) or \
(uid in uids)" value=":uid" (uid in uids)" value=":uid"
title=":title">:ztool.truncateValue(title, field.width)</option> title=":title">:ztool.truncateValue(title, field.width)</option>
</select>''') </select>
<!-- A button for opening the popup if field.link is "popup". -->
<a if="field.link == 'popup'" target="appyIFrame"
var2="tiedClassName=ztool.getPortalType(field.klass);
className=ztool.getPortalType(obj.klass)"
href=":'%s/query?className=%s&amp;search=%s:%s&amp;popup=1' % \
(ztool.absolute_url(), tiedClassName, className, field.name)">
<input type="button" class="buttonSmall button"
var="label=_('search_button')" value=":label"
style=":'%s; %s' % (url('search', bg=True), \
ztool.getButtonWidth(label))"
onclick="openPopup('iframePopup')"/>
</a></x>''')
pxSearch = Px(''' pxSearch = Px('''
<!-- The "and" / "or" radio buttons --> <!-- The "and" / "or" radio buttons -->
@ -509,7 +523,8 @@ class Ref(Field):
# "list", the user will, on the view page, choose objects from a list # "list", the user will, on the view page, choose objects from a list
# of objects which is similar to those rendered in pxViewList; # of objects which is similar to those rendered in pxViewList;
# "popup", the user will, on the edit page, choose objects from a popup # "popup", the user will, on the edit page, choose objects from a popup
# menu. # menu. In this case, parameter "select" must hold a Search
# instance.
self.link = link self.link = link
# May the user unlink existing objects? # May the user unlink existing objects?
self.unlink = unlink self.unlink = unlink
@ -558,7 +573,6 @@ class Ref(Field):
self.isBack = False self.isBack = False
# Initialise the backward reference # Initialise the backward reference
self.back = back self.back = back
back.isBack = True
back.back = self back.back = self
# klass may be None in the case we are defining an auto-Ref to the # klass may be None in the case we are defining an auto-Ref to the
# same class as the class where this field is defined. In this case, # same class as the class where this field is defined. In this case,
@ -568,6 +582,8 @@ class Ref(Field):
# K.myField.klass = K # K.myField.klass = K
# setattr(K, K.myField.back.attribute, K.myField.back) # setattr(K, K.myField.back.attribute, K.myField.back)
if klass: setattr(klass, back.attribute, back) if klass: setattr(klass, back.attribute, back)
else:
self.isBack = True
# When displaying a tabular list of referenced objects, must we show # When displaying a tabular list of referenced objects, must we show
# the table headers? # the table headers?
self.showHeaders = showHeaders self.showHeaders = showHeaders
@ -586,9 +602,13 @@ class Ref(Field):
# - necessary because in some cases we do not have an instance at our # - necessary because in some cases we do not have an instance at our
# disposal, ie, when we need to compute a list of objects on a # disposal, ie, when we need to compute a list of objects on a
# search screen. # search screen.
# "select" can also hold a Search instance. In this case, put any name
# in Search's mandatory parameter "name": it will be ignored and
# replaced with an internal technical name.
# NOTE that when a method is defined in field "masterValue" (see parent # NOTE that when a method is defined in field "masterValue" (see parent
# class "Field"), it will be used instead of select (or sselect below). # class "Field"), it will be used instead of select (or sselect below).
self.select = select self.select = select
if isinstance(select, Search): self.select.name = '_field_'
# If you want to specify, for the search screen, a list of objects that # If you want to specify, for the search screen, a list of objects that
# is different from the one produced by self.select, define an # is different from the one produced by self.select, define an
# alternative method in field "sselect" below. # alternative method in field "sselect" below.
@ -658,6 +678,17 @@ class Ref(Field):
historized, mapping, label, sdefault, scolspan, swidth, historized, mapping, label, sdefault, scolspan, swidth,
sheight, persist) sheight, persist)
self.validable = bool(self.link) self.validable = bool(self.link)
self.checkParameters()
def checkParameters(self):
'''Ensures this Ref is correctly defined.'''
# For forward Refs, "add" and "link" can't both be used.
if not self.isBack and (self.add and self.link):
raise Exception('Parameters "add" and "link" can\'t both be used.')
# If link is "popup", "select" must hold a Search instance.
if (self.link == 'popup') and not isinstance(self.select, Search):
raise Exception('When "link" is "popup", "select" must be a ' \
'appy.fields.search.Search instance.')
def getDefaultLayouts(self): def getDefaultLayouts(self):
return {'view': Table('l-f', width='100%'), 'edit': 'lrv-f'} return {'view': Table('l-f', width='100%'), 'edit': 'lrv-f'}
@ -744,6 +775,8 @@ class Ref(Field):
''' '''
req = obj.REQUEST req = obj.REQUEST
obj = obj.appy() obj = obj.appy()
paginated = startNumber != None
isSearch = False
if 'masterValues' in req: if 'masterValues' in req:
# Convert masterValue(s) from id(s) to real object(s). # Convert masterValue(s) from id(s) to real object(s).
masterValues = req['masterValues'].strip() masterValues = req['masterValues'].strip()
@ -764,10 +797,20 @@ class Ref(Field):
objects = [] objects = []
else: else:
if not self.select: if not self.select:
# No select method has been defined: we must retrieve all # No select method or search has been defined: we must
# objects of the referred type that the user is allowed to # retrieve all objects of the referred type that the user
# access. # is allowed to access.
objects = obj.search(self.klass) objects = obj.search(self.klass)
else:
if isinstance(self.select, Search):
isSearch = True
maxResults = paginated and self.maxPerPage or 'NO_LIMIT'
start = startNumber or 0
className = obj.tool.o.getPortalType(self.klass)
objects = obj.o.executeQuery(className,
startNumber=start, search=self.select,
maxResults=maxResults)
objects.objects = [o.appy() for o in objects.objects]
else: else:
objects = self.select(obj) objects = self.select(obj)
# Remove already linked objects if required. # Remove already linked objects if required.
@ -775,19 +818,25 @@ class Ref(Field):
uids = getattr(obj.o.aq_base, self.name, None) uids = getattr(obj.o.aq_base, self.name, None)
if uids: if uids:
# Browse objects in reverse order and remove linked objects. # Browse objects in reverse order and remove linked objects.
i = len(objects) - 1 if isSearch: objs = objects.objects
else: objs = objects
i = len(objs) - 1
while i >= 0: while i >= 0:
if objects[i].uid in uids: del objects[i] if objs[i].id in uids: del objs[i]
i -= 1 i -= 1
# Restrict, if required, the result to self.maxPerPage starting at # If possible values are not retrieved from a Search, restrict (if
# p_startNumber. Unlike m_getValue, we already have all objects in # required) the result to self.maxPerPage starting at p_startNumber.
# "objects": we can't limit objects "waking up" to at most # Indeed, in this case, unlike m_getValue, we already have all objects
# in "objects": we can't limit objects "waking up" to at most
# self.maxPerPage. # self.maxPerPage.
if paginated and not isSearch:
total = len(objects) total = len(objects)
if startNumber != None:
objects = objects[startNumber:startNumber + self.maxPerPage] objects = objects[startNumber:startNumber + self.maxPerPage]
# Return the result, wrapped in a SomeObjects instance if required. # Return the result, wrapped in a SomeObjects instance if required.
if not someObjects: return objects if not someObjects:
if isSearch: return objects.objects
return objects
if isSearch: return objects
res = gutils.SomeObjects() res = gutils.SomeObjects()
res.totalNumber = total res.totalNumber = total
res.batchSize = self.maxPerPage res.batchSize = self.maxPerPage

View file

@ -167,12 +167,12 @@ class UiSearch:
label = '%s_plural' % className label = '%s_plural' % className
elif search.name == 'customSearch': elif search.name == 'customSearch':
label = 'search_results' label = 'search_results'
elif search.name == '_field_':
label = None
else: else:
label = '%s_search_%s' % (className, search.name) label = '%s_search_%s' % (className, search.name)
labelDescr = label + '_descr' labelDescr = label + '_descr'
self.translated = tool.translate(label) _ = tool.translate
if labelDescr: self.translated = label and _(label) or ''
self.translatedDescr = tool.translate(labelDescr) self.translatedDescr = labelDescr and _(labelDescr) or ''
else:
self.translatedDescr = ''
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -73,13 +73,15 @@ class ToolMixin(BaseMixin):
url = self.goto('%s/home' % self.absolute_url()) url = self.goto('%s/home' % self.absolute_url())
return url return url
def getHomeObject(self): def getHomeObject(self, inPopup=False):
'''The concept of "home object" is the object where the user must "be", '''The concept of "home object" is the object where the user must "be",
even if he is "nowhere". For example, if the user is on a search even if he is "nowhere". For example, if the user is on a search
screen, there is no contextual object. In this case, if we have a screen, there is no contextual object. In this case, if we have a
home object for him, we will use it as contextual object, and its home object for him, we will use it as contextual object, and its
portlet menu will nevertheless appear: the user will not have the portlet menu will nevertheless appear: the user will not have the
feeling of being lost.''' feeling of being lost.'''
# If we are in the popup, we do not want any home object in the way.
if inPopup: return
# If the app defines a method "getHomeObject", call it. # If the app defines a method "getHomeObject", call it.
try: try:
return self.appy().getHomeObject() return self.appy().getHomeObject()
@ -727,6 +729,10 @@ class ToolMixin(BaseMixin):
# It is a custom search whose parameters are in the session. # It is a custom search whose parameters are in the session.
fields = self.REQUEST.SESSION['searchCriteria'] fields = self.REQUEST.SESSION['searchCriteria']
res = Search('customSearch', **fields) res = Search('customSearch', **fields)
elif ':' in name:
# The search is defined in a Ref field with link=popup
refClass, ref = name.split(':')
res = getattr(self.getAppyClass(refClass), ref).select
elif name: elif name:
appyClass = self.getAppyClass(className) appyClass = self.getAppyClass(className)
# Search among static searches # Search among static searches

View file

@ -210,11 +210,11 @@ function askAjaxChunk(hook,mode,url,px,params,beforeSend,onGet) {
/* The functions below wrap askAjaxChunk for getting specific content through /* The functions below wrap askAjaxChunk for getting specific content through
an Ajax request. */ an Ajax request. */
function askQueryResult(hookId, objectUrl, className, searchName, function askQueryResult(hookId, objectUrl, className, searchName, popup,
startNumber, sortKey, sortOrder, filterKey) { startNumber, sortKey, sortOrder, filterKey) {
// Sends an Ajax request for getting the result of a query. // Sends an Ajax request for getting the result of a query.
var params = {'className': className, 'search': searchName, var params = {'className': className, 'search': searchName,
'startNumber': startNumber}; 'startNumber': startNumber, 'popup': popup};
if (sortKey) params['sortKey'] = sortKey; if (sortKey) params['sortKey'] = sortKey;
if (sortOrder) params['sortOrder'] = sortOrder; if (sortOrder) params['sortOrder'] = sortOrder;
if (filterKey) { if (filterKey) {
@ -725,15 +725,24 @@ function openPopup(popupId, msg, width, height) {
} }
// Open the popup // Open the popup
var popup = document.getElementById(popupId); var popup = document.getElementById(popupId);
// Put it at the right place on the screen // Put it at the right place on the screen and give it the right dimensions
var scrollTop = document.documentElement.scrollTop || window.pageYOffset || 0; var scrollTop = document.documentElement.scrollTop || window.pageYOffset || 0;
popup.style.top = (scrollTop + 150) + 'px'; popup.style.top = (scrollTop + 150) + 'px';
if (width) popup.style.width = width + 'px'; if (width) popup.style.width = width + 'px';
if (height) popup.style.height = height + 'px';
if (popupId == 'iframePopup') { if (popupId == 'iframePopup') {
// Initialize iframe's width. // Initialize iframe's width.
var iframe = document.getElementById('appyIFrame'); var iframe = document.getElementById('appyIFrame');
if (!width) width = window.innerWidth - 200;
if (!height) {
height = window.innerHeight - 200;
popup.style.top = ((window.innerHeight - height) / 2).toFixed() + 'px';
}
popup.style.left = ((window.innerWidth - width) / 2).toFixed() + 'px';
popup.style.width = width + 'px';
iframe.style.width = (width-20) + 'px'; iframe.style.width = (width-20) + 'px';
if (height) iframe.style.height = height + 'px'; popup.style.height = height + 'px';
iframe.style.height = (height-20) + 'px';
} }
popup.style.display = 'block'; popup.style.display = 'block';
} }

View file

@ -431,9 +431,9 @@ class ToolWrapper(AbstractWrapper):
batchSize=queryResult.batchSize; batchSize=queryResult.batchSize;
batchNumber=len(zobjects); batchNumber=len(zobjects);
ajaxHookId='queryResult'; ajaxHookId='queryResult';
navBaseCall='askQueryResult(%s,%s,%s,%s,**v**)' % \ navBaseCall='askQueryResult(%s,%s,%s,%s,%s,**v**)' % \
(q(ajaxHookId), q(ztool.absolute_url()), q(className), \ (q(ajaxHookId), q(ztool.absolute_url()), q(className), \
q(searchName)); q(searchName),int(inPopup));
showNewSearch=showNewSearch|True; showNewSearch=showNewSearch|True;
enableLinks=enableLinks|True; enableLinks=enableLinks|True;
newSearchUrl='%s/search?className=%s%s' % \ newSearchUrl='%s/search?className=%s%s' % \
@ -446,7 +446,7 @@ class ToolWrapper(AbstractWrapper):
<!-- Display here POD templates if required. --> <!-- Display here POD templates if required. -->
<table var="fields=ztool.getResultPodFields(className); <table var="fields=ztool.getResultPodFields(className);
layoutType='view'" layoutType='view'"
if="zobjects and fields" align=":dright"> if="not inPopup and zobjects and fields" align=":dright">
<tr> <tr>
<td var="zobj=zobjects[0]; obj=zobj.appy()" <td var="zobj=zobjects[0]; obj=zobj.appy()"
for="field in fields" for="field in fields"
@ -455,7 +455,7 @@ class ToolWrapper(AbstractWrapper):
</table> </table>
<!-- The title of the search --> <!-- The title of the search -->
<p> <p if="not inPopup">
<x>::uiSearch.translated</x> (<span class="discreet">:totalNumber</span>) <x>::uiSearch.translated</x> (<span class="discreet">:totalNumber</span>)
<x if="showNewSearch and (searchName == 'customSearch')">&nbsp;&mdash; <x if="showNewSearch and (searchName == 'customSearch')">&nbsp;&mdash;
&nbsp;<i><a href=":newSearchUrl">:_('search_new')</a></i> &nbsp;<i><a href=":newSearchUrl">:_('search_new')</a></i>

View file

@ -6,6 +6,7 @@ from appy.gen import utils as gutils
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class UserWrapper(AbstractWrapper): class UserWrapper(AbstractWrapper):
workflow = WorkflowOwner workflow = WorkflowOwner
specialUsers = ('system', 'anon', 'admin')
def showLogin(self): def showLogin(self):
'''When must we show the login field?''' '''When must we show the login field?'''
@ -42,7 +43,7 @@ class UserWrapper(AbstractWrapper):
if not self.login or (login != self.login): if not self.login or (login != self.login):
# A new p_login is requested. Check if it is valid and free. # A new p_login is requested. Check if it is valid and free.
# Some logins are not allowed. # Some logins are not allowed.
if login in ('admin', 'anon', 'system'): if login in self.specialUsers:
return self.translate('login_reserved') return self.translate('login_reserved')
# Check that no user or group already uses this login. # Check that no user or group already uses this login.
if self.count('User', noSecurity=True, login=login) or \ if self.count('User', noSecurity=True, login=login) or \
@ -219,7 +220,7 @@ class UserWrapper(AbstractWrapper):
def mayEdit(self): def mayEdit(self):
'''No one can edit users "system" and "anon".''' '''No one can edit users "system" and "anon".'''
if self.o.id in ('system', 'anon'): return if self.o.id in ('anon', 'system'): return
# Call custom "mayEdit" when present. # Call custom "mayEdit" when present.
custom = self._getCustomMethod('mayEdit') custom = self._getCustomMethod('mayEdit')
if custom: return self._callCustom('mayEdit') if custom: return self._callCustom('mayEdit')
@ -227,7 +228,7 @@ class UserWrapper(AbstractWrapper):
def mayDelete(self): def mayDelete(self):
'''No one can delete users "system", "anon" and "admin".''' '''No one can delete users "system", "anon" and "admin".'''
if self.o.id in ('system', 'anon', 'admin'): return if self.o.id in self.specialUsers: return
# Call custom "mayDelete" when present. # Call custom "mayDelete" when present.
custom = self._getCustomMethod('mayDelete') custom = self._getCustomMethod('mayDelete')
if custom: return self._callCustom('mayDelete') if custom: return self._callCustom('mayDelete')

View file

@ -75,16 +75,16 @@ class AbstractWrapper(object):
# The template PX for all pages. # The template PX for all pages.
pxTemplate = Px(''' pxTemplate = Px('''
<html var="ztool=tool.o; user=tool.user; <html var="ztool=tool.o; user=tool.user;
obj=obj or ztool.getHomeObject(); req=ztool.REQUEST; resp=req.RESPONSE;
inPopup=req.get('popup') == '1';
obj=obj or ztool.getHomeObject(inPopup);
zobj=obj and obj.o or None; zobj=obj and obj.o or None;
isAnon=user.login=='anon'; app=ztool.getApp(); isAnon=user.login=='anon'; app=ztool.getApp();
appFolder=app.data; url = ztool.getIncludeUrl; appFolder=app.data; url = ztool.getIncludeUrl;
appName=ztool.getAppName(); _=ztool.translate; appName=ztool.getAppName(); _=ztool.translate;
req=ztool.REQUEST; resp=req.RESPONSE;
dummy=setattr(req, 'pxContext', _ctx_); dummy=setattr(req, 'pxContext', _ctx_);
lang=ztool.getUserLanguage(); q=ztool.quote; lang=ztool.getUserLanguage(); q=ztool.quote;
layoutType=ztool.getLayoutType(); layoutType=ztool.getLayoutType();
inPopup=req.get('popup') == '1';
showPortlet=not inPopup and ztool.showPortlet(obj, layoutType); showPortlet=not inPopup and ztool.showPortlet(obj, layoutType);
dir=ztool.getLanguageDirection(lang); dir=ztool.getLanguageDirection(lang);
cfg=ztool.getProductConfig(True); cfg=ztool.getProductConfig(True);