MoinMoin/forms.py
author Ashutosh Singla <ashu1461@gmail.com>
Tue, 09 Apr 2013 22:33:42 +0530
changeset 2085 7f4520c9b5f1
parent 2019 ecd902b614bd
child 2093 be05c7f6d216
permissions -rw-r--r--
solves Issue #328, multiple name must be unique with all names, added a function to validatename which takes care of the issue
pavel@1463
     1
# Copyright: 2012 MoinMoin:PavelSviderski
xiaqqaix@1420
     2
# Copyright: 2012 MoinMoin:CheerXiao
xiaqqaix@1420
     3
# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
xiaqqaix@1420
     4
xiaqqaix@1420
     5
"""
xiaqqaix@1420
     6
    MoinMoin - Flatland widgets
xiaqqaix@1420
     7
xiaqqaix@1420
     8
    General Flatland widgets containing hints for the templates.
xiaqqaix@1420
     9
"""
xiaqqaix@1420
    10
xiaqqaix@1420
    11
tw@1975
    12
import re
tw@1975
    13
import datetime
xiaqqaix@1626
    14
import json
xiaqqaix@2019
    15
from operator import itemgetter
xiaqqaix@1437
    16
xiaqqaix@2019
    17
from flatland import (Element, Form, String, Integer, Boolean, Enum as BaseEnum, Dict, JoinedString, List, Array,
tw@1975
    18
                      DateTime as _DateTime)
xiaqqaix@1668
    19
from flatland.util import class_cloner, Unspecified
xiaqqaix@1420
    20
from flatland.validation import Validator, Present, IsEmail, ValueBetween, URLValidator, Converted, ValueAtLeast
pavel@1640
    21
from flatland.exc import AdaptationError
xiaqqaix@1420
    22
ashu1461@2085
    23
from whoosh.query import Term, Or, Not, And
ashu1461@2085
    24
xiaqqaix@1668
    25
from flask import g as flaskg
xiaqqaix@1668
    26
xiaqqaix@1420
    27
from MoinMoin.constants.forms import *
ashu1461@2085
    28
from MoinMoin.constants.keys import ITEMID, NAME, LATEST_REVS
xiaqqaix@1420
    29
from MoinMoin.i18n import _, L_, N_
xiaqqaix@1420
    30
from MoinMoin.util.forms import FileStorage
xiaqqaix@1420
    31
xiaqqaix@1420
    32
xiaqqaix@2019
    33
class Enum(BaseEnum):
xiaqqaix@2019
    34
    """
xiaqqaix@2019
    35
    An Enum with a convenience class method out_of.
xiaqqaix@2019
    36
    """
xiaqqaix@2019
    37
    @classmethod
xiaqqaix@2019
    38
    def out_of(cls, choice_specs, sort_by=None):
xiaqqaix@2019
    39
        """
xiaqqaix@2019
    40
        A convenience class method to build Enum with extra data attached to
xiaqqaix@2019
    41
        each valid value.
xiaqqaix@2019
    42
xiaqqaix@2019
    43
        :param choice_specs: An iterable of tuples. The elements are collected
xiaqqaix@2019
    44
                             into the choice_specs property; the tuples' first
xiaqqaix@2019
    45
                             elements become the valid values of the Enum. e.g.
xiaqqaix@2019
    46
                             for choice_specs = [(v1, ...), (v2, ...), ... ],
xiaqqaix@2019
    47
                             the valid values are v1, v2, ...
xiaqqaix@2019
    48
xiaqqaix@2019
    49
        :param sort_by: If not None, sort choice_specs by the sort_by'th
xiaqqaix@2019
    50
                        element.
xiaqqaix@2019
    51
        """
xiaqqaix@2019
    52
        if sort_by is not None:
xiaqqaix@2019
    53
            choice_specs = sorted(choice_specs, key=itemgetter(sort_by))
xiaqqaix@2019
    54
        else:
xiaqqaix@2019
    55
            choice_specs = list(choice_specs)
xiaqqaix@2019
    56
        return cls.valued(*[e[0] for e in choice_specs]).with_properties(choice_specs=choice_specs)
xiaqqaix@2019
    57
xiaqqaix@2019
    58
xiaqqaix@1420
    59
Text = String.with_properties(widget=WIDGET_TEXT)
xiaqqaix@1420
    60
xiaqqaix@1420
    61
MultilineText = String.with_properties(widget=WIDGET_MULTILINE_TEXT)
xiaqqaix@1420
    62
xiaqqaix@1420
    63
OptionalText = Text.using(optional=True)
xiaqqaix@1420
    64
xiaqqaix@1420
    65
RequiredText = Text.validated_by(Present())
xiaqqaix@1420
    66
xiaqqaix@1420
    67
OptionalMultilineText = MultilineText.using(optional=True)
xiaqqaix@1420
    68
xiaqqaix@1420
    69
RequiredMultilineText = MultilineText.validated_by(Present())
xiaqqaix@1420
    70
xiaqqaix@1625
    71
xiaqqaix@1625
    72
class ValidJSON(Validator):
xiaqqaix@1625
    73
    """Validator for JSON
xiaqqaix@1625
    74
    """
xiaqqaix@1625
    75
    invalid_json_msg = L_('Invalid JSON.')
ashu1461@2085
    76
    invalid_name_msg = ""
ashu1461@2085
    77
ashu1461@2085
    78
    def validname(self, meta, name, itemid):
ashu1461@2085
    79
        names = meta.get(NAME)
ashu1461@2085
    80
        if names is None:
ashu1461@2085
    81
            self.invalid_name_msg = L_("No name field in the JSON meta.")
ashu1461@2085
    82
            return False
ashu1461@2085
    83
        if len(names) != len(set(names)):
ashu1461@2085
    84
            self.invalid_name_msg = L_("The names in the JSON name list must be unique.")
ashu1461@2085
    85
            return False
ashu1461@2085
    86
        query = Or([Term(NAME, x) for x in names])
ashu1461@2085
    87
        if itemid is not None:
ashu1461@2085
    88
            query = And([query, Not(Term(ITEMID, itemid))])
ashu1461@2085
    89
        duplicate_names = set()
ashu1461@2085
    90
        with flaskg.storage.indexer.ix[LATEST_REVS].searcher() as searcher:
ashu1461@2085
    91
            results = searcher.search(query)
ashu1461@2085
    92
            for result in results:
ashu1461@2085
    93
                duplicate_names |= set([x for x in result[NAME] if x in names])
ashu1461@2085
    94
        if duplicate_names:
ashu1461@2085
    95
            self.invalid_name_msg = L_("Item(s) named %(duplicate_names)s already exist.", duplicate_names=", ".join(duplicate_names))
ashu1461@2085
    96
            return False
ashu1461@2085
    97
        return True
xiaqqaix@1625
    98
xiaqqaix@1625
    99
    def validate(self, element, state):
xiaqqaix@1625
   100
        try:
ashu1461@2085
   101
            meta = json.loads(element.value)
tw@1975
   102
        except:  # catch ANY exception that happens due to unserializing
xiaqqaix@1625
   103
            return self.note_error(element, state, 'invalid_json_msg')
ashu1461@2085
   104
        if not self.validname(meta, state[NAME], state[ITEMID]):
ashu1461@2085
   105
            return self.note_error(element, state, 'invalid_name_msg')
xiaqqaix@1625
   106
        return True
xiaqqaix@1625
   107
xiaqqaix@1625
   108
JSON = OptionalMultilineText.with_properties(lang='en', dir='ltr').validated_by(ValidJSON())
xiaqqaix@1625
   109
xiaqqaix@1420
   110
URL = String.with_properties(widget=WIDGET_TEXT).validated_by(URLValidator())
xiaqqaix@1420
   111
xiaqqaix@1420
   112
OpenID = URL.using(label=L_('OpenID')).with_properties(placeholder=L_("OpenID address"))
xiaqqaix@1420
   113
xiaqqaix@1420
   114
YourOpenID = OpenID.with_properties(placeholder=L_("Your OpenID address"))
xiaqqaix@1420
   115
tw@1975
   116
Email = String.using(label=L_('E-Mail')).with_properties(widget=WIDGET_EMAIL,
tw@1975
   117
                                                         placeholder=L_("E-Mail address")).validated_by(IsEmail())
xiaqqaix@1420
   118
xiaqqaix@1420
   119
YourEmail = Email.with_properties(placeholder=L_("Your E-Mail address"))
xiaqqaix@1420
   120
xiaqqaix@1420
   121
Password = Text.with_properties(widget=WIDGET_PASSWORD).using(label=L_('Password'))
xiaqqaix@1420
   122
xiaqqaix@1420
   123
RequiredPassword = Password.validated_by(Present())
xiaqqaix@1420
   124
xiaqqaix@1420
   125
Checkbox = Boolean.with_properties(widget=WIDGET_CHECKBOX).using(optional=True, default=1)
xiaqqaix@1420
   126
xiaqqaix@1420
   127
InlineCheckbox = Checkbox.with_properties(widget=WIDGET_INLINE_CHECKBOX)
xiaqqaix@1420
   128
xiaqqaix@1420
   129
Select = Enum.with_properties(widget=WIDGET_SELECT)
xiaqqaix@1420
   130
xiaqqaix@1994
   131
# SelectSubmit is like Select in that it is rendered as a group of controls
xiaqqaix@1994
   132
# with different (predefined) `value`s for the same `name`. But the controls are
xiaqqaix@1994
   133
# submit buttons instead of radio buttons.
xiaqqaix@1994
   134
#
xiaqqaix@1994
   135
# This is used to present the user several "OK" buttons with slightly different
xiaqqaix@1994
   136
# semantics, like "Update" and "Update and Close" on a ticket page, or
xiaqqaix@1994
   137
# "Save as Draft" and "Publish" when editing a blog entry.
xiaqqaix@1994
   138
SelectSubmit = Enum.with_properties(widget=WIDGET_SELECT_SUBMIT)
xiaqqaix@1994
   139
xiaqqaix@1628
   140
xiaqqaix@1628
   141
# Need a better name to capture the behavior
xiaqqaix@1628
   142
class MyJoinedString(JoinedString):
xiaqqaix@1628
   143
    """
xiaqqaix@1628
   144
    A JoinedString that offers the list of children (not the joined string) as
xiaqqaix@1628
   145
    value property.
xiaqqaix@1628
   146
    """
xiaqqaix@1628
   147
    @property
xiaqqaix@1628
   148
    def value(self):
xiaqqaix@1628
   149
        return [child.value for child in self]
xiaqqaix@1628
   150
xiaqqaix@1628
   151
    @property
xiaqqaix@1628
   152
    def u(self):
xiaqqaix@1628
   153
        return self.separator.join(child.u for child in self)
xiaqqaix@1628
   154
tw@2011
   155
Tags = MyJoinedString.of(String).with_properties(widget=WIDGET_TEXT).using(
tw@2011
   156
    label=L_('Tags'), optional=True, separator=', ', separator_regex=re.compile(r'\s*,\s*'))
xiaqqaix@1420
   157
tw@2011
   158
Names = MyJoinedString.of(String).with_properties(widget=WIDGET_TEXT).using(
tw@2011
   159
    label=L_('Names'), optional=True, separator=', ', separator_regex=re.compile(r'\s*,\s*'))
tw@1952
   160
xiaqqaix@1420
   161
Search = Text.using(default=u'', optional=True).with_properties(widget=WIDGET_SEARCH, placeholder=L_("Search Query"))
xiaqqaix@1420
   162
xiaqqaix@1420
   163
_Integer = Integer.validated_by(Converted())
xiaqqaix@1420
   164
xiaqqaix@1420
   165
AnyInteger = _Integer.with_properties(widget=WIDGET_ANY_INTEGER)
xiaqqaix@1420
   166
xiaqqaix@1420
   167
Natural = AnyInteger.validated_by(ValueAtLeast(0))
xiaqqaix@1420
   168
xiaqqaix@1420
   169
SmallNatural = _Integer.with_properties(widget=WIDGET_SMALL_NATURAL)
xiaqqaix@1420
   170
pavel@1640
   171
pavel@1640
   172
class DateTimeUNIX(_DateTime):
pavel@1640
   173
    """
pavel@1640
   174
    A DateTime that uses a UNIX timestamp instead of datetime as internal
pavel@1640
   175
    representation of DateTime.
pavel@1640
   176
    """
pavel@1640
   177
    def serialize(self, value):
pavel@1640
   178
        """Serializes value to string."""
pavel@1640
   179
        if isinstance(value, int):
pavel@1640
   180
            try:
pavel@1640
   181
                value = datetime.datetime.utcfromtimestamp(value)
pavel@1640
   182
            except ValueError:
pavel@1640
   183
                pass
pavel@1640
   184
        return super(DateTimeUNIX, self).serialize(value)
pavel@1640
   185
pavel@1640
   186
    def adapt(self, value):
pavel@1640
   187
        """Coerces value to a native UNIX timestamp.
pavel@1640
   188
pavel@1640
   189
        If value is an instance of int and it is a correct UNIX timestamp,
pavel@1640
   190
        returns it unchanged. Otherwise uses DateTime superclass to parse it.
pavel@1640
   191
        """
pavel@1640
   192
        if isinstance(value, int):
pavel@1640
   193
            try:
pavel@1640
   194
                # check if a value is a correct timestamp
pavel@1640
   195
                dt = datetime.datetime.utcfromtimestamp(value)
pavel@1640
   196
                return value
pavel@1640
   197
            except ValueError:
pavel@1640
   198
                raise AdaptationError()
pavel@1640
   199
        dt = super(DateTimeUNIX, self).adapt(value)
pavel@1640
   200
        if isinstance(dt, datetime.datetime):
pavel@1640
   201
            # XXX forces circular dependency when it is in the head import block
pavel@1640
   202
            from MoinMoin.themes import utctimestamp
pavel@1640
   203
            # TODO: Add support for timezones
pavel@1640
   204
            dt = utctimestamp(dt)
pavel@1640
   205
        return dt
pavel@1640
   206
tw@1975
   207
DateTime = (DateTimeUNIX.with_properties(widget=WIDGET_DATETIME,
tw@1975
   208
                                         placeholder=_("YYYY-MM-DD HH:MM:SS (example: 2013-12-31 23:59:59)"))
pavel@1464
   209
            .validated_by(Converted(incorrect=L_("Please use the following format: YYYY-MM-DD HH:MM:SS"))))
pavel@1463
   210
xiaqqaix@1420
   211
File = FileStorage.with_properties(widget=WIDGET_FILE)
xiaqqaix@1420
   212
xiaqqaix@1420
   213
Hidden = String.using(optional=True).with_properties(widget=WIDGET_HIDDEN)
xiaqqaix@1668
   214
xiaqqaix@1673
   215
# optional=True is needed to get rid of the "required field" indicator on the UI (usually an asterisk)
xiaqqaix@1673
   216
ReadonlyStringList = List.of(String).using(optional=True).with_properties(widget=WIDGET_READONLY_STRING_LIST)
xiaqqaix@1673
   217
xiaqqaix@1673
   218
ReadonlyItemLinkList = ReadonlyStringList.with_properties(widget=WIDGET_READONLY_ITEM_LINK_LIST)
xiaqqaix@1673
   219
xiaqqaix@1668
   220
xiaqqaix@1668
   221
# XXX When some user chooses a Reference candidate that is removed before the
xiaqqaix@1668
   222
# user POSTs, the validator fails. This can be confusing.
xiaqqaix@1668
   223
class ValidReference(Validator):
xiaqqaix@1668
   224
    """
xiaqqaix@1668
   225
    Validator for Reference
xiaqqaix@1668
   226
    """
xiaqqaix@1668
   227
    invalid_reference_msg = L_('Invalid Reference.')
xiaqqaix@1668
   228
xiaqqaix@1668
   229
    def validate(self, element, state):
xiaqqaix@1669
   230
        if element.value not in element.valid_values:
xiaqqaix@1668
   231
            return self.note_error(element, state, 'invalid_reference_msg')
xiaqqaix@1668
   232
        return True
xiaqqaix@1668
   233
tw@1975
   234
xiaqqaix@1669
   235
class Reference(Select.with_properties(empty_label=L_(u'(None)')).validated_by(ValidReference())):
xiaqqaix@1668
   236
    """
xiaqqaix@1668
   237
    A metadata property that points to another item selected out of the
xiaqqaix@1668
   238
    Results of a search query.
xiaqqaix@1668
   239
    """
xiaqqaix@1668
   240
    @class_cloner
xiaqqaix@1668
   241
    def to(cls, query, query_args={}):
xiaqqaix@1668
   242
        cls._query = query
xiaqqaix@1668
   243
        cls._query_args = query_args
xiaqqaix@1668
   244
        return cls
xiaqqaix@1668
   245
xiaqqaix@1668
   246
    @classmethod
xiaqqaix@1668
   247
    def _get_choices(cls):
xiaqqaix@1668
   248
        revs = flaskg.storage.search(cls._query, **cls._query_args)
xiaqqaix@1668
   249
        choices = [(rev.meta[ITEMID], rev.meta[NAME]) for rev in revs]
xiaqqaix@1668
   250
        if cls.optional:
xiaqqaix@1668
   251
            choices.append((u'', cls.properties['empty_label']))
xiaqqaix@1668
   252
        return choices
xiaqqaix@1668
   253
xiaqqaix@1668
   254
    def __init__(self, value=Unspecified, **kw):
xiaqqaix@1668
   255
        super(Reference, self).__init__(value, **kw)
xiaqqaix@1668
   256
        # NOTE There is a slight chance of two instances of the same Reference
xiaqqaix@1668
   257
        # subclass having different set of choices when the storage changes
xiaqqaix@1668
   258
        # between their initialization.
xiaqqaix@1668
   259
        choices = self._get_choices()
xiaqqaix@1668
   260
        self.properties['labels'] = dict(choices)
xiaqqaix@1668
   261
        self.valid_values = [id_ for id_, name in choices]
xiaqqaix@1673
   262
xiaqqaix@1673
   263
xiaqqaix@1673
   264
class BackReference(ReadonlyItemLinkList):
xiaqqaix@1673
   265
    """
xiaqqaix@1673
   266
    Back references built from Whoosh query.
xiaqqaix@1673
   267
    """
xiaqqaix@1673
   268
    def set(self, query, **query_args):
xiaqqaix@1673
   269
        revs = flaskg.storage.search(query, **query_args)
xiaqqaix@1673
   270
        super(BackReference, self).set([rev.meta[NAME] for rev in revs])
xiaqqaix@1742
   271
xiaqqaix@1742
   272
xiaqqaix@1742
   273
MultiSelect = Array.with_properties(widget=WIDGET_MULTI_SELECT)