diff --git a/.gitignore b/.gitignore index 788259e..844e260 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.pyc *.pyo +dist/ *.project *.pydevproject *.sublime-project diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..0be9a0d --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,11 @@ +global-include *.cfg +global-include *.css *.js +global-include *.gif *.jpg *.png +global-include *.dmp +global-include *.md *.txt +global-include *.mo *.po *.pot +global-include *.pdf +global-include *.pt +global-include *.zcml + +graft loops/integrator/testdata diff --git a/README.md b/README.md new file mode 100644 index 0000000..122d1f7 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Introduction + +This is the main part of the code of the semantic +web application platform *loops*, based on +Zope 3 / bluebream. + +More information: see https://www.cyberconcepts.org. diff --git a/README.txt b/README.txt index 295405a..5d5a1bc 100755 --- a/README.txt +++ b/README.txt @@ -737,7 +737,9 @@ on data provided in this form: >>> component.provideAdapter(NameChooser) >>> request = TestRequest(form={'title': u'Test Note', - ... 'form.type': u'.loops/concepts/note'}) + ... 'form.type': u'.loops/concepts/note', + ... 'contentType': u'text/restructured', + ... 'linkUrl': u'http://'}) >>> view = NodeView(m112, request) >>> cont = CreateObject(view, request) >>> cont.update() @@ -802,7 +804,7 @@ The new technique uses the ``fields`` and ``data`` attributes... linkText textline False None >>> view.data - {'linkUrl': u'http://', 'contentType': 'text/restructured', 'data': u'', + {'linkUrl': u'http://', 'contentType': u'text/restructured', 'data': u'', 'linkText': u'', 'title': u'Test Note'} The object is changed via a FormController adapter created for @@ -913,6 +915,12 @@ relates ISO country codes with the full name of the country. >>> sorted(adapted(concepts['countries']).data.items()) [('at', ['Austria']), ('de', ['Germany'])] + >>> countries.dataAsRecords() + [{'value': 'Austria', 'key': 'at'}, {'value': 'Germany', 'key': 'de'}] + + >>> countries.getRowsByValue('value', 'Germany') + [{'value': 'Germany', 'key': 'de'}] + Caching ======= @@ -932,6 +940,12 @@ Security >>> from loops.security.browser import admin, audit +Paster Shell Utilities - Repair Scripts +======================================= + + >>> from loops.repair.base import removeRecords + + Import/Export ============= diff --git a/__init__.py b/__init__.py index 5973df8..5d2b421 100644 --- a/__init__.py +++ b/__init__.py @@ -1,22 +1,17 @@ -# -# Copyright (c) 2008 Helmut Merz helmutm@cy55.de -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# +# package loops -""" +# intid monkey patch for avoiding ForbiddenAttribute error + +from zope import component +from zope.intid.interfaces import IIntIds +from zope import intid +from zope.security.proxy import removeSecurityProxy + +def queryId(self, ob, default=None): + try: + return self.getId(removeSecurityProxy(ob)) + except KeyError: + return default + +intid.IntIds.queryId = queryId -$Id$ -""" diff --git a/base.py b/base.py index 26c7576..02b9925 100644 --- a/base.py +++ b/base.py @@ -1,5 +1,3 @@ -# -*- coding: UTF-8 -*- -# -*- Mode: Python; py-indent-offset: 4 -*- # # Copyright (c) 2019 Helmut Merz helmutm@cy55.de # @@ -19,7 +17,7 @@ # """ -The loops container class. +Implementation of loops root object. """ from zope.app.container.btree import BTreeContainer diff --git a/browser/action.py b/browser/action.py index 4ac0b03..b51e40a 100644 --- a/browser/action.py +++ b/browser/action.py @@ -92,6 +92,8 @@ class DialogAction(Action): urlParams['fixed_type'] = 'yes' if self.viewTitle: urlParams['view_title'] = self.viewTitle + #for k, v in self.page.sortInfo.items(): + # urlParams['sortinfo_' + k] = v['fparam'] urlParams.update(self.addParams) if self.target is not None: url = self.page.getUrlForTarget(self.target) diff --git a/browser/common.py b/browser/common.py index abce04e..fddcc3e 100755 --- a/browser/common.py +++ b/browser/common.py @@ -22,7 +22,7 @@ Common base class for loops browser view classes. from cgi import parse_qs, parse_qsl #import mimetypes # use more specific assignments from cybertools.text -from datetime import datetime +from datetime import date, datetime from logging import getLogger import re from time import strptime @@ -62,17 +62,21 @@ from cybertools.stateful.interfaces import IStateful from cybertools.text import mimetypes from cybertools.typology.interfaces import IType, ITypeManager from cybertools.util.date import toLocalTime +from cybertools.util.format import formatDate from cybertools.util.jeep import Jeep from loops.browser.util import normalizeForUrl from loops.common import adapted, baseObject from loops.config.base import DummyOptions from loops.i18n.browser import I18NView from loops.interfaces import IResource, IView, INode, ITypeConcept +from loops.organize.personal import favorite +from loops.organize.party import getPersonForUser from loops.organize.tracking import access from loops.organize.util import getRolesForPrincipal from loops.resource import Resource from loops.security.common import checkPermission from loops.security.common import canAccessObject, canListObject, canWriteObject +from loops.security.common import canEditRestricted from loops.type import ITypeConcept, LoopsTypeInfo from loops import util from loops.util import _, saveRequest @@ -137,7 +141,58 @@ class EditForm(form.EditForm): return parentUrl + '/contents.html' -class BaseView(GenericView, I18NView): +class SortableMixin(object): + + @Lazy + def sortInfo(self): + result = {} + for k, v in self.request.form.items(): + if k.startswith('sortinfo_'): + tableName = k[len('sortinfo_'):] + if ',' in v: + fn, dir = v.split(',') + else: + fn = v + dir = 'asc' + result[tableName] = dict( + colName=fn, ascending=(dir=='asc'), fparam=v) + result = favorite.updateSortInfo(getPersonForUser( + self.context, self.request), self.target, result) + return result + + def isSortableColumn(self, tableName, colName): + return False # overwrite in subclass + + def getSortUrl(self, tableName, colName): + url = str(self.request.URL) + paramChar = '?' in url and '&' or '?' + si = self.sortInfo.get(tableName) + if si is not None and si.get('colName') == colName: + dir = si['ascending'] and 'desc' or 'asc' + else: + dir = 'asc' + return '%s%ssortinfo_%s=%s,%s' % (url, paramChar, tableName, colName, dir) + + def getSortParams(self, tableName): + url = str(self.request.URL) + paramChar = '?' in url and '&' or '?' + si = self.sortInfo.get(tableName) + if si is not None: + colName = si['colName'] + dir = si['ascending'] and 'asc' or 'desc' + return '%ssortinfo_%s=%s,%s' % (paramChar, tableName, colName, dir) + return '' + + def getSortImage(self, tableName, colName): + si = self.sortInfo.get(tableName) + if si is not None and si.get('colName') == colName: + if si['ascending']: + return '/@@/cybertools.icons/arrowdown.gif' + else: + return '/@@/cybertools.icons/arrowup.gif' + + +class BaseView(GenericView, I18NView, SortableMixin): actions = {} portlet_actions = [] @@ -146,6 +201,7 @@ class BaseView(GenericView, I18NView): icon = None modeName = 'view' isToplevel = False + isVisible = True def __init__(self, context, request): context = baseObject(context) @@ -163,6 +219,10 @@ class BaseView(GenericView, I18NView): pass saveRequest(request) + def todayFormatted(self): + return formatDate(date.today(), 'date', 'short', + self.languageInfo.language) + def checkPermissions(self): return canAccessObject(self.context) @@ -214,6 +274,16 @@ class BaseView(GenericView, I18NView): result.append(view) return result + @Lazy + def urlParamString(self): + return self.getUrlParamString() + + def getUrlParamString(self): + qs = self.request.get('QUERY_STRING') + if qs: + return '?' + qs + return '' + @Lazy def principalId(self): principal = self.request.principal @@ -347,6 +417,10 @@ class BaseView(GenericView, I18NView): def isPartOfPredicate(self): return self.conceptManager.get('ispartof') + @Lazy + def queryTargetPredicate(self): + return self.conceptManager.get('querytarget') + @Lazy def memberPredicate(self): return self.conceptManager.get('ismember') @@ -395,6 +469,10 @@ class BaseView(GenericView, I18NView): def description(self): return self.adapted.description + @Lazy + def tabTitle(self): + return u'Info' + @Lazy def additionalInfos(self): return [] @@ -747,6 +825,8 @@ class BaseView(GenericView, I18NView): return result def checkState(self): + if checkPermission('loops.ManageSite', self.context): + return True if not self.allStates: return True for stf in self.allStates: @@ -821,6 +901,10 @@ class BaseView(GenericView, I18NView): def canAccessRestricted(self): return checkPermission('loops.ViewRestricted', self.context) + @Lazy + def canEditRestricted(self): + return canEditRestricted(self.context) + def openEditWindow(self, viewName='edit.html'): if self.editable: if checkPermission('loops.ManageSite', self.context): @@ -943,6 +1027,12 @@ class BaseView(GenericView, I18NView): jsCall = 'dojo.require("dojox.image.Lightbox");' self.controller.macros.register('js-execute', jsCall, jsCall=jsCall) + def registerDojoComboBox(self): + self.registerDojo() + jsCall = ('dojo.require("dijit.form.ComboBox");') + self.controller.macros.register('js-execute', + 'dojo.require.ComboBox', jsCall=jsCall) + def registerDojoFormAll(self): self.registerDojo() self.registerDojoEditor() @@ -996,6 +1086,7 @@ class LoggedIn(object): params = parse_qsl(qs) params = [(k, v) for k, v in params if k != 'loops.messages.top:record'] params.append(('loops.messages.top:record', message.encode('UTF-8'))) + url = url.encode('utf-8') return '%s?%s' % (url, urlencode(params)) # vocabulary stuff diff --git a/browser/concept.py b/browser/concept.py index 32b63fc..12babb7 100644 --- a/browser/concept.py +++ b/browser/concept.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2013 Helmut Merz helmutm@cy55.de +# Copyright (c) 2016 Helmut Merz helmutm@cy55.de # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -254,18 +254,35 @@ class ConceptView(BaseView): result.append(view) return result + def viewModes(self): + modes = Jeep() + current = self.request.form.get('loops.viewName') + parts = (self.options('view_tabs') or + self.typeOptions('view_tabs') or []) + if not parts: + return modes + activeMode = None + for p in parts: + view = component.queryMultiAdapter( + (self.adapted, self.request), name=p) + if view is None: + view = component.queryMultiAdapter( + (self.context, self.request), name=p) + if view is None: + continue + active = (activeMode is None and p == current) + if active: + activeMode = p + url = '%s?loops.viewName=%s' % (self.targetUrl, p) + modes.append(ViewMode(p, view.tabTitle, url, active)) + if activeMode is None: + modes[0].active = True + return modes + @Lazy def adapted(self): return adapted(self.context, self.languageInfo) - @Lazy - def title(self): - return self.adapted.title or getName(self.context) - - @Lazy - def description(self): - return self.adapted.description - @Lazy def targetUrl(self): return self.nodeView.getUrlForTarget(self.context) @@ -282,8 +299,17 @@ class ConceptView(BaseView): def breadcrumbsTitle(self): return self.title + @Lazy + def showInBreadcrumbs(self): + return (self.options('show_in_breadcrumbs') or + self.typeOptions('show_in_breadcrumbs')) + @Lazy def breadcrumbsParent(self): + for p in self.context.getParents([self.defaultPredicate]): + view = self.nodeView.getViewForTarget(p) + if view.showInBreadcrumbs: + return view return None def getData(self, omit=('title', 'description')): @@ -389,7 +415,8 @@ class ConceptView(BaseView): children = getChildren def childrenAlphaGroups(self, predicates=None): - result = Jeep() + #result = Jeep() + result = {} rels = self.getChildren(predicates=predicates or [self.defaultPredicate], topLevelOnly=False, sort=False) rels = sorted(rels, key=lambda r: r.title.lower()) @@ -449,7 +476,7 @@ class ConceptView(BaseView): if r.order != pos: r.order = pos - def getResources(self): + def getResources(self, relView=None, sort='default'): form = self.request.form #if form.get('loops.viewName') == 'index.html' and self.editable: if self.editable: @@ -458,13 +485,17 @@ class ConceptView(BaseView): tokens = form.get('resources_tokens') if tokens: self.reorderResources(tokens) - from loops.browser.resource import ResourceRelationView + if relView is None: + from loops.browser.resource import ResourceRelationView + relView = ResourceRelationView from loops.organize.personal.browser.filter import FilterView fv = FilterView(self.context, self.request) - rels = self.context.getResourceRelations() + rels = self.context.getResourceRelations(sort=sort) for r in rels: if fv.check(r.first): - yield ResourceRelationView(r, self.request, contextIsSecond=True) + view = relView(r, self.request, contextIsSecond=True) + if view.checkState(): + yield view def resources(self): return self.getResources() diff --git a/browser/concept_macros.pt b/browser/concept_macros.pt index 46e08c0..7358b5c 100644 --- a/browser/concept_macros.pt +++ b/browser/concept_macros.pt @@ -53,7 +53,7 @@