class Blog(Default): itemtype = ITEMTYPE_BLOG display_name = L_('Blog') description = L_('Blog item') order = 0 class _ModifyForm(Default._ModifyForm): meta_form = BlogMetaForm meta_template = 'blog/modify_main_meta.html' def do_show(self, revid): """ Show a blog item and a list of its blog entries below it. If tag GET-parameter is defined, the list of blog entries consists only of those entries that contain the tag value in their lists of tags. """ # for now it is just one tag=value, later it could be tag=value1&tag=value2&... tag = request.values.get('tag') prefix = self.name + '/' current_timestamp = int(time.time()) terms = [ Term(WIKINAME, app.cfg.interwikiname), # Only blog entry itemtypes Term(ITEMTYPE, ITEMTYPE_BLOG_ENTRY), # Only sub items of this item Prefix(NAME_EXACT, prefix), ] if tag: terms.append(Term(TAGS, tag)) query = And(terms) def ptime_sort_key(searcher, docnum): """ Compute the publication time key for blog entries sorting. If PTIME is not defined, we use MTIME. """ fields = searcher.stored_fields(docnum) ptime = fields.get(PTIME, fields[MTIME]) return ptime ptime_sort_facet = FunctionFacet(ptime_sort_key) revs = flaskg.storage.search(query, sortedby=ptime_sort_facet, reverse=True, limit=None) blog_entry_items = [ Item.create(rev.name, rev_id=rev.revid) for rev in revs ] return render_template( 'blog/main.html', item_name=self.name, fqname=split_fqname(self.name), blog_item=self, blog_entry_items=blog_entry_items, tag=tag, item=self, )
def _load(self, item): super(TicketUpdateForm, self)._load(item) self['submit'].properties['labels'] = { 'update': L_('Update ticket'), 'update_negate_status': (L_('Update & reopen ticket') if item.meta.get('closed') else L_('Update & close ticket')) }
class BlogEntry(Default): itemtype = ITEMTYPE_BLOG_ENTRY display_name = L_('Blog entry') description = L_('Blog entry item') order = 0 class _ModifyForm(Default._ModifyForm): meta_form = BlogEntryMetaForm meta_template = 'blog/modify_entry_meta.html' @classmethod def from_item(cls, item): form = super(BlogEntry._ModifyForm, cls).from_item(item) # preload PTIME with the current datetime if not form['meta_form']['ptime']: form['meta_form']['ptime'].set(datetime.utcnow()) return form def do_show(self, revid): blog_item_name = self.name.rsplit('/', 1)[0] try: blog_item = Item.create(blog_item_name) except AccessDenied: abort(403) if not isinstance(blog_item, Blog): # The parent item of this blog entry item is not a Blog item. abort(403) return render_template( 'blog/entry.html', item_name=self.name, fqname=blog_item.fqname, blog_item=blog_item, blog_entry_item=self, item=self, )
class ValidJSON(Validator): """Validator for JSON """ invalid_json_msg = L_('Invalid JSON.') invalid_itemid_msg = L_('Itemid not a proper UUID') invalid_namespace_msg = '' def validitemid(self, itemid): if not itemid: self.invalid_itemid_msg = L_("No ITEMID field") return False return uuid_validator(String(itemid), None) def validnamespace(self, current_namespace): if current_namespace is None: self.invalid_namespace_msg = L_("No namespace field in the meta.") return False namespaces = [namespace.rstrip('/') for namespace, _ in app.cfg.namespace_mapping] if current_namespace not in namespaces: # current_namespace must be an existing namespace. self.invalid_namespace_msg = L_("%(_namespace)s is not a valid namespace.", _namespace=current_namespace) return False return True def validate(self, element, state): try: meta = json.loads(element.value) except: # noqa - catch ANY exception that happens due to unserializing return self.note_error(element, state, 'invalid_json_msg') if not self.validnamespace(meta.get(NAMESPACE)): return self.note_error(element, state, 'invalid_namespace_msg') if state[FQNAME].field == ITEMID: if not self.validitemid(meta.get(ITEMID, state[FQNAME].value)): return self.note_error(element, state, 'invalid_itemid_msg') return True
def modify_acl(item_name): fqname = split_fqname(item_name) item = Item.create(item_name) meta = dict(item.meta) old_acl = meta.get(ACL, '') new_acl = request.form.get(fqname.fullname) is_valid = acl_validate(new_acl) if is_valid: if new_acl in ('Empty', ''): meta[ACL] = '' elif new_acl == 'None' and ACL in meta: del (meta[ACL]) else: meta[ACL] = new_acl try: item._save(meta=meta) except AccessDenied: # superuser viewed item acl report and tried to change acl but lacked admin permission flash( L_("Failed! Not authorized.<br>Item: %(item_name)s<br>ACL: %(acl_rule)s", item_name=fqname.fullname, acl_rule=old_acl), "error") return redirect(url_for('.item_acl_report')) flash( L_("Success! ACL saved.<br>Item: %(item_name)s<br>ACL: %(acl_rule)s", item_name=fqname.fullname, acl_rule=new_acl), "info") else: flash( L_("Nothing changed, invalid ACL.<br>Item: %(item_name)s<br>ACL: %(acl_rule)s", item_name=fqname.fullname, acl_rule=new_acl), "error") return redirect(url_for('.item_acl_report'))
class Userprofile(Item): """ Currently userprofile is implemented as a contenttype. This is a stub of an itemtype implementation of userprofile. """ itemtype = ITEMTYPE_USERPROFILE display_name = L_('User profile') description = L_('User profile item (not implemented yet!)')
def validnamespace(self, current_namespace): if current_namespace is None: self.invalid_namespace_msg = L_("No namespace field in the meta.") return False namespaces = [namespace.rstrip('/') for namespace, _ in app.cfg.namespace_mapping] if current_namespace not in namespaces: # current_namespace must be an existing namespace. self.invalid_namespace_msg = L_("%(_namespace)s is not a valid namespace.", _namespace=current_namespace) return False return True
class RegisterNewUserForm(Form): """ Simple user registration form for use by SuperUsers to create new accounts. """ name = 'register_new_user' username = RequiredText.using(label=L_('Username')).with_properties(placeholder=L_("User Name"), autofocus=True) email = YourEmail submit_label = L_('Register') validators = [ValidRegisterNewUser()]
def validate_name(meta, itemid): """ Check whether the names are valid. Will just return, if they are valid, will raise a NameNotValidError if not. """ names = meta.get(NAME) current_namespace = meta.get(NAMESPACE) if current_namespace is None: raise NameNotValidError(L_("No namespace field in the meta.")) namespaces = [ namespace.rstrip('/') for namespace, _ in app.cfg.namespace_mapping ] if len(names) != len(set(names)): msg = L_("The names in the name list must be unique.") flash(msg, "error") # duplicate message at top of form raise NameNotValidError(msg) # Item names must not start with '@' or '+', '@something' denotes a field where as '+something' denotes a view. invalid_names = [name for name in names if name.startswith(('@', '+'))] if invalid_names: msg = L_( "Item names (%(invalid_names)s) must not start with '@' or '+'", invalid_names=", ".join(invalid_names)) flash(msg, "error") # duplicate message at top of form raise NameNotValidError(msg) namespaces = namespaces + NAMESPACES_IDENTIFIER # Also dont allow item names to match with identifier namespaces. # Item names must not match with existing namespaces. invalid_names = [ name for name in names if name.split('/', 1)[0] in namespaces ] if invalid_names: msg = L_( "Item names (%(invalid_names)s) must not match with existing namespaces.", invalid_names=", ".join(invalid_names)) flash(msg, "error") # duplicate message at top of form raise NameNotValidError(msg) query = And([ Or([Term(NAME, name) for name in names]), Term(NAMESPACE, current_namespace) ]) # There should be not item existing with the same name. if itemid is not None: query = And([query, Not(Term(ITEMID, itemid)) ]) # search for items except the current item. with flaskg.storage.indexer.ix[LATEST_REVS].searcher() as searcher: results = searcher.search(query) duplicate_names = { name for result in results for name in result[NAME] if name in names } if duplicate_names: msg = L_("Item(s) named %(duplicate_names)s already exist.", duplicate_names=", ".join(duplicate_names)) flash(msg, "error") # duplicate message at top of form raise NameNotValidError(msg)
class TicketBackRefForm(Form): supersedes = BackReference.using(label=L_("Supersedes")) required_by = BackReference.using(label=L_("Required By")) subscribers = BackReference.using(label=L_("Subscribers")) 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(SUBSCRIPTIONS, id_))
class TicketForm(BaseModifyForm): meta = TicketMetaForm backrefs = TicketBackRefForm message = OptionalMultilineText.using(label=L_("Message")).with_properties(rows=8, cols=80) data_file = File.using(optional=True, label=L_('Upload file:')) def _load(self, item): meta = item.prepare_meta_for_modify(item.meta) self['meta'].set(meta, 'duck') # XXX need a more explicit way to test for item creation/modification if ITEMID in item.meta: self['backrefs']._load(item)
class TicketMetaForm(Form): summary = Text.using(label=L_("Summary"), optional=False).with_properties( widget=WIDGET_SEARCH, placeholder=L_("One-line summary")) effort = Rating.using(label=L_("Effort")) difficulty = Rating.using(label=L_("Difficulty")) severity = Rating.using(label=L_("Severity")) priority = Rating.using(label=L_("Priority")) tags = Tags.using(optional=True) assigned_to = OptionalUserReference.using(label=L_("Assigned To")) superseded_by = OptionalTicketReference.using(label=L_("Superseded By")) depends_on = OptionalTicketReference.using(label=L_("Depends On"))
def delete(self, comment=u''): """ delete this item (remove current name from NAME list) """ item_modified.send(app, fqname=self.fqname, action=ACTION_TRASH, meta=self.meta, content=self.rev.data, comment=comment) ret = self._rename(None, comment, action=ACTION_TRASH, delete=True) if [self.name] == self.names: flash(L_('The item "%(name)s" was deleted.', name=self.name), 'info') else: # the item has several names, we deleted only one name name_list = [x for x in self.names if not x == self.name] msg = L_('The item name "%(name)s" was deleted, the item persists with alias names: "%(name_list)s"', name=self.name, name_list=name_list) flash(msg, 'info') return ret
def themed_error(e): item_name = request.view_args.get('item_name', u'') if e.code == 403: title = L_('Access Denied') description = L_('You are not allowed to access this resource.') if e.description.startswith(' '): # leading blank indicates supplemental info, not standard werkzeug message description += e.description else: # if we have no special code, we just return the HTTPException instance return e content = render_template('error.html', item_name=item_name, title=title, description=description) return content, e.code
def unlock_item(self, cancel=False): """ Return None if OK, else return 'locked by someone else' message. Called on Cancel and OK/save processing. """ user_name = self.user_name locked = self.get_lock_status() if locked: i_id, i_name, u_name, timeout = locked if u_name == user_name: self.cursor.execute( '''DELETE FROM editlock WHERE item_id = ? ''', (self.item_id, )) self.conn.commit() return elif not cancel: # bug: someone else has active edit lock, relock_item() should have been called prior to item save logging.error( "User {0} tried to unlock item that was locked by someone else: {1}" .format(user_name, i_name)) msg = L_( "Item '%(item_name)s' is locked by %(user_name)s. Edit lock error, check Item History to verify no changes were lost.", item_name=i_name, user_name=u_name, ) return msg if not cancel: # bug: there should have been a lock_item call prior to unlock call logging.error( "User {0} tried to unlock item that was not locked: {1}". format(user_name, self.item_name))
def register_new_user(): """ Create a new account and send email with link to create password. """ if not _using_moin_auth(): return Response('No MoinAuth in auth list', 403) title_name = _('Register New User') FormClass = RegisterNewUserForm if request.method in ['GET', 'HEAD']: form = FormClass.from_defaults() elif request.method == 'POST': form = FormClass.from_flat(request.form) if form.validate(): username = form['username'].value email = form['email'].value user_profile = user.UserProfile() user_profile[ITEMID] = make_uuid() user_profile[NAME] = [ username, ] user_profile[EMAIL] = email user_profile[DISABLED] = False user_profile[ACTION] = ACTION_SAVE users = user.search_users(**{NAME_EXACT: username}) if users: flash(_('User already exists'), 'error') emails = None if app.cfg.user_email_unique: emails = user.search_users(email=email) if emails: flash(_("This email already belongs to somebody else."), 'error') if not (users or emails): user_profile.save() flash(_("Account for %(username)s created", username=username), "info") form = FormClass.from_defaults() u = user.User(auth_username=username) if u.valid: is_ok, msg = u.mail_password_recovery() if not is_ok: flash(msg, "error") else: flash( L_("%(username)s has been sent a password recovery email.", username=username), "info") else: flash( _("%(username)s is an invalid user, no email has been sent.", username=username), "error") return render_template( 'admin/register_new_user.html', title_name=title_name, form=form, )
class Reference( Select.with_properties(empty_label=L_('(None)')).validated_by( ValidReference())): """ A metadata property that points to another item selected out of the Results of a search query. """ @class_cloner def to(cls, query, query_args={}): cls._query = query cls._query_args = query_args return cls @classmethod def _get_choice_specs(cls): revs = flaskg.storage.search(cls._query, **cls._query_args) label_getter = cls.properties['label_getter'] choices = [(rev.meta[ITEMID], label_getter(rev)) for rev in revs] if cls.optional: choices.append(('', cls.properties['empty_label'])) return choices def __init__(self, value=Unspecified, **kw): super(Reference, self).__init__(value, **kw) # 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. choice_specs = self._get_choice_specs() self.properties['choice_specs'] = choice_specs self.valid_values = [id_ for id_, name in choice_specs]
def modify_acl(item_name): fqname = split_fqname(item_name) item = Item.create(item_name) meta = dict(item.meta) new_acl = request.form.get(fqname.fullname) is_valid = acl_validate(new_acl) if is_valid: if new_acl in ('Empty', ''): meta[ACL] = '' elif new_acl == 'None' and ACL in meta: del(meta[ACL]) else: meta[ACL] = new_acl item._save(meta=meta) flash(L_("Success! ACL saved.<br>Item: %(item_name)s<br>ACL: %(acl_rule)s", item_name=fqname.fullname, acl_rule=new_acl), "info") else: flash(L_("Nothing changed, invalid ACL.<br>Item: %(item_name)s<br>ACL: %(acl_rule)s", item_name=fqname.fullname, acl_rule=new_acl), "error") return redirect(url_for('.item_acl_report'))
class AdvancedSearchForm(Form): q = Search summary = Text.using(label=L_("Summary"), optional=False).with_properties( widget=WIDGET_SEARCH, placeholder=L_("Find Tickets")) effort = Rating.using(label=L_("Effort")) difficulty = Rating.using(label=L_("Difficulty")) severity = Rating.using(label=L_("Severity")) priority = Rating.using(label=L_("Priority")) tags = Tags.using(optional=True) assigned_to = OptionalUserReference.using(label=L_("Assigned To")) author = OptionalUserReference.using(label=L_("Author"))
def message_markup(message): """ Add a heading with author and timestamp to message (aka ticket description). """ timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") heading = L_('{author} wrote on {timestamp}:').format(author=flaskg.user.name[0], timestamp=timestamp) message = u'{heading}\n\n{message}'.format(heading=heading, message=message) return u"""{{{{{{#!wiki moin-ticket %(message)s }}}}}}""" % dict(message=message)
def destroy(self, comment=u'', destroy_item=False, subitem_names=[]): # called from destroy UI/POST action = DESTROY_ALL if destroy_item else DESTROY_REV item_modified.send(app, fqname=self.fqname, action=action, meta=self.meta, content=self.rev.data, comment=comment) close_file(self.rev.data) if destroy_item: # destroy complete item with all revisions, metadata, etc. self.rev.item.destroy_all_revisions() # destroy all subitems for subitem_name in subitem_names: item = Item.create(subitem_name, rev_id=CURRENT) close_file(item.rev.data) item.rev.item.destroy_all_revisions() flash(L_('The item "%(name)s" was destroyed.', name=self.name), 'info') else: # just destroy this revision self.rev.item.destroy_revision(self.rev.revid) flash(L_('Rev Number %(rev_number)s of the item "%(name)s" was destroyed.', rev_number=self.meta['rev_number'], name=self.name), 'info')
class ValidReference(Validator): """ Validator for Reference """ invalid_reference_msg = L_('Invalid Reference.') def validate(self, element, state): if element.value not in element.valid_values: return self.note_error(element, state, 'invalid_reference_msg') return True
def rename(self, name, comment=u''): """ rename this item to item <name> (replace current name by another name in the NAME list) """ fqname = CompositeName(self.fqname.namespace, self.fqname.field, name) if flaskg.storage.get_item(**fqname.query): raise NameNotUniqueError(L_("An item named %s already exists in the namespace %s." % (name, fqname.namespace))) # if this is a subitem, verify all parent items exist _verify_parents(self, name, self.fqname.namespace, old_name=self.fqname.value) return self._rename(name, comment, action=ACTION_RENAME)
class _ModifyForm(BaseModifyForm): """ ModifyForm (the form used on +modify view), sans the content part. Combined dynamically with the ModifyForm of the Content subclass in Contentful.ModifyForm. Subclasses of Contentful should generally override this instead of ModifyForm. """ meta_form = BaseMetaForm extra_meta_text = JSON.using( label=L_("Extra MetaData (JSON)")).with_properties(rows=ROWS_META, cols=COLS) meta_template = 'modify_meta.html' def _load(self, item): """ Load metadata and data from :item into :self. Used by BaseModifyForm.from_item. """ meta = item.prepare_meta_for_modify(item.meta) # Default value of `policy` argument of Flatland.Dict.set's is # 'strict', which causes KeyError to be thrown when meta contains # meta keys that are not present in self['meta_form']. Setting # policy to 'duck' suppresses this behavior. if 'acl' not in meta: meta['acl'] = "None" self['meta_form'].set(meta, policy='duck') for k in self['meta_form'].field_schema_mapping.keys( ) + IMMUTABLE_KEYS: meta.pop(k, None) self['extra_meta_text'].set(item.meta_dict_to_text(meta)) self['content_form']._load(item.content) def _dump(self, item): """ Dump useful data out of :self. :item contains the old item and should not be the primary data source; but it can be useful in case the data in :self is not sufficient. :returns: a tuple (meta, data, contenttype_guessed, comment), suitable as arguments of the same names to pass to item.modify """ # Since the metadata form for tickets is an incomplete one, we load the # original meta and update it with those from the metadata editor # e.g. we get PARENTID in here meta = item.meta_filter(item.prepare_meta_for_modify(item.meta)) meta.update(self['meta_form'].value) meta.update(item.meta_text_to_dict(self['extra_meta_text'].value)) data, contenttype_guessed = self['content_form']._dump( item.content) comment = self['comment'].value return meta, data, contenttype_guessed, comment
class BaseMetaForm(Form): itemtype = RequiredText.using(label=L_("Item type")).with_properties(placeholder=L_("Item type")) contenttype = RequiredText.using(label=L_("Content type")).with_properties(placeholder=L_("Content type")) # Flatland doesn't distinguish between empty value and nonexistent value, use None for noneexistent and Empty for empty acl = RequiredText.using(label=L_("ACL")).with_properties(placeholder=L_("Access Control List - Use 'None' for default")).validated_by(ACLValidator()) summary = OptionalText.using(label=L_("Summary")).with_properties(placeholder=L_("One-line summary of the item")) name = Names tags = Tags
class TextChaValid(Validator): """Validator for TextChas """ textcha_incorrect_msg = L_('The entered TextCha was incorrect.') textcha_invalid_msg = L_( 'The TextCha question is invalid or has expired. Please try again.') def validate(self, element, state): textcha = TextCha(element.parent) if textcha.is_enabled(): if textcha.answer_re is None: textcha.init_qa() textcha.amend_form() element.set("") return self.note_error(element, state, 'textcha_invalid_msg') if textcha.answer_re.match(element.value.strip()) is None: return self.note_error(element, state, 'textcha_incorrect_msg') return True
class ACLValidator(Validator): """ Meta Validator - currently used for validating ACLs only """ acl_fail_msg = L_("The ACL string is invalid.") def validate(self, element, state): if acl_validate(element) is True: return True flash(L_("The ACL string is invalid."), "error") return self.note_error(element, state, 'acl_fail_msg')
class ValidSearch(Validator): """Validator for a valid search form """ too_short_query_msg = L_('Search query too short.') def validate(self, element, state): if element['q'].value is None: # no query, nothing to search for return False if len(element['q'].value) < 2: return self.note_error(element, state, 'too_short_query_msg') return True
def send_notifications(app, fqname, **kwargs): """ Send mail notifications to subscribers on item change :param app: local proxy app :param fqname: fqname of the changed item :param kwargs: key/value pairs that contain extra information about the item required in order to create a notification """ action = kwargs.get('action') revs = get_item_last_revisions(app, fqname) if action not in [ DESTROY_REV, DESTROY_ALL, ] else [] notification = Notification(app, fqname, revs, **kwargs) try: content_diff = notification.get_content_diff() except Exception: # current user has likely corrupted an item or fixed a corrupted item # or changed ACL and removed read access for himself # if current item is corrupt, another exception will occur in a downstream script content_diff = [ u'- ' + _('An error has occurred, the current or prior revision of this item may be corrupt.' ) ] meta_diff = notification.get_meta_diff() u = flaskg.user meta = kwargs.get('meta') if action in [ DESTROY_REV, DESTROY_ALL, ] else revs[0].meta._meta subscribers = { subscriber for subscriber in get_subscribers(**meta) if subscriber.itemid != u.itemid } subscribers_locale = {subscriber.locale for subscriber in subscribers} for locale in subscribers_locale: with force_locale(locale): txt_msg, html_msg = notification.render_templates( content_diff, meta_diff) subject = L_( '[%(moin_name)s] Update of "%(fqname)s" by %(user_name)s', moin_name=app.cfg.interwikiname, fqname=unicode(fqname), user_name=u.name0) subscribers_emails = [ subscriber.email for subscriber in subscribers if subscriber.locale == locale ] sendmail(subject, txt_msg, to=subscribers_emails, html=html_msg)
class TicketSubmitForm(TicketForm): submit_label = L_("Submit ticket") def _dump(self, item): # initial metadata for Ticket-itemtyped item meta = { ITEMTYPE: item.itemtype, # XXX support other markups CONTENTTYPE: 'text/x.moin.wiki;charset=utf-8', 'closed': False, } meta.update(self['meta'].value) return meta, message_markup(self['message'].value), None, self['data_file'].value