From e11e75430522e84ae819584e2f96eab93ec6dd43 Mon Sep 17 00:00:00 2001 From: Gaetan Delannay Date: Mon, 16 Jun 2014 00:58:45 +0200 Subject: [PATCH] [gen] Class.popup: finalized the development of 'popup' classes. --- fields/file.py | 2 +- fields/phase.py | 5 +- fields/ref.py | 157 ++++++++++++++++++++++++------------ gen/mixins/ToolMixin.py | 37 +++++++-- gen/mixins/__init__.py | 58 +++++++------ gen/tr/Appy.pot | 4 + gen/ui/appy.css | 9 ++- gen/ui/appy.js | 38 +++++++-- gen/ui/close.png | Bin 226 -> 219 bytes gen/wrappers/ToolWrapper.py | 58 ++++++++----- gen/wrappers/__init__.py | 39 ++++++--- 11 files changed, 286 insertions(+), 121 deletions(-) diff --git a/fields/file.py b/fields/file.py index 657b90e..5e42a18 100644 --- a/fields/file.py +++ b/fields/file.py @@ -307,7 +307,7 @@ class File(Field): size=":field.width"/> ''') + (fName, isDisabled)''') pxSearch = '' diff --git a/fields/phase.py b/fields/phase.py index 8858167..21ce046 100644 --- a/fields/phase.py +++ b/fields/phase.py @@ -33,12 +33,13 @@ class Phase: class=":(aPage == page) and 'currentPage' or ''"> - ::aPageInfo.page.getLabel(zobj) + ::aPageInfo.page.getLabel(zobj) + href=":zobj.getUrl(mode='edit', page=aPage, inPopup=inPopup)"> ::tied.o.getSupTitle(navInfo) :(not includeShownInfo) and \ + linkInPopup=inPopup or (target.target != '_self'); + fullUrl=tied.o.getUrl(page=pageName, nav=navInfo, \ + inPopup=linkInPopup)" + href=":fullUrl" class=":cssClass" target=":target.target" + onclick=":target.openPopup">:(not includeShownInfo) and \ tied.title or field.getReferenceLabel(tied) ::tied.o.getSubTitle() - ''') + 'display:none'">::tied.o.getSubTitle()''') # This PX displays buttons for triggering global actions on several linked # objects (delete many, unlink many,...) @@ -84,7 +85,8 @@ class Ref(Field): - @@ -140,24 +145,27 @@ class Ref(Field): # Displays the button allowing to add a new object through a Ref field, if # it has been declared as addable and if multiplicities allow it. pxAdd = Px(''' - ''') + + + + + + + ''') # This PX displays, in a cell header from a ref table, icons for sorting the # ref field according to the field that corresponds to this column. @@ -309,39 +317,60 @@ class Ref(Field): req.get('showSubTitles', 'true') == 'true'; subLabel='selectable_objects'">:field.pxViewList''') - # PX that displays referred objects as menus. + # PX that displays referred objects as dropdown menus. + pxMenu = Px(''' + :menu.text + + :len(menu.objects)''') + pxViewMenus = Px(''' -
+ +
-
+ + +
- - - - :menu.text 1 - - - -
''') +
:field.pxAdd ''') # Simplified widget showing minimal info about tied objects. pxViewMinimal = Px(''' @@ -359,6 +388,7 @@ class Ref(Field): linkList=field.link == 'list'; renderAll=req.get('scope') != 'objs'; inPickList=False; + inMenu=False; startNumber=field.getStartNumber(render, req, ajaxHookId); info=field.getValue(zobj,startNumber=startNumber,someObjects=True); objects=info.objects; @@ -368,6 +398,7 @@ class Ref(Field): batchNumber=len(objects); folder=zobj.getCreateFolder(); tiedClassName=ztool.getPortalType(field.klass); + target=ztool.getLinksTargetInfo(field.klass); mayEdit=not field.isBack and zobj.mayEdit(field.writePermission); mayUnlink=mayEdit and field.getAttribute(zobj, 'unlink'); mayAdd=mayEdit and field.mayAdd(zobj, checkMayEdit=False); @@ -729,6 +760,7 @@ class Ref(Field): def getLinkedObjectsByMenu(self, obj, objects): '''This method groups p_objects into sub-lists of objects, grouped by menu (happens when self.render == 'menus').''' + if not objects: return () res = [] # We store in "menuIds" the already encountered menus: # ~{s_menuId : i_indexInRes}~ @@ -948,6 +980,29 @@ class Ref(Field): obj.raiseUnauthorized("User can't write Ref field '%s' (%s)." % \ (self.name, may.msg)) + def getOnAdd(self, q, formName, addConfirmMsg, target, navBaseCall, + startNumber): + '''Computes the JS code to execute when button "add" is clicked.''' + if self.noForm: + # Ajax-refresh the Ref with a special param to link a newly created + # object. + res = navBaseCall.replace('**v**', + "%d,'doCreateWithoutForm'" % startNumber) + if self.addConfirm: + res = "askConfirm('script', %s, %s)" % \ + (q(res, False), q(addConfirmMsg)) + else: + # In the basic case, no JS code is executed: target.openPopup is + # empty and the button-related form is submitted in the main page. + res = target.openPopup + if self.addConfirm and not target.openPopup: + res = "askConfirm('form','%s',%s)" % (formName,q(addConfirmMsg)) + elif self.addConfirm and target.openPopup: + res = "askConfirm('form+script',%s,%s)" % \ + (q(formName + '+' + target.openPopup, False), \ + q(addConfirmMsg)) + return res + def getCbJsInit(self, obj): '''When checkboxes are enabled, this method defines a JS associative array (named "_appy_objs_cbs") that will store checkboxes' statuses. diff --git a/gen/mixins/ToolMixin.py b/gen/mixins/ToolMixin.py index 8d47a1a..6d5b305 100644 --- a/gen/mixins/ToolMixin.py +++ b/gen/mixins/ToolMixin.py @@ -416,10 +416,11 @@ class ToolMixin(BaseMixin): sub-lists of p_sub elements.''' return sutils.splitList(l, sub) - def quote(self, s): + def quote(self, s, escapeWithEntity=True): '''Returns the quoted version of p_s.''' if not isinstance(s, basestring): s = str(s) - s = s.replace('\r\n', '').replace('\n', '').replace("'", "'") + repl = escapeWithEntity and ''' or "\\'" + s = s.replace('\r\n', '').replace('\n', '').replace("'", repl) return "'%s'" % s def getLayoutType(self): @@ -789,7 +790,7 @@ class ToolMixin(BaseMixin): startNumber += batchSize return startNumber - def getNavigationInfo(self): + def getNavigationInfo(self, inPopup=False): '''Extracts navigation information from request/nav and returns an object with the info that a page can use for displaying object navigation.''' @@ -841,7 +842,7 @@ class ToolMixin(BaseMixin): uids = getattr(masterObj, fieldName) # Display the reference widget at the page where the current object # lies. - startNumberKey = '%s%s_startNumber' % (masterObj.UID(), fieldName) + startNumberKey = '%s%s_startNumber' % (masterObj.id, fieldName) startNumber = self.computeStartNumberFrom(res.currentNumber-1, res.totalNumber, batchSize) res.sourceUrl = masterObj.getUrl(**{startNumberKey:startNumber, @@ -895,7 +896,7 @@ class ToolMixin(BaseMixin): sibling = brain.getObject() setattr(res, urlKey, sibling.getUrl(\ nav=newNav % (index + 1), - page=rq.get('page', 'main'))) + page=rq.get('page', 'main'), inPopup=inPopup)) return res def getGroupedSearchFields(self, searchInfo): @@ -1368,4 +1369,30 @@ class ToolMixin(BaseMixin): # Set a minimum width for small labels. if len(label) < 15: return 'width:130px' return 'padding-left: 26px; padding-right: 8px' + + def getLinksTargetInfo(self, klass): + '''Appy allows to open links to view or edit instances of p_klass + either via the same browser window, or via a popup. This method + returns info about that, as an object having 2 attributes: + - target is "_self" if the link leads to the same browser window, + "appyIFrame" if the link must be opened in a popup; + - openPopup is unused if target is "_self" and contains the + Javascript code to open the popup.''' + res = Object(target='_self', openPopup='') + if hasattr(klass, 'popup'): + res.target = 'appyIFrame' + d = klass.popup + if isinstance(d, basestring): + # Width only + params = int(d[:-2]) + else: + # Width and height + params = "%s, %s" % (d[0][:-2], d[1][:-2]) + res.openPopup = "openPopup('iframePopup',null,%s)" % params + return res + + def backFromPopup(self): + '''Returns the PX allowing to close the iframe popup and refresh the + base page.''' + return self.appy().pxBack({'ztool': self}) # ------------------------------------------------------------------------------ diff --git a/gen/mixins/__init__.py b/gen/mixins/__init__.py index 092ddd5..9c4238c 100644 --- a/gen/mixins/__init__.py +++ b/gen/mixins/__init__.py @@ -196,7 +196,8 @@ class BaseMixin: className = rq.get('className') # Create the params to add to the URL we will redirect the user to # create the object. - urlParams = {'mode':'edit', 'page':'main', 'nav':''} + urlParams = {'mode':'edit', 'page':'main', 'nav':'', + 'inPopup':rq.get('popup') == '1'} initiator, initiatorPage, initiatorField = self.getInitiatorInfo() if initiator: # The object to create will be linked to an initiator object through @@ -379,12 +380,15 @@ class BaseMixin: tool = self.getTool() errorMessage = self.translate('validation_error') isNew = self.isTemporary() + inPopup = rq.get('popup') == '1' # If this object is created from an initiator, get info about him. initiator, initiatorPage, initiatorField = self.getInitiatorInfo() # If the user clicked on 'Cancel', go back to the previous page. buttonClicked = rq.get('button') if buttonClicked == 'cancel': - if initiator: + if inPopup: + back = tool.backFromPopup() + elif initiator: # Go back to the initiator page. urlBack = initiator.getUrl(page=initiatorPage, nav='') else: @@ -395,6 +399,7 @@ class BaseMixin: urlBack = self.getUrl() self.say(self.translate('object_canceled')) self.removeLock(rq['page']) + if inPopup: return back return self.goto(urlBack) # Object for storing validation errors @@ -433,18 +438,18 @@ class BaseMixin: if not msg: msg = self.translate('object_saved') # If the object has already been deleted (ie, it is a kind of transient # object like a one-shot form and has already been deleted in method - # onEdit), redirect to the main site page. - if not getattr(obj.getParentNode().aq_base, obj.id, None): - return self.goto(tool.getSiteUrl(), msg) - # If the user can't access the object anymore, redirect him to its home - # page. - if not obj.mayView(): return self.goto(tool.getHomePage(), msg) + # onEdit) or if the user can't access the object anymore, redirect him + # to the user's home page. + if not getattr(obj.getParentNode().aq_base, obj.id, None) or \ + not obj.mayView(): + if inPopup: return tool.backFromPopup() + return self.goto(tool.getHomePage(), msg) if (buttonClicked == 'save') or saveConfirmed: obj.say(msg) + if inPopup: return tool.backFromPopup() if isNew and initiator: return self.goto(initiator.getUrl(page=initiatorPage, nav='')) - else: - return self.goto(obj.getUrl()) + return self.goto(obj.getUrl()) if buttonClicked == 'previous': # Go to the previous page for this object. # We recompute the list of phases and pages because things @@ -462,29 +467,30 @@ class BaseMixin: # I do not use gotoEdit here because I really need to # redirect the user to the edit page. Indeed, the object # edit URL may have moved from temp_folder to another place. - return self.goto(obj.getUrl(mode='edit', page=pageName)) + return self.goto(obj.getUrl(mode='edit', page=pageName, + inPopup=inPopup)) else: - return self.goto(obj.getUrl(page=pageName)) + return self.goto(obj.getUrl(page=pageName, inPopup=inPopup)) else: obj.say(msg) - return self.goto(obj.getUrl()) + return self.goto(obj.getUrl(inPopup=inPopup)) if buttonClicked == 'next': # Go to the next page for this object. # We remember page name, because the next method may set a new # current page if the current one is not visible anymore. - pageName = rq['page'] phaseObj = self.getAppyPhases(currentOnly=True, layoutType='edit') - pageName, pageInfo = phaseObj.getNextPage(pageName) + pageName, pageInfo = phaseObj.getNextPage(rq['page']) if pageName: # Return to the edit or view page? if pageInfo.showOnEdit: # Same remark as above (click on "previous"). - return self.goto(obj.getUrl(mode='edit', page=pageName)) + return self.goto(obj.getUrl(mode='edit', page=pageName, + inPopup=inPopup)) else: - return self.goto(obj.getUrl(page=pageName)) + return self.goto(obj.getUrl(page=pageName, inPopup=inPopup)) else: obj.say(msg) - return self.goto(obj.getUrl()) + return self.goto(obj.getUrl(inPopup=inPopup)) return obj.gotoEdit() def reindex(self, indexes=None, unindex=False): @@ -1326,11 +1332,15 @@ class BaseMixin: return layoutType in showValue getUrlDefaults = {'page':True, 'nav':True} - def getUrl(self, base=None, mode='view', **kwargs): + def getUrl(self, base=None, mode='view', inPopup=False, **kwargs): '''Returns an URL for this object. * If p_base is None, it will be the base URL for this object (ie, Zope self.absolute_url()). * p_mode can be "edit", "view" or "raw" (a non-param, base URL) + * If p_inPopup is True, the link will be opened in the Appy iframe. + An additional param "popup=1" will be added to URL params, in order + to tell Appy that the link target will be shown in a popup, in a + minimalistic way (no portlet...). * p_kwargs can store additional parameters to add to the URL. In this dict, every value that is a string will be added to the URL as-is. Every value that is True will be replaced by the value @@ -1355,6 +1365,7 @@ class BaseMixin: if not kwargs: kwargs = self.getUrlDefaults if 'page' not in kwargs: kwargs['page'] = True if 'nav' not in kwargs: kwargs['nav'] = True + kwargs['popup'] = inPopup and '1' or '0' # Create URL parameters from kwargs params = [] for name, value in kwargs.iteritems(): @@ -1392,18 +1403,19 @@ class BaseMixin: return False if parent.meta_type not in ('Folder', 'Temporary Folder'): return parent - def getBreadCrumb(self): + def getBreadCrumb(self, inPopup=False): '''Gets breadcrumb info about this object and its parents (if it must be shown).''' # Return an empty breadcrumb if it must not be shown. klass = self.getClass() if hasattr(klass, 'breadcrumb') and not klass.breadcrumb: return () # Compute the breadcrumb - res = [Object(url=self.absolute_url(), + res = [Object(url=self.getUrl(inPopup=inPopup), title=self.getFieldValue('title', layoutType='view'))] + # In a popup: limit the breadcrumb to the current object. + if inPopup: return res parent = self.getParent() - if parent: - res = parent.getBreadCrumb() + res + if parent: res = parent.getBreadCrumb() + res return res def index_html(self): diff --git a/gen/tr/Appy.pot b/gen/tr/Appy.pot index 87b03a9..d637e23 100644 --- a/gen/tr/Appy.pot +++ b/gen/tr/Appy.pot @@ -714,3 +714,7 @@ msgstr "" #. Default: "You are not allowed to consult this." msgid "unauthorized" msgstr "" + +#. Default: "Close" +msgid "window_close" +msgstr "" diff --git a/gen/ui/appy.css b/gen/ui/appy.css index 0d80c27..19b1517 100644 --- a/gen/ui/appy.css +++ b/gen/ui/appy.css @@ -62,7 +62,7 @@ input.button { color: #666666; height: 20px; margin-bottom: 5px; background-color: white; background-repeat: no-repeat; background-position: 8px 25%; box-shadow: 2px 2px 2px #888888} input.buttonSmall { width: 100px !important; font-size: 85%; height: 18px; - margin-bottom: 3px} + margin-bottom: 3px } .fake { background-color: #e6e6e6 !important ; cursor:help !important } .xhtml { background-color: white; padding: 6px; font-size: 95% } .xhtml img { margin-right: 5px } @@ -74,6 +74,7 @@ input.buttonSmall { width: 100px !important; font-size: 85%; height: 18px; width: 600px; border: 1px #F0C36D solid; padding: 6px; background-color: #F9EDBE; text-align: center; margin-left: -300px; border-radius: 2px 2px 2px 2px; box-shadow: 0 2px 4px #A9A9A9 } +.messagePopup { width: 80%; top: 0; left: 0; margin-left: 10px } .focus { font-size: 90%; margin: 7px 0 7px 0; padding: 7px; background-color: #d7dee4; border-radius: 2px 2px 2px 2px; box-shadow: 0 2px 4px #A9A9A9 } @@ -101,6 +102,8 @@ td.search { padding-top: 8px } font-weight: normal; text-align: left; z-index: 2 } .dropdownMenu { cursor: pointer; font-size: 93%; position: relative } .dropdown a:hover { text-decoration: underline } +.addForm { display: inline } +.addFormMenu { display: table-cell; padding-left: 7px } .list { margin-bottom: 3px } .list td, .list th { border: 3px solid #ededed; color: grey; padding: 3px 5px 3px 5px } @@ -127,8 +130,8 @@ td.search { padding-top: 8px } .odd { background-color: #f6f6f6 } .odd2 { background-color: #f2f2f2 } .refMenuItem { border-top: grey 1px dashed; margin: 3px 0; padding-top: 3px } -.summary { margin-bottom: 5px; background-color: #f9f9f9; - border: 2px solid #f9f9f9 } +.summary { margin-bottom: 5px; background-color: whitesmoke; + border: 3px solid white } .by { padding: 5px; color: grey; font-size: 97% } .underline { border-bottom: 1px dotted grey } .state { font-weight: bold; border-bottom: 1px dashed grey } diff --git a/gen/ui/appy.js b/gen/ui/appy.js index 835cc3e..624225e 100644 --- a/gen/ui/appy.js +++ b/gen/ui/appy.js @@ -703,7 +703,7 @@ function protectAppyForm() { } // Functions for opening and closing a popup -function openPopup(popupId, msg) { +function openPopup(popupId, msg, width, height) { // Put the message into the popup if (msg) { var msgHook = (popupId == 'alertPopup')? 'appyAlertText': 'appyConfirmText'; @@ -715,13 +715,35 @@ function openPopup(popupId, msg) { // Put it at the right place on the screen var scrollTop = document.documentElement.scrollTop || window.pageYOffset || 0; popup.style.top = (scrollTop + 150) + 'px'; + if (width) popup.style.width = width + 'px'; + if (popupId == 'iframePopup') { + // Initialize iframe's width. + var iframe = document.getElementById('appyIFrame'); + iframe.style.width = (width-20) + 'px'; + if (height) iframe.style.height = height + 'px'; + } popup.style.display = 'block'; } function closePopup(popupId) { // Close the popup - var popup = document.getElementById(popupId); + var container = window.parent.document; + var popup = container.getElementById(popupId); popup.style.display = 'none'; + popup.style.width = null; + if (popupId == 'iframePopup') { + // Reinitialise the enclosing iframe. + var iframe = container.getElementById('appyIFrame'); + iframe.style.width = null; + iframe.innerHTML = ''; + // Leave the form silently if we are on an edit page + iframe.contentWindow.onbeforeunload = null; + } +} + +function backFromPopup() { + closePopup('iframePopup'); + window.parent.location = window.parent.location; } // Function triggered when an action needs to be confirmed by the user @@ -734,8 +756,8 @@ function askConfirm(actionType, action, msg, showComment) { confirmForm.actionType.value = actionType; confirmForm.action.value = action; var commentArea = document.getElementById('commentArea'); - if (showComment) commentArea.style.display = "block"; - else commentArea.style.display = "none"; + if (showComment) commentArea.style.display = 'block'; + else commentArea.style.display = 'none'; openPopup("confirmActionPopup", msg); } @@ -762,6 +784,12 @@ function doConfirm() { // We must execute Javascript code in "action" eval(action); } + else if (actionType == 'form+script') { + var elems = action.split('+'); + // Submit the form in elems[0] and execute the JS code in elems[1] + document.getElementById(elems[0]).submit(); + eval(elems[1]); + } } // Function triggered when the user asks password reinitialisation @@ -952,4 +980,4 @@ function onSelectDate(cal) { if (update && p.singleClick && cal.dateClicked) { cal.callCloseHandler(); } -}; +} diff --git a/gen/ui/close.png b/gen/ui/close.png index 3eb5e55ba6a2fbc3f12d00633d742f6709e8dde5..539e525defa99dd1ccc13fe93e7209e9482ff86d 100644 GIT binary patch delta 132 zcmV-~0DJ%90owtPUVYvM1q?J0^ZCe{00016Nklzht1xbd9hyW0hF+n+74gjiern?IPaxe=R_Fcc%3 uh)n|%F3(`ofD#(mf(}`n1z#AW0{~!pK3fV={pA1v002ovP6b4+LSTY!L^&D& diff --git a/gen/wrappers/ToolWrapper.py b/gen/wrappers/ToolWrapper.py index a47ba1d..1e3feb5 100644 --- a/gen/wrappers/ToolWrapper.py +++ b/gen/wrappers/ToolWrapper.py @@ -176,22 +176,24 @@ class ToolWrapper(AbstractWrapper): 'current' or ''">::_(className + '_plural') - - - - +
+ + + + -
+ ztool.getButtonWidth(label))"/> + + -
@@ -232,7 +234,8 @@ class ToolWrapper(AbstractWrapper): # The message that is shown when a user triggers an action. pxMessage = Px(''' -
+
@@ -308,8 +311,11 @@ class ToolWrapper(AbstractWrapper): (className, searchName, startNumber+currentNumber, totalNumber); cssClass=zobj.getCssFor('title')"> ::zobj.getSupTitle(navInfo) - :zobj.Title():zobj.Title():zobj.Title()::zobj.getSubTitle() @@ -320,9 +326,11 @@ class ToolWrapper(AbstractWrapper):
@@ -108,8 +110,11 @@ class Ref(Field): + field.pageName, loop.tied.nb + 1 + startNumber, totalNumber); + linkInPopup=inPopup or (target.target != '_self')" + href=":tied.o.getUrl(mode='edit', page='main', nav=navInfo, \ + inPopup=linkInPopup)" + target=":target.target" onclick=":target.openPopup">
+ nav=navInfo, inPopup=linkInPopup)"> @@ -429,7 +437,8 @@ class ToolWrapper(AbstractWrapper): newSearchUrl='%s/search?className=%s%s' % \ (ztool.absolute_url(), className, refUrlPart); showSubTitles=req.get('showSubTitles', 'true') == 'true'; - resultMode=ztool.getResultMode(className)"> + resultMode=ztool.getResultMode(className); + target=ztool.getLinksTargetInfo(ztool.getAppyClass(className))"> @@ -532,6 +541,17 @@ class ToolWrapper(AbstractWrapper): ''', template=AbstractWrapper.pxTemplate, hook='content') + pxBack = Px(''' + + + + + + + + ''') + def isManager(self): '''Some pages on the tool can only be accessed by managers.''' if self.user.has_role('Manager'): return 'view' diff --git a/gen/wrappers/__init__.py b/gen/wrappers/__init__.py index dff9f5c..177c036 100644 --- a/gen/wrappers/__init__.py +++ b/gen/wrappers/__init__.py @@ -21,9 +21,9 @@ class AbstractWrapper(object): # Buttons for going to next/previous objects if this one is among bunch of # referenced or searched objects. currentNumber starts with 1. pxNavigateSiblings = Px(''' -
+
-