Mercurial > moin-2.0-moinboot
changeset 2138:c00408ad34f1
Merge theme tweaks merge into moinboot
| author | Andreas Kloeckner <inform@tiker.net> |
|---|---|
| date | Sun, 07 Jul 2013 11:16:59 -0400 |
| parents | 9226fc133d70 (current diff) ef151b24b567 (diff) |
| children | 49594879f2b8 |
| files | MoinMoin/templates/ticket.html |
| diffstat | 65 files changed, 852 insertions(+), 373 deletions(-) [+] |
line wrap: on
line diff
--- a/.hgignore Tue Apr 02 01:18:42 2013 -0400 +++ b/.hgignore Sun Jul 07 11:16:59 2013 -0400 @@ -8,6 +8,8 @@ ^moin.egg-info/ ^MoinMoin/_tests/wiki/data/cache/ ^wiki/data/cache/ +^wiki/data/default/data/ +^wiki/data/default/meta/ ^wiki/data/content/ ^wiki/data/userprofiles/ ^wiki/index/
--- a/MoinMoin/__init__.py Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/__init__.py Sun Jul 07 11:16:59 2013 -0400 @@ -12,8 +12,8 @@ project = "MoinMoin" import sys -if sys.hexversion < 0x2060000: - sys.exit("%s requires Python 2.6 or greater.\n" % project) +if sys.hexversion < 0x2070000: + sys.exit("%s requires Python 2.7.x.\n" % project) from MoinMoin.util.version import Version
--- a/MoinMoin/_template.py Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/_template.py Sun Jul 07 11:16:59 2013 -0400 @@ -1,4 +1,4 @@ -# Copyright: 2012 MoinMoin:YourNameHere +# Copyright: 2013 MoinMoin:YourNameHere # License: GNU GPL v2 (or any later version), see LICENSE.txt for details. """
--- a/MoinMoin/_tests/test_wikiutil.py Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/_tests/test_wikiutil.py Sun Jul 07 11:16:59 2013 -0400 @@ -33,7 +33,7 @@ def test_anchor_name_encoding(self): tests = [ # text, expected output - (u'\xf6\xf6ll\xdf\xdf', 'A.2BAPYA9g-ll.2BAN8A3w-'), + (u'\xf6\xf6ll\xdf\xdf', 'A_APYA9g-ll_AN8A3w-'), (u'level 2', 'level_2'), (u'level_2', 'level_2'), (u'', 'A'),
--- a/MoinMoin/apps/admin/views.py Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/apps/admin/views.py Sun Jul 07 11:16:59 2013 -0400 @@ -61,8 +61,8 @@ """ u = user.User(auth_username=user_name) if request.method == 'GET': - return _(u"User profile of %(username)s: %(email)r", username=user_name, - email=(u.email, u.disabled)) + return _(u"User profile of %(username)s: %(email)s %(disabled)s", username=user_name, + email=u.email, disabled=u.disabled) if request.method == 'POST': key = request.form.get('key', '')
--- a/MoinMoin/apps/frontend/views.py Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/apps/frontend/views.py Sun Jul 07 11:16:59 2013 -0400 @@ -57,6 +57,7 @@ from MoinMoin.util import crypto from MoinMoin.util.interwiki import url_for_item from MoinMoin.search import SearchForm +from MoinMoin.search.analyzers import item_name_analyzer from MoinMoin.security.textcha import TextCha, TextChaizedForm from MoinMoin.signalling import item_displayed, item_modified from MoinMoin.storage.middleware.protecting import AccessDenied @@ -141,6 +142,10 @@ submit_label = L_('Lookup') +def analyze(analyzer, text): + return [token.text for token in analyzer(text, mode='index')] + + @frontend.route('/+lookup', methods=['GET', 'POST']) def lookup(): """ @@ -188,27 +193,37 @@ term = Term(key, value) terms.append(term) if terms: + LookupEntry = namedtuple('LookupEntry', 'name revid wikiname') + name = lookup_form[NAME].value + name_exact = lookup_form[NAME_EXACT].value or u'' terms.append(Term(WIKINAME, app.cfg.interwikiname)) q = And(terms) with flaskg.storage.indexer.ix[idx_name].searcher() as searcher: flaskg.clock.start('lookup') results = searcher.search(q, limit=100) flaskg.clock.stop('lookup') - num_results = results.scored_length() - if num_results == 1: - result = results[0] - rev = result[REVID] if history else CURRENT - url = url_for('.show_item', item_name=result[NAME], rev=rev) + lookup_results = [] + for result in results: + analyzer = item_name_analyzer() + lookup_results += [LookupEntry(n, result[REVID], result[WIKINAME]) + for n in result[NAME] + if not name or name.lower() in analyze(analyzer, n) + if n.startswith(name_exact)] + + if len(lookup_results) == 1: + result = lookup_results[0] + rev = result.revid if history else CURRENT + url = url_for('.show_item', item_name=result.name, rev=rev) return redirect(url) else: flaskg.clock.start('lookup render') html = render_template('lookup.html', title_name=title_name, lookup_form=lookup_form, - results=results, + results=lookup_results, ) flaskg.clock.stop('lookup render') - if not num_results: + if not lookup_results: status = 404 return Response(html, status) html = render_template('lookup.html', @@ -348,9 +363,10 @@ item_name=item_name) try: item = Item.create(item_name, rev_id=rev) + result = item.do_show(rev) except AccessDenied: abort(403) - return item.do_show(rev) + return result @frontend.route('/<itemname:item_name>/') # note: unwanted trailing slash @@ -1476,7 +1492,7 @@ for name in new_names: if user.search_users(**{NAME_EXACT: name}): # duplicate name - response['flash'].append((_("The username %(name)r is already in use.", name=name), + response['flash'].append((_("The username '%(name)s' is already in use.", name=name), "error")) success = False if part == 'notification':
--- a/MoinMoin/auth/_tests/test_ldap_login.py Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/auth/_tests/test_ldap_login.py Sun Jul 07 11:16:59 2013 -0400 @@ -15,7 +15,6 @@ from MoinMoin._tests import wikiconfig from MoinMoin.auth import handle_login -# first check if we have python 2.4, python-ldap and slapd: msg = check_environ() if msg: pytest.skip(msg)
--- a/MoinMoin/config/default.py Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/config/default.py Sun Jul 07 11:16:59 2013 -0400 @@ -261,7 +261,8 @@ # the options dictionary. -def _default_password_checker(cfg, username, password): +def _default_password_checker(cfg, username, password, + min_length=8, min_different=5): """ Check if a password is secure enough. We use a built-in check to get rid of the worst passwords. @@ -274,10 +275,12 @@ some unicode object with an error msg, if the password is problematic. """ # in any case, do a very simple built-in check to avoid the worst passwords - if len(password) < 6: - return _("Password is too short.") - if len(set(password)) < 4: - return _("Password has not enough different characters.") + if len(password) < min_length: + return _("For a password a minimum length of %(min_length)d characters is required.", + min_length=min_length) + if len(set(password)) < min_different: + return _("For a password a minimum of %(min_different)d different characters is required.", + min_different=min_different) username_lower = username.lower() password_lower = password.lower() @@ -329,7 +332,7 @@ [], "Exclude unwanted endpoints (list of strings)"), ('password_checker', DefaultExpression('_default_password_checker'), - 'checks whether a password is acceptable (default check is length >= 6, at least 4 different chars, no keyboard sequence, not username used somehow (you can switch this off by using `None`)'), + 'does simple checks whether a password is acceptable (you can switch this off by using `None` or enhance it by using a custom checker)'), ('passlib_crypt_context', dict( # schemes we want to support (or deprecated schemes for which we still have @@ -481,6 +484,7 @@ SHOW_COMMENTS: False, WANT_TRIVIAL: False, ENC_PASSWORD: u'', # empty value == invalid hash + RECOVERPASS_KEY: u'', # empty value == invalid key SESSION_KEY: u'', # empty value == invalid key DISABLED: False, BOOKMARKS: {},
--- a/MoinMoin/constants/keys.py Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/constants/keys.py Sun Jul 07 11:16:59 2013 -0400 @@ -5,90 +5,86 @@ MoinMoin - meta data key / index field name related constants """ -# IMPORTANT: until we require a python >= 2.6.5, we need to keep the keys as -# str (not unicode), because of "Issue #4978: Passing keyword -# arguments as unicode strings is now allowed." (from 2.6.5 chglog) - # metadata keys -NAME = "name" -NAME_OLD = "name_old" -NAMESPACE = "namespace" +NAME = u"name" +NAME_OLD = u"name_old" +NAMESPACE = u"namespace" # if an item is reverted, we store the revision number we used for reverting there: -REVERTED_TO = "reverted_to" +REVERTED_TO = u"reverted_to" # some metadata key constants: -ACL = "acl" +ACL = u"acl" # keys for storing group and dict information # group of user names, e.g. for ACLs: -USERGROUP = "usergroup" +USERGROUP = u"usergroup" # needs more precise name / use case: -SOMEDICT = "somedict" +SOMEDICT = u"somedict" -CONTENTTYPE = "contenttype" -ITEMTYPE = "itemtype" -SIZE = "size" -LANGUAGE = "language" -EXTERNALLINKS = "externallinks" -ITEMLINKS = "itemlinks" -ITEMTRANSCLUSIONS = "itemtransclusions" -TAGS = "tags" +CONTENTTYPE = u"contenttype" +ITEMTYPE = u"itemtype" +SIZE = u"size" +LANGUAGE = u"language" +EXTERNALLINKS = u"externallinks" +ITEMLINKS = u"itemlinks" +ITEMTRANSCLUSIONS = u"itemtransclusions" +TAGS = u"tags" -ACTION = "action" -ADDRESS = "address" -HOSTNAME = "hostname" -USERID = "userid" -MTIME = "mtime" -EXTRA = "extra" -COMMENT = "comment" -SUMMARY = "summary" +ACTION = u"action" +ADDRESS = u"address" +HOSTNAME = u"hostname" +USERID = u"userid" +MTIME = u"mtime" +EXTRA = u"extra" +COMMENT = u"comment" +SUMMARY = u"summary" # we need a specific hash algorithm to store hashes of revision data into meta # data. meta[HASH_ALGORITHM] = hash(rev_data, HASH_ALGORITHM) # some backends may use this also for other purposes. -HASH_ALGORITHM = 'sha1' +HASH_ALGORITHM = u"sha1" HASH_LEN = 40 # length of hex str representation of hash value # some field names for whoosh index schema / documents in index: -NAME_EXACT = "name_exact" -ITEMID = "itemid" -REVID = "revid" -PARENTID = "parentid" -DATAID = "dataid" -WIKINAME = "wikiname" -CONTENT = "content" +NAME_EXACT = u"name_exact" +ITEMID = u"itemid" +REVID = u"revid" +PARENTID = u"parentid" +DATAID = u"dataid" +WIKINAME = u"wikiname" +CONTENT = u"content" # magic REVID for current revision: -CURRENT = "current" +CURRENT = u"current" # stuff from user profiles / for whoosh index -EMAIL = "email" -OPENID = "openid" -DISPLAY_NAME = "display_name" -THEME_NAME = "theme_name" -LOCALE = "locale" -TIMEZONE = "timezone" -ENC_PASSWORD = "enc_password" -SUBSCRIBED_ITEMS = "subscribed_items" -BOOKMARKS = "bookmarks" -QUICKLINKS = "quicklinks" -SESSION_KEY = "session_key" -SESSION_TOKEN = "session_token" -RECOVERPASS_KEY = "recoverpass_key" -EDIT_ON_DOUBLECLICK = "edit_on_doubleclick" -SCROLL_PAGE_AFTER_EDIT = "scroll_page_after_edit" -SHOW_COMMENTS = "show_comments" -MAILTO_AUTHOR = "mailto_author" -CSS_URL = "css_url" -EDIT_ROWS = "edit_rows" -RESULTS_PER_PAGE = "results_per_page" -WANT_TRIVIAL = "want_trivial" -EMAIL_SUBSCRIBED_EVENTS = "email_subscribed_events" -DISABLED = "disabled" +EMAIL = u"email" +OPENID = u"openid" +DISPLAY_NAME = u"display_name" +THEME_NAME = u"theme_name" +LOCALE = u"locale" +TIMEZONE = u"timezone" +ENC_PASSWORD = u"enc_password" +SUBSCRIBED_ITEMS = u"subscribed_items" +BOOKMARKS = u"bookmarks" +QUICKLINKS = u"quicklinks" +SESSION_KEY = u"session_key" +SESSION_TOKEN = u"session_token" +RECOVERPASS_KEY = u"recoverpass_key" +EDIT_ON_DOUBLECLICK = u"edit_on_doubleclick" +SCROLL_PAGE_AFTER_EDIT = u"scroll_page_after_edit" +SHOW_COMMENTS = u"show_comments" +MAILTO_AUTHOR = u"mailto_author" +CSS_URL = u"css_url" +EDIT_ROWS = u"edit_rows" +RESULTS_PER_PAGE = u"results_per_page" +WANT_TRIVIAL = u"want_trivial" +EMAIL_SUBSCRIBED_EVENTS = u"email_subscribed_events" +DISABLED = u"disabled" # in which backend is some revision stored? -BACKENDNAME = "backendname" +BACKENDNAME = u"backendname" USEROBJ_ATTRS = [ # User objects proxy these attributes of the UserProfile objects: @@ -99,10 +95,20 @@ ] # keys for blog homepages -LOGO = "logo" -SUPERTAGS = "supertags" +LOGO = u"logo" +SUPERTAGS = u"supertags" # keys for blog entries -PTIME = "ptime" +PTIME = u"ptime" + +# keys for tickets +EFFORT = u"effort" +DIFFICULTY = u"difficulty" +SEVERITY = u"severity" +PRIORITY = u"priority" +ASSIGNED_TO = u"assigned_to" +SUPERSEDED_BY = u"superseded_by" +DEPENDS_ON = u"depends_on" +CLOSED = u"closed" # index names LATEST_REVS = 'latest_revs'
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/converter/_tests/test_image.py Sun Jul 07 11:16:59 2013 -0400 @@ -0,0 +1,40 @@ +# Copyright: 2013 MoinMoin:RishabhRaj +# License: GNU GPL v2 (or any later version), see LICENSE.txt for details. + +""" +MoinMoin - Tests for MoinMoin.converter for different imagetypes +""" + +import pytest + +from emeraldtree import tree as ET + +from MoinMoin.converter.html_out import ConverterPage + + +class TestImg(object): + def setup_class(self): + self.converter = ConverterPage() + + def testImage(self): + tree_xml = ('<ns0:page ns0:page-href="wiki:///Home" xmlns:ns0="http://moinmo.in/namespaces/page" ' + 'xmlns:ns1="http://www.w3.org/2001/XInclude" xmlns:ns2="http://www.w3.org/1999/xhtml" ' + 'xmlns:ns3="http://www.w3.org/1999/xlink"><ns0:body><ns0:p ns2:data-lineno="1">' + '<ns0:page ns2:class="moin-transclusion" ns0:page-href="wiki:///imagetest" ns2:data-href="/imagetest">' + '<ns0:body><ns0:object ns3:href="/+get/+2882c905b2ab409fbf79cd05637a112d/imagetest" ns0:type="{0}" />' + '</ns0:body></ns0:page></ns0:p></ns0:body></ns0:page>') + tests = [ + ('image/jpeg', 'img'), + ('image/svg+xml', 'img'), + ('image/png', 'img'), + ('image/gif', 'img'), + ] + + for imagetype, tag_expected in tests: + self.runTest(tree_xml.format(imagetype), tag_expected) + + def runTest(self, tree_xml, tag_expected): + tree = ET.XML(tree_xml) + tree = self.converter(tree) + assert len(tree) and len(tree[0]) and len(tree[0][0]) == 1 + assert tree[0][0][0].tag.name == tag_expected
--- a/MoinMoin/converter/html_out.py Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/converter/html_out.py Sun Jul 07 11:16:59 2013 -0400 @@ -338,9 +338,7 @@ Return value is an str, one of the following: image, video, audio, object """ - if Type('image/').issupertype(mimetype) and not Type('image/svg+xml').issupertype(mimetype): - # Firefox fails completely to show svg in img tags (displays: nothing). - # Firefox displays them with on object tag (but sometimes displays scrollbars without need). + if Type('image/').issupertype(mimetype): return "img" elif Type('video/').issupertype(mimetype): return "video"
--- a/MoinMoin/converter/link.py Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/converter/link.py Sun Jul 07 11:16:59 2013 -0400 @@ -169,6 +169,7 @@ path = self.absolute_path(path, page.path) item_name = unicode(path) if not flaskg.storage.has_item(item_name): + # XXX these index accesses slow down the link converter quite a bit elem.set(moin_page.class_, 'moin-nonexistent') else: item_name = unicode(page.path[1:])
--- a/MoinMoin/converter/text_csv_in.py Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/converter/text_csv_in.py Sun Jul 07 11:16:59 2013 -0400 @@ -27,7 +27,7 @@ def __call__(self, data, contenttype=None, arguments=None): text = decode_data(data, contenttype) content = normalize_split_text(text) - # as of py 2.6.5 (and in the year 2010), the csv module seems to still + # as of py 2.7.x (and in the year 2013), the csv module seems to still # have troubles with unicode, thus we encode to utf-8 ... content = [line.encode('utf-8') for line in content] dialect = csv.Sniffer().sniff(content[0])
--- a/MoinMoin/forms.py Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/forms.py Sun Jul 07 11:16:59 2013 -0400 @@ -20,10 +20,12 @@ from flatland.validation import Validator, Present, IsEmail, ValueBetween, URLValidator, Converted, ValueAtLeast from flatland.exc import AdaptationError +from whoosh.query import Term, Or, Not, And + from flask import g as flaskg from MoinMoin.constants.forms import * -from MoinMoin.constants.keys import ITEMID, NAME +from MoinMoin.constants.keys import ITEMID, NAME, LATEST_REVS from MoinMoin.i18n import _, L_, N_ from MoinMoin.util.forms import FileStorage @@ -71,12 +73,36 @@ """Validator for JSON """ invalid_json_msg = L_('Invalid JSON.') + invalid_name_msg = "" + + def validname(self, meta, name, itemid): + names = meta.get(NAME) + if names is None: + self.invalid_name_msg = L_("No name field in the JSON meta.") + return False + if len(names) != len(set(names)): + self.invalid_name_msg = L_("The names in the JSON name list must be unique.") + return False + query = Or([Term(NAME, x) for x in names]) + if itemid is not None: + query = And([query, Not(Term(ITEMID, itemid))]) + duplicate_names = set() + with flaskg.storage.indexer.ix[LATEST_REVS].searcher() as searcher: + results = searcher.search(query) + for result in results: + duplicate_names |= set([x for x in result[NAME] if x in names]) + if duplicate_names: + self.invalid_name_msg = L_("Item(s) named %(duplicate_names)s already exist.", duplicate_names=", ".join(duplicate_names)) + return False + return True def validate(self, element, state): try: - json.loads(element.value) + meta = json.loads(element.value) except: # catch ANY exception that happens due to unserializing return self.note_error(element, state, 'invalid_json_msg') + if not self.validname(meta, state[NAME], state[ITEMID]): + return self.note_error(element, state, 'invalid_name_msg') return True JSON = OptionalMultilineText.with_properties(lang='en', dir='ltr').validated_by(ValidJSON()) @@ -218,9 +244,9 @@ return cls @classmethod - def _get_choices(cls): + def _get_choice_specs(cls): revs = flaskg.storage.search(cls._query, **cls._query_args) - choices = [(rev.meta[ITEMID], rev.meta[NAME]) for rev in revs] + choices = [(rev.meta[ITEMID], rev.meta[NAME][0]) for rev in revs] if cls.optional: choices.append((u'', cls.properties['empty_label'])) return choices @@ -230,9 +256,9 @@ # NOTE There is a slight chance of two instances of the same Reference # subclass having different set of choices when the storage changes # between their initialization. - choices = self._get_choices() - self.properties['labels'] = dict(choices) - self.valid_values = [id_ for id_, name in choices] + choice_specs = self._get_choice_specs() + self.properties['choice_specs'] = choice_specs + self.valid_values = [id_ for id_, name in choice_specs] class BackReference(ReadonlyItemLinkList):
--- a/MoinMoin/items/__init__.py Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/items/__init__.py Sun Jul 07 11:16:59 2013 -0400 @@ -455,7 +455,7 @@ """ raise NotImplementedError - def _save(self, meta, data=None, name=None, action=u'SAVE', contenttype_guessed=None, comment=u'', + def _save(self, meta, data=None, name=None, action=u'SAVE', contenttype_guessed=None, comment=None, overwrite=False, delete=False): backend = flaskg.storage storage_item = backend[self.name] @@ -490,7 +490,7 @@ else: meta[NAME] = [name] - if comment: + if comment is not None: meta[COMMENT] = unicode(comment) if not overwrite and REVID in meta: @@ -524,7 +524,6 @@ def subitems_prefix(self): return self.name + u'/' if self.name else u'' - @timed() def get_subitem_revs(self): """ Create a list of subitems of this item. @@ -539,7 +538,6 @@ revs = flaskg.storage.search(query, sortedby=NAME_EXACT, limit=None) return revs - @timed() def make_flat_index(self, subitems): """ Create two IndexEntry lists - ``dirs`` and ``files`` - from a list of @@ -727,7 +725,8 @@ # break them return "OK" form = self.ModifyForm.from_request(request) - if form.validate(): + state = dict(name=self.name, itemid=self.meta.get(ITEMID)) + if form.validate(state): meta, data, contenttype_guessed, comment = form._dump(self) contenttype_qs = request.values.get('contenttype') try:
--- a/MoinMoin/items/_tests/test_Blog.py Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/items/_tests/test_Blog.py Sun Jul 07 11:16:59 2013 -0400 @@ -7,6 +7,7 @@ import re +from datetime import datetime from flask import url_for from MoinMoin._tests import become_trusted, update_item @@ -15,6 +16,7 @@ from MoinMoin.constants.misc import ANON from MoinMoin.items.blog import ITEMTYPE_BLOG, ITEMTYPE_BLOG_ENTRY from MoinMoin.items.blog import Blog, BlogEntry +from MoinMoin.themes import utctimestamp class TestView(object): @@ -69,25 +71,39 @@ def test_do_show_entries(self): item = Item.create(self.name, itemtype=ITEMTYPE_BLOG) item._save(self.meta, self.data, comment=self.comment) - # store some unpublished entries + # store entries without PTIME + for entry in self.entries: + item = Item.create(entry['name'], itemtype=ITEMTYPE_BLOG_ENTRY) + item._save(self.entry_meta, entry['data'], comment=self.comment) + # the blog is not empty + exclude_data_tokens = [self.NO_ENTRIES_MSG, ] + # all stored blog entries are listed on the blog index page + data_tokens = [self.data, ] + [entry['data'] for entry in self.entries] + self._test_view(self.name, data_tokens=data_tokens, exclude_data_tokens=exclude_data_tokens) + + def test_do_show_sorted_entries(self): + item = Item.create(self.name, itemtype=ITEMTYPE_BLOG) + item._save(self.meta, self.data, comment=self.comment) + # store entries for entry in self.entries: item = Item.create(entry['name'], itemtype=ITEMTYPE_BLOG_ENTRY) item._save(self.entry_meta, entry['data'], comment=self.comment) - # still empty blog - data_tokens = [self.data, self.NO_ENTRIES_MSG, ] - self._test_view(self.name, data_tokens=data_tokens) - # publish the first three entries, ptime value is a UNIX timestamp - self._publish_entry(self.entries[0], ptime=1000) - self._publish_entry(self.entries[1], ptime=3000) - self._publish_entry(self.entries[2], ptime=2000) - # the blog is not empty and the 4th entry is not published - exclude_data_tokens = [self.NO_ENTRIES_MSG, self.entries[3]['data'], ] - # blog entries are published in reverse order relative to their PTIMEs + # Add PTIME to some of the entries, ptime value is a UNIX timestamp. If PTIME + # is not defined, we use MTIME as publication time (which is usually in the past). + self._publish_entry(self.entries[0], ptime=2000) + self._publish_entry(self.entries[1], ptime=1000) + time_in_future = utctimestamp(datetime(2029, 1, 1)) + self._publish_entry(self.entries[2], ptime=time_in_future) + # the blog is not empty + exclude_data_tokens = [self.NO_ENTRIES_MSG, ] + # blog entries are listed in reverse order relative to their PTIME/MTIMEs, + # entries published in the future are also listed here ordered_data = [self.data, - self.entries[1]['data'], self.entries[2]['data'], - self.entries[0]['data'], ] - regex = re.compile(r'{0}.*{1}.*{2}.*{3}'.format(*ordered_data), re.DOTALL) + self.entries[3]['data'], + self.entries[0]['data'], + self.entries[1]['data'], ] + regex = re.compile(r'{0}.*{1}.*{2}.*{3}.*{4}'.format(*ordered_data), re.DOTALL) self._test_view(self.name, exclude_data_tokens=exclude_data_tokens, regex=regex) def test_filter_by_tag(self):
--- a/MoinMoin/items/content.py Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/items/content.py Sun Jul 07 11:16:59 2013 -0400 @@ -125,7 +125,6 @@ return cls -@timed() def conv_serialize(doc, namespaces, method='polyglot'): out = array('u') doc.write(out.fromunicode, namespaces=namespaces, method=method)
--- a/MoinMoin/items/ticket.py Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/items/ticket.py Sun Jul 07 11:16:59 2013 -0400 @@ -22,7 +22,7 @@ from MoinMoin.forms import (Form, OptionalText, OptionalMultilineText, SmallNatural, Tags, Reference, BackReference, SelectSubmit) from MoinMoin.storage.middleware.protecting import AccessDenied -from MoinMoin.constants.keys import ITEMTYPE, CONTENTTYPE, ITEMID, CURRENT +from MoinMoin.constants.keys import ITEMTYPE, CONTENTTYPE, ITEMID, CURRENT, SUPERSEDED_BY, DEPENDS_ON, SUBSCRIBED_ITEMS from MoinMoin.constants.contenttypes import CONTENTTYPE_USER from MoinMoin.items import Item, Contentful, register, BaseModifyForm from MoinMoin.items.content import NonExistentContent @@ -57,9 +57,9 @@ def _load(self, item): id_ = item.meta[ITEMID] - self['supersedes'].set(Term('superseded_by', id_)) - self['required_by'].set(Term('depends_on', id_)) - self['subscribers'].set(Term('subscribed_items', id_)) + self['supersedes'].set(Term(SUPERSEDED_BY, id_)) + self['required_by'].set(Term(DEPENDS_ON, id_)) + self['subscribers'].set(Term(SUBSCRIBED_ITEMS, id_)) class TicketForm(BaseModifyForm): @@ -86,6 +86,7 @@ CONTENTTYPE: 'text/x.moin.wiki;charset=utf-8', 'closed': False, } + meta.update(self['meta'].value) return meta, message_markup(self['message'].value) @@ -129,7 +130,7 @@ %(message)s }}}}}} -''' % dict(author=flaskg.user.name, timestamp=time.time(), message=message) +''' % dict(author=flaskg.user.name[0], timestamp=time.time(), message=message) @register @@ -137,7 +138,8 @@ itemtype = ITEMTYPE_TICKET display_name = L_('Ticket') description = L_('Ticket item') - modify_template = 'ticket.html' + submit_template = 'ticket/submit.html' + modify_template = 'ticket/modify.html' def do_show(self, revid): if revid != CURRENT: @@ -168,7 +170,7 @@ # XXX When creating new item, suppress the "foo doesn't exist. Create it?" dummy content data_rendered = None if is_new else Markup(self.content._render_data()) - return render_template(self.modify_template, + return render_template(self.submit_template if is_new else self.modify_template, is_new=is_new, closed=closed, item_name=self.name,
--- a/MoinMoin/mail/_tests/test_sendmail.py Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/mail/_tests/test_sendmail.py Sun Jul 07 11:16:59 2013 -0400 @@ -7,8 +7,8 @@ """ -from email.Charset import Charset, QP -from email.Header import Header +from email.charset import Charset, QP +from email.header import Header from MoinMoin.mail import sendmail from MoinMoin.constants.contenttypes import CHARSET
--- a/MoinMoin/mail/sendmail.py Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/mail/sendmail.py Sun Jul 07 11:16:59 2013 -0400 @@ -10,7 +10,7 @@ import os import re -from email.Header import Header +from email.header import Header from MoinMoin import log logging = log.getLogger(__name__) @@ -82,9 +82,9 @@ """ import smtplib import socket - from email.Message import Message - from email.Charset import Charset, QP - from email.Utils import formatdate, make_msgid + from email.message import Message + from email.charset import Charset, QP + from email.utils import formatdate, make_msgid cfg = app.cfg if not cfg.mail_enabled:
--- a/MoinMoin/script/account/resetpw.py Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/script/account/resetpw.py Sun Jul 07 11:16:59 2013 -0400 @@ -8,6 +8,9 @@ """ +import sys + +from flask import current_app as app from flask.ext.script import Command, Option from MoinMoin.constants.keys import ITEMID, NAME, NAME_EXACT, EMAIL @@ -23,17 +26,25 @@ """raised if no such user exists""" +class UserHasNoEMail(Fault): + """raised if user has no e-mail address in his profile""" + + class MailFailed(Fault): """raised if e-mail sending failed""" -def set_password(uid, password, notify=False): +def set_password(uid, password, notify=False, skip_invalid=False, subject=None, text=None): u = user.User(uid) if u and u.exists(): + if skip_invalid and u.has_invalidated_password(): + return u.set_password(password) u.save() - if notify and not u.disabled and u.email: - mailok, msg = u.mail_password_recovery() + if not u.email: + raise UserHasNoEMail('User profile does not have an E-Mail address (name: %r id: %r)!' % (u.name, u.id)) + if notify and not u.disabled: + mailok, msg = u.mail_password_recovery(subject=subject, text=text) if not mailok: raise MailFailed(msg) else: @@ -55,14 +66,29 @@ help='Notify user(s), send them an E-Mail with a password reset link.'), Option('--verbose', '-v', required=False, dest='verbose', action='store_true', default=False, help='Verbose operation'), + Option('--subject', required=False, dest='subject', type=unicode, + help='Subject text for the password reset notification E-Mail.'), + Option('--text', required=False, dest='text', type=unicode, + help='Template text for the password reset notification E-Mail. Default: use the builtin standard template'), + Option('--text-from-file', required=False, dest='text_file', type=unicode, + help='Read full template for the password reset notification E-Mail from the given file, overrides --text. Default: None'), + Option('--skip-invalid', required=False, dest='skip_invalid', action='store_true', + help='If a user\'s password hash is already invalid (pw is already reset), skip this user.'), ) - def run(self, name, uid, password, all_users, notify, verbose): + def run(self, name, uid, password, all_users, notify, verbose, subject, text, text_file, skip_invalid): flags_given = name or uid or all_users if not flags_given: print 'incorrect number of arguments' - import sys - sys.exit() + sys.exit(1) + + if notify and not app.cfg.mail_enabled: + print "This wiki is not enabled for mail processing. The --notify option requires this. Aborting..." + sys.exit(1) + + if text_file: + with open(text_file) as f: + text = f.read().decode('utf-8') before_wiki() if uid: @@ -79,8 +105,9 @@ name = meta[NAME] email = meta[EMAIL] try: - set_password(uid, password, notify=notify) - except Fault, err: + set_password(uid, password, notify=notify, skip_invalid=skip_invalid, + subject=subject, text=text) + except Fault as err: status = "FAILURE: [%s]" % str(err) else: status = "SUCCESS"
--- a/MoinMoin/security/__init__.py Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/security/__init__.py Sun Jul 07 11:16:59 2013 -0400 @@ -152,7 +152,7 @@ +SomeUser:read -OtherUser:write - The acl line above will grant SomeUser read right, and OtherUser + The acl line above will grant SomeUser read right, and deny OtherUser write right, but will NOT block automatically all other rights for these users. For example, if SomeUser asks to write, the above acl line does not define if he can or can not write. He
--- a/MoinMoin/storage/backends/_tests/__init__.py Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/storage/backends/_tests/__init__.py Sun Jul 07 11:16:59 2013 -0400 @@ -62,7 +62,7 @@ assert m == meta assert d.read() == data d.close() - self.be.remove(metaid) + self.be.remove(metaid, destroy_data=True) with pytest.raises(KeyError): self.be.retrieve(metaid)
--- a/MoinMoin/storage/backends/stores.py Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/storage/backends/stores.py Sun Jul 07 11:16:59 2013 -0400 @@ -168,8 +168,9 @@ def _del_data(self, dataid): del self.data_store[dataid] - def remove(self, metaid): + def remove(self, metaid, destroy_data): meta = self._get_meta(metaid) dataid = meta[DATAID] self._del_meta(metaid) - self._del_data(dataid) + if destroy_data: + self._del_data(dataid)
--- a/MoinMoin/storage/middleware/_tests/test_routing.py Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/storage/middleware/_tests/test_routing.py Sun Jul 07 11:16:59 2013 -0400 @@ -63,8 +63,8 @@ assert other_name.split(':')[1] == other_meta[NAME][0] # delete revs: - router.remove(default_backend_name, default_revid) - router.remove(other_backend_name, other_revid) + router.remove(default_backend_name, default_revid, destroy_data=True) + router.remove(other_backend_name, other_revid, destroy_data=True) def test_store_readonly_fails(router):
--- a/MoinMoin/storage/middleware/indexing.py Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/storage/middleware/indexing.py Sun Jul 07 11:16:59 2013 -0400 @@ -72,14 +72,9 @@ from MoinMoin import log logging = log.getLogger(__name__) -from MoinMoin.constants.keys import (WIKINAME, NAMESPACE, NAME, NAME_EXACT, MTIME, CONTENTTYPE, TAGS, LANGUAGE, - USERID, ADDRESS, HOSTNAME, SIZE, ACTION, COMMENT, SUMMARY, CONTENT, - EXTERNALLINKS, ITEMLINKS, ITEMTRANSCLUSIONS, ACL, DISABLED, EMAIL, OPENID, - ITEMID, REVID, CURRENT, PARENTID, PTIME, LATEST_REVS, ALL_REVS, BACKENDNAME) +from MoinMoin.constants.keys import * from MoinMoin.constants.contenttypes import CONTENTTYPE_USER from MoinMoin.constants.namespaces import NAMESPACE_DEFAULT -from MoinMoin.constants import keys -from MoinMoin.constants.keys import ITEMTYPE from MoinMoin import user from MoinMoin.search.analyzers import item_name_analyzer, MimeTokenizer, AclTokenizer @@ -139,7 +134,7 @@ :param wikiname: interwikiname of this wiki :returns: document to put into whoosh index """ - doc = dict([(str(key), value) + doc = dict([(key, value) for key, value in meta.items() if key in schema]) for key in [MTIME, PTIME]: @@ -301,6 +296,8 @@ COMMENT: TEXT(stored=True), # SUMMARY from metadata SUMMARY: TEXT(stored=True), + # DATAID from metadata + DATAID: ID(stored=True), # data (content), converted to text/plain and tokenized CONTENT: TEXT(stored=True), } @@ -330,14 +327,14 @@ # XXX This is a highly adhoc way to support indexing of ticket items. ticket_fields = { - 'effort': NUMERIC(stored=True), - 'difficulty': NUMERIC(stored=True), - 'severity': NUMERIC(stored=True), - 'priority': NUMERIC(stored=True), - 'assigned_to': ID(stored=True), - 'superseded_by': ID(stored=True), - 'depends_on': ID(stored=True), - 'closed': BOOLEAN(stored=True), + EFFORT: NUMERIC(stored=True), + DIFFICULTY: NUMERIC(stored=True), + SEVERITY: NUMERIC(stored=True), + PRIORITY: NUMERIC(stored=True), + ASSIGNED_TO: ID(stored=True), + SUPERSEDED_BY: ID(stored=True), + DEPENDS_ON: ID(stored=True), + CLOSED: BOOLEAN(stored=True), } latest_revs_fields.update(**ticket_fields) @@ -703,9 +700,9 @@ return userid_pseudo_field qp.add_plugin(PseudoFieldPlugin(dict( # username:JoeDoe searches for revisions modified by JoeDoe - username=userid_pseudo_field_factory(keys.USERID), + username=userid_pseudo_field_factory(USERID), # assigned:JoeDoe searches for tickets assigned to JoeDoe - assigned=userid_pseudo_field_factory('assigned_to'), # XXX should be keys.ASSIGNED_TO + assigned=userid_pseudo_field_factory(ASSIGNED_TO), ))) return qp @@ -1042,18 +1039,18 @@ if wikiname is None: wikiname = app.cfg.interwikiname state = {'trusted': trusted, - keys.NAME: [name], - keys.ACTION: action, - keys.ADDRESS: remote_addr, - keys.USERID: userid, - keys.WIKINAME: wikiname, - keys.NAMESPACE: None, - keys.ITEMID: self.itemid, # real itemid or None + NAME: [name], + ACTION: action, + ADDRESS: remote_addr, + USERID: userid, + WIKINAME: wikiname, + NAMESPACE: None, + ITEMID: self.itemid, # real itemid or None 'contenttype_current': contenttype_current, 'contenttype_guessed': contenttype_guessed, 'acl_parent': acl_parent, } - ct = meta.get(keys.CONTENTTYPE) + ct = meta.get(CONTENTTYPE) if ct == CONTENTTYPE_USER: Schema = UserMetaSchema else: @@ -1111,7 +1108,10 @@ Destroy revision <revid>. """ rev = Revision(self, revid) - self.backend.remove(rev.backend_name, revid) + query = {DATAID: rev.meta[DATAID]} + with flaskg.storage.indexer.ix[ALL_REVS].searcher() as searcher: + refcount = len(list(searcher.document_numbers(**query))) + self.backend.remove(rev.backend_name, revid, destroy_data=refcount == 1) self.indexer.remove_revision(revid) def destroy_all_revisions(self):
--- a/MoinMoin/storage/middleware/routing.py Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/storage/middleware/routing.py Sun Jul 07 11:16:59 2013 -0400 @@ -132,8 +132,8 @@ meta[BACKENDNAME] = backend_name return backend_name, revid - def remove(self, backend_name, revid): + def remove(self, backend_name, revid, destroy_data): backend = self.backends[backend_name] if not isinstance(backend, MutableBackendBase): raise TypeError('backend {0} is readonly'.format(backend_name)) - backend.remove(revid) + backend.remove(revid, destroy_data)
--- a/MoinMoin/templates/base.html Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/templates/base.html Sun Jul 07 11:16:59 2013 -0400 @@ -41,6 +41,11 @@ {% endblock %} {% block head_links %} + + {% block bootstrap_stylesheet %} + <link rel="stylesheet" href="{{ url_for('serve.files', name='bootstrap', filename='bootstrap/css/bootstrap.css') }}" /> + {% endblock %} + <link rel="shortcut icon" href="{{ url_for('static', filename='logos/favicon.ico') }}" /> {% block theme_stylesheets %}
--- a/MoinMoin/templates/common.js Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/templates/common.js Sun Jul 07 11:16:59 2013 -0400 @@ -325,10 +325,10 @@ if ($('#moin-usersettings').length === 0) { return; } // create a UL that will be displayed as row of tabs or column of buttons - // "nav nav-tabs" for Bootstrap compatibility. $(function () { var tabs = $('#moin-usersettings'), titles = $('<ul class="moin-tab-titles nav nav-tabs">'); + hashTag = window.location.hash; // for each form on page, create a corresponding LI $('.moin-tab-body').each(function () { var li = $(document.createElement('li')), @@ -342,7 +342,7 @@ // parent() active for Bootstrap compatibility $('.moin-current-tab').parent().removeClass('active'); $(ev.target).parent().addClass('active'); - + window.location.hash = tab; $('.moin-current-tab').removeClass('moin-current-tab'); $(ev.target).addClass('moin-current-tab'); tabs.children('.moin-tab-body').hide().removeClass('moin-current-form');
--- a/MoinMoin/templates/destroy.html Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/templates/destroy.html Sun Jul 07 11:16:59 2013 -0400 @@ -17,7 +17,7 @@ {{ gen.form.close() }} </div> {% else %} - <h1>{{ _("DESTROY REVISION '%(item_name)s' (rev %(rev_id)s)", item_name=item.name, rev_id=rev_id) }}</h1> + <h1>{{ _("DESTROY REVISION '%(item_name)s' (rev %(rev_id)s)", item_name=item.name, rev_id=rev_id | shorten_id) }}</h1> <div class="moin-form"> {{ gen.form.open(form, method="post", action=url_for('frontend.destroy_item', item_name=item.name, rev=rev_id)) }} {{ forms.render_errors(form) }}
--- a/MoinMoin/templates/lookup.html Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/templates/lookup.html Sun Jul 07 11:16:59 2013 -0400 @@ -35,14 +35,14 @@ {% for result in results %} {% if result['wikiname'] == cfg.interwikiname %} <tr> - <td class="moin-wordbreak">{{ result.pos + 1 }} - <a href="{{ url_for_item(item_name=result['name'], wiki_name='Self', rev=result['revid']) }}"><b>{{ result['name'] }}</b></a> + <td class="moin-wordbreak">{{ loop.index }} + <a href="{{ url_for_item(item_name=result.name, wiki_name='Self', rev=result.revid) }}"><b>{{ result.name }}</b></a> </td> </tr> {% else %} <tr> - <td class="moin-wordbreak">{{ result.pos + 1 }} - <a class="moin-interwiki" href="{{ url_for_item(item_name=result['name'], wiki_name=result['wikiname'], rev=result['revid']) }}"><b>{{ "%s:%s" % (result['wikiname'], result['name']) }}</b></a> + <td class="moin-wordbreak">{{ loop.index }} + <a class="moin-interwiki" href="{{ url_for_item(item_name=result.name, wiki_name=result.wikiname, rev=result.revid) }}"><b>{{ "%s:%s" % (result.wikiname, result.name) }}</b></a> </td> </tr> {% endif %}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/templates/mail/account_verification.txt Sun Jul 07 11:16:59 2013 -0400 @@ -0,0 +1,10 @@ +{{ _("""\ +Somebody has created an account with this email address. + +Please use the link below to verify your email address: + +%(link)s + +If you didn't create this account, please ignore this email. + +""", link=link) }} \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/templates/mail/password_recovery.txt Sun Jul 07 11:16:59 2013 -0400 @@ -0,0 +1,10 @@ +{{ _("""\ +Somebody has requested to email you a password recovery link. + +Please use the link below to change your password to a known value: + +%(link)s + +If you didn't forget your password, please ignore this email. + +""", link=link) }} \ No newline at end of file
--- a/MoinMoin/templates/meta.html Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/templates/meta.html Sun Jul 07 11:16:59 2013 -0400 @@ -9,7 +9,7 @@ {% block content_data %} <h1> {{ title }} - {% if show_revision %}({{ _("Revision") }} {{ rev.revid }}){% endif %} + {% if show_revision %}({{ _("Revision") }} {{ rev.revid | shorten_id }}){% endif %} </h1> {% if meta_rendered %} <div id="moin-content-meta">
--- a/MoinMoin/templates/modify.html Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/templates/modify.html Sun Jul 07 11:16:59 2013 -0400 @@ -23,11 +23,8 @@ {% call(fullname, shortname, contenttype, has_children) utils.render_subitem_navigation(item_name, True) %} {% set shortname = shortname|json_dumps %} {% set fullname = fullname|json_dumps %} - <button class="link-action" onclick='linkSubitem({{ shortname }}, {{ fullname }})' - title="{{ _('Link to Subitem') }}">{{ _('Link') }}</button> - <button class="transclude-action" - onclick='transcludeSubitem({{ shortname }}, {{ fullname }})' - title="{{ _('Transclude Subitem') }}">{{ _('Transclude') }}</button> + <button class="moin-insertname-action" onclick='InsertName({{ fullname }})' + title="{{ _('Insert Name') }}">{{ _('Insert Name') }}</button> {% endcall %} {% endblock %} @@ -43,7 +40,7 @@ POSTs originate from their respective applets. #} {% if not form['content_form'].is_draw %} - {{ forms.render_submit(form) }} + {{ forms.render_submit(form, id='moin-save-text-button') }} <dl> {{ forms.render_textcha(gen, form) }} {{ forms.render(form['comment']) }}
--- a/MoinMoin/templates/recoverpass.html Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/templates/recoverpass.html Sun Jul 07 11:16:59 2013 -0400 @@ -6,8 +6,8 @@ {{ gen.form.open(form, method="post", action=url_for('frontend.recoverpass')) }} {{ forms.render_errors(form) }} <dl> + {{ forms.render(form['token']) }} {{ forms.render(form['username']) }} - {{ forms.render(form['token']) }} {{ forms.render(form['password1']) }} {{ forms.render(form['password2']) }} </dl>
--- a/MoinMoin/templates/revert.html Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/templates/revert.html Sun Jul 07 11:16:59 2013 -0400 @@ -1,7 +1,7 @@ {% import "forms.html" as forms %} {% extends theme("layout.html") %} {% block content %} -<h1>{{ _("Revert '%(item_name)s' (rev %(rev_id)s)", item_name=item.name, rev_id=rev_id) }}</h1> +<h1>{{ _("Revert '%(item_name)s' (rev %(rev_id)s)", item_name=item.name, rev_id=rev_id | shorten_id) }}</h1> <div class="moin-form"> {{ gen.form.open(form, method="post", action=url_for('frontend.revert_item', item_name=item.name, rev=rev_id)) }} {{ forms.render_errors(form) }}
--- a/MoinMoin/templates/search.html Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/templates/search.html Sun Jul 07 11:16:59 2013 -0400 @@ -56,7 +56,7 @@ {% endif %} <tr> <td> - <p class="info searchhitinfobar">{{ _("Revision: %(revid)s Last Change: %(mtime)s", revid=result['revid'], mtime=result['mtime']|datetimeformat) }}</p> + <p class="info searchhitinfobar">{{ _("Revision: %(revid)s Last Change: %(mtime)s", revid=result['revid']|shorten_id, mtime=result['mtime']|datetimeformat) }}</p> </td> </tr> <tr>
--- a/MoinMoin/templates/ticket.html Tue Apr 02 01:18:42 2013 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,78 +0,0 @@ -{% import "forms.html" as forms %} -{% extends theme("layout.html") %} - -{% if is_new %} - {% set title = _("Creating new ticket: '%(item_name)s'", item_name=item_name) %} -{% else %} - {% set title = _("Ticket: '%(item_name)s'", item_name=item_name) %} -{% endif %} - -{% block head %} - {{ super() }} - <link media="all" rel="stylesheet" href="{{ url_for('static', filename='css/ticket.css') }}" /> -{% endblock %} - -{% block content %} -<h1> - {{ title }} - {% if closed %} - {# TODO style .moin-ticket-closed #} - <span class="moin-ticket-closed">{{ _("(Closed)") }}</span> - {% endif %} -</h1> -<div class="moin-form" id="moin-ticket-form"> - {{ gen.form.open(form, method='post', enctype='multipart/form-data') }} - - {% if data_rendered %} - <div id="moin-content-data"> - {{ data_rendered }} - </div> - {% endif %} - - {% if is_new %} - <h2>Describe the ticket</h2> - {% else %} - <h2>Add comment</h2> - {% endif %} - <dl> - {{ forms.render(form['message']) }} - </dl> - - <h2>Edit metadata</h2> - <dl> - {{ forms.render_errors(form) }} - {% for e in [ - 'summary', - 'effort', - 'difficulty', - 'severity', - 'priority', - 'tags', - 'assigned_to', - 'superseded_by', - 'depends_on', - ] %} - {{ forms.render(form['meta'][e]) }} - {% endfor %} - </dl> - - {# see comments concerning the submit button within TicketForm in items/ticket.py #} - {% if is_new %} - {{ forms.render_submit(form) }} - {% else %} - {{ forms.render(form['submit']) }} - {% endif %} - - <h2>Back references</h2> - <dl> - {% for e in [ - 'supersedes', - 'required_by', - 'subscribers', - ] %} - {{ forms.render(form['backrefs'][e]) }} - {% endfor %} - - {{ gen.form.close() }} -</div> -{% endblock %}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/templates/ticket/base.html Sun Jul 07 11:16:59 2013 -0400 @@ -0,0 +1,58 @@ +{% import "forms.html" as forms %} +{% extends theme("layout.html") %} + +{% macro render_meta() %} + {% for e in [ + 'summary', + 'effort', + 'difficulty', + 'severity', + 'priority', + 'tags', + 'assigned_to', + 'superseded_by', + 'depends_on', + ] %} + {{ forms.render(form['meta'][e]) }} + {% endfor %} +{% endmacro %} + +{% macro render_backref() %} + {% for e in [ + 'supersedes', + 'required_by', + 'subscribers', + ] %} + {{ forms.render(form['backrefs'][e]) }} + {% endfor %} +{% endmacro %} + +{% block head %} + {{ super() }} + <link media="all" rel="stylesheet" href="{{ url_for('static', filename='css/ticket.css') }}" /> +{% endblock %} + +{% block content %} +<h1> + {% block title_text %} + {% endblock %} + {% if closed %} + {# TODO style .moin-ticket-closed #} + <span class="moin-ticket-closed">{{ _("(Closed)") }}</span> + {% endif %} +</h1> +<div class="moin-form" id="moin-ticket-form"> + {{ gen.form.open(form, method='post', enctype='multipart/form-data') }} + + {% if data_rendered %} + <div id="moin-content-data"> + {{ data_rendered }} + </div> + {% endif %} + + {% block form_controls %} + {% endblock %} + + {{ gen.form.close() }} +</div> +{% endblock %}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/templates/ticket/modify.html Sun Jul 07 11:16:59 2013 -0400 @@ -0,0 +1,25 @@ +{% extends "ticket/base.html" %} + +{% block title_text %} + {{ _("Ticket: '%(item_name)s'", item_name=item_name) }} +{% endblock %} + +{% block form_controls %} + <h2>{{ _("Add comment") }}</h2> + <dl> + {{ forms.render(form['message']) }} + </dl> + + <h2>{{ _("Edit metadata") }}</h2> + <dl> + {{ forms.render_errors(form) }} + {{ render_meta() }} + </dl> + + {{ forms.render(form['submit']) }} + + <h2>{{ _("Back references") }}</h2> + <dl> + {{ render_backref() }} + </dl> +{% endblock %}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/templates/ticket/submit.html Sun Jul 07 11:16:59 2013 -0400 @@ -0,0 +1,21 @@ +{% extends "ticket/base.html" %} + +{% block title_text %} + {{ _("Creating new ticket: '%(item_name)s'", item_name=item_name) }} +{% endblock %} + +{% block form_controls %} + <h2>{{ _("Describe the ticket") }}</h2> + + <dl> + {{ forms.render(form['message']) }} + </dl> + + <h2>{{ _("Provide metadata") }}</h2> + <dl> + {{ forms.render_errors(form) }} + {{ render_meta() }} + </dl> + + {{ forms.render_submit(form) }} +{% endblock %}
--- a/MoinMoin/themes/__init__.py Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/themes/__init__.py Sun Jul 07 11:16:59 2013 -0400 @@ -47,7 +47,6 @@ return get_theme(theme_name) -@timed() def render_template(template, **context): return render_theme_template(get_current_theme(), template, **context)
--- a/MoinMoin/themes/foobar/static/css/common.css Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/themes/foobar/static/css/common.css Sun Jul 07 11:16:59 2013 -0400 @@ -176,14 +176,14 @@ .moin-diff-removed span{background-color:#f1eeb9} body{color:#000;background-color:#d6d5d0;font-family:Helvetica,Arial,sans-serif;background-image:url("../img/base.png")} p{margin:.2em .4em;font-size:1em;line-height:1.3em} -#moin-global-tray{float:left;width:16.666666666666668%;padding:.5%;overflow:hidden} -#moin-main-container{float:left;width:81.13333333333333%;padding:.5%} +#moin-global-tray{float:left;width:16.55%;padding:.5%;overflow:hidden} +#moin-main-container{float:left;width:81.095%;padding:.5%} #moin-main-box{background-color:#f4f4f4;border:1px solid #ccc;border-radius:6px;box-shadow:2px 2px 4px #9d9d9b;overflow:hidden} #moin-header{background-color:#2d2d2d;color:#fff;border-bottom:1px solid #ccc;padding:0;overflow:auto;} #moin-header a{color:#fff} -#moin-content-no-panel{float:left;width:98.56746096959736%;padding:.6162695152013148%;border-right:1px solid #ccc} -#moin-content{float:left;width:76.79260476581759%;padding:.6162695152013148%;border-right:1px solid #ccc} -#moin-local-panel{float:left;width:20.54231717337716%;padding:.6162695152013148%;margin-left:-1px;border-left:1px solid #ccc} +#moin-content-no-panel{float:left;width:98.307%;padding:.5%;border-right:1px solid #ccc} +#moin-content{float:left;width:81.095%;padding:.5%;border-right:1px solid #ccc} +#moin-local-panel{float:left;width:16.55%;padding:.5%;margin-left:-1px;border-left:1px solid #ccc} #moin-footer{clear:both;font-size:.8em;color:#616161;padding:.5% .5%;overflow:auto} #moin-breadcrumbs-location{float:left;padding:.5em;font-weight:bold} #moin-interwiki{vertical-align:middle;}
--- a/MoinMoin/themes/foobar/static/css/stylus/main.styl Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/themes/foobar/static/css/stylus/main.styl Sun Jul 07 11:16:59 2013 -0400 @@ -38,14 +38,15 @@ scale = 1 // we have the whole screen width as base global_tray_width = 100% / 6 * scale +local_panel_width = 100% / 6 * scale global_padding_width = 0.5% * scale -main_container_width = 99.8% - global_tray_width - (4 * global_padding_width) +local_padding_width = 0.5% * scale + +main_container_width = 99.3% - global_tray_width - (4 * global_padding_width) scale = 100% / main_container_width // we have the main container width as base -local_panel_width = 100% / 6 * scale -local_padding_width = 0.5% * scale -content_width_with_panel = 99.8% - local_panel_width - (4 * local_padding_width) -content_width_no_panel = 99.8% - (2 * local_padding_width) +content_width_with_panel = 99.3% - local_panel_width - (4 * local_padding_width) +content_width_no_panel = 99.3% - (2 * local_padding_width) body color font_color @@ -1037,8 +1038,7 @@ text-indent -9000% box-shadow none -.link-action, -.transclude-action +.moin-insertname-action border: hidden text-indent: -9000% padding: 0 @@ -1049,12 +1049,8 @@ overflow: hidden cursor: pointer box-shadow none - -.link-action background: url(../img/moin-link.png) no-repeat center center -.transclude-action - background: url(../img/moin-transclusion.png) no-repeat center center // transclusion .moin-transclusion
--- a/MoinMoin/themes/modernized/static/css/stylus/main.styl Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/themes/modernized/static/css/stylus/main.styl Sun Jul 07 11:16:59 2013 -0400 @@ -1412,8 +1412,7 @@ border hidden text-indent -9000% -.link-action, -.transclude-action +.moin-insertname-action border hidden text-indent -9000% padding 0 @@ -1424,13 +1423,8 @@ overflow hidden cursor pointer box-shadow none - -.moin-subitem-navigation .link-action background url(../img/moin-link.png) no-repeat center center -.moin-subitem-navigation .transclude-action - background url(../img/moin-transclusion.png) no-repeat center center - #moin-footer clear both margin 0 0
--- a/MoinMoin/user.py Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/user.py Sun Jul 07 11:16:59 2013 -0400 @@ -29,7 +29,7 @@ from flask import current_app as app from flask import g as flaskg -from flask import session, request, url_for +from flask import session, request, url_for, render_template from MoinMoin import log logging = log.getLogger(__name__) @@ -473,6 +473,12 @@ # Invalidate all other browser sessions except this one. session['user.session_token'] = self.generate_session_token(False) + def has_invalidated_password(self): + """ + Check if the password hash of this user is invalid. + """ + return self.profile[ENC_PASSWORD] == '' + def save(self, force=False): """ Save user account data to user account file on disk. @@ -752,25 +758,22 @@ self.save() return True - def mail_password_recovery(self, cleartext_passwd=None): + def mail_password_recovery(self, cleartext_passwd=None, subject=None, text=None): """ Mail a user who forgot his password a message enabling him to login again. """ + if not self.email: + return False, "user has no E-Mail address in his profile." + token = self.generate_recovery_token() - text = _("""\ -Somebody has requested to email you a password recovery link. - -Please use the link below to change your password to a known value: - -%(link)s + if subject is None: + subject = _('[%(sitename)s] Your wiki password recovery link', sitename='%(sitename)s') + subject = subject % dict(sitename=self._cfg.sitename or "Wiki") + if text is None: + link = url_for('frontend.recoverpass', username=self.name0, token=token, _external=True) + text = render_template('mail/password_recovery.txt', link=link) -If you didn't forget your password, please ignore this email. - -""", link=url_for('frontend.recoverpass', username=self.name0, token=token, _external=True)) - - subject = _('[%(sitename)s] Your wiki password recovery link', - sitename=self._cfg.sitename or "Wiki") mailok, msg = sendmail.sendmail(subject, text, to=[self.email], mail_from=self._cfg.mail_from) return mailok, msg @@ -778,16 +781,8 @@ """ Mail a user a link to verify his email address. """ token = self.generate_recovery_token() - text = _("""\ -Somebody has created an account with this email address. - -Please use the link below to verify your email address: - -%(link)s - -If you didn't create this account, please ignore this email. - -""", link=url_for('frontend.verifyemail', username=self.name0, token=token, _external=True)) + link = url_for('frontend.verifyemail', username=self.name0, token=token, _external=True) + text = render_template('mail/account_verification.txt', link=link) subject = _('[%(sitename)s] Please verify your email address', sitename=self._cfg.sitename or "Wiki")
--- a/MoinMoin/util/_tests/test_interwiki.py Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/util/_tests/test_interwiki.py Sun Jul 07 11:16:59 2013 -0400 @@ -39,25 +39,25 @@ # Method signature to understand the tuple parameters # (item_name, wiki_name='', namespace='', rev=CURRENT, endpoint='frontend.show_item', _external=False): (('SomePage', '', '', CURRENT, 'frontend.show_item', True), 'http://localhost:8080/SomePage'), - (('SomePage', '', '', CURRENT, 'frontend.modify_item', False), '/%2Bmodify/SomePage'), + (('SomePage', '', '', CURRENT, 'frontend.modify_item', False), '/+modify/SomePage'), # FIXME if you set interwiki_map = dict(Self='http://localhost:8080', MoinMoin='http://moinmo.in/', ), - # the above line make it fails, it returns http://localhost/%2Bmodify/SomePage - # (('SomePage', '', '', CURRENT, 'frontend.modify_item', True), 'http://localhost:8080/%2Bmodify/SomePage'), - (('SomePage', '', '', revid, 'frontend.show_item', False), '/%2Bshow/%2B{0}/SomePage'.format(revid)), - (('SomePage', '', '', revid, 'frontend.show_item_meta', False), '/%2Bmeta/%2B{0}/SomePage'.format(revid)), + # the above line make it fails, it returns http://localhost/+modify/SomePage + # (('SomePage', '', '', CURRENT, 'frontend.modify_item', True), 'http://localhost:8080/+modify/SomePage'), + (('SomePage', '', '', revid, 'frontend.show_item', False), '/+show/+{0}/SomePage'.format(revid)), + (('SomePage', '', '', revid, 'frontend.show_item_meta', False), '/+meta/+{0}/SomePage'.format(revid)), # Valid namespaces (('SomePage', '', 'ns1', CURRENT, 'frontend.show_item', False), '/:ns1:SomePage'), (('SomePage', '', 'ns1:ns2', CURRENT, 'frontend.show_item', True), 'http://localhost:8080/:ns1:ns2:SomePage'), - (('SomePage', '', 'ns1', CURRENT, 'frontend.modify_item', False), '/%2Bmodify/:ns1:SomePage'), - (('SomePage', '', 'ns1:ns2', CURRENT, 'frontend.show_item_meta', True), 'http://localhost:8080/%2Bmeta/:ns1:ns2:SomePage'), - (('SomePage', '', 'ns1', revid, 'frontend.show_item', False), '/%2Bshow/%2B{0}/:ns1:SomePage'.format(revid)), - (('SomePage', '', 'ns1:ns2', revid, 'frontend.show_item_meta', False), '/%2Bmeta/%2B{0}/:ns1:ns2:SomePage'.format(revid)), + (('SomePage', '', 'ns1', CURRENT, 'frontend.modify_item', False), '/+modify/:ns1:SomePage'), + (('SomePage', '', 'ns1:ns2', CURRENT, 'frontend.show_item_meta', True), 'http://localhost:8080/+meta/:ns1:ns2:SomePage'), + (('SomePage', '', 'ns1', revid, 'frontend.show_item', False), '/+show/+{0}/:ns1:SomePage'.format(revid)), + (('SomePage', '', 'ns1:ns2', revid, 'frontend.show_item_meta', False), '/+meta/+{0}/:ns1:ns2:SomePage'.format(revid)), (('SomePage', 'MoinMoin', 'ns1', CURRENT, 'frontend.show_item', False), 'http://moinmo.in/:ns1:SomePage'), (('SomePage', 'MoinMoin', '', CURRENT, 'frontend.show_item', False), 'http://moinmo.in/SomePage'), # FIXME will exist a map for this case? maybe there should be a placeholder for it. # we need that for wiki farms with common search index and search in non-current revisions. - (('SomePage', 'MoinMoin', '', revid, 'frontend.show_item', False), 'http://moinmo.in/%2Bshow/%2B{0}/SomePage'.format(revid)), + (('SomePage', 'MoinMoin', '', revid, 'frontend.show_item', False), 'http://moinmo.in/+show/+{0}/SomePage'.format(revid)), (('SomePage', 'non-existent', '', CURRENT, 'frontend.show_item', False), '/non-existent:SomePage'), (('SomePage', 'non-existent', 'ns1', CURRENT, 'frontend.show_item', False), '/non-existent:ns1:SomePage'), ]
--- a/MoinMoin/util/crypto.py Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/util/crypto.py Sun Jul 07 11:16:59 2013 -0400 @@ -9,8 +9,6 @@ - generate password recovery tokens - verify password recovery tokens - generate random strings of given length (for salting) - -Code is tested on Python 2.6/2.7. """ from __future__ import absolute_import, division
--- a/MoinMoin/util/interwiki.py Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/util/interwiki.py Sun Jul 07 11:16:59 2013 -0400 @@ -89,7 +89,7 @@ # we know that everything left of the + belongs to script url, but we # just want e.g. +show/42/FooBar to append it to the other wiki's # base URL. - i = local_url.index('/%2B') + i = local_url.index('/+') path = local_url[i + 1:] url = wiki_base_url + path return url
--- a/MoinMoin/util/tree.py Tue Apr 02 01:18:42 2013 -0400 +++ b/MoinMoin/util/tree.py Sun Jul 07 11:16:59 2013 -0400 @@ -50,7 +50,7 @@ return Name(key, self) def __repr__(self): - return '<{0}({1!r})>'.format(self.__class__.__name__, self) + return '<{0}({1!r})>'.format(self.__class__.__name__, str(self)) @property def namespace(self):
--- a/docs/admin/configure.rst Tue Apr 02 01:18:42 2013 -0400 +++ b/docs/admin/configure.rst Sun Jul 07 11:16:59 2013 -0400 @@ -621,12 +621,34 @@ password checker that is enabled by default and does some sanity checks, so users don't choose easily crackable passwords. -If your site has rather low security requirements, you can disable the checker by:: +It **does** check: + +* length of password (default minimum: 8) +* amount of different characters in password (default minimum: 5) +* password does not contain user name +* user name does not contain password +* password is not a keyboard sequence (like "ASDFghjkl" or "987654321"), + currently we have only US and DE keyboard data built-in. + +It **does not** check: + +* whether the password is in a well-known dictionary or password list +* whether a password cracker can break it - password_checker = None # no password checking +If you are not satisfied with the default values, you can easily customize the +checker:: + + from MoinMoin.config.default import DefaultConfig, _default_password_checker + password_checker = lambda cfg, name, pw: _default_password_checker( + cfg, name, pw, min_length=10, min_different=6) -Note that the builtin password checker only does a few very fundamental -checks, it e.g. won't forbid using a dictionary word as password. +You could also completely replace it with your own implementation. + +If your site has rather low security requirements, you can disable the checker +by:: + + password_checker = None # no password checking + Password storage ----------------
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/admin/password-reset.rst Sun Jul 07 11:16:59 2013 -0400 @@ -0,0 +1,134 @@ +=============================== +Password Resetting/Invalidation +=============================== +There might be circumstances when the wiki admin wants or needs to reset one +user's or all users' password (hash). + +For example: + +* you had a security breach on your wiki server (or somewhere else) and the + old password hashes (or passwords) were exposed +* you want to make sure some user or all users set a new password, e.g. if: + + - your password policy has changed (requiring longer passwords for example) + - you changed your passlib configuration and want to immediately have all + hashes upgraded + +Note: if we say "reset a password" (to use a commonly used term), we mean to +"invalidate the password hash" (so that no password exists that validates +against that hash). MoinMoin does not keep user passwords in cleartext. + +The files we refer to below are located in docs/examples/password-reset/... + + +Resetting one or few password(s) +================================ +If you somehow interact with the users corresponding to the user accounts in +question (by phone or directly), you don't need the extensive procedure as +described below, just use:: + + moin account-password --name JoeDoe + +That will reset JoeDoe's password. Tell him to visit the login URL and use +the "forgot my password" functionality to define a new password. + +If that doesn't work (e.g. if e-mail is not enabled for your wiki or he has +a non-working e-mail address in his profile), you can also set a password for +him:: + + moin account-password --name JoeDoe --password uIkV9.-a3 + +Choose a rather complicated password to make sure they change it a minute +afterwards (to another, hopefully safe password). + + +Resetting many or all password(s) +================================= +If you have a lot of passwords to reset, you need a better procedure that +avoids having to deal with too many users individually. + + +Preparing your users +-------------------- +Tell your users beforehands that you will be doing a password reset, otherwise +they might find the automatically generated E-Mail they'll get suspicious and +you'll have to explain it to them individually that the E-Mail is legitimate. + +Also, remind your users that having a valid E-Mail address in their user +settings is essential for getting a password recovery E-Mail. + +If an active user does somehow not get such a mail, you likely will have to +manually define a valid E-Mail address (or even password) for that user. + + +Make sure E-Mail functionality works +------------------------------------ +If you know you have working E-Mail functionality, skip this section. + +Password recovery and password reset notification work via E-Mail, so you +should have it configured:: + + # the E-Mail address used for From: (consider using an address that + # can be directly replied to, at least while doing the pw reset): + mail_from = 'wiki@example.org' + # your smtp mail server hostname:port (default is 25) + mail_smarthost = 'mail.example.org:587' + # the login there, if authentication is needed + mail_username = 'wiki@example.org' + mail_password = 'SuperSecretSMTPPassword' + +You can try whether it works by using the "forgot my password" functionality +on the login page. + + +Editing mailtemplate.txt +------------------------ +If you edit mailtemplate.txt, please be very careful and follow these rules +(otherwise you might just see the script command crashing): + +The contents must be utf-8 (or ascii, which is a subset of utf-8). +In case of doubt, just use plain English. + +Some places you likely should edit are marked with XXX. + +Do not use any % character in your text (except for the placeholders). +If you need a verbatim % character, you need to write %%. + +It is a very good idea to give some URL (e.g. of a web or wiki page) in +the text where users can read more information. + +Of course the information at that URL should be readable without requiring +a wiki login (you just have invalidated his/her password!), so the user can +get informed before clicking links he got from someone via E-Mail. + +We have added a wikitemplate.txt you can use to create such a wiki page. + +Instead of creating a web or wiki page with the information, you could +also write all the stuff into the mail template directly, but please consider +that E-Mail delivery to some users might fail for misc. reasons, so having +some information on the web/wiki is usually better. + + +Editing wikitemplate.txt +------------------------ +Just copy & paste it to some public page in your wiki, e.g. "PasswordReset". + +Some places you likely should edit are marked with XXX. + + +Doing the password reset +------------------------ +Maybe first try it with a single user account:: + + moin account-password --name JoeDoe --notify --subject 'Wiki password reset' --text-from-file mailtemplate.txt + +Use some valid name, maybe a testing account of yourself. You should now have +mail. If that worked ok, you can now do a global password reset for your wiki:: + + moin account-password --verbose --all-users --notify --subject 'Wiki password reset' --text-from-file mailtemplate.txt + +The subject may contain a placeholder for the sitename, which is useful for +wiki farms (showing the builtin default here):: + + '[%(sitename)s] Your wiki account data' +
--- a/docs/admin/requirements.rst Tue Apr 02 01:18:42 2013 -0400 +++ b/docs/admin/requirements.rst Sun Jul 07 11:16:59 2013 -0400 @@ -2,10 +2,10 @@ Requirements ============ -MoinMoin requires Python >= 2.6 and < 3.0. +MoinMoin requires Python 2.7.x. We usually test using CPython and this is what we recommend. -You can also try PyPy: PyPy >= 1.6 seems to work with moin. +You can also try PyPy, it seems to work. Servers =======
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/examples/password-reset/mailtemplate-de.txt Sun Jul 07 11:16:59 2013 -0400 @@ -0,0 +1,14 @@ +Der Wiki-Administrator hat Ihr Passwort invalidiert und angefordert, +Ihnen diese E-Mail zu senden, damit Sie ein neues Passwort setzen koennen. + +Bitte lesen Sie wichtige Informationen hierzu (sowie Hinweise zur +Problembeseitigung und Wiki-Administrator-Kontaktinformationen) dort: + +(XXX hier die URL angeben XXX) + + +Bitte besuchen Sie nun die unten angezeigte Passwort-Ruecksetz-URL und +setzen Sie ein neues Passwort. + +%(link)s +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/examples/password-reset/mailtemplate.txt Sun Jul 07 11:16:59 2013 -0400 @@ -0,0 +1,13 @@ +The wiki administrator has invalidated your wiki password and requested +to send this E-Mail to you, so you can set a new one. + +Important information about this (including troubleshooting information and +wiki administrator contact information) is available there, please read it: + +(XXX give URL here XXX) + + +Now, please go to the password reset URL below and set a new password. + +%(link)s +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/examples/password-reset/wikitemplate-de.txt Sun Jul 07 11:16:59 2013 -0400 @@ -0,0 +1,67 @@ +Hinweis: einige Stellen, die man editieren muss, sind mit XXX markiert. + += (XXX Datum angeben) YYYY-MM-DD Passworte zurückgesetzt = + +Der Wiki-Administrator hat für alle Benutzerkonten dieses Wikis die Passwörter invalidiert (auf ungültig gesetzt). + +Sie müssen eine Passwort-Rücksetzung ausführen, um ein ''neues und anderes'' Passwort für Ihr Konto zu setzen. + +== Gründe für die Passwort-Invalidierung == + + * (XXX Grund angeben) + * (XXX Grund angeben) + +== Was Sie tun müssen == +=== Lesen Sie wichtige Informationen === +Bitte besuchen Sie diese URLs für weitere Informationen: + * http://moinmo.in/HowToHandleSecurityBreach/de (allgemeine Hinweise, unbedingt lesen!) + * http://moinmo.in/SecurityFixes/CVE-YYYY-NNNN (über den spezifischen Vorfall, wenn Sie mehr Informationen möchten) (XXX URL korrigieren) + +=== Ein neues Passwort setzen === +Sie sollten eine E-Mail (inklusive eines Links, um Ihr Passwort zurückzusetzen) erhalten haben. Lesen und befolgen Sie sie. + +=== Problembehandlung === +==== Ungültiges Token? ==== +Das Token hat eine begrenzte Gültigkeitsdauer. + +Wenn es "Token ist ungültig" sagt, müssen Sie einfach ein neues Token anfordern, siehe unten. + +==== Sie haben eine unerwartete E-Mail über ein unbekanntes Benutzerkonto erhalten? ==== +Wenn Sie eine E-Mail erhalten haben, die Sie auffordert, das Passwort ''für ein Ihnen unbekanntes Benutzerkonto'' zurückzusetzen: +ignorieren Sie sie einfach, setzen Sie das Passwort ''nicht'' zurück + +Wahrscheinlich hat jemand (z.B. ein Spammer) Ihre E-Mail-Adresse missbraucht, als er ein Benutzerkonto angelegt hat. +Das passiert oft, Sie können das auch in Ihrem E-Mail-Spam-Ordner sehen. + +==== Sie haben die E-Mail nicht erhalten? ==== +Wenn Sie die E-Mail noch nicht gesehen haben, prüfen Sie bitte Ihren Spam-Ordner (oder auch andere Ordner, wo E-Mails vom Wiki sein könnten). + +Wenn Sie die von uns an Sie gesandte E-Mail nicht finden können, können Sie alternativ auch die normale Passwort-Rücksetz-Funktion des Wikis benutzen, siehe unten. + +==== Ein neues Passwort-Rücksetz-Token erhalten ==== + * gehen Sie zur Anmelde-Seite des Wiki + * klicken Sie auf den "Password vergessen?"-Link + * Dann: + * ''entweder'' Ihren Wiki-Benutzernamen eingeben (empfehlenswert, wenn Sie ihn wissen oder einfach herausfinden können) + * ''oder'' Ihre E-Mail-Addresse (die gleiche wie in Ihrem Wiki-Benutzerprofil - möglicherweise brauchen Sie mehrere Versuche, wenn Sie sich hier nicht sicher sind) + * prüfen Sie Ihren E-Mail-Eingang, Sie sollten nun eine E-Mail mit einem Passwort-Rücksetz-Link haben + * klicken Sie auf den Link, definieren Sie ein neues Passwort + +==== Brauchen Sie Hilfe? ==== +Wenn das nicht funktioniert hat, brauchen Sie vermutlich Hilfe durch einen Wiki-Administrator, bitte kontaktieren Sie: + +(XXX hier Name und E-Mail des Wiki-Administrators angeben XXX) + +In Ihrer E-Mail, geben Sie bitte Folgendes an: + * die Wiki-Adresse (URL) + * Ihren Wiki-Benutzernamen + * die E-Mail-Adresse, die Sie in Ihrem Wiki-Benutzerprofil verwendet haben + * falls diese E-Mail-Adresse nicht mehr funktioniert, teilen Sie uns das bitte mit + +== Fragen? == +Wenn Sie Fragen haben, die nicht bereits auf dieser oder den verlinkten Seiten beantwortet werden, fragen Sie den Wiki-Administrator. + +E-Mails mit Fragen, die dort bereits beantwortet sind, werden ignoriert. + +Anstelle Fragen individuell über E-Mail zu beantworten, können oft gestellte Fragen auch auf dieser Seite oder den verlinkten Seiten beantwortet werden. +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/examples/password-reset/wikitemplate.txt Sun Jul 07 11:16:59 2013 -0400 @@ -0,0 +1,59 @@ +Note: some stuff that needs editing is marked with XXX. + += (XXX give date) YYYY-MM-DD Password Reset = + +The wiki administrator has invalidated the passwords for all user accounts on this wiki. + +You need to do a password recovery to set a ''new and changed'' password for your account. + +== Reason(s) for the password invalidation == + + * (XXX give reason) + * (XXX give reason) + +== What you need to do == +=== Read important information === +Please visit these URLs for more information: + * http://moinmo.in/HowToHandleSecurityBreach ('''generic advice, MUST READ!''') + * http://moinmo.in/SecurityFixes/CVE-YYYY-NNNN (about the specific incident, if you want more information) (XXX fix url) + +=== Enter a new password === +You should have received an E-Mail (including a link to reset your password). Just read and follow it. + +==== Invalid token? ==== +The token has a limited lifetime. + +If it says "Token is invalid", you just need to get a new password recovery token, see below. + +==== You didn't get the E-Mail? ==== +If you didn't see that E-Mail yet, please check your spam folder (or other folders where mails from the wiki could be). + +If you can't find the E-Mail we sent to you, you can alternatively just use the normal password recovery function of the wiki, see below. + +==== Getting a new password recovery token ==== + * go to the login page + * click the "Forgot your password?" link + * now: + * ''either'' enter your wiki username (recommended, if you still remember it or you can find out easily) + * ''or'' your E-Mail address (same as in your wiki user profile - you might need multiple tries if you are not sure about this) + * check your E-Mail, you should have one now with a password recovery link + * click on the link, define a new password + +==== Need help? ==== +If that didn't work, you will need help by a wiki administrator, please contact: + +(XXX give name and E-Mail address of wiki administrator here XXX) + +In your E-Mail, please give: + * the wiki address (URL) + * your wiki user name + * the E-Mail address that you used in your wiki userprofile + * if that E-Mail address is not working any more, please tell so + +== Questions? == +If you have any questions that are not answered by reading this page or the pages linked from here, ask the wiki administrator. + +E-Mails asking questions that ''are'' answered on these pages will be ignored. + +Instead of answering individually via E-Mail, frequently asked questions might might also get answered on this page or the pages linked from here. +
--- a/docs/index.rst Tue Apr 02 01:18:42 2013 -0400 +++ b/docs/index.rst Sun Jul 07 11:16:59 2013 -0400 @@ -39,6 +39,7 @@ admin/upgrade admin/backup admin/index + admin/password-reset man/moin Getting Support for and Contributing to MoinMoin
--- a/docs/licenses/COPYING Tue Apr 02 01:18:42 2013 -0400 +++ b/docs/licenses/COPYING Sun Jul 07 11:16:59 2013 -0400 @@ -1,12 +1,12 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 - Copyright (C) 1989, 1991 Free Software Foundation, Inc. - 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. - Preamble + Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public @@ -15,7 +15,7 @@ General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by -the GNU Library General Public License instead.) You can apply it to +the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not @@ -55,8 +55,8 @@ The precise terms and conditions for copying, distribution and modification follow. - - GNU GENERAL PUBLIC LICENSE + + GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains @@ -110,7 +110,7 @@ License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) - + These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in @@ -168,7 +168,7 @@ access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. - + 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is @@ -225,7 +225,7 @@ This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. - + 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License @@ -255,7 +255,7 @@ of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. - NO WARRANTY + NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN @@ -277,9 +277,9 @@ PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it @@ -303,17 +303,16 @@ 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 - + 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., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: - Gnomovision version 69, Copyright (C) year name of author + Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. @@ -336,5 +335,5 @@ This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Library General +library. If this is what you want to do, use the GNU Lesser General Public License instead of this License.
--- a/docs/licenses/pikipiki.txt Tue Apr 02 01:18:42 2013 -0400 +++ b/docs/licenses/pikipiki.txt Sun Jul 07 11:16:59 2013 -0400 @@ -16,7 +16,7 @@ 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 + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA The PikiPiki homepage is at http://sourcefrog.net/projects/piki/.
--- a/moin.spec Tue Apr 02 01:18:42 2013 -0400 +++ b/moin.spec Sun Jul 07 11:16:59 2013 -0400 @@ -33,7 +33,7 @@ BuildArchitectures: noarch BuildRequires: python-devel -Requires: python >= 2.6 +Requires: python >= 2.7 %description @@ -66,4 +66,6 @@ %changelog * Sun Jun 21 2010 Thomas Waldmann - Raised requirement to Python 2.6 (for MoinMoin 2.0.0alpha). +* Sat May 4 2013 Thomas Waldmann +- Raised requirement to Python 2.7 (for MoinMoin 2.0.0alpha).
--- a/quickinstall Tue Apr 02 01:18:42 2013 -0400 +++ b/quickinstall Sun Jul 07 11:16:59 2013 -0400 @@ -17,14 +17,14 @@ # find the right python version if [ -z "$PYTHON" ]; then - for PYTHON in python{2.7,2.6,2,}; do + for PYTHON in python{2.7,2,}; do hash $PYTHON 2>&- && break done fi # find the right virtualenv version if [ -z "$VIRTUALENV" ]; then - for VIRTUALENV in virtualenv{2.7,2.6,2,}; do + for VIRTUALENV in virtualenv{2.7,2,}; do hash $VIRTUALENV 2>&- && break done fi
--- a/setup.py Tue Apr 02 01:18:42 2013 -0400 +++ b/setup.py Sun Jul 07 11:16:59 2013 -0400 @@ -8,9 +8,9 @@ from MoinMoin import project, version -if sys.hexversion < 0x2060000: - # we require 2.6.x or 2.7.x, python 3.x does not work yet. - sys.stderr.write("%s %s requires Python 2.6 or greater.\n" % (project, str(version))) +if sys.hexversion < 0x2070000: + # we require 2.7.x, python 3.x does not work yet. + sys.stderr.write("%s %s requires Python 2.7.x.\n" % (project, str(version))) sys.exit(1) with open('README.txt') as f: @@ -44,7 +44,6 @@ Natural Language :: English Operating System :: OS Independent Programming Language :: Python -Programming Language :: Python :: 2.6 Programming Language :: Python :: 2.7 Topic :: Internet :: WWW/HTTP :: WSGI Topic :: Internet :: WWW/HTTP :: WSGI :: Application @@ -75,27 +74,28 @@ include_package_data=True, zip_safe=False, dependency_links=[ - # 'https://github.com/mitsuhiko/werkzeug/tarball/master#egg=Werkzeug-0.7dev', - # 'https://bitbucket.org/thomaswaldmann/whoosh/get/2.4x.tar.gz#egg=Whoosh-2.4.99dev', + #'https://github.com/mitsuhiko/werkzeug/tarball/master#egg=Werkzeug-0.9.2-dev', + #'https://github.com/mitsuhiko/flask/tarball/master#egg=Flask-0.11-dev', 'https://bitbucket.org/thomaswaldmann/whoosh/get/default.tar.gz#egg=Whoosh-2.5.99dev', - # fixed flask-themes, 0.1.3 does not work for flask 0.8.x, thus we use a faked 0.1.3.1: - 'https://bitbucket.org/thomaswaldmann/flask-themes/get/24dcc703953f.tar.gz#egg=Flask-Themes-0.1.3.1', - 'https://bitbucket.org/thomaswaldmann/emeraldtree/get/tip.tar.gz#egg=emeraldtree-0.9.2', + # use a pre-release and fixed flask-themes 0.2.0 as 0.1.3 does not work for flask 0.8.x: + 'https://bitbucket.org/thomaswaldmann/flask-themes/get/a87399231886.tar.gz#egg=Flask-Themes-0.2.0', + # use a pre-release emeraldtree (port to 2.6/2.7/3.3): + 'https://bitbucket.org/thomaswaldmann/emeraldtree/get/tip.tar.gz#egg=emeraldtree-0.9.3dev', ], install_requires=[ 'blinker>=1.1', # event signalling (e.g. for change notification trigger) 'docutils>=0.8.1', # reST markup processing 'Markdown>=2.1.1', # Markdown markup processing - 'Flask>=0.8', # micro framework + 'Flask>=0.10', # micro framework 'Flask-Babel>=0.7', # i18n support 'Flask-Cache>=0.3.4', # caching support 'Flask-Script>=0.3.3', # scripting support - 'Flask-Themes>=0.1.3.1', # theme support - 'emeraldtree>=0.9.2', # xml processing + 'Flask-Themes>=0.2.0', # theme support + 'emeraldtree>=0.9.3dev', # xml processing 'flatland==dev', # repo checkout at revision 269:6c5d262d7eff works - 'Jinja2>=2.6', # template engine + 'Jinja2>=2.7', # template engine 'pygments>=1.4', # src code / text file highlighting - 'Werkzeug>=0.8.1', # wsgi toolkit + 'Werkzeug>=0.9', # wsgi toolkit 'pytest>=2.1, <2.3', # pytest is needed by unit tests # note: currently 2.3.x is not compatible with our test code, # likely due to the fixtures changes. @@ -107,6 +107,7 @@ 'pdfminer', # pdf -> text/plain conversion 'passlib>=1.6.0', # strong password hashing (1.6 needed for consteq) 'XStatic>=0.0.2', # support for static file pypi packages + 'XStatic-Bootstrap>=2.1.0.1', 'XStatic-CKEditor>=3.6.1.2', 'XStatic-jQuery>=1.8.2', 'XStatic-jQuery-File-Upload>=4.4.2',
