blob: 60b97d3de63fe1bbb7d1ab93a0d3b2a57b851571 [file] [log] [blame]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#-*- python -*-
import logging
import urllib
import json
import os
# Non-stdlib imports
from pylons import tmpl_context as c, app_globals as g
from pylons import request
from tg import expose, redirect, flash, validate, jsonify
from tg.decorators import with_trailing_slash
from bson import ObjectId
from ming import schema
# Pyforge-specific imports
from allura import model as M
from allura.app import Application, ConfigOption, SitemapEntry, DefaultAdminController
from allura.lib import helpers as h
from allura.lib.decorators import require_post
from allura.lib.security import require_access, has_access
from allura.lib.utils import JSONForExport
# Local imports
from forgediscussion import model as DM
from forgediscussion import utils
from forgediscussion import version
from .controllers import RootController, RootRestController
from widgets.admin import OptionsAdmin, AddForum
log = logging.getLogger(__name__)
class W:
options_admin = OptionsAdmin()
add_forum = AddForum()
class ForgeDiscussionApp(Application):
__version__ = version.__version__
permissions = ['configure', 'read',
'unmoderated_post', 'post', 'moderate', 'admin']
permissions_desc = {
'configure': 'Create new forums.',
'read': 'View posts.',
'admin': 'Set permissions. Edit forum properties.',
}
config_options = Application.config_options + [
ConfigOption('PostingPolicy',
schema.OneOf('ApproveOnceModerated', 'ModerateAll'), 'ApproveOnceModerated'),
ConfigOption('AllowEmailPosting', bool, True)
]
PostClass = DM.ForumPost
AttachmentClass = DM.ForumAttachment
searchable = True
exportable = True
tool_label = 'Discussion'
tool_description = """
Discussion forums are a place to talk about any topics related to your project.
You may set up multiple forums within the Discussion tool.
"""
default_mount_label = 'Discussion'
default_mount_point = 'discussion'
ordinal = 7
icons = {
24: 'images/forums_24.png',
32: 'images/forums_32.png',
48: 'images/forums_48.png'
}
def __init__(self, project, config):
Application.__init__(self, project, config)
self.root = RootController()
self.api_root = RootRestController()
self.admin = ForumAdminController(self)
def has_access(self, user, topic):
f = DM.Forum.query.get(shortname=topic.replace('.', '/'),
app_config_id=self.config._id)
return has_access(f, 'post', user=user)()
def handle_message(self, topic, message):
log.info('Message from %s (%s)',
topic, self.config.options.mount_point)
log.info('Headers are: %s', message['headers'])
shortname = urllib.unquote_plus(topic.replace('.', '/'))
forum = DM.Forum.query.get(
shortname=shortname, app_config_id=self.config._id)
if forum is None:
log.error("Error looking up forum: %r", shortname)
return
self.handle_artifact_message(forum, message)
def main_menu(self):
'''Apps should provide their entries to be added to the main nav
:return: a list of :class:`SitemapEntries <allura.app.SitemapEntry>`
'''
return [SitemapEntry(
self.config.options.mount_label,
'.')]
@property
@h.exceptionless([], log)
def sitemap(self):
menu_id = self.config.options.mount_label
with h.push_config(c, app=self):
return [
SitemapEntry(menu_id, '.')[self.sidebar_menu()]]
@property
def forums(self):
return DM.Forum.query.find(dict(app_config_id=self.config._id)).all()
@property
def top_forums(self):
return self.subforums_of(None)
def subforums_of(self, parent_id):
return DM.Forum.query.find(dict(
app_config_id=self.config._id,
parent_id=parent_id,
)).all()
def admin_menu(self):
admin_url = c.project.url() + 'admin/' + \
self.config.options.mount_point + '/'
links = []
if has_access(self, 'configure')():
links.append(SitemapEntry('Forums', admin_url + 'forums'))
links += super(ForgeDiscussionApp, self).admin_menu()
return links
def sidebar_menu(self):
try:
l = []
moderate_link = None
forum_links = []
forums = DM.Forum.query.find(dict(
app_config_id=c.app.config._id,
parent_id=None, deleted=False))
for f in forums:
if has_access(f, 'read')():
if f.url() in request.url and h.has_access(f, 'moderate')():
num_moderate = DM.ForumPost.query.find({
'discussion_id': f._id,
'status': {'$ne': 'ok'},
'deleted': False,
}).count()
moderate_link = SitemapEntry(
'Moderate', "%smoderate/" % f.url(), ui_icon=g.icons['moderate'],
small=num_moderate)
forum_links.append(
SitemapEntry(f.name, f.url(), small=f.num_topics))
url = c.app.url + 'create_topic/'
url = h.urlquote(
url + c.forum.shortname if getattr(c, 'forum', None) and c.forum else url)
l.append(
SitemapEntry('Create Topic', url, ui_icon=g.icons['add']))
if has_access(c.app, 'configure')():
l.append(SitemapEntry('Add Forum', c.app.url +
'new_forum', ui_icon=g.icons['conversation']))
l.append(SitemapEntry('Admin Forums', c.project.url() + 'admin/' +
self.config.options.mount_point + '/forums', ui_icon=g.icons['admin']))
if moderate_link:
l.append(moderate_link)
# if we are in a thread and not anonymous, provide placeholder
# links to use in js
if '/thread/' in request.url and c.user not in (None, M.User.anonymous()):
l.append(SitemapEntry(
'Mark as Spam', 'flag_as_spam',
ui_icon=g.icons['flag'], className='sidebar_thread_spam'))
l.append(SitemapEntry('Stats Graph', c.app.url +
'stats', ui_icon=g.icons['stats']))
if forum_links:
l.append(SitemapEntry('Forums'))
l = l + forum_links
l.append(SitemapEntry('Help'))
l.append(
SitemapEntry('Formatting Help', c.app.url + 'markdown_syntax'))
return l
except: # pragma no cover
log.exception('sidebar_menu')
return []
def install(self, project):
'Set up any default permissions and roles here'
# Don't call super install here, as that sets up discussion for a tool
# Setup permissions
role_admin = M.ProjectRole.by_name('Admin')._id
role_developer = M.ProjectRole.by_name('Developer')._id
role_auth = M.ProjectRole.by_name('*authenticated')._id
role_anon = M.ProjectRole.by_name('*anonymous')._id
self.config.acl = [
M.ACE.allow(role_anon, 'read'),
M.ACE.allow(role_auth, 'post'),
M.ACE.allow(role_auth, 'unmoderated_post'),
M.ACE.allow(role_developer, 'moderate'),
M.ACE.allow(role_admin, 'configure'),
M.ACE.allow(role_admin, 'admin'),
]
utils.create_forum(self, new_forum=dict(
shortname='general',
create='on',
name='General Discussion',
description='Forum about anything you want to talk about.',
parent='',
members_only=False,
anon_posts=False,
monitoring_email=None))
def uninstall(self, project):
"Remove all the tool's artifacts from the database"
DM.Forum.query.remove(dict(app_config_id=self.config._id))
DM.ForumThread.query.remove(dict(app_config_id=self.config._id))
DM.ForumPost.query.remove(dict(app_config_id=self.config._id))
super(ForgeDiscussionApp, self).uninstall(project)
def bulk_export(self, f, export_path='', with_attachments=False):
f.write('{"forums": [')
forums = list(DM.Forum.query.find(dict(app_config_id=self.config._id)))
if with_attachments:
GenericJSON = JSONForExport
for forum in forums:
self.export_attachments(forum.threads, export_path)
else:
GenericJSON = jsonify.GenericJSON
for i, forum in enumerate(forums):
if i > 0:
f.write(',')
json.dump(forum, f, cls=GenericJSON, indent=2)
f.write(']}')
def export_attachments(self, threads, export_path):
for thread in threads:
for post in thread.query_posts(status='ok'):
post_path = self.get_attachment_export_path(
export_path,
str(thread.artifact._id),
thread._id,
post.slug
)
self.save_attachments(post_path, post.attachments)
class ForumAdminController(DefaultAdminController):
def _check_security(self):
require_access(self.app, 'admin')
@with_trailing_slash
def index(self, **kw):
redirect('forums')
@expose('jinja:forgediscussion:templates/discussionforums/admin_options.html')
def options(self):
c.options_admin = W.options_admin
return dict(app=self.app,
form_value=dict(
PostingPolicy=self.app.config.options.get('PostingPolicy'),
AllowEmailPosting=self.app.config.options.get('AllowEmailPosting', True)))
@expose('jinja:forgediscussion:templates/discussionforums/admin_forums.html')
def forums(self, add_forum=None, **kw):
c.add_forum = W.add_forum
return dict(app=self.app,
allow_config=has_access(self.app, 'configure')())
@h.vardec
@expose()
@require_post()
def update_forums(self, forum=None, **kw):
if forum is None:
forum = []
for f in forum:
forum = DM.Forum.query.get(_id=ObjectId(str(f['id'])))
if f.get('delete'):
forum.deleted = True
elif f.get('undelete'):
forum.deleted = False
else:
if '.' in f['shortname'] or '/' in f['shortname'] or ' ' in f['shortname']:
flash('Shortname cannot contain space . or /', 'error')
redirect('.')
forum.name = f['name']
forum.shortname = f['shortname']
forum.description = f['description']
forum.monitoring_email = f['monitoring_email']
if 'members_only' in f:
if 'anon_posts' in f:
flash(
'You cannot have anonymous posts in a members only forum.', 'warning')
forum.anon_posts = False
del f['anon_posts']
forum.members_only = True
else:
forum.members_only = False
if 'anon_posts' in f:
forum.anon_posts = True
else:
forum.anon_posts = False
role_anon = M.ProjectRole.anonymous()._id
if forum.members_only:
role_developer = M.ProjectRole.by_name('Developer')._id
forum.acl = [
M.ACE.allow(role_developer, M.ALL_PERMISSIONS),
M.DENY_ALL]
elif forum.anon_posts:
forum.acl = [M.ACE.allow(role_anon, 'post')]
else:
forum.acl = []
flash('Forums updated')
redirect(request.referrer)
@h.vardec
@expose()
@require_post()
@validate(form=W.add_forum, error_handler=forums)
def add_forum(self, add_forum=None, **kw):
f = utils.create_forum(self.app, add_forum)
redirect(f.url())