diff --git a/docs/admin/configure.rst b/docs/admin/configure.rst index 554f492f4..66853dd6e 100644 --- a/docs/admin/configure.rst +++ b/docs/admin/configure.rst @@ -1068,16 +1068,12 @@ The WikiGroups backend is enabled by default so there is no need to add the foll To create a WikiGroup that can be used in an ACL rule: * Create a wiki item with a name ending in "Group" (the content of the item is not relevant) -* Edit the metadata and add an entry for "usergroup" under the heading "Extra Metadata (JSON)":: - - { - "itemid": "36b6cd973d7e4daa9cfa265dcf751e79", - "namespace": "", - "usergroup": [ - "JaneDoe", - "JohnDoe" - ] - } +* Edit the metadata and add entries under the heading "Wiki Groups", one entry per line. +* Leading and trailing spaces are ignored, internal spaces are accepted.:: + + JaneDoe + JohnDoe + SomeOtherGroup * Use the new group name in one or more ACL rules. @@ -1120,17 +1116,11 @@ The WikiDicts backend is enabled by default so there is no need to add the follo To create a WikiDict that can be used in an GetVal macro: * Create a wiki item with a name ending in "Dict" (the content of the item is not relevant) -* Edit the metadata and add an entry for "somedict" under the heading "Extra Metadata (JSON)":: - - { - "itemid": "332458ceab334991868de8970980494e", - "namespace": "", - "somedict": { - "apple": "red", - "banana": "yellow", - "pear": "green" - } - } +* Edit the metadata and add an entry under the heading "Wiki Dict":: + + apple=red + banana=yellow + pear=green The ConfigDicts backend uses dicts defined in the configuration file. Adding the following to wikiconfig creates a OneDict and a NumbersDict and prevents diff --git a/src/moin/constants/keys.py b/src/moin/constants/keys.py index 4c4d67d31..7d4be3dde 100644 --- a/src/moin/constants/keys.py +++ b/src/moin/constants/keys.py @@ -21,8 +21,7 @@ # keys for storing group and dict information # group of user names, e.g. for ACLs: USERGROUP = "usergroup" -# needs more precise name / use case: -SOMEDICT = "somedict" +WIKIDICT = "wikidict" # TODO review plural constants CONTENTTYPE = "contenttype" @@ -66,6 +65,8 @@ WIKINAME = "wikiname" CONTENT = "content" REFERS_TO = "refers_to" +# list of metadata fields that editors cannot modify +# excludes COMMENT, SUMMARY, TAG, USERGROUP and WIKIDICT IMMUTABLE_KEYS = [ ACTION, ADDRESS, @@ -82,6 +83,11 @@ SIZE, USERID, WIKINAME, + CONTENTTYPE, + ITEMID, + ITEMTYPE, + NAMESPACE, + REV_NUMBER, ] # magic REVID for current revision: diff --git a/src/moin/datastructures/backends/_tests/test_wiki_dicts.py b/src/moin/datastructures/backends/_tests/test_wiki_dicts.py index 6b7430a23..caebc62de 100644 --- a/src/moin/datastructures/backends/_tests/test_wiki_dicts.py +++ b/src/moin/datastructures/backends/_tests/test_wiki_dicts.py @@ -11,7 +11,7 @@ from moin.datastructures.backends._tests import DictsBackendTest from moin.datastructures.backends import wiki_dicts -from moin.constants.keys import SOMEDICT +from moin.constants.keys import WIKIDICT from moin._tests import become_trusted, update_item import pytest @@ -29,15 +29,15 @@ class TestWikiDictsBackend(DictsBackendTest): def custom_setup(self): become_trusted() - somedict = {"First": "first item", + wikidict = {"First": "first item", "text with spaces": "second item", 'Empty string': '', "Last": "last item"} - update_item('SomeTestDict', {SOMEDICT: somedict}, DATA) + update_item('SomeTestDict', {WIKIDICT: wikidict}, DATA) - somedict = {"One": "1", + wikidict = {"One": "1", "Two": "2"} - update_item('SomeOtherTestDict', {SOMEDICT: somedict}, DATA) + update_item('SomeOtherTestDict', {WIKIDICT: wikidict}, DATA) def test__retrieve_items(self): wikidict_obj = wiki_dicts.WikiDicts() diff --git a/src/moin/datastructures/backends/wiki_dicts.py b/src/moin/datastructures/backends/wiki_dicts.py index fd24d1178..54b07d7d3 100644 --- a/src/moin/datastructures/backends/wiki_dicts.py +++ b/src/moin/datastructures/backends/wiki_dicts.py @@ -11,8 +11,9 @@ from flask import g as flaskg -from moin.constants.keys import CURRENT, SOMEDICT +from moin.constants.keys import CURRENT, WIKIDICT from moin.datastructures.backends import BaseDict, BaseDictsBackend, DictDoesNotExistError +from flask import flash class WikiDict(BaseDict): @@ -26,9 +27,10 @@ def _load_dict(self): item = flaskg.unprotected_storage[dict_name] try: rev = item[CURRENT] - somedict = rev.meta.get(SOMEDICT, {}) - return somedict + wikidict = rev.meta.get(WIKIDICT, {}) + return wikidict except KeyError: + flash('WikiDict "{dict_name}" has invalid syntax within metadata.'.format(dict_name=dict_name)) raise DictDoesNotExistError(dict_name) @@ -43,5 +45,5 @@ def __getitem__(self, dict_name): def _retrieve_items(self, dict_name): item = flaskg.unprotected_storage[dict_name] rev = item.get_revision(CURRENT) - somedict = rev.meta.get(SOMEDICT, {}) - return somedict + wikidict = rev.meta.get(WIKIDICT, {}) + return wikidict diff --git a/src/moin/help/en/WikiDict.data b/src/moin/help/en/WikiDict.data index ff2bb17f5..cd5a92054 100644 --- a/src/moin/help/en/WikiDict.data +++ b/src/moin/help/en/WikiDict.data @@ -1,24 +1,25 @@ = WikiDict = -MOINTODO: There is no longer a way to add or view arbitrary metadata. This feature must be reworked. One suggestion is to follow moin 1.9 implementation and add data to item content rather than item metadata. +This item is a WikiDict because the item name ends with "Dict" and it has metadata defined under the "Wiki Dict" heading that visible when this item is edited. -This item is a WikiDict because it has a "somedict" defined in "extra" metadata: +To add metadata enter key=value pairs one per line. {{{ -{ - "itemid": "932eec2324c3494c9ac8b9dcb2e46359", - "namespace": "", - "somedict": { - "var1": "value1", - "var2": "value2", - "var3": "value3" - } -} + var1=value1 + var2=value2 + var3=value3 }}} The above is used by the GetVal macro: {{{ -<> + <> }}} + <> + +Clicking the Meta link under Item Views is an alternative way to view this item's metadata. + +{{{ + Wiki Dict: {'var1': 'value1', 'var2': 'value2', 'var3': 'value3'} +}}} diff --git a/src/moin/help/en/WikiDict.meta b/src/moin/help/en/WikiDict.meta index 49c7eb128..53f5ce467 100644 --- a/src/moin/help/en/WikiDict.meta +++ b/src/moin/help/en/WikiDict.meta @@ -3,28 +3,28 @@ "address": "127.0.0.1", "comment": "", "contenttype": "text/x.moin.wiki;charset=utf-8", - "dataid": "d69dcb7cfc524e2fadaf4322293ae9d5", + "dataid": "30cc072ac1e342c8970aa4cb49571b87", "externallinks": [], "itemid": "932eec2324c3494c9ac8b9dcb2e4635a", "itemlinks": [], "itemtransclusions": [], "itemtype": "default", - "mtime": 1681742420, + "mtime": 1704050058, "name": [ "WikiDict" ], "name_old": [], "namespace": "help-en", "rev_number": 1, - "revid": "56f6ff214ca549f0bc8895bd1a11caf0", - "sha1": "b89a611ce9754260976617b0720c21e202dba9b7", - "size": 613, - "somedict": { + "revid": "519deb6d2d7747d8979bde44c54d5ee6", + "sha1": "d9428a030ba7c34296ed9b8bd4174b91f38d5f06", + "size": 606, + "summary": "", + "tags": [], + "wikidict": { "var1": "value1", "var2": "value2", "var3": "value3" }, - "summary": "", - "tags": [], "wikiname": "MyMoinMoin" } diff --git a/src/moin/items/__init__.py b/src/moin/items/__init__.py index eb5dfd248..65b3450e1 100644 --- a/src/moin/items/__init__.py +++ b/src/moin/items/__init__.py @@ -48,14 +48,17 @@ from moin.utils.registry import RegistryBase from moin.utils.diff_html import diff as html_diff from moin.utils import diff3 -from moin.forms import RequiredText, OptionalText, JSON, Tags, Names, validate_name, NameNotValidError +from moin.forms import ( + RequiredText, OptionalText, Tags, Names, validate_name, + NameNotValidError, OptionalMultilineText +) from moin.constants.keys import ( NAME, NAMES, NAMENGRAM, NAME_OLD, NAME_EXACT, WIKINAME, MTIME, ITEMTYPE, CONTENTTYPE, SIZE, ACTION, ADDRESS, HOSTNAME, USERID, COMMENT, HASH_ALGORITHM, ITEMID, REVID, DATAID, CURRENT, PARENTID, NAMESPACE, - IMMUTABLE_KEYS, UFIELDS_TYPELIST, UFIELDS, TRASH, REV_NUMBER, + UFIELDS_TYPELIST, UFIELDS, TRASH, REV_NUMBER, ACTION_SAVE, ACTION_REVERT, ACTION_TRASH, ACTION_RENAME, TAGS, TEMPLATE, - LATEST_REVS, EDIT_ROWS, FQNAMES + LATEST_REVS, EDIT_ROWS, FQNAMES, USERGROUP, WIKIDICT ) from moin.constants.chartypes import CHARS_UPPER, CHARS_LOWER from moin.constants.namespaces import NAMESPACE_ALL, NAMESPACE_USERPROFILES @@ -217,6 +220,38 @@ def _verify_parents(self, new_name, namespace, old_name=''): )) +def str_to_dict(strg): + """ + Convert wikidicts from multi-line input form: + 'First=first item\ntext with spaces=second item\nEmpty string=\nLast=last item\n', + To dictionary: + {'Last': 'last item', 'text with spaces': 'second item', 'Empty string': '', 'First': 'first item'} + """ + new_dict = {} + lines = strg.splitlines() + for kv in lines: + try: + k, v = kv.split('=', 1) + new_dict[k] = v + except ValueError: + flash(_('Invalid line in wikidict meta data; ignored: "%(data)s"}', data=kv), 'error') + return new_dict + + +def dict_to_str(dic): + """ + convert dict: + {'First': 'first item', 'text with spaces': 'second item', 'Empty string': '', 'Last': 'last item'} + to str: + 'First=first item\ntext with spaces=second item\nEmpty string=\nLast=last item\n' + """ + new_str = [] + for k, v in dic.items(): + new_str.append(k + '=' + v) + new_str = '\r\n'.join(new_str) + return new_str + + class RegistryItem(RegistryBase): class Entry(namedtuple('Entry', 'factory itemtype display_name description order')): def __call__(self, itemtype, *args, **kw): @@ -790,7 +825,8 @@ class _ModifyForm(BaseModifyForm): ModifyForm. """ meta_form = BaseMetaForm - extra_meta_text = JSON.using(label=L_("Extra MetaData (JSON)")).with_properties(rows=ROWS_META, cols=COLS) + wikidict = OptionalMultilineText.using(label=L_("Wiki Dict")).with_properties(rows=ROWS_META, cols=COLS) + usergroup = OptionalMultilineText.using(label=L_("User Group")).with_properties(rows=ROWS_META, cols=COLS) meta_template = 'modify_meta.html' def _load(self, item): @@ -805,11 +841,19 @@ def _load(self, item): # policy to 'duck' suppresses this behavior. if 'acl' not in meta: meta['acl'] = "None" - self['meta_form'].set(meta, policy='duck') - for k in list(self['meta_form'].field_schema_mapping.keys()) + IMMUTABLE_KEYS: - meta.pop(k, None) - self['extra_meta_text'].set(item.meta_dict_to_text(meta)) + if meta[NAME][0].endswith('Dict'): + try: + self[WIKIDICT].set(dict_to_str(item.meta[WIKIDICT])) + except KeyError: + pass + if meta[NAME][0].endswith('Group'): + try: + user_group = '\r\n'.join(item.meta[USERGROUP]) + self[USERGROUP].set(user_group) + except KeyError: + pass + self['content_form']._load(item.content) def _dump(self, item): @@ -827,11 +871,10 @@ def _dump(self, item): # e.g. we get PARENTID in here meta = item.meta_filter(item.prepare_meta_for_modify(item.meta)) meta.update(self['meta_form'].value) - try: - meta.update(item.meta_text_to_dict(self['extra_meta_text'].value)) - except TypeError: - # only items with names ending in Group or Dict have extra_meta.test - pass + if item.name.endswith('Dict'): + meta.update({WIKIDICT: str_to_dict(self[WIKIDICT].value)}) + if item.name.endswith('Group'): + meta.update({USERGROUP: self[USERGROUP].value.splitlines()}) data, contenttype_guessed = self['content_form']._dump(item.content) comment = self['comment'].value return meta, data, contenttype_guessed, comment @@ -873,7 +916,6 @@ def _save(self, meta, data=None, names=None, action=ACTION_SAVE, contenttype_gue # this is treated as a rule which matches nothing elif meta['acl'] == 'Empty': meta['acl'] = '' - # we store the previous (if different) and current item names into revision metadata # this is useful for deletes, rename history and backends that use item uids internally if self.fqname.field == NAME_EXACT: @@ -1239,7 +1281,7 @@ def doc_link(self, content_name, link_text): def meta_changed(self, meta): """ Return true if user changed any of the following meta data: - comment, ACL, summary, tags, names + comment, ACL, summary, tags, names, extra_meta wikidict, usergroup """ if request.values.get(COMMENT): return True @@ -1247,6 +1289,20 @@ def meta_changed(self, meta): return True if request.values.get('meta_form_summary') != meta.get('summary', None): return True + if meta[NAME][0].endswith('Group'): + try: + new = request.values.get(USERGROUP).splitlines() + except KeyError: + new = None + old = meta.get(USERGROUP, None) + if new != old: + return True + if meta[NAME][0].endswith('Dict'): + new = request.values.get(WIKIDICT) + new = str_to_dict(new) + old = meta.get(WIKIDICT, None) + if new != old: + return True new_tags = request.values.get('meta_form_tags').replace(" ", "").split(',') if new_tags == [""]: new_tags = [] diff --git a/src/moin/macros/GetVal.py b/src/moin/macros/GetVal.py index 9cfaa2e87..70eb74080 100644 --- a/src/moin/macros/GetVal.py +++ b/src/moin/macros/GetVal.py @@ -29,4 +29,7 @@ def macro(self, content, arguments, page_url, alternative): except DictDoesNotExistError: raise ValueError(_("GetVal: dict not found: ") + item_name) result = d.get(key, '') + if not result: + raise ValueError(_('GetVal macro is invalid, {item_name} missing key: {key_name}'). + format(item_name=item_name, key_name=key)) return result diff --git a/src/moin/macros/_tests/test_GetVal.py b/src/moin/macros/_tests/test_GetVal.py index ec9b87bc8..5c84a6309 100644 --- a/src/moin/macros/_tests/test_GetVal.py +++ b/src/moin/macros/_tests/test_GetVal.py @@ -9,7 +9,7 @@ from flask import g as flaskg from moin.macros.GetVal import Macro -from moin.constants.keys import SOMEDICT +from moin.constants.keys import WIKIDICT from moin._tests import become_trusted, update_item @@ -17,9 +17,9 @@ class TestMacro: @pytest.fixture def test_dict(self): become_trusted() - somedict = {"One": "1", + wikidict = {"One": "1", "Two": "2"} - update_item('TestDict', {SOMEDICT: somedict}, "This is a dict item.") + update_item('TestDict', {WIKIDICT: wikidict}, "This is a dict item.") return "TestDict" diff --git a/src/moin/templates/modify.html b/src/moin/templates/modify.html index e5fca1ebb..d01606a49 100644 --- a/src/moin/templates/modify.html +++ b/src/moin/templates/modify.html @@ -17,7 +17,7 @@ {% extends theme("show.html") %} -{% from form.meta_template import meta_editor %} +{% from form.meta_template import meta_editor with context %} {# Import macros data_editor and extra_head from content_form's template. extra_head is optional, so instead of a simple "import from" we need to do this manually #} @@ -68,11 +68,19 @@

{{ title }}

{{ data_editor(form['content_form'], item_name) }} {% set may_admin = user.may.admin(fqname) %} {{ meta_editor(form['meta_form'], may_admin) }} - {% if fqname.fullname.endswith(('Group', 'Dict')) %} -
- {{ forms.render(form['extra_meta_text']) }} -
- {% endif %} + + {% if item.meta['name'][0].endswith('Group') %} + {{ forms.render(form['usergroup']) }} +
+ {{ _('Enter list of user names, one name per line.') }} +
+ {% endif %} + {% if item.meta['name'][0].endswith('Dict') %} + {{ forms.render(form['wikidict']) }} +
+ {{ _('Enter "key=value" strings, one per line, no quotes, no blank lines.') }} +
+ {% endif %} {{ gen.form.close() }} diff --git a/src/moin/templates/utils.html b/src/moin/templates/utils.html index 11e76c35a..40aed4936 100644 --- a/src/moin/templates/utils.html +++ b/src/moin/templates/utils.html @@ -206,7 +206,13 @@ {%- endif -%}
  • Trash: {{ meta['trash'] or False }}
  • + {% if 'usergroup' in meta %} +
  • User Group: {{ meta['usergroup'] }}
  • + {% endif %}
  • User ID: {{ meta['userid'] }}
  • + {% if 'wikidict' in meta %} +
  • Wiki Dict: {{ meta['wikidict'] }}
  • + {% endif %}
  • Wiki Name: {{ meta['wikiname'] }}
  • {% endmacro %}