# HG changeset patch # User Paul Boddie # Date 1390325161 -3600 # Node ID c7fd6b245e138f29304a7d84f633981065981d1d # Parent 66d831c42a44655be0842763beefe6135bb5899d# Parent 000e771c5377a06fbe49aa3695aec8e5e7cebbbc Merged subpage-items with default since the changes were overdue and some miscellaneous changes had also been implemented on the subpage-items branch. diff -r 66d831c42a44 -r c7fd6b245e13 DateSupport.py --- a/DateSupport.py Mon Nov 11 13:42:06 2013 +0100 +++ b/DateSupport.py Tue Jan 21 18:26:01 2014 +0100 @@ -2,7 +2,7 @@ """ MoinMoin - DateSupport library (derived from EventAggregatorSupport) - @copyright: 2008, 2009, 2010, 2011, 2012, 2013 by Paul Boddie + @copyright: 2008, 2009, 2010, 2011, 2012, 2013, 2014 by Paul Boddie @license: GNU GPL (v2 or later), see COPYING.txt for details. """ @@ -392,7 +392,7 @@ def __str__(self): return Date.__str__(self) + self.time_string() - def time_string(self, zone_as_offset=False, time_prefix=" ", zone_prefix=" "): + def time_string(self, zone_as_offset=False, time_prefix=" ", zone_prefix=" ", zone_separator=":"): if self.has_time(): data = self.as_tuple() time_str = "%s%02d:%02d" % ((time_prefix,) + data[3:5]) @@ -402,13 +402,23 @@ if zone_as_offset: utc_offset = self.utc_offset() if utc_offset: - time_str += "%s%+03d:%02d" % ((zone_prefix,) + utc_offset) + time_str += "%s%+03d%s%02d" % (zone_prefix, utc_offset[0], zone_separator, utc_offset[1]) else: time_str += "%s%s" % (zone_prefix, data[6]) return time_str else: return "" + def as_RFC2822_datetime_string(self): + weekday = calendar.weekday(*self.data[:3]) + return "%s, %02d %s %04d %s" % ( + getDayLabel(weekday)[:3], + self.data[2], + getMonthLabel(self.data[1])[:3], + self.data[0], + self.time_string(zone_as_offset=True, time_prefix="", zone_prefix=" ", zone_separator="") + ) + def as_HTTP_datetime_string(self): weekday = calendar.weekday(*self.data[:3]) return "%s, %02d %s %04d %02d:%02d:%02d GMT" % (( diff -r 66d831c42a44 -r c7fd6b245e13 ItemSupport.py --- a/ItemSupport.py Mon Nov 11 13:42:06 2013 +0100 +++ b/ItemSupport.py Tue Jan 21 18:26:01 2014 +0100 @@ -2,135 +2,65 @@ """ MoinMoin - ItemSupport library - @copyright: 2013 by Paul Boddie + @copyright: 2013, 2014 by Paul Boddie @license: GNU GPL (v2 or later), see COPYING.txt for details. """ +from MoinMoin.Page import Page +from MoinMoin.PageEditor import PageEditor +from MoinMoin.security import Permissions from MoinMoin.util import lock +from MoinSupport import getMetadata, getPagesForSearch import os # Content storage support. -class ItemDirectoryStore: +class SpecialPermissionsForPage(Permissions): + + "Permit saving of ACL-enabled pages." + + def __init__(self, user, pagename): + Permissions.__init__(self, user) + self.pagename = pagename - "A directory-based item store." + def admin(self, pagename): + return pagename == self.pagename + + write = admin + +class ReadPermissionsForSubpages(Permissions): + + "Permit listing of ACL-affected subpages." - def __init__(self, path, lock_dir): + def __init__(self, user, pagename): + Permissions.__init__(self, user) + self.pagename = pagename + + def read(self, pagename): + return pagename.startswith("%s/" % self.pagename) + +# Underlying storage mechanisms. - "Initialise an item store for the given 'path' and 'lock_dir'." +class GeneralItemStore: + + "Common item store functionality." - self.path = path - self.next_path = os.path.join(self.path, "next") + def __init__(self, lock_dir): + + "Initialise an item store with the given 'lock_dir' guarding access." + self.lock_dir = lock_dir self.writelock = lock.WriteLock(lock_dir) self.readlock = lock.ReadLock(lock_dir) - def mtime(self): - - "Return the last modified time of the item store directory." - - return os.path.getmtime(self.path) - - def get_next(self): - - "Return the next item number." - - next = self.read_next() - if next is None: - next = self.deduce_next() - self.write_next(next) - return next - - def get_keys(self): - - "Return the item keys." - - return [int(filename) for filename in os.listdir(self.path) if filename.isdigit()] - def deduce_next(self): "Deduce the next item number from the existing item files." return max(self.get_keys() or [-1]) + 1 - def read_next(self): - - "Read the next item number from a special file." - - if not os.path.exists(self.next_path): - return None - - f = open(self.next_path) - try: - try: - return int(f.read()) - except ValueError: - return None - finally: - f.close() - - def write_next(self, next): - - "Write the 'next' item number to a special file." - - f = open(self.next_path, "w") - try: - f.write(str(next)) - finally: - f.close() - - def write_item(self, item, next): - - "Write the given 'item' to a file with the given 'next' item number." - - f = open(self.get_item_path(next), "w") - try: - f.write(item) - finally: - f.close() - - def read_item(self, number): - - "Read the item with the given item 'number'." - - f = open(self.get_item_path(number)) - try: - return f.read() - finally: - f.close() - - def remove_item(self, number): - - "Remove the item with the given item 'number'." - - os.remove(self.get_item_path(number)) - - def get_item_path(self, number): - - "Get the path for the given item 'number'." - - path = os.path.abspath(os.path.join(self.path, str(number))) - basepath = os.path.join(self.path, "") - - if os.path.commonprefix([path, basepath]) != basepath: - raise OSError, path - - return path - # High-level methods. - def append(self, item): - - "Append the given 'item' to the store." - - self.writelock.acquire() - try: - next = self.get_next() - self.write_item(item, next) - self.write_next(next + 1) - finally: - self.writelock.release() - def __len__(self): """ @@ -194,6 +124,239 @@ finally: self.writelock.release() +class DirectoryItemStore(GeneralItemStore): + + "A directory-based item store." + + def __init__(self, path, lock_dir): + + "Initialise an item store for the given 'path' and 'lock_dir'." + + self.path = path + self.next_path = os.path.join(self.path, "next") + self.lock_dir = lock_dir + self.writelock = lock.WriteLock(lock_dir) + self.readlock = lock.ReadLock(lock_dir) + + def mtime(self): + + "Return the last modified time of the item store directory." + + return os.path.getmtime(self.path) + + def get_next(self): + + "Return the next item number." + + next = self.read_next() + if next is None: + next = self.deduce_next() + self.write_next(next) + return next + + def get_keys(self): + + "Return the item keys." + + return [int(filename) for filename in os.listdir(self.path) if filename.isdigit()] + + def read_next(self): + + "Read the next item number from a special file." + + if not os.path.exists(self.next_path): + return None + + f = open(self.next_path) + try: + try: + return int(f.read()) + except ValueError: + return None + finally: + f.close() + + def write_next(self, next): + + "Write the 'next' item number to a special file." + + f = open(self.next_path, "w") + try: + f.write(str(next)) + finally: + f.close() + + def write_item(self, item, next): + + "Write the given 'item' to a file with the given 'next' item number." + + f = open(self.get_item_path(next), "wb") + try: + f.write(item) + finally: + f.close() + + def read_item(self, number): + + "Read the item with the given item 'number'." + + f = open(self.get_item_path(number), "rb") + try: + return f.read() + finally: + f.close() + + def remove_item(self, number): + + "Remove the item with the given item 'number'." + + os.remove(self.get_item_path(number)) + + def get_item_path(self, number): + + "Get the path for the given item 'number'." + + path = os.path.abspath(os.path.join(self.path, str(number))) + basepath = os.path.join(self.path, "") + + if os.path.commonprefix([path, basepath]) != basepath: + raise OSError, path + + return path + + # High-level methods. + + def append(self, item): + + "Append the given 'item' to the store." + + self.writelock.acquire() + try: + next = self.get_next() + self.write_item(item, next) + self.write_next(next + 1) + finally: + self.writelock.release() + +class SubpageItemStore(GeneralItemStore): + + "A subpage-based item store." + + def __init__(self, page, lock_dir): + + "Initialise an item store for subpages under the given 'page'." + + GeneralItemStore.__init__(self, lock_dir) + self.page = page + + def mtime(self): + + "Return the last modified time of the item store." + + keys = self.get_keys() + if not keys: + page = self.page + else: + page = Page(self.page.request, self.get_item_path(max(keys))) + + return wikiutil.version2timestamp( + getMetadata(page)["last-modified"] + ) + + def get_next(self): + + "Return the next item number." + + return self.deduce_next() + + def get_keys(self): + + "Return the item keys." + + request = self.page.request + + # Collect the strict subpages of the parent page. + + leafnames = [] + parentname = self.page.page_name + + # To list pages whose ACLs may prevent access, a special policy is required. + + may = request.user.may + request.user.may = ReadPermissionsForSubpages(request.user, parentname) + + try: + for page in getPagesForSearch("title:regex:^%s/" % parentname, self.page.request): + basename, leafname = page.page_name.rsplit("/", 1) + + # Only collect numbered pages immediately below the parent. + + if basename == parentname and leafname.isdigit(): + leafnames.append(int(leafname)) + + return leafnames + + # Restore the original policy. + + finally: + request.user.may = may + + def write_item(self, item, next): + + "Write the given 'item' to a file with the given 'next' item number." + + request = self.page.request + pagename = self.get_item_path(next) + + # To add a page with an ACL, a special policy is required. + + may = request.user.may + request.user.may = SpecialPermissionsForPage(request.user, pagename) + + # Attempt to save the page, copying any ACL. + + try: + page = PageEditor(request, pagename) + page.saveText(item, 0) + + # Restore the original policy. + + finally: + request.user.may = may + + def read_item(self, number): + + "Read the item with the given item 'number'." + + page = Page(self.page.request, self.get_item_path(number)) + return page.get_raw_body() + + def remove_item(self, number): + + "Remove the item with the given item 'number'." + + page = PageEditor(self.page.request, self.get_item_path(number)) + page.deletePage() + + def get_item_path(self, number): + + "Get the path for the given item 'number'." + + return "%s/%s" % (self.page.page_name, number) + + # High-level methods. + + def append(self, item): + + "Append the given 'item' to the store." + + self.writelock.acquire() + try: + next = self.get_next() + self.write_item(item, next) + finally: + self.writelock.release() + class ItemIterator: "An iterator over items in a store." @@ -242,4 +405,139 @@ def __iter__(self): return self +def getDirectoryItemStoreForPage(page, item_dir, lock_dir): + + """ + A convenience function returning a directory-based store for the given + 'page', using the given 'item_dir' and 'lock_dir'. + """ + + item_dir_path = tuple(item_dir.split("/")) + lock_dir_path = tuple(lock_dir.split("/")) + return DirectoryItemStore(page.getPagePath(*item_dir_path), page.getPagePath(*lock_dir_path)) + +def getSubpageItemStoreForPage(page, lock_dir): + + """ + A convenience function returning a subpage-based store for the given + 'page', using the given 'lock_dir'. + """ + + lock_dir_path = tuple(lock_dir.split("/")) + return SubpageItemStore(page, page.getPagePath(*lock_dir_path)) + +# Page-oriented item store classes. + +class ItemStoreBase: + + "Access item stores via pages, observing page access restrictions." + + def __init__(self, page, store): + self.page = page + self.store = store + + def can_write(self): + + """ + Return whether the user associated with the request can write to the + page owning this store. + """ + + user = self.page.request.user + return user and user.may.write(self.page.page_name) + + def can_read(self): + + """ + Return whether the user associated with the request can read from the + page owning this store. + """ + + user = self.page.request.user + return user and user.may.read(self.page.page_name) + + def can_delete(self): + + """ + Return whether the user associated with the request can delete the + page owning this store. + """ + + user = self.page.request.user + return user and user.may.delete(self.page.page_name) + + # Store-specific methods. + + def mtime(self): + return self.store.mtime() + + # High-level methods. + + def keys(self): + + "Return a list of keys for items in the store." + + if not self.can_read(): + return 0 + + return self.store.keys() + + def append(self, item): + + "Append the given 'item' to the store." + + if not self.can_write(): + return + + self.store.append(item) + + def __len__(self): + + "Return the number of items in the store." + + if not self.can_read(): + return 0 + + return len(self.store) + + def __getitem__(self, number): + + "Return the item with the given 'number'." + + if not self.can_read(): + raise IndexError, number + + return self.store.__getitem__(number) + + def __delitem__(self, number): + + "Remove the item with the given 'number'." + + if not self.can_delete(): + return + + return self.store.__delitem__(number) + + def __iter__(self): + return self.store.__iter__() + + def next(self): + return self.store.next() + +# Convenience store classes. + +class ItemStore(ItemStoreBase): + + "Store items in a directory via a page." + + def __init__(self, page, item_dir="items", lock_dir="item_locks"): + ItemStoreBase.__init__(self, page, getDirectoryItemStoreForPage(page, item_dir, lock_dir)) + +class ItemSubpageStore(ItemStoreBase): + + "Store items in subpages of a page." + + def __init__(self, page, lock_dir="item_locks"): + ItemStoreBase.__init__(self, page, getSubpageItemStoreForPage(page, lock_dir)) + # vim: tabstop=4 expandtab shiftwidth=4 diff -r 66d831c42a44 -r c7fd6b245e13 MoinSupport.py --- a/MoinSupport.py Mon Nov 11 13:42:06 2013 +0100 +++ b/MoinSupport.py Tue Jan 21 18:26:01 2014 +0100 @@ -2,7 +2,7 @@ """ MoinMoin - MoinSupport library (derived from EventAggregatorSupport) - @copyright: 2008, 2009, 2010, 2011, 2012, 2013 by Paul Boddie + @copyright: 2008, 2009, 2010, 2011, 2012, 2013, 2014 by Paul Boddie @copyright: 2000-2004 Juergen Hermann 2004 by Florian Festi 2006 by Mikko Virkkil @@ -13,10 +13,8 @@ """ from DateSupport import * -from ItemSupport import ItemDirectoryStore from MoinMoin.parser import text_moin_wiki from MoinMoin.Page import Page -from MoinMoin.util import lock from MoinMoin import config, search, wikiutil from shlex import shlex import re @@ -36,7 +34,7 @@ except ImportError: pass -__version__ = "0.4.1" +__version__ = "0.5" # Extraction of shared fragments. @@ -192,6 +190,12 @@ else: return None +def groupHasMember(request, groupname, username): + if hasattr(request.dicts, "has_member"): + return request.dicts.has_member(groupname, username) + else: + return username in request.groups.get(groupname, []) + # Searching-related functions. def getPagesFromResults(result_pages, request): @@ -912,6 +916,17 @@ buf.close() return unicode(text, "utf-8") +class RawParser: + + "A parser that just formats everything as text." + + def __init__(self, raw, request, **kw): + self.raw = raw + self.request = request + + def format(self, fmt, write=None): + (write or self.request.write)(fmt.text(self.raw)) + # Finding components for content types. def getParsersForContentType(cfg, mimetype): @@ -1091,87 +1106,4 @@ else: return title -# Content storage support. - -class ItemStore(ItemDirectoryStore): - - "A page-specific item store." - - def __init__(self, page, item_dir="items", lock_dir="item_locks"): - - "Initialise an item store for the given 'page'." - - item_dir_path = tuple(item_dir.split("/")) - lock_dir_path = tuple(lock_dir.split("/")) - ItemDirectoryStore.__init__(self, page.getPagePath(*item_dir_path), page.getPagePath(*lock_dir_path)) - self.page = page - - def can_write(self): - - """ - Return whether the user associated with the request can write to the - page owning this store. - """ - - user = self.page.request.user - return user and user.may.write(self.page.page_name) - - def can_read(self): - - """ - Return whether the user associated with the request can read from the - page owning this store. - """ - - user = self.page.request.user - return user and user.may.read(self.page.page_name) - - def can_delete(self): - - """ - Return whether the user associated with the request can delete the - page owning this store. - """ - - user = self.page.request.user - return user and user.may.delete(self.page.page_name) - - # High-level methods. - - def append(self, item): - - "Append the given 'item' to the store." - - if not self.can_write(): - return - - ItemDirectoryStore.append(self, item) - - def __len__(self): - - "Return the number of items in the store." - - if not self.can_read(): - return 0 - - return ItemDirectoryStore.__len__(self) - - def __getitem__(self, number): - - "Return the item with the given 'number'." - - if not self.can_read(): - raise IndexError, number - - return ItemDirectoryStore.__getitem__(self, number) - - def __delitem__(self, number): - - "Remove the item with the given 'number'." - - if not self.can_delete(): - return - - return ItemDirectoryStore.__delitem__(self, number) - # vim: tabstop=4 expandtab shiftwidth=4 diff -r 66d831c42a44 -r c7fd6b245e13 PKG-INFO --- a/PKG-INFO Mon Nov 11 13:42:06 2013 +0100 +++ b/PKG-INFO Tue Jan 21 18:26:01 2014 +0100 @@ -1,12 +1,12 @@ Metadata-Version: 1.1 Name: MoinSupport -Version: 0.4.1 +Version: 0.5 Author: Paul Boddie Author-email: paul at boddie org uk Maintainer: Paul Boddie Maintainer-email: paul at boddie org uk Home-page: http://hgweb.boddie.org.uk/MoinSupport -Download-url: http://hgweb.boddie.org.uk/MoinSupport/archive/rel-0-4-1.tar.bz2 +Download-url: http://hgweb.boddie.org.uk/MoinSupport/archive/rel-0-5.tar.bz2 Summary: Support libraries for MoinMoin extensions License: GPL (version 2 or later) Description: The MoinSupport distribution provides libraries handling datetime diff -r 66d831c42a44 -r c7fd6b245e13 README.txt --- a/README.txt Mon Nov 11 13:42:06 2013 +0100 +++ b/README.txt Tue Jan 21 18:26:01 2014 +0100 @@ -5,8 +5,7 @@ extensions. Some of the provided modules can be used independently of MoinMoin, such as the ContentTypeSupport, DateSupport, GeneralSupport, LocationSupport and ViewSupport modules which do not themselves import any -MoinMoin functionality. The ItemSupport module only imports file-locking -functionality from MoinMoin and could potentially be used independently. +MoinMoin functionality. Installation ------------ @@ -64,6 +63,17 @@ If time zone handling is not required, pytz need not be installed. It is, however, highly recommended that pytz be installed. +New in MoinSupport 0.5 (Changes since MoinSupport 0.4.1) +-------------------------------------------------------- + + * Moved ItemStore and related functionality into ItemSupport. + * Added support for subpage-based item stores. + * Added groupHasMember from ApproveChanges. + * Added the TokenSupport module to try and have a reliable shell-like + tokeniser. + * Added RFC 2822 datetime formatting. + * Added a "raw" parser which just formats its input as text. + New in MoinSupport 0.4.1 (Changes since MoinSupport 0.4) -------------------------------------------------------- diff -r 66d831c42a44 -r c7fd6b245e13 TokenSupport.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/TokenSupport.py Tue Jan 21 18:26:01 2014 +0100 @@ -0,0 +1,109 @@ +# -*- coding: iso-8859-1 -*- +""" + MoinMoin - TokenSupport library + + @copyright: 2013 by Paul Boddie + @license: GNU GPL (v2 or later), see COPYING.txt for details. +""" + +import re + +identifier_expr = re.compile( + """(?P[^'" ]+)""" + "|" + "(?P +)" + "|" + "(?P'[^']*')" + "|" + '(?P"[^"]*")' + ) + +def getIdentifiers(s, doubling=False): + + """ + Return 's' containing space-separated quoted identifiers, parsed into + regions that hold the individual identifiers. The optional 'doubling' + argument can be used to support convenient quote doubling to reproduce + single quote characters. + + Quoting of identifiers can be done using the single-quote and double-quote + characters in order to include spaces within identifiers. For example: + + 'contains space' + -> contains space (a single identifier) + + Where one kind of quote (or apostrophe) is to be included in an identifier, + the other quoting character can be used to delimit the identifier. For + example: + + "Python's syntax" + -> Python's syntax (a single identifier) + + Where the 'doubling' argument is set to a true value, a quote character can + be doubled to include it in an identifier. For example: + + Python''s syntax + -> Python's syntax (a single identifier) + + Where a mixture of quotes is required in a single identifier, adjacent + quoted regions can be used. For example: + + "Python's "'"intuitive" syntax' + -> "Python's " (region #1) + + '"intuitive" syntax' (region #2) + -> Python's "intuitive" syntax (a single identifier) + + Where unquoted regions are adjacent to quoted regions, the regions are + combined. For example: + + "Python's "intuitive" syntax" + -> "Python's " (region #1) + + intuitive (region #2) + + " syntax" (region #3) + -> Python's intuitive syntax (a single identifier) + """ + + regions = [] + in_literal = False + + for match in identifier_expr.finditer(s): + non_literal, spaces, literal1, literal2 = match.groups() + + identifier = None + + # Spaces prevent continuation of identifier regions. + + if spaces: + in_literal = False + + # Unquoted regions contribute to the current identifier. + + if non_literal and non_literal.strip(): + identifier = non_literal.strip() + + # Quoted regions also contribute to the current identifier. + + for s in (literal1, literal2): + if s is not None: + + # Either strip the quoting or for empty regions, adopt the + # quote character. + + if not doubling or len(s) > 2: + identifier = s[1:-1] + elif doubling: + identifier = s[0] + + # Either continue or add an identifier, and indicate possible + # continuation. + + if identifier: + if in_literal: + regions[-1] += identifier + else: + regions.append(identifier) + in_literal = True + + return regions + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 66d831c42a44 -r c7fd6b245e13 setup.py --- a/setup.py Mon Nov 11 13:42:06 2013 +0100 +++ b/setup.py Tue Jan 21 18:26:01 2014 +0100 @@ -8,8 +8,9 @@ author = "Paul Boddie", author_email = "paul@boddie.org.uk", url = "http://hgweb.boddie.org.uk/MoinSupport", - version = "0.4.1", + version = "0.5", py_modules = ["ContentTypeSupport", "DateSupport", "GeneralSupport", "ItemSupport", "LocationSupport", "MoinDateSupport", - "MoinRemoteSupport", "MoinSupport", "ViewSupport"] + "MoinRemoteSupport", "MoinSupport", "TokenSupport", + "ViewSupport"] ) diff -r 66d831c42a44 -r c7fd6b245e13 tests/test_tokens.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test_tokens.py Tue Jan 21 18:26:01 2014 +0100 @@ -0,0 +1,19 @@ +#!/usr/bin/env python + +from TokenSupport import getIdentifiers + +tests = [ + (1, False, """'contains space'""", ["contains space"]), + (2, False, """contains space""", ["contains", "space"]), + (1, False, '''"Python's syntax"''', ["Python's syntax"]), + (2, False, """Python''s syntax""", ["Pythons", "syntax"]), + (2, True, """Python''s syntax""", ["Python's", "syntax"]), + (1, False, '''"Python's "'"intuitive" syntax' ''', ['''Python's "intuitive" syntax''']), + (1, False, '''"Python's "intuitive" syntax" ''', ['''Python's intuitive syntax''']), + ] + +for n, doubling, s, e in tests: + l = getIdentifiers(s, doubling) + print l == e, l, "==", e, len(l) == n, len(l), "==", n, "<-", doubling, s + +# vim: tabstop=4 expandtab shiftwidth=4