You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by he...@apache.org on 2015/05/29 22:40:51 UTC
[29/45] allura git commit: [#7878] Used 2to3 to see what issues would
come up
http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/ext/admin/admin_main.py
----------------------------------------------------------------------
diff --git a/ext/admin/admin_main.py b/ext/admin/admin_main.py
new file mode 100644
index 0000000..83797b9
--- /dev/null
+++ b/ext/admin/admin_main.py
@@ -0,0 +1,1235 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
+# 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.
+
+import logging
+from datetime import datetime
+from urllib.parse import urlparse
+import json
+from operator import itemgetter
+import pkg_resources
+from pylons import tmpl_context as c, app_globals as g
+from pylons import request
+from paste.deploy.converters import asbool, aslist
+from tg import expose, redirect, flash, validate, config, jsonify
+from tg.decorators import with_trailing_slash, without_trailing_slash
+from webob import exc
+from bson import ObjectId
+from ming.orm.ormsession import ThreadLocalORMSession
+from ming.odm import session
+from ming.utils import LazyProperty
+from allura.app import Application, DefaultAdminController, SitemapEntry
+from allura.lib import helpers as h
+from allura import version
+from allura import model as M
+from allura.lib.repository import RepositoryApp
+from allura.lib.security import has_access, require_access
+from allura.lib.widgets import form_fields as ffw
+from allura.lib import exceptions as forge_exc
+from allura.lib import plugin
+from allura.controllers import BaseController
+from allura.lib.decorators import require_post
+from allura.tasks import export_tasks
+from allura.lib.widgets.project_list import ProjectScreenshots
+
+from . import widgets as aw
+
+
+log = logging.getLogger(__name__)
+
+
+class W:
+ markdown_editor = ffw.MarkdownEdit()
+ label_edit = ffw.LabelEdit()
+ mount_delete = ffw.Lightbox(name='mount_delete', trigger='a.mount_delete')
+ admin_modal = ffw.Lightbox(name='admin_modal', trigger='a.admin_modal')
+ install_modal = ffw.Lightbox(
+ name='install_modal', trigger='a.install_trig')
+ explain_export_modal = ffw.Lightbox(
+ name='explain_export', trigger='#why_export')
+ group_card = aw.GroupCard()
+ permission_card = aw.PermissionCard()
+ group_settings = aw.GroupSettings()
+ new_group_settings = aw.NewGroupSettings()
+ screenshot_admin = aw.ScreenshotAdmin()
+ screenshot_list = ProjectScreenshots(draggable=True)
+ metadata_admin = aw.MetadataAdmin()
+ audit = aw.AuditLog()
+ page_list = ffw.PageList()
+
+
+class AdminApp(Application):
+ '''This is the admin app. It is pretty much required for
+ a functioning allura project.
+ '''
+ __version__ = version.__version__
+ _installable_tools = None
+ max_instances = 0
+ tool_label = 'admin'
+ icons = {
+ 24: 'images/admin_24.png',
+ 32: 'images/admin_32.png',
+ 48: 'images/admin_48.png'
+ }
+ exportable = True
+
+ def __init__(self, project, config):
+ Application.__init__(self, project, config)
+ self.root = ProjectAdminController()
+ self.api_root = ProjectAdminRestController()
+ self.admin = AdminAppAdminController(self)
+ self.templates = pkg_resources.resource_filename(
+ 'allura.ext.admin', 'templates')
+ self.sitemap = [SitemapEntry('Admin', '.')]
+
+ def is_visible_to(self, user):
+ '''Whether the user can view the app.'''
+ return has_access(c.project, 'create')(user=user)
+
+ @staticmethod
+ def installable_tools_for(project):
+ tools = []
+ for name, App in g.entry_points['tool'].items():
+ cfg = M.AppConfig(project_id=project._id, tool_name=name)
+ app = App(project, cfg)
+ if app.installable:
+ tools.append(dict(name=name, app=App))
+ # prevent from saving temporary config to db
+ session(cfg).expunge(cfg)
+ tools.sort(key=lambda t: (t['app'].status_int(), t['app'].ordinal))
+ return [t for t in tools
+ if t['app'].status in project.allowed_tool_status]
+
+ @staticmethod
+ def exportable_tools_for(project):
+ tools = []
+ for tool in project.app_configs:
+ if project.app_instance(tool).exportable:
+ tools.append(tool)
+ return sorted(tools, key=lambda t: t.options.mount_point)
+
+ 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('Admin', '.')]
+
+ @h.exceptionless([], log)
+ def sidebar_menu(self):
+ links = []
+ admin_url = c.project.url() + 'admin/'
+
+ if c.project.is_nbhd_project:
+ links.append(SitemapEntry('Add Project', c.project.url()
+ + 'add_project', ui_icon=g.icons['plus']))
+ nbhd_admin_url = c.project.neighborhood.url() + '_admin/'
+ links = links + [
+ SitemapEntry('Neighborhood'),
+ SitemapEntry('Overview', nbhd_admin_url + 'overview'),
+ SitemapEntry('Awards', nbhd_admin_url + 'accolades')]
+ else:
+ links += [SitemapEntry('Metadata', admin_url + 'overview'), ]
+ if c.project.neighborhood.name != "Users":
+ links += [
+ SitemapEntry('Screenshots', admin_url + 'screenshots'),
+ SitemapEntry('Categorization', admin_url + 'trove')
+ ]
+ links.append(SitemapEntry('Tools', admin_url + 'tools'))
+ if asbool(config.get('bulk_export_enabled', True)):
+ links.append(SitemapEntry('Export', admin_url + 'export'))
+ if c.project.is_root and has_access(c.project, 'admin')():
+ links.append(
+ SitemapEntry('User Permissions', admin_url + 'groups/'))
+ if not c.project.is_root and has_access(c.project, 'admin')():
+ links.append(
+ SitemapEntry('Permissions', admin_url + 'permissions/'))
+ if len(c.project.neighborhood_invitations):
+ links.append(
+ SitemapEntry('Invitation(s)', admin_url + 'invitations'))
+ links.append(SitemapEntry('Audit Trail', admin_url + 'audit/'))
+ if c.project.is_nbhd_project:
+ links.append(SitemapEntry('Statistics', nbhd_admin_url + 'stats/'))
+ links.append(None)
+ links.append(SitemapEntry('Help', nbhd_admin_url + 'help/'))
+
+ for ep_name in sorted(g.entry_points['admin'].keys()):
+ admin_extension = g.entry_points['admin'][ep_name]
+ admin_extension().update_project_sidebar_menu(links)
+
+ return links
+
+ def admin_menu(self):
+ return []
+
+ def install(self, project):
+ pass
+
+ def bulk_export(self, f):
+ json.dump(self.project, f, cls=jsonify.GenericJSON, indent=2)
+
+
+class AdminExtensionLookup(object):
+ @expose()
+ def _lookup(self, name, *remainder):
+ for ep_name in sorted(g.entry_points['admin'].keys()):
+ admin_extension = g.entry_points['admin'][ep_name]
+ controller = admin_extension().project_admin_controllers.get(name)
+ if controller:
+ return controller(), remainder
+ raise exc.HTTPNotFound(name)
+
+
+class ProjectAdminController(BaseController):
+ def _check_security(self):
+ require_access(c.project, 'admin')
+
+ def __init__(self):
+ self.permissions = PermissionsController()
+ self.groups = GroupsController()
+ self.audit = AuditController()
+ self.ext = AdminExtensionLookup()
+
+ @with_trailing_slash
+ @expose('jinja:allura.ext.admin:templates/project_admin.html')
+ def index(self, **kw):
+ scm_tools = []
+ for tool in c.project.app_configs:
+ app = g.entry_points["tool"].get(tool.tool_name)
+ if app and issubclass(app, RepositoryApp):
+ scm_tools.append(tool)
+ return dict(scm_tools=scm_tools)
+
+ @without_trailing_slash
+ @expose('jinja:allura.ext.admin:templates/project_invitations.html')
+ def invitations(self):
+ return dict()
+
+ @without_trailing_slash
+ @expose('jinja:allura.ext.admin:templates/project_overview.html')
+ def overview(self, **kw):
+ c.markdown_editor = W.markdown_editor
+ c.metadata_admin = W.metadata_admin
+ c.explain_export_modal = W.explain_export_modal
+ show_export_control = asbool(config.get('show_export_control', False))
+ allow_project_delete = asbool(config.get('allow_project_delete', True))
+ explain_export_text = '''The purpose of this section is to determine whether your project is subject to the
+ provisions of the US Export Administration Regulations. You should consult section 734.4 and Supplement 2
+ to Part 734 for information on such items and the calculation of U.S. controlled content.
+ <a href="http://www.bis.doc.gov/encryption/default.htm" target="_blank">
+ http://www.bis.doc.gov/encryption/default.htm</a>'''
+ if 'us_export_contact' in config:
+ explain_export_text += \
+ 'If you have additional questions, ' \
+ 'please contact <a href="mailto:{contact}">{contact}</a>.'.format(contact=config['us_export_contact'])
+ return dict(show_export_control=show_export_control,
+ allow_project_delete=allow_project_delete,
+ explain_export_text=explain_export_text)
+
+ @without_trailing_slash
+ @expose('jinja:allura.ext.admin:templates/project_screenshots.html')
+ def screenshots(self, **kw):
+ c.screenshot_admin = W.screenshot_admin
+ c.screenshot_list = W.screenshot_list
+ return dict()
+
+ @without_trailing_slash
+ @expose('jinja:allura.ext.admin:templates/project_trove.html')
+ def trove(self):
+ c.label_edit = W.label_edit
+ base_troves = M.TroveCategory.query.find(
+ dict(trove_parent_id=0)).sort('fullname').all()
+ topic_trove = M.TroveCategory.query.get(
+ trove_parent_id=0, shortname='topic')
+ license_trove = M.TroveCategory.query.get(
+ trove_parent_id=0, shortname='license')
+ return dict(base_troves=base_troves, license_trove=license_trove, topic_trove=topic_trove)
+
+ @without_trailing_slash
+ @expose('jinja:allura.ext.admin:templates/project_tools.html')
+ def tools(self, page=None, limit=200, **kw):
+ c.markdown_editor = W.markdown_editor
+ c.label_edit = W.label_edit
+ c.mount_delete = W.mount_delete
+ c.admin_modal = W.admin_modal
+ c.install_modal = W.install_modal
+ c.page_list = W.page_list
+ mounts = c.project.ordered_mounts()
+ total_mounts = len(mounts)
+ limit, page = h.paging_sanitizer(limit, page or total_mounts / int(limit), total_mounts)
+ start = page * limit
+ return dict(
+ page=page,
+ limit=limit,
+ total_mounts=total_mounts,
+ mounts=mounts[start:start + limit],
+ installable_tools=AdminApp.installable_tools_for(c.project),
+ roles=M.ProjectRole.query.find(
+ dict(project_id=c.project.root_project._id)).sort('_id').all(),
+ categories=M.ProjectCategory.query.find(dict(parent_id=None)).sort('label').all())
+
+ @expose()
+ @require_post()
+ def configure_tool_grouping(self, grouping_threshold='1', page=0, limit=200, **kw):
+ try:
+ grouping_threshold = int(grouping_threshold)
+ if grouping_threshold < 1:
+ raise ValueError('Invalid threshold')
+ c.project.set_tool_data(
+ 'allura', grouping_threshold=grouping_threshold)
+ except ValueError:
+ flash('Invalid threshold', 'error')
+ redirect('tools?limit=%s&page=%s' % (limit, page))
+
+ @expose()
+ @require_post()
+ def update_labels(self, labels=None, **kw):
+ require_access(c.project, 'admin')
+ c.project.labels = labels.split(',')
+ M.AuditLog.log('updated labels')
+ redirect('trove')
+
+ @without_trailing_slash
+ @expose()
+ def clone(self,
+ repo_type=None, source_url=None,
+ mount_point=None, mount_label=None,
+ **kw):
+ require_access(c.project, 'admin')
+ if repo_type is None:
+ return (
+ '<form method="get">'
+ '<input name="repo_type" value="Git">'
+ '<input name="source_url">'
+ '<input type="submit">'
+ '</form>')
+ for ep in h.iter_entry_points('allura', repo_type):
+ break
+ if ep is None or source_url is None:
+ raise exc.HTTPNotFound
+ h.log_action(log, 'install tool').info(
+ 'clone repo from %s', source_url,
+ meta=dict(tool_type=repo_type, mount_point=mount_point, mount_label=mount_label))
+ c.project.install_app(
+ repo_type,
+ mount_point=mount_point,
+ mount_label=mount_label,
+ init_from_url=source_url)
+ M.AuditLog.log('Create repo as clone')
+ redirect('tools')
+
+ @without_trailing_slash
+ @expose('jinja:allura.ext.admin:templates/project_permissions.html')
+ def groups(self, **kw):
+ return dict()
+
+ @expose()
+ def _lookup(self, name, *remainder):
+ app = c.project.app_instance(name)
+ if app is None:
+ raise exc.HTTPNotFound(name)
+ return app.admin, remainder
+
+ @expose()
+ @require_post()
+ @validate(W.metadata_admin, error_handler=overview)
+ def update(self, name=None,
+ short_description=None,
+ summary='',
+ icon=None,
+ category=None,
+ external_homepage='',
+ video_url='',
+ support_page='',
+ support_page_url='',
+ twitter_handle='',
+ facebook_page='',
+ removal='',
+ moved_to_url='',
+ export_controlled=False,
+ export_control_type=None,
+ tracking_id='',
+ **kw):
+ require_access(c.project, 'update')
+
+ if removal != c.project.removal:
+ M.AuditLog.log('change project removal status to %s', removal)
+ h.log_action(log, 'change project removal status').info('')
+ c.project.removal = removal
+ c.project.removal_changed_date = datetime.utcnow()
+ if 'delete_icon' in kw:
+ M.ProjectFile.query.remove(
+ dict(project_id=c.project._id, category='icon'))
+ M.AuditLog.log('remove project icon')
+ h.log_action(log, 'remove project icon').info('')
+ g.post_event('project_updated')
+ redirect('overview')
+ elif 'delete' in kw:
+ allow_project_delete = asbool(
+ config.get('allow_project_delete', True))
+ if allow_project_delete or not c.project.is_root:
+ M.AuditLog.log('delete project')
+ h.log_action(log, 'delete project').info('')
+ plugin.ProjectRegistrationProvider.get().delete_project(
+ c.project, c.user)
+ redirect('overview')
+ elif 'undelete' in kw:
+ h.log_action(log, 'undelete project').info('')
+ M.AuditLog.log('undelete project')
+ plugin.ProjectRegistrationProvider.get().undelete_project(
+ c.project, c.user)
+ redirect('overview')
+ if name and name != c.project.name:
+ h.log_action(log, 'change project name').info('')
+ M.AuditLog.log('change project name to %s', name)
+ c.project.name = name
+ if short_description != c.project.short_description:
+ h.log_action(log, 'change project short description').info('')
+ M.AuditLog.log('change short description to %s', short_description)
+ c.project.short_description = short_description
+ if summary != c.project.summary:
+ h.log_action(log, 'change project summary').info('')
+ M.AuditLog.log('change summary to %s', summary)
+ c.project.summary = summary
+ category = category and ObjectId(category) or None
+ if category != c.project.category_id:
+ h.log_action(log, 'change project category').info('')
+ M.AuditLog.log('change category to %s', category)
+ c.project.category_id = category
+ if external_homepage != c.project.external_homepage:
+ h.log_action(log, 'change external home page').info('')
+ M.AuditLog.log('change external home page to %s',
+ external_homepage)
+ c.project.external_homepage = external_homepage
+ if video_url != c.project.video_url:
+ h.log_action(log, 'change video url').info('')
+ M.AuditLog.log('change video url to %s', video_url)
+ c.project.video_url = video_url
+ if support_page != c.project.support_page:
+ h.log_action(log, 'change project support page').info('')
+ M.AuditLog.log('change project support page to %s', support_page)
+ c.project.support_page = support_page
+ old_twitter = c.project.social_account('Twitter')
+ if not old_twitter or twitter_handle != old_twitter.accounturl:
+ h.log_action(log, 'change project twitter handle').info('')
+ M.AuditLog.log('change project twitter handle to %s',
+ twitter_handle)
+ c.project.set_social_account('Twitter', twitter_handle)
+ old_facebook = c.project.social_account('Facebook')
+ if not old_facebook or facebook_page != old_facebook.accounturl:
+ if not facebook_page or 'facebook.com' in urlparse(facebook_page).netloc:
+ h.log_action(log, 'change project facebook page').info('')
+ M.AuditLog.log(
+ 'change project facebook page to %s', facebook_page)
+ c.project.set_social_account('Facebook', facebook_page)
+ if support_page_url != c.project.support_page_url:
+ h.log_action(log, 'change project support page url').info('')
+ M.AuditLog.log('change project support page url to %s',
+ support_page_url)
+ c.project.support_page_url = support_page_url
+ if moved_to_url != c.project.moved_to_url:
+ h.log_action(log, 'change project moved to url').info('')
+ M.AuditLog.log('change project moved to url to %s', moved_to_url)
+ c.project.moved_to_url = moved_to_url
+ export_controlled = asbool(export_controlled)
+ if export_controlled != c.project.export_controlled:
+ h.log_action(
+ log, 'change project export controlled status').info('')
+ M.AuditLog.log(
+ 'change project export controlled status to %s', export_controlled)
+ c.project.export_controlled = export_controlled
+ if not export_controlled:
+ export_control_type = None
+ if export_control_type != c.project.export_control_type:
+ h.log_action(log, 'change project export control type').info('')
+ M.AuditLog.log('change project export control type to %s',
+ export_control_type)
+ c.project.export_control_type = export_control_type
+ if tracking_id != c.project.tracking_id:
+ h.log_action(log, 'change project tracking ID').info('')
+ M.AuditLog.log('change project tracking ID to %s', tracking_id)
+ c.project.tracking_id = tracking_id
+
+ if icon is not None and icon != '':
+ if c.project.icon:
+ M.ProjectFile.remove(
+ dict(project_id=c.project._id, category='icon'))
+ M.AuditLog.log('update project icon')
+ M.ProjectFile.save_image(
+ icon.filename, icon.file, content_type=icon.type,
+ square=True, thumbnail_size=(48, 48),
+ thumbnail_meta=dict(project_id=c.project._id, category='icon'))
+ g.post_event('project_updated')
+ flash('Saved', 'success')
+ redirect('overview')
+
+ def _add_trove(self, type, new_trove):
+ current_troves = getattr(c.project, 'trove_%s' % type)
+ trove_obj = M.TroveCategory.query.get(trove_cat_id=int(new_trove))
+ error_msg = None
+ if type in ['license', 'audience', 'developmentstatus', 'language'] and len(current_troves) >= 6:
+ error_msg = 'You may not have more than 6 of this category.'
+ elif type in ['topic'] and len(current_troves) >= 3:
+ error_msg = 'You may not have more than 3 of this category.'
+ elif trove_obj is not None:
+ if trove_obj._id not in current_troves:
+ current_troves.append(trove_obj._id)
+ M.AuditLog.log('add trove %s: %s', type, trove_obj.fullpath)
+ # just in case the event handling is super fast
+ ThreadLocalORMSession.flush_all()
+ c.project.last_updated = datetime.utcnow()
+ g.post_event('project_updated')
+ else:
+ error_msg = 'This category has already been assigned to the project.'
+ return (trove_obj, error_msg)
+
+ @expose('json:')
+ @require_post()
+ def add_trove_js(self, type, new_trove, **kw):
+ require_access(c.project, 'update')
+ trove_obj, error_msg = self._add_trove(type, new_trove)
+ return dict(trove_full_path=trove_obj.fullpath, trove_cat_id=trove_obj.trove_cat_id, error_msg=error_msg)
+
+ @expose()
+ @require_post()
+ def add_trove(self, type, new_trove, **kw):
+ require_access(c.project, 'update')
+ trove_obj, error_msg = self._add_trove(type, new_trove)
+ if error_msg:
+ flash(error_msg, 'error')
+ redirect('trove')
+
+ @expose()
+ @require_post()
+ def delete_trove(self, type, trove, **kw):
+ require_access(c.project, 'update')
+ trove_obj = M.TroveCategory.query.get(trove_cat_id=int(trove))
+ current_troves = getattr(c.project, 'trove_%s' % type)
+ if trove_obj is not None and trove_obj._id in current_troves:
+ M.AuditLog.log('remove trove %s: %s', type, trove_obj.fullpath)
+ current_troves.remove(trove_obj._id)
+ # just in case the event handling is super fast
+ ThreadLocalORMSession.flush_all()
+ c.project.last_updated = datetime.utcnow()
+ g.post_event('project_updated')
+ redirect('trove')
+
+ @expose()
+ @require_post()
+ @validate(W.screenshot_admin)
+ def add_screenshot(self, screenshot=None, caption=None, **kw):
+ require_access(c.project, 'update')
+ screenshots = c.project.get_screenshots()
+ if len(screenshots) >= 6:
+ flash('You may not have more than 6 screenshots per project.',
+ 'error')
+ elif screenshot is not None and screenshot != '':
+ M.AuditLog.log('add screenshot')
+ sort = 1 + max([ss.sort or 0 for ss in screenshots] or [0])
+ M.ProjectFile.save_image(
+ screenshot.filename, screenshot.file, content_type=screenshot.type,
+ save_original=True,
+ original_meta=dict(
+ project_id=c.project._id,
+ category='screenshot',
+ caption=caption,
+ sort=sort),
+ square=True, thumbnail_size=(150, 150),
+ thumbnail_meta=dict(project_id=c.project._id, category='screenshot_thumb'))
+ g.post_event('project_updated')
+ redirect('screenshots')
+
+ @expose()
+ @require_post()
+ def sort_screenshots(self, **kw):
+ """Sort project screenshots.
+
+ Called via ajax when screenshots are reordered via drag/drop on
+ the Screenshots admin page.
+
+ ``kw`` is a mapping of (screenshot._id, sort_order) pairs.
+
+ """
+ for s in c.project.get_screenshots():
+ if str(s._id) in kw:
+ s.sort = int(kw[str(s._id)])
+ g.post_event('project_updated')
+
+ @expose()
+ @require_post()
+ def delete_screenshot(self, id=None, **kw):
+ require_access(c.project, 'update')
+ if id is not None and id != '':
+ M.AuditLog.log('remove screenshot')
+ M.ProjectFile.query.remove(
+ dict(project_id=c.project._id, _id=ObjectId(id)))
+ g.post_event('project_updated')
+ redirect('screenshots')
+
+ @expose()
+ @require_post()
+ def edit_screenshot(self, id=None, caption=None, **kw):
+ require_access(c.project, 'update')
+ if id is not None and id != '':
+ M.ProjectFile.query.get(
+ project_id=c.project._id, _id=ObjectId(id)).caption = caption
+ g.post_event('project_updated')
+ redirect('screenshots')
+
+ @expose()
+ @require_post()
+ def join_neighborhood(self, nid):
+ require_access(c.project, 'admin')
+ if not nid:
+ n = M.Neighborhood.query.get(name='Projects')
+ c.project.neighborhood_id = n._id
+ flash('Joined %s' % n.name)
+ redirect(c.project.url() + 'admin/')
+ nid = ObjectId(str(nid))
+ if nid not in c.project.neighborhood_invitations:
+ flash('No invitation to that neighborhood', 'error')
+ redirect('.')
+ c.project.neighborhood_id = nid
+ n = M.Neighborhood.query.get(_id=nid)
+ flash('Joined %s' % n.name)
+ redirect('invitations')
+
+ @h.vardec
+ @expose()
+ @require_post()
+ def update_mount_order(self, subs=None, tools=None, **kw):
+ if subs:
+ for sp in subs:
+ p = M.Project.query.get(shortname=sp['shortname'],
+ neighborhood_id=c.project.neighborhood_id)
+ p.ordinal = int(sp['ordinal'])
+ if tools:
+ for p in tools:
+ c.project.app_config(
+ p['mount_point']).options.ordinal = int(p['ordinal'])
+ redirect('tools')
+
+ def _update_mounts(self, subproject=None, tool=None, new=None, **kw):
+ if subproject is None:
+ subproject = []
+ if tool is None:
+ tool = []
+ for sp in subproject:
+ p = M.Project.query.get(shortname=sp['shortname'],
+ neighborhood_id=c.project.neighborhood_id)
+ if sp.get('delete'):
+ require_access(c.project, 'admin')
+ M.AuditLog.log('delete subproject %s', sp['shortname'])
+ h.log_action(log, 'delete subproject').info(
+ 'delete subproject %s', sp['shortname'],
+ meta=dict(name=sp['shortname']))
+ p.removal = 'deleted'
+ plugin.ProjectRegistrationProvider.get().delete_project(
+ p, c.user)
+ elif not new:
+ M.AuditLog.log('update subproject %s', sp['shortname'])
+ p.name = sp['name']
+ p.ordinal = int(sp['ordinal'])
+ for p in tool:
+ if p.get('delete'):
+ require_access(c.project, 'admin')
+ M.AuditLog.log('uninstall tool %s', p['mount_point'])
+ h.log_action(log, 'uninstall tool').info(
+ 'uninstall tool %s', p['mount_point'],
+ meta=dict(mount_point=p['mount_point']))
+ c.project.uninstall_app(p['mount_point'])
+ elif not new:
+ M.AuditLog.log('update tool %s', p['mount_point'])
+ options = c.project.app_config(p['mount_point']).options
+ options.mount_label = p['mount_label']
+ options.ordinal = int(p['ordinal'])
+ if new and new.get('install'):
+ ep_name = new.get('ep_name', None)
+ if not ep_name:
+ require_access(c.project, 'create')
+ mount_point = new['mount_point'].lower() or h.nonce()
+ M.AuditLog.log('create subproject %s', mount_point)
+ h.log_action(log, 'create subproject').info(
+ 'create subproject %s', mount_point,
+ meta=dict(mount_point=mount_point, name=new['mount_label']))
+ sp = c.project.new_subproject(mount_point)
+ sp.name = new['mount_label']
+ sp.ordinal = int(new['ordinal'])
+ else:
+ require_access(c.project, 'admin')
+ installable_tools = AdminApp.installable_tools_for(c.project)
+ if not ep_name.lower() in [t['name'].lower() for t in installable_tools]:
+ flash('Installation limit exceeded.', 'error')
+ return
+ mount_point = new['mount_point'] or ep_name
+ M.AuditLog.log('install tool %s', mount_point)
+ h.log_action(log, 'install tool').info(
+ 'install tool %s', mount_point,
+ meta=dict(tool_type=ep_name, mount_point=mount_point, mount_label=new['mount_label']))
+ return c.project.install_app(
+ ep_name, mount_point, mount_label=new['mount_label'], ordinal=new['ordinal'])
+ g.post_event('project_updated')
+
+ @h.vardec
+ @expose()
+ @require_post()
+ def update_mounts(self, subproject=None, tool=None, new=None, page=0, limit=200, **kw):
+ try:
+ new_app = self._update_mounts(subproject, tool, new, **kw)
+ if new_app:
+ # force redir to last page of tools, where new app will be
+ page = ''
+ except forge_exc.ForgeError as exc:
+ flash('%s: %s' % (exc.__class__.__name__, exc.args[0]),
+ 'error')
+ redirect('tools?limit=%s&page=%s' % (limit, page))
+
+ @expose('jinja:allura.ext.admin:templates/export.html')
+ def export(self, tools=None):
+ if not asbool(config.get('bulk_export_enabled', True)):
+ raise exc.HTTPNotFound()
+ if request.method == 'POST':
+ try:
+ ProjectAdminRestController().export(tools, send_email=True)
+ except (exc.HTTPBadRequest, exc.HTTPServiceUnavailable) as e:
+ flash(str(e), 'error')
+ redirect('.')
+ else:
+ flash(
+ 'Export scheduled. You will recieve an email with download instructions when complete.', 'ok')
+ redirect('export')
+
+ exportable_tools = AdminApp.exportable_tools_for(c.project)
+ return {
+ 'tools': exportable_tools,
+ 'status': c.project.bulk_export_status()
+ }
+
+
+class ProjectAdminRestController(BaseController):
+ """
+ Exposes RESTful APi for project admin actions.
+ """
+
+ def _check_security(self):
+ require_access(c.project, 'admin')
+
+ @expose('json:')
+ @require_post()
+ def export(self, tools=None, send_email=False, **kw):
+ """
+ Initiate a bulk export of the project data.
+
+ Must be given a list of tool mount points to include in the export.
+ The list can either be comma-separated or a repeated param, e.g.,
+ `export?tools=tickets&tools=discussion`.
+
+ If the tools are not provided, an invalid mount point is listed, or
+ there is some other problems with the arguments, a `400 Bad Request`
+ response will be returned.
+
+ If an export is already currently running for this project, a
+ `503 Unavailable` response will be returned.
+
+ Otherwise, a JSON object of the form
+ `{"status": "in progress", "filename": FILENAME}` will be returned,
+ where `FILENAME` is the filename of the export artifact relative to
+ the users shell account directory.
+ """
+ if not asbool(config.get('bulk_export_enabled', True)):
+ raise exc.HTTPNotFound()
+ if not tools:
+ raise exc.HTTPBadRequest(
+ 'Must give at least one tool mount point to export')
+ tools = aslist(tools, ',')
+ exportable_tools = AdminApp.exportable_tools_for(c.project)
+ allowed = set(t.options.mount_point for t in exportable_tools)
+ if not set(tools).issubset(allowed):
+ raise exc.HTTPBadRequest('Invalid tool')
+ if c.project.bulk_export_status() == 'busy':
+ raise exc.HTTPServiceUnavailable(
+ 'Export for project %s already running' % c.project.shortname)
+ # filename (potentially) includes a timestamp, so we have
+ # to pre-generate to be able to return it to the user
+ filename = c.project.bulk_export_filename()
+ export_tasks.bulk_export.post(tools, filename, send_email=send_email)
+ return {
+ 'status': 'in progress',
+ 'filename': filename,
+ }
+
+ @expose('json:')
+ def export_status(self, **kw):
+ """
+ Check the status of a bulk export.
+
+ Returns an object containing only one key, `status`, whose value is
+ either `'busy'` or `'ready'`.
+ """
+ status = c.project.bulk_export_status()
+ return {'status': status or 'ready'}
+
+ @expose('json:')
+ @require_post()
+ def install_tool(self, tool=None, mount_point=None, mount_label=None, order=None, **kw):
+ """API for installing tools in current project.
+
+ Requires a valid tool, mount point and mount label names.
+ (All arguments are required.)
+
+ Usage example::
+
+ POST to:
+ /rest/p/testproject/admin/install_tool/
+
+ with params:
+ {
+ 'tool': 'tickets',
+ 'mount_point': 'mountpoint',
+ 'mount_label': 'mountlabel',
+ 'order': 'first|last|alpha_tool'
+ }
+
+ Example output (in successful case)::
+
+ {
+ "info": "Tool tickets with mount_point mountpoint and mount_label mountlabel was created.",
+ "success": true
+ }
+
+ """
+ controller = ProjectAdminController()
+
+ if not tool or not mount_point or not mount_label:
+ return {
+ 'success': False,
+ 'info': 'All arguments required.'
+ }
+ installable_tools = AdminApp.installable_tools_for(c.project)
+ tools_names = [t['name'] for t in installable_tools]
+ if not (tool in tools_names):
+ return {
+ 'success': False,
+ 'info': 'Incorrect tool name, or limit is reached.'
+ }
+ if c.project.app_instance(mount_point) is not None:
+ return {
+ 'success': False,
+ 'info': 'Mount point already exists.',
+ }
+
+ if order is None:
+ order = 'last'
+ mounts = [{'ordinal': ac.options.ordinal,
+ 'label': ac.options.mount_label,
+ 'mount': ac.options.mount_point,
+ 'type': ac.tool_name.lower()}
+ for ac in c.project.app_configs]
+ subs = {p.shortname: p for p in M.Project.query.find({'parent_id': c.project._id})}
+ for sub in list(subs.values()):
+ mounts.append({'ordinal': sub.ordinal,
+ 'mount': sub.shortname,
+ 'type': 'sub-project'})
+ mounts.sort(key=itemgetter('ordinal'))
+ if order == 'first':
+ ordinal = 0
+ elif order == 'last':
+ ordinal = len(mounts)
+ elif order == 'alpha_tool':
+ tool = tool.lower()
+ for i, mount in enumerate(mounts):
+ if mount['type'] == tool and mount['label'] > mount_label:
+ ordinal = i
+ break
+ else:
+ ordinal = len(mounts)
+ mounts.insert(ordinal, {'ordinal': ordinal, 'type': 'new'})
+ for i, mount in enumerate(mounts):
+ if mount['type'] == 'new':
+ pass
+ elif mount['type'] == 'sub-project':
+ subs[mount['mount']].ordinal = i
+ else:
+ c.project.app_config(mount['mount']).options.ordinal = i
+
+ data = {
+ 'install': 'install',
+ 'ep_name': tool,
+ 'ordinal': ordinal,
+ 'mount_point': mount_point,
+ 'mount_label': mount_label
+ }
+ try:
+ controller._update_mounts(new=data)
+ except forge_exc.ForgeError as e:
+ return {
+ 'success': False,
+ 'info': str(e),
+ }
+ return {
+ 'success': True,
+ 'info': 'Tool %s with mount_point %s and mount_label %s was created.'
+ % (tool, mount_point, mount_label)
+ }
+
+ @expose()
+ def _lookup(self, *args):
+ if len(args) == 0:
+ raise exc.HTTPNotFound(args)
+ name, remainder = args[0], args[1:]
+ app = c.project.app_instance(name)
+ if app is None or app.admin_api_root is None:
+ raise exc.HTTPNotFound(name)
+ return app.admin_api_root, remainder
+
+
+class PermissionsController(BaseController):
+ def _check_security(self):
+ require_access(c.project, 'admin')
+
+ @with_trailing_slash
+ @expose('jinja:allura.ext.admin:templates/project_permissions.html')
+ def index(self, **kw):
+ c.card = W.permission_card
+ return dict(permissions=self._index_permissions())
+
+ @without_trailing_slash
+ @expose()
+ @h.vardec
+ @require_post()
+ def update(self, card=None, **kw):
+ permissions = self._index_permissions()
+ old_permissions = dict(permissions)
+ for args in card:
+ perm = args['id']
+ new_group_ids = args.get('new', [])
+ group_ids = args.get('value', [])
+ if isinstance(new_group_ids, str):
+ new_group_ids = [new_group_ids]
+ if isinstance(group_ids, str):
+ group_ids = [group_ids]
+ # make sure the admin group has the admin permission
+ if perm == 'admin':
+ if c.project.is_root:
+ pid = c.project._id
+ else:
+ pid = c.project.parent_id
+ admin_group_id = str(
+ M.ProjectRole.query.get(project_id=pid, name='Admin')._id)
+ if admin_group_id not in group_ids + new_group_ids:
+ flash(
+ 'You cannot remove the admin group from the admin permission.', 'warning')
+ group_ids.append(admin_group_id)
+ permissions[perm] = []
+ role_ids = list(map(ObjectId, group_ids + new_group_ids))
+ permissions[perm] = role_ids
+ c.project.acl = []
+ for perm, role_ids in permissions.items():
+ role_names = lambda ids: ','.join(sorted(
+ pr.name for pr in M.ProjectRole.query.find(dict(_id={'$in': ids}))))
+ old_role_ids = old_permissions.get(perm, [])
+ if old_role_ids != role_ids:
+ M.AuditLog.log('updated "%s" permissions: "%s" => "%s"',
+ perm, role_names(old_role_ids), role_names(role_ids))
+ c.project.acl += [M.ACE.allow(rid, perm) for rid in role_ids]
+ g.post_event('project_updated')
+ redirect('.')
+
+ def _index_permissions(self):
+ permissions = dict(
+ (p, []) for p in c.project.permissions)
+ for ace in c.project.acl:
+ if ace.access == M.ACE.ALLOW:
+ permissions[ace.permission].append(ace.role_id)
+ return permissions
+
+
+class GroupsController(BaseController):
+ def _check_security(self):
+ require_access(c.project, 'admin')
+
+ def _index_permissions(self):
+ permissions = dict(
+ (p, []) for p in c.project.permissions)
+ for ace in c.project.acl:
+ if ace.access == M.ACE.ALLOW:
+ permissions[ace.permission].append(ace.role_id)
+ return permissions
+
+ def _map_group_permissions(self):
+ roles = c.project.named_roles
+ permissions = self._index_permissions()
+ permissions_by_role = dict()
+ auth_role = M.ProjectRole.authenticated()
+ anon_role = M.ProjectRole.anonymous()
+ for role in roles + [auth_role, anon_role]:
+ permissions_by_role[str(role._id)] = []
+ for perm in permissions:
+ perm_info = dict(has="no", text="Does not have permission %s" %
+ perm, name=perm)
+ role_ids = permissions[perm]
+ if role._id in role_ids:
+ perm_info['text'] = "Has permission %s" % perm
+ perm_info['has'] = "yes"
+ else:
+ for r in role.child_roles():
+ if r._id in role_ids:
+ perm_info['text'] = "Inherited permission %s from %s" % (
+ perm, r.name)
+ perm_info['has'] = "inherit"
+ break
+ if perm_info['has'] == "no":
+ if anon_role._id in role_ids:
+ perm_info[
+ 'text'] = "Inherited permission %s from Anonymous" % perm
+ perm_info['has'] = "inherit"
+ elif auth_role._id in role_ids and role != anon_role:
+ perm_info[
+ 'text'] = "Inherited permission %s from Authenticated" % perm
+ perm_info['has'] = "inherit"
+ permissions_by_role[str(role._id)].append(perm_info)
+ return permissions_by_role
+
+ @without_trailing_slash
+ @expose()
+ @h.vardec
+ def delete_group(self, group_name, **kw):
+ role = M.ProjectRole.by_name(group_name)
+ if not role:
+ flash('Group "%s" does not exist.' % group_name, 'error')
+ else:
+ role.delete()
+ M.AuditLog.log('delete group %s', group_name)
+ flash('Group "%s" deleted successfully.' % group_name)
+ g.post_event('project_updated')
+ redirect('.')
+
+ @with_trailing_slash
+ @expose('jinja:allura.ext.admin:templates/project_groups.html')
+ def index(self, **kw):
+ c.admin_modal = W.admin_modal
+ c.card = W.group_card
+ permissions_by_role = self._map_group_permissions()
+ auth_role = M.ProjectRole.authenticated()
+ anon_role = M.ProjectRole.anonymous()
+ roles = c.project.named_roles
+ roles.append(None)
+ return dict(roles=roles, permissions_by_role=permissions_by_role,
+ auth_role=auth_role, anon_role=anon_role)
+
+ @without_trailing_slash
+ @expose('json:')
+ @require_post()
+ @h.vardec
+ def change_perm(self, role_id, permission, allow="true", **kw):
+ if allow == "true":
+ M.AuditLog.log('granted permission %s to group %s', permission,
+ M.ProjectRole.query.get(_id=ObjectId(role_id)).name)
+ c.project.acl.append(M.ACE.allow(ObjectId(role_id), permission))
+ else:
+ admin_group_id = str(M.ProjectRole.by_name('Admin')._id)
+ if admin_group_id == role_id and permission == 'admin':
+ return dict(error='You cannot remove the admin permission from the admin group.')
+ M.AuditLog.log('revoked permission %s from group %s', permission,
+ M.ProjectRole.query.get(_id=ObjectId(role_id)).name)
+ c.project.acl.remove(M.ACE.allow(ObjectId(role_id), permission))
+ g.post_event('project_updated')
+ return self._map_group_permissions()
+
+ @without_trailing_slash
+ @expose('json:')
+ @require_post()
+ @h.vardec
+ def add_user(self, role_id, username, **kw):
+ if not username or username == '*anonymous':
+ return dict(error='You must choose a user to add.')
+ group = M.ProjectRole.query.get(_id=ObjectId(role_id))
+ user = M.User.by_username(username.strip())
+ if not group:
+ return dict(error='Could not find group with id %s' % role_id)
+ if not user:
+ return dict(error='User %s not found' % username)
+ user_role = M.ProjectRole.by_user(user, upsert=True)
+ if group._id in user_role.roles:
+ return dict(error='%s (%s) is already in the group %s.' % (user.display_name, username, group.name))
+ M.AuditLog.log('add user %s to %s', username, group.name)
+ user_role.roles.append(group._id)
+ if group.name == 'Admin':
+ for ac in c.project.app_configs:
+ c.project.app_instance(ac).subscribe(user)
+ g.post_event('project_updated')
+ return dict(username=username, displayname=user.display_name)
+
+ @without_trailing_slash
+ @expose('json:')
+ @require_post()
+ @h.vardec
+ def remove_user(self, role_id, username, **kw):
+ group = M.ProjectRole.query.get(_id=ObjectId(role_id))
+ user = M.User.by_username(username.strip())
+ if group.name == 'Admin' and len(group.users_with_role()) == 1:
+ return dict(error='You must have at least one user with the Admin role.')
+ if not group:
+ return dict(error='Could not find group with id %s' % role_id)
+ if not user:
+ return dict(error='User %s not found' % username)
+ user_role = M.ProjectRole.by_user(user)
+ if not user_role or group._id not in user_role.roles:
+ return dict(error='%s (%s) is not in the group %s.' % (user.display_name, username, group.name))
+ M.AuditLog.log('remove user %s from %s', username, group.name)
+ user_role.roles.remove(group._id)
+ g.post_event('project_updated')
+ return dict()
+
+ @without_trailing_slash
+ @expose()
+ @require_post()
+ @h.vardec
+ def update(self, card=None, **kw):
+ for pr in card:
+ group = M.ProjectRole.query.get(_id=ObjectId(pr['id']))
+ assert group.project == c.project, 'Security violation'
+ user_ids = pr.get('value', [])
+ new_users = pr.get('new', [])
+ if isinstance(user_ids, str):
+ user_ids = [user_ids]
+ if isinstance(new_users, str):
+ new_users = [new_users]
+ # Handle new users in groups
+ user_added = False
+ for username in new_users:
+ user = M.User.by_username(username.strip())
+ if not user:
+ flash('User %s not found' % username, 'error')
+ redirect('.')
+ if not user._id:
+ continue # never add anon users to groups
+ M.AuditLog.log('add user %s to %s', username, group.name)
+ M.ProjectRole.by_user(
+ user, upsert=True).roles.append(group._id)
+ user_added = True
+ # Make sure we aren't removing all users from the Admin group
+ if group.name == 'Admin' and not (user_ids or user_added):
+ flash('You must have at least one user with the Admin role.',
+ 'warning')
+ redirect('.')
+ # Handle users removed from groups
+ user_ids = set(
+ uid and ObjectId(uid)
+ for uid in user_ids)
+ for role in M.ProjectRole.query.find(dict(user_id={'$ne': None}, roles=group._id)):
+ if role.user_id and role.user_id not in user_ids:
+ role.roles = [
+ rid for rid in role.roles if rid != group._id]
+ M.AuditLog.log('remove user %s from %s',
+ role.user.username, group.name)
+ g.post_event('project_updated')
+ redirect('.')
+
+ @without_trailing_slash
+ @expose('jinja:allura.ext.admin:templates/project_group.html')
+ def new(self):
+ c.form = W.new_group_settings
+ return dict(
+ group=None,
+ show_settings=True,
+ action="create")
+
+ @expose()
+ @require_post()
+ @validate(W.new_group_settings)
+ @h.vardec
+ def create(self, name=None, **kw):
+ if M.ProjectRole.by_name(name):
+ flash('%s already exists' % name, 'error')
+ else:
+ M.ProjectRole(project_id=c.project._id, name=name)
+ M.AuditLog.log('create group %s', name)
+ g.post_event('project_updated')
+ redirect('.')
+
+ @expose()
+ def _lookup(self, name, *remainder):
+ return GroupController(name), remainder
+
+
+class GroupController(BaseController):
+ def __init__(self, name):
+ self._group = M.ProjectRole.query.get(_id=ObjectId(name))
+
+ @with_trailing_slash
+ @expose('jinja:allura.ext.admin:templates/project_group.html')
+ def index(self):
+ if self._group.name in ('Admin', 'Developer', 'Member'):
+ show_settings = False
+ action = None
+ else:
+ show_settings = True
+ action = self._group.settings_href + 'update'
+ c.form = W.group_settings
+ return dict(
+ group=self._group,
+ show_settings=show_settings,
+ action=action)
+
+ @expose()
+ @h.vardec
+ @require_post()
+ @validate(W.group_settings)
+ def update(self, _id=None, delete=None, name=None, **kw):
+ pr = M.ProjectRole.by_name(name)
+ if pr and pr._id != _id._id:
+ flash('%s already exists' % name, 'error')
+ redirect('..')
+ if delete:
+ _id.delete()
+ M.AuditLog.log('delete group %s', _id.name)
+ flash('%s deleted' % name)
+ redirect('..')
+ M.AuditLog.log('update group name %s=>%s', _id.name, name)
+ _id.name = name
+ flash('%s updated' % name)
+ redirect('..')
+
+
+class AuditController(BaseController):
+ @with_trailing_slash
+ @expose('jinja:allura.ext.admin:templates/audit.html')
+ def index(self, limit=25, page=0, **kwargs):
+ limit = int(limit)
+ page = int(page)
+ count = M.AuditLog.query.find(dict(project_id=c.project._id)).count()
+ q = M.AuditLog.query.find(dict(project_id=c.project._id))
+ q = q.sort('timestamp', -1)
+ q = q.skip(page * limit)
+ if count > limit:
+ q = q.limit(limit)
+ else:
+ limit = count
+ c.widget = W.audit
+ return dict(
+ entries=q.all(),
+ limit=limit,
+ page=page,
+ count=count)
+
+
+class AdminAppAdminController(DefaultAdminController):
+ '''Administer the admin app'''
+ pass
http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/ext/admin/templates/__init__.py
----------------------------------------------------------------------
diff --git a/ext/admin/templates/__init__.py b/ext/admin/templates/__init__.py
new file mode 100644
index 0000000..144e298
--- /dev/null
+++ b/ext/admin/templates/__init__.py
@@ -0,0 +1,16 @@
+# 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.
http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/ext/admin/templates/widgets/__init__.py
----------------------------------------------------------------------
diff --git a/ext/admin/templates/widgets/__init__.py b/ext/admin/templates/widgets/__init__.py
new file mode 100644
index 0000000..144e298
--- /dev/null
+++ b/ext/admin/templates/widgets/__init__.py
@@ -0,0 +1,16 @@
+# 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.
http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/ext/admin/widgets.py
----------------------------------------------------------------------
diff --git a/ext/admin/widgets.py b/ext/admin/widgets.py
new file mode 100644
index 0000000..068e203
--- /dev/null
+++ b/ext/admin/widgets.py
@@ -0,0 +1,247 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
+# 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.
+
+from pylons import tmpl_context as c
+
+import ew as ew_core
+from ew import jinja2_ew as ew
+import formencode
+from formencode import validators as fev
+
+from allura import model as M
+from allura.lib import validators as V
+from allura.lib.widgets import forms as ff
+from allura.lib.widgets import form_fields as ffw
+
+from bson import ObjectId
+
+
+class CardField(ew._Jinja2Widget):
+ template = 'jinja:allura.ext.admin:templates/admin_widgets/card_field.html'
+ sort_key = None
+ defaults = dict(
+ ew_core.Widget.defaults,
+ id=None,
+ name='Deck',
+ icon_name='group',
+ items=None,
+ roles=[],
+ settings_href=None)
+
+ def item_display(self, item):
+ return repr(item)
+
+ def item_id(self, item):
+ return repr(item)
+
+ def resources(self):
+ yield ew.CSSScript('''.deck li input, .deck li select {
+margin: 2px 0 2px 3px;
+width: 148px;
+}''')
+ yield ew.JSScript('''$(function() {
+ $('.active-card').each(function() {
+ var newitem = $('.new-item', this);
+ var adder = $('.adder', this);
+ var deleters = $('.deleter', this);
+ newitem.remove();
+ newitem.removeClass('new-item');
+ deleters.click(function(evt) {
+ evt.stopPropagation();
+ evt.preventDefault();
+ var $this = $(this);
+ $this.closest('li').remove();
+ });
+ adder.click(function(evt) {
+ evt.stopPropagation();
+ evt.preventDefault();
+ newitem.clone().insertBefore(adder.closest('li'));
+ });
+ });
+});''')
+
+
+class GroupCard(CardField):
+ new_item = ew.InputField(
+ field_type='text', attrs=dict(placeholder='type a username'))
+ sort_key = 'user.username'
+
+ def item_display(self, item):
+ return item.user.username
+
+ def item_id(self, item):
+ return item.user._id
+
+ def role_name(self, role_id):
+ return M.ProjectRole.query.get(_id=ObjectId(role_id)).name
+
+
+class _GroupSelect(ew.SingleSelectField):
+
+ def options(self):
+ auth_role = M.ProjectRole.authenticated()
+ anon_role = M.ProjectRole.anonymous()
+ options = [
+ ew.Option(py_value=role._id, label=role.name)
+ for role in c.project.named_roles]
+ options.append(ew.Option(py_value=auth_role._id, label=auth_role.name))
+ options.append(ew.Option(py_value=anon_role._id, label=anon_role.name))
+ return options
+
+
+class PermissionCard(CardField):
+ new_item = _GroupSelect()
+ sort_key = 'name'
+
+ def item_display(self, role):
+ return role.name
+
+ def item_id(self, role):
+ return role._id
+
+
+class GroupSettings(ff.CsrfForm):
+ submit_text = None
+
+ @property
+ def hidden_fields(self):
+ f = super(GroupSettings, self).hidden_fields
+ f.append(ew.HiddenField(name='_id', validator=V.Ming(M.ProjectRole)))
+ return f
+
+ class fields(ew_core.NameList):
+ name = ew.InputField(label='Name')
+
+ class buttons(ew_core.NameList):
+ save = ew.SubmitButton(label='Save')
+ delete = ew.SubmitButton(label='Delete Group')
+
+
+class NewGroupSettings(ff.AdminForm):
+ submit_text = 'Save'
+
+ class fields(ew_core.NameList):
+ name = ew.InputField(label='Name')
+
+
+class ScreenshotAdmin(ff.ForgeForm):
+ defaults = dict(
+ ff.ForgeForm.defaults,
+ enctype='multipart/form-data',
+ submit_text='Upload',
+ )
+
+ @property
+ def fields(self):
+ fields = [
+ ew.InputField(name='screenshot', field_type='file',
+ label='New Screenshot'),
+ ew.InputField(name='caption', field_type="text", label='Caption')
+ ]
+ return fields
+
+
+class MetadataAdmin(ff.AdminForm):
+ template = 'jinja:allura.ext.admin:templates/admin_widgets/metadata_admin.html'
+ defaults = dict(
+ ff.AdminForm.defaults,
+ show_export_control=False,
+ enctype='multipart/form-data')
+
+ class fields(ew_core.NameList):
+ name = ew.InputField(field_type='text',
+ label='Name',
+ validator=formencode.All(
+ fev.UnicodeString(not_empty=True, max=40),
+ V.MaxBytesValidator(max=40)),
+ attrs=dict(maxlength=40,
+ title="This is the publicly viewable name of the project, and will appear on project listings. It should be what you want to see as the project title in search listing."))
+ summary = ew.InputField(field_type="text", label='Short Summary',
+ validator=formencode.All(
+ fev.UnicodeString(max=70),
+ V.MaxBytesValidator(max=70)),
+ attrs=dict(maxlength=70))
+ short_description = ew.TextArea(label='Full Description',
+ validator=formencode.All(
+ fev.UnicodeString(max=1000),
+ V.MaxBytesValidator(max=1000)),
+ attrs=dict(title="Add a few paragraphs describing your project to new users."))
+ icon = ew.FileField(label='Icon')
+ external_homepage = ew.InputField(field_type="text", label='Homepage',
+ validator=fev.URL(add_http=True))
+ video_url = ew.InputField(field_type="text", label="Video (YouTube)",
+ validator=V.YouTubeConverter())
+ support_page = ew.InputField(field_type="text", label='Support Page')
+ support_page_url = ew.InputField(
+ field_type="text", label='Support Page URL',
+ validator=fev.URL(add_http=True, if_empty=''))
+ removal = ew.InputField(field_type="text", label='Removal')
+ moved_to_url = ew.InputField(
+ field_type="text", label='Moved Project to URL',
+ validator=fev.URL(add_http=True, if_empty=''))
+ export_controlled = ew.InputField(
+ field_type="text", label='Export Control')
+ export_control_type = ew.InputField(
+ field_type="text", label='Export Control Type')
+ delete = ew.InputField(field_type="hidden", label='Delete')
+ delete_icon = ew.InputField(field_type="hidden", label='Delete Icon')
+ undelete = ew.InputField(field_type="hidden", label='Undelete')
+ tracking_id = ew.InputField(
+ field_type="text", label="Analytics Tracking ID")
+ twitter_handle = ew.InputField(
+ field_type="text", label='Twitter Handle')
+ facebook_page = ew.InputField(field_type="text", label='Facebook page',
+ validator=fev.URL(add_http=True))
+
+
+class AuditLog(ew_core.Widget):
+ template = 'jinja:allura.ext.admin:templates/widgets/audit.html'
+ defaults = dict(
+ ew_core.Widget.defaults,
+ entries=None,
+ limit=None,
+ page=0,
+ count=0)
+
+ class fields(ew_core.NameList):
+ page_list = ffw.PageList()
+ page_size = ffw.PageSize()
+
+ def resources(self):
+ for f in self.fields:
+ for r in f.resources():
+ yield r
+
+
+class BlockUser(ffw.Lightbox):
+ defaults = dict(
+ ffw.Lightbox.defaults,
+ name='block-user-modal',
+ trigger='a.block-user',
+ content_template='allura.ext.admin:templates/widgets/block_user.html')
+
+
+class BlockList(ffw.Lightbox):
+ defaults = dict(
+ ffw.Lightbox.defaults,
+ name='block-list-modal',
+ trigger='a.block-list',
+ content_template='allura.ext.admin:templates/widgets/block_list.html')
http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/ext/project_home/__init__.py
----------------------------------------------------------------------
diff --git a/ext/project_home/__init__.py b/ext/project_home/__init__.py
new file mode 100644
index 0000000..d523d65
--- /dev/null
+++ b/ext/project_home/__init__.py
@@ -0,0 +1,22 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
+# 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.
+
+from .project_main import ProjectHomeApp
http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/ext/project_home/project_main.py
----------------------------------------------------------------------
diff --git a/ext/project_home/project_main.py b/ext/project_home/project_main.py
new file mode 100644
index 0000000..cb28555
--- /dev/null
+++ b/ext/project_home/project_main.py
@@ -0,0 +1,95 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
+# 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.
+
+import logging
+
+import pkg_resources
+from pylons import tmpl_context as c
+from tg import expose, redirect
+from tg.decorators import with_trailing_slash
+
+from allura import version
+from allura.app import Application, SitemapEntry
+from allura.lib import helpers as h
+from allura.controllers import BaseController
+from allura import model
+
+
+log = logging.getLogger(__name__)
+
+
+class ProjectHomeApp(Application):
+ __version__ = version.__version__
+ tool_label = 'home'
+ default_mount_label = 'Project Home'
+ max_instances = 0
+ icons = {
+ 24: 'images/home_24.png',
+ 32: 'images/home_32.png',
+ 48: 'images/home_48.png'
+ }
+
+ def __init__(self, project, config):
+ Application.__init__(self, project, config)
+ self.root = ProjectHomeController()
+ self.templates = pkg_resources.resource_filename(
+ 'allura.ext.project_home', 'templates')
+
+ def is_visible_to(self, user):
+ '''Whether the user can view the app.'''
+ return True
+
+ 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):
+ return [
+ SitemapEntry('Home', '..')]
+
+ @h.exceptionless([], log)
+ def sidebar_menu(self):
+ return []
+
+ def admin_menu(self):
+ return []
+
+ def install(self, project):
+ super(ProjectHomeApp, self).install(project)
+ pr = model.ProjectRole.by_user(c.user)
+ if pr:
+ self.config.acl = [
+ model.ACE.allow(pr._id, perm)
+ for perm in self.permissions]
+
+
+class ProjectHomeController(BaseController):
+
+ @with_trailing_slash
+ @expose()
+ def index(self, **kw):
+ redirect('..')
http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/ext/search/__init__.py
----------------------------------------------------------------------
diff --git a/ext/search/__init__.py b/ext/search/__init__.py
new file mode 100644
index 0000000..2c6e656
--- /dev/null
+++ b/ext/search/__init__.py
@@ -0,0 +1,22 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
+# 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.
+
+from .search_main import SearchApp
http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/ext/search/search_main.py
----------------------------------------------------------------------
diff --git a/ext/search/search_main.py b/ext/search/search_main.py
new file mode 100644
index 0000000..7678128
--- /dev/null
+++ b/ext/search/search_main.py
@@ -0,0 +1,98 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
+# 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.
+
+import logging
+
+import pkg_resources
+from pylons import tmpl_context as c
+from tg import expose, validate
+from tg.decorators import with_trailing_slash
+from formencode import validators as V
+
+from allura.app import Application
+from allura import version
+from allura.lib.search import search_app
+from allura.lib.widgets.search import SearchResults, SearchHelp
+from allura.controllers import BaseController
+
+log = logging.getLogger(__name__)
+
+
+class SearchApp(Application):
+
+ '''This is the HelloWorld application for Allura, showing
+ all the rich, creamy goodness that is installable apps.
+ '''
+ __version__ = version.__version__
+ max_instances = 0
+ hidden = True
+ sitemap = []
+
+ def __init__(self, project, config):
+ Application.__init__(self, project, config)
+ self.root = SearchController()
+ self.templates = pkg_resources.resource_filename(
+ 'allura.ext.search', 'templates')
+
+ def main_menu(self): # pragma no cover
+ return []
+
+ def sidebar_menu(self): # pragma no cover
+ return []
+
+ def admin_menu(self): # pragma no cover
+ return []
+
+ def install(self, project):
+ pass # pragma no cover
+
+ def uninstall(self, project):
+ pass # pragma no cover
+
+
+class SearchController(BaseController):
+
+ @expose('jinja:allura:templates/search_index.html')
+ @validate(dict(q=V.UnicodeString(),
+ history=V.StringBool(if_empty=False)))
+ @with_trailing_slash
+ def index(self, q=None, history=None, **kw):
+ c.search_results = SearchResults()
+ c.help_modal = SearchHelp(comments=False)
+ pids = [c.project._id] + [
+ p._id for p in c.project.subprojects]
+ project_match = ' OR '.join(
+ 'project_id_s:%s' % pid
+ for pid in pids)
+ search_params = kw
+ search_params.update({
+ 'q': q,
+ 'history': history,
+ 'app': False,
+ 'fq': [
+ 'project_id_s:(%s)' % project_match,
+ '-deleted_b:true',
+ ],
+ })
+ d = search_app(**search_params)
+ d['search_comments_disable'] = True
+ d['hide_app_project_switcher'] = True
+ return d
http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/ext/user_profile/__init__.py
----------------------------------------------------------------------
diff --git a/ext/user_profile/__init__.py b/ext/user_profile/__init__.py
new file mode 100644
index 0000000..9569e6c
--- /dev/null
+++ b/ext/user_profile/__init__.py
@@ -0,0 +1,22 @@
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import unicode_literals
+# 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.
+
+from .user_main import UserProfileApp, ProfileSectionBase
http://git-wip-us.apache.org/repos/asf/allura/blob/d52f8e2a/ext/user_profile/templates/__init__.py
----------------------------------------------------------------------
diff --git a/ext/user_profile/templates/__init__.py b/ext/user_profile/templates/__init__.py
new file mode 100644
index 0000000..144e298
--- /dev/null
+++ b/ext/user_profile/templates/__init__.py
@@ -0,0 +1,16 @@
+# 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.