예제 #1
0
 def add_nav_mapping(cls, name, title, **kwargs):
     """Create a top level nav item."""
     menu_item = cls.root_menu_group.get_child(name)
     if menu_item is None:
         is_link = kwargs.get('href')
         menu_cls = menus.MenuItem if is_link else menus.MenuGroup
         menu_item = menu_cls(name,
                              title,
                              group=cls.root_menu_group,
                              **kwargs)
         if not is_link:
             # create the basic buckets
             pinned = menus.MenuGroup('pinned',
                                      None,
                                      placement=1000,
                                      group=menu_item)
             default = menus.MenuGroup('default',
                                       None,
                                       placement=2000,
                                       group=menu_item)
             advanced = menus.MenuGroup(
                 'advanced',
                 None,
                 placement=menus.MenuGroup.DEFAULT_PLACEMENT * 2,
                 group=menu_item)
     return menu_item
예제 #2
0
 def test_ordering_unordered_sort_stable(self):
     group = menus.MenuGroup('menu', 'Menu')
     menus.MenuItem('a', 'A', group=group, can_view=can_view)
     menus.MenuItem('b', 'B', group=group, can_view=can_view)
     menus.MenuItem('c', 'C', group=group, can_view=can_view)
     menus.MenuItem('d', 'D', group=group, can_view=can_view)
     self._assert_name_order(group, ['a', 'b', 'c', 'd'])
예제 #3
0
 def test_explicit_placements(self):
     group = menus.MenuGroup('menu', 'Menu')
     menus.MenuItem('a', 'A', group=group, can_view=can_view, placement=5000)
     menus.MenuItem('b', 'B', group=group, can_view=can_view, placement=3000)
     menus.MenuItem('c', 'C', group=group, can_view=can_view, placement=1000)
     menus.MenuItem('d', 'D', group=group, can_view=can_view, placement=4000)
     menus.MenuItem('e', 'E', group=group, can_view=can_view, placement=2000)
     self._assert_name_order(group, ['c', 'e', 'b', 'd', 'a'])
예제 #4
0
 def test_force_multiple_first(self):
     group = menus.MenuGroup('menu', 'Menu')
     menus.MenuItem('a', 'A', group=group, can_view=can_view, placement=0)
     menus.MenuItem('b', 'B', group=group, can_view=can_view)
     menus.MenuItem('c', 'C', group=group, can_view=can_view)
     menus.MenuItem('d', 'D', group=group, can_view=can_view)
     menus.MenuItem('e', 'E', group=group, can_view=can_view, placement=0)
     self._assert_name_order(group, ['a', 'e', 'b', 'c', 'd'])
예제 #5
0
 def test_force_last(self):
     group = menus.MenuGroup('menu', 'Menu')
     menus.MenuItem('e', 'E', group=group, can_view=can_view,
         placement=float('inf'))
     menus.MenuItem('a', 'A', group=group, can_view=can_view)
     menus.MenuItem('b', 'B', group=group, can_view=can_view)
     menus.MenuItem('c', 'C', group=group, can_view=can_view)
     menus.MenuItem('d', 'D', group=group, can_view=can_view)
     self._assert_name_order(group, ['a', 'b', 'c', 'd', 'e'])
예제 #6
0
    def make_site_menu(cls, root_menu_group, placement):

        group = menus.MenuGroup(
            'admin', 'Site admin', group=root_menu_group, placement=placement)

        def bind(key, label, handler, href=None):
            if href:
                target = '_blank'
            else:
                target = None
                href = "{}?action={}".format(cls.LINK_URL, key)

            def can_view(app_context):
                return can_view_admin_action(key)

            menu_item = menus.MenuItem(
                key, label, action=key, can_view=can_view, group=group,
                href=href, target=target)

            if handler:
                cls.get_actions.append(key)
                cls.actions_to_menu_items[key] = menu_item

        bind('courses', 'Courses', cls.get_courses)
        bind('settings', 'Site settings', cls.get_settings)
        bind('perf', 'Metrics', cls.get_perf)
        bind('deployment', 'Deployment', cls.get_deployment)

        if DIRECT_CODE_EXECUTION_UI_ENABLED:
            bind('console', 'Console', cls.get_console)

        if appengine_config.gcb_appstats_enabled():
            bind('stats', 'Appstats', None, href='/admin/stats/')

        if appengine_config.PRODUCTION_MODE:
            app_id = app.get_application_id()
            href = (
                'https://appengine.google.com/'
                'dashboard?app_id=s~%s' % app_id)
            bind('gae', 'Google App Engine', None, href=href)
        else:
            bind(
                 'gae', 'Google App Engine', None,
                 href='http://localhost:8000/')
        bind('welcome', 'Welcome', None, href='/admin/welcome')
        bind(
             'help', 'Site help', None,
             href='https://code.google.com/p/course-builder/wiki/AdminPage')
        bind(
             'news', 'News', None,
             href=(
                'https://groups.google.com/forum/'
                '?fromgroups#!forum/course-builder-announce'))
    def setUp(self):
        super(DashboardAccessTestCase, self).setUp()
        actions.login(self.ADMIN_EMAIL, is_admin=True)

        context = actions.simple_add_course(self.ACCESS_COURSE_NAME,
                                            self.ADMIN_EMAIL,
                                            'Course with access')

        self.course_with_access = courses.Course(None, context)

        with utils.Namespace(self.course_with_access.app_context.namespace):
            role_dto = models.RoleDTO(
                None, {
                    'name': self.ROLE,
                    'users': [self.USER_EMAIL],
                    'permissions': {
                        dashboard.custom_module.name: [self.PERMISSION]
                    }
                })
            models.RoleDAO.save(role_dto)

        context = actions.simple_add_course(self.NO_ACCESS_COURSE_NAME,
                                            self.ADMIN_EMAIL,
                                            'Course with no access')

        self.course_without_access = courses.Course(None, context)

        def test_content(self):
            return self.render_page({
                'main_content': 'test',
                'page_title': 'test'
            })

        # save properties
        self.old_menu_group = dashboard.DashboardHandler.root_menu_group
        # pylint: disable=W0212
        self.old_get_acitons = dashboard.DashboardHandler._custom_get_actions
        # pylint: enable=W0212

        # put a dummy method in
        menu_group = menus.MenuGroup('test', 'Test Dashboard')
        dashboard.DashboardHandler.root_menu_group = menu_group
        dashboard.DashboardHandler.default_action = self.ACTION
        dashboard.DashboardHandler.add_nav_mapping(self.ACTION, self.ACTION)
        dashboard.DashboardHandler.add_sub_nav_mapping(self.ACTION,
                                                       self.ACTION,
                                                       self.ACTION,
                                                       action=self.ACTION,
                                                       contents=test_content)
        dashboard.DashboardHandler.map_get_action_to_permission(
            self.ACTION, dashboard.custom_module, self.PERMISSION)
        actions.logout()
예제 #8
0
def make_help_menu(root_group):
    anyone_can_view = lambda x: True

    group = menus.MenuGroup('help', 'Help', group=root_group, placement=6000)

    menus.MenuItem(
        'documentation',
        'Documentation',
        href='https://www.google.com/edu/openonline/tech/index.html',
        can_view=anyone_can_view,
        group=group,
        placement=1000,
        target='_blank')

    menus.MenuItem(
        'videos',
        'Demo videos',
        href='https://www.youtube.com/playlist?list=PLFB_aGY5EfxeltJfJZwkjqDLAW'
        'dMfSpES',
        can_view=anyone_can_view,
        group=group,
        placement=2000,
        target='_blank')

    menus.MenuItem('showcase',
                   'Showcase courses',
                   href='https://www.google.com/edu/openonline/index.html',
                   can_view=anyone_can_view,
                   group=group,
                   placement=3000,
                   target='_blank')

    menus.MenuItem(
        'forum',
        'Support forum',
        href=('https://groups.google.com/forum/?fromgroups#!categories/'
              'course-builder-forum/general-troubleshooting'),
        can_view=anyone_can_view,
        group=group,
        placement=4000,
        target='_blank')
예제 #9
0
class DashboardHandler(
    CourseHandler, FileManagerAndEditor,
    LabelManagerAndEditor, TrackManagerAndEditor, QuestionGroupManagerAndEditor,
    QuestionManagerAndEditor, ReflectiveRequestHandler, RoleManagerAndEditor):
    """Handles all pages and actions required for managing a course."""

    # This dictionary allows the dashboard module to optionally nominate a
    # specific sub-tab within each major tab group as the default sub-tab to
    # open when first navigating to that major tab.  The default may be
    # explicitly specified here so that sub-tab registrations from other
    # modules do not inadvertently take over the first position due to order
    # of module registration.
    default_subtab_action = collections.defaultdict(lambda: None)
    get_actions = [
        'edit_settings', 'edit_unit_lesson',
        'manage_asset', 'manage_text_asset',
        'add_mc_question', 'add_sa_question',
        'edit_question', 'add_question_group', 'edit_question_group',
        'question_preview', 'question_group_preview',
        'add_label', 'edit_label', 'add_track', 'edit_track',
        'add_role', 'edit_role',
        'import_gift_questions']
    # Requests to these handlers automatically go through an XSRF token check
    # that is implemented in ReflectiveRequestHandler.
    post_actions = [
        'create_or_edit_settings',
        'add_to_question_group',
        'clone_question']
    child_routes = [
            (AssetItemRESTHandler.URI, AssetItemRESTHandler),
            (FilesItemRESTHandler.URI, FilesItemRESTHandler),
            (LabelRestHandler.URI, LabelRestHandler),
            (TrackRestHandler.URI, TrackRestHandler),
            (McQuestionRESTHandler.URI, McQuestionRESTHandler),
            (GiftQuestionRESTHandler.URI, GiftQuestionRESTHandler),
            (SaQuestionRESTHandler.URI, SaQuestionRESTHandler),
            (GeneralQuestionRESTHandler.URI, GeneralQuestionRESTHandler),
            (TextAssetRESTHandler.URI, TextAssetRESTHandler),
            (QuestionGroupRESTHandler.URI, QuestionGroupRESTHandler),
            (RoleRESTHandler.URI, RoleRESTHandler)]

    # List of functions which are used to generate content displayed at the top
    # of every dashboard page. Use this with caution, as it is extremely
    # invasive of the UX. Each function receives the handler as arg and returns
    # an object to be inserted into a Jinja template (e.g. a string, a safe_dom
    # Node or NodeList, or a jinja2.Markup).
    PAGE_HEADER_HOOKS = []

    # A list of hrefs for extra CSS files to be included in dashboard pages.
    # Files listed here by URL will be available on every Dashboard page.
    EXTRA_CSS_HREF_LIST = []

    # A list of hrefs for extra JS files to be included in dashboard pages.
    # Files listed here by URL will be available on every Dashboard page.
    EXTRA_JS_HREF_LIST = []

    # A list of template locations to be included in dashboard pages
    ADDITIONAL_DIRS = []

    # Dictionary that maps external permissions to their descriptions
    _external_permissions = {}
    # Dictionary that maps actions to permissions
    _get_action_to_permission = {}
    _post_action_to_permission = {}

    default_action = None
    GetAction = collections.namedtuple('GetAction', ['handler', 'in_action'])
    _custom_get_actions = {}  # Map of name to GetAction
    _custom_post_actions = {}  # Map of name to handler callback.

    # Create top level menu groups which other modules can register against.
    # I would do this in "register", but other modules register first.
    actions_to_menu_items = {}
    root_menu_group = menus.MenuGroup('dashboard', 'Dashboard')

    @classmethod
    def add_nav_mapping(cls, name, title, **kwargs):
        """Create a top level nav item."""
        menu_item = cls.root_menu_group.get_child(name)
        if menu_item is None:
            is_link = kwargs.get('href')
            menu_cls = menus.MenuItem if is_link else menus.MenuGroup
            menu_item = menu_cls(
                name, title, group=cls.root_menu_group, **kwargs)
            if not is_link:
                # create the basic buckets
                pinned = menus.MenuGroup(
                    'pinned', None, placement=1000, group=menu_item)
                default = menus.MenuGroup(
                    'default', None, placement=2000, group=menu_item)
                advanced = menus.MenuGroup(
                    'advanced', None,
                    placement=menus.MenuGroup.DEFAULT_PLACEMENT * 2,
                    group=menu_item)
        return menu_item

    @classmethod
    def get_nav_title(cls, action):
        item = cls.actions_to_menu_items.get(action)
        if item:
            return item.group.group.title + " > " + item.title
        else:
            return None

    @classmethod
    def add_sub_nav_mapping(
            cls, group_name, item_name, title, action=None, contents=None,
            can_view=None, href=None, no_app_context=False,
            sub_group_name=None, **kwargs):
        """Create a second level nav item.

        Args:
            group_name: Name of an existing top level nav item to use as the
                parent
            item_name: A unique key for this item
            title: Human-readable label
            action: A unique operation ID for
            contents: A handler which will be added as a custom get-action on
                DashboardHandler
            can_view: Pass a boolean function here if your handler has
                additional permissions logic in it that the dashboard does not
                check for you.  You must additionally check it in your handler.
            sub_group_name: The sub groups 'pinned', 'default', and 'advanced'
                exist in that order and 'default' is used by default.  You can
                pass some other string to create a new group at the end.
            other arguments: see common/menus.py
        """

        group = cls.root_menu_group.get_child(group_name)
        if group is None:
            logging.critical('The group %s does not exist', group_name)
            return

        if sub_group_name is None:
            sub_group_name = 'default'

        sub_group = group.get_child(sub_group_name)
        if not sub_group:
            sub_group = menus.MenuGroup(
                sub_group_name, None, group=group)

        item = sub_group.get_child(item_name)
        if item:
            logging.critical(
                'There is already a sub-menu item named "%s" registered in '
                'group %s subgroup %s.', item_name, group_name, sub_group_name)
            return

        if contents:
            action = action or group_name + '_' + item_name

        if action and not href:
            href = "dashboard?action={}".format(action)

        def combined_can_view(app_context):
            if action:
                # Current design disallows actions at the global level.
                # This might change in the future.
                if not app_context and not no_app_context:
                    return False

                # Check permissions in the dashboard
                if not cls.can_view(action):
                    return False

            # Additional custom visibility check
            if can_view and not can_view(app_context):
                return False

            return True

        item = menus.MenuItem(
            item_name, title, action=action, group=sub_group,
            can_view=combined_can_view, href=href, **kwargs)
        cls.actions_to_menu_items[action] = item

        if contents:
            cls.add_custom_get_action(action, handler=contents)

    @classmethod
    def add_custom_get_action(cls, action, handler=None, in_action=None,
                              overwrite=False):
        if not action:
            logging.critical('Action not specified. Ignoring.')
            return False

        if not handler:
            logging.critical(
                'For action : %s handler can not be null.', action)
            return False

        if ((action in cls._custom_get_actions or action in cls.get_actions)
            and not overwrite):
            logging.critical(
                'action : %s already exists. Ignoring the custom get action.',
                action)
            return False

        cls._custom_get_actions[action] = cls.GetAction(handler, in_action)
        return True

    @classmethod
    def remove_custom_get_action(cls, action):
        if action in cls._custom_get_actions:
            cls._custom_get_actions.pop(action)

    @classmethod
    def add_custom_post_action(cls, action, handler, overwrite=False):
        if not handler or not action:
            logging.critical('Action or handler can not be null.')
            return False

        if ((action in cls._custom_post_actions or action in cls.post_actions)
            and not overwrite):
            logging.critical(
                'action : %s already exists. Ignoring the custom post action.',
                action)
            return False

        cls._custom_post_actions[action] = handler
        return True

    @classmethod
    def remove_custom_post_action(cls, action):
        if action in cls._custom_post_actions:
            cls._custom_post_actions.pop(action)

    @classmethod
    def get_child_routes(cls):
        """Add child handlers for REST."""
        return cls.child_routes

    @classmethod
    def can_view(cls, action):
        """Checks if current user has viewing rights."""
        app_context = sites.get_app_context_for_current_request()
        if action in cls._get_action_to_permission:
            return cls._get_action_to_permission[action](app_context)
        return roles.Roles.is_course_admin(app_context)

    @classmethod
    def can_edit(cls, action):
        """Checks if current user has editing rights."""
        app_context = sites.get_app_context_for_current_request()
        if action in cls._post_action_to_permission:
            return cls._post_action_to_permission[action](app_context)
        return roles.Roles.is_course_admin(app_context)

    def default_action_for_current_permissions(self):
        """Set the default or first active navigation tab as default action."""
        item = self.root_menu_group.first_visible_item(self.app_context)
        if item:
            return item.action

    def get(self):
        """Enforces rights to all GET operations."""
        action = self.request.get('action')
        print("in get")
        if not action:
            self.default_action = self.default_action_for_current_permissions()
            action = self.default_action
        self.action = action

        if not self.can_view(action):
            self.redirect(self.app_context.get_slug())
            return

        if action in self._custom_get_actions:
            result = self._custom_get_actions[action].handler(self)
            if result is None:
                return

            # The following code handles pages for actions that do not write out
            # their responses.

            template_values = {
                'page_title': self.format_title(self.get_nav_title(action)),
            }
            if isinstance(result, dict):
                template_values.update(result)
            else:
                template_values['main_content'] = result

            self.render_page(template_values)
            return


        # Force reload of properties. It is expensive, but admin deserves it!
        config.Registry.get_overrides(force_update=True)
        return super(DashboardHandler, self).get()

    def post(self):
        """Enforces rights to all POST operations."""
        action = self.request.get('action')
        print(action);
        self.action = action
        if not self.can_edit(action):
            self.redirect(self.app_context.get_slug())
            return
        if action in self._custom_post_actions:
            # Each POST request must have valid XSRF token.
            xsrf_token = self.request.get('xsrf_token')
            if not crypto.XsrfTokenManager.is_xsrf_token_valid(
                xsrf_token, action):
                self.error(403)
                return
            self._custom_post_actions[action](self)
            return

        return super(DashboardHandler, self).post()

    def get_template(self, template_name, dirs=None):
        """Sets up an environment and Gets jinja template."""
        return jinja_utils.get_template(
            template_name, (dirs or []) + [TEMPLATE_DIR], handler=self)

    def get_alerts(self):
        alerts = []
        if not self.app_context.is_editable_fs():
            alerts.append('Read-only course.')
        if not self.app_context.now_available:
            alerts.append('The course is not publicly available.')
        return '\n'.join(alerts)

    def _get_current_menu_action(self):
        registered_action = self._custom_get_actions.get(self.action)
        if registered_action:
            registered_in_action = registered_action.in_action
            if registered_in_action:
                return registered_in_action

        return self.action

    def render_page(self, template_values, in_action=None):
        """Renders a page using provided template values."""
        template_values['header_title'] = template_values['page_title']
        template_values['page_headers'] = [
            hook(self) for hook in self.PAGE_HEADER_HOOKS]
        template_values['course_title'] = self.app_context.get_title()

        current_action = in_action or self._get_current_menu_action()
        template_values['current_menu_item'] = self.actions_to_menu_items.get(
            current_action)
        template_values['courses_menu_item'] = self.actions_to_menu_items.get(
            'courses')
        template_values['root_menu_group'] = self.root_menu_group

        template_values['course_app_contexts'] = get_visible_courses()
        template_values['app_context'] = self.app_context
        template_values['current_course'] = self.get_course()
        template_values['gcb_course_base'] = self.get_base_href(self)
        template_values['user_nav'] = safe_dom.NodeList().append(
            safe_dom.Text('%s | ' % users.get_current_user().email())
        ).append(
            safe_dom.Element(
                'a', href=users.create_logout_url(self.request.uri)
            ).add_text('Logout'))
        template_values[
            'page_footer'] = 'Page created on: %s' % datetime.datetime.now()
        template_values['coursebuilder_version'] = (
            os.environ['GCB_PRODUCT_VERSION'])
        template_values['application_id'] = app_identity.get_application_id()
        template_values['application_version'] = (
            os.environ['CURRENT_VERSION_ID'])
        template_values['extra_css_href_list'] = self.EXTRA_CSS_HREF_LIST
        template_values['extra_js_href_list'] = self.EXTRA_JS_HREF_LIST
        template_values['powered_by_url'] = services.help_urls.get(
            'dashboard:powered_by')
        if not template_values.get('sections'):
            template_values['sections'] = []
        if not appengine_config.PRODUCTION_MODE:
            template_values['page_uuid'] = str(uuid.uuid1())
            
        self.response.write(
            self.get_template('view.html').render(template_values))

    @classmethod
    def register_courses_menu_item(cls, menu_item):
        cls.actions_to_menu_items['courses'] = menu_item

    def format_title(self, text):
        """Formats standard title with or without course picker."""
        ret = safe_dom.NodeList()
        cb_text = 'Course Builder '
        ret.append(safe_dom.Text(cb_text))
        ret.append(safe_dom.Entity('>'))
        ret.append(safe_dom.Text(' %s ' % self.app_context.get_title()))
        ret.append(safe_dom.Entity('>'))
        dashboard_text = ' Dashboard '
        ret.append(safe_dom.Text(dashboard_text))
        ret.append(safe_dom.Entity('>'))
        ret.append(safe_dom.Text(' %s' % text))
        return ret

    def get_action_url(self, action, key=None, extra_args=None, fragment=None):
        args = {'action': action}
       
        if key:
            args['key'] = key
        if extra_args:
            args.update(extra_args)
        url = '/dashboard?%s' % urllib.urlencode(args)
        
        if fragment:
            url += '#' + fragment
           
        return self.canonicalize_url(url)

    def _render_roles_list(self):
        """Render roles list to HTML."""
        all_roles = sorted(RoleDAO.get_all(), key=lambda role: role.name)
        return safe_dom.Template(
            self.get_template('role_list.html'), roles=all_roles)

    def _render_roles_view(self):
        """Renders course roles view."""
        actions = [{
            'id': 'add_role',
            'caption': 'Add Role',
            'href': self.get_action_url('add_role')}]
        sections = [{
                'description': messages.ROLES_DESCRIPTION,
                'actions': actions,
                'pre': self._render_roles_list()
        }]
        template_values = {
            'page_title': self.format_title('Roles'),
            'sections': sections,
        }
        return template_values

    @classmethod
    def map_get_action_to_permission(cls, action, module, perm):
        """Maps a view/get action to a permission.

        Map a GET action that goes through the dashboard to a
        permission to control which users have access.

        Example:
            The i18n module maps multiple actions to the permission
            'access_i18n_dashboard'.  Users who have a role assigned with this
            permission are then allowed to perform these actions and thus
            access the translation tools.

        Args:
            action: a string specifying the action to map.
            module: The module with which the permission was registered via
                a call to models.roles.Roles.register_permission()
            permission: a string specifying the permission to which the action
                should be mapped.
        """
        checker = lambda ctx: roles.Roles.is_user_allowed(ctx, module, perm)
        cls.map_get_action_to_permission_checker(action, checker)

    @classmethod
    def map_get_action_to_permission_checker(cls, action, checker):
        """Map an action to a function to check permissions.

        Some actions (notably settings and the course overview) produce pages
        that have items that may be controlled by multiple permissions or
        more complex verification than a single permission allows.  This
        function allows modules to specify check functions.

        Args:
          action: A string specifying the name of the action being checked.
              This should have been registered via add_custom_get_action(),
              or present in the 'get_actions' list above in this file.
          checker: A function which is run when the named action is accessed.
              Registered functions should expect one parameter: the application
              context object, and return a Boolean value.
        """
        cls._get_action_to_permission[action] = checker

    @classmethod
    def unmap_get_action_to_permission(cls, action):
        del cls._get_action_to_permission[action]

    @classmethod
    def map_post_action_to_permission(cls, action, module, perm):
        """Maps an edit action to a permission. (See 'get' version, above.)"""
        checker = lambda ctx: roles.Roles.is_user_allowed(ctx, module, perm)
        cls.map_post_action_to_permission_checker(action, checker)

    @classmethod
    def map_post_action_to_permission_checker(cls, action, checker):
        """Map an edit action to check function.  (See 'get' version, above)."""
        cls._post_action_to_permission[action] = checker

    @classmethod
    def unmap_post_action_to_permission(cls, action):
        """Remove mapping to edit action.  (See 'get' version, above)."""
        del cls._post_action_to_permission[action]

    @classmethod
    def deprecated_add_external_permission(cls, permission_name,
                                           permission_description):
        """Adds extra permissions that will be registered by the Dashboard.

        Normally, permissions should be registered in their own modules.
        Due to historical accident, the I18N module registers permissions
        with the dashboard.  For backward compatibility with existing roles,
        this API is preserved, but not suggested for use by future modules.
        """
        cls._external_permissions[permission_name] = permission_description

    @classmethod
    def remove_external_permission(cls, permission_name):
        del cls._external_permissions[permission_name]

    @classmethod
    def permissions_callback(cls, unused_app_context):
        return cls._external_permissions.iteritems()

    @classmethod
    def current_user_has_access(cls, app_context):
        return cls.root_menu_group.can_view(app_context, exclude_links=True)

    @classmethod
    def generate_dashboard_link(cls, app_context):
        if cls.current_user_has_access(app_context):
            return [('dashboard', 'Dashboard')]
        return []
예제 #10
0
    def add_sub_nav_mapping(
            cls, group_name, item_name, title, action=None, contents=None,
            can_view=None, href=None, no_app_context=False,
            sub_group_name=None, **kwargs):
        """Create a second level nav item.

        Args:
            group_name: Name of an existing top level nav item to use as the
                parent
            item_name: A unique key for this item
            title: Human-readable label
            action: A unique operation ID for
            contents: A handler which will be added as a custom get-action on
                DashboardHandler
            can_view: Pass a boolean function here if your handler has
                additional permissions logic in it that the dashboard does not
                check for you.  You must additionally check it in your handler.
            sub_group_name: The sub groups 'pinned', 'default', and 'advanced'
                exist in that order and 'default' is used by default.  You can
                pass some other string to create a new group at the end.
            other arguments: see common/menus.py
        """

        group = cls.root_menu_group.get_child(group_name)
        if group is None:
            logging.critical('The group %s does not exist', group_name)
            return

        if sub_group_name is None:
            sub_group_name = 'default'

        sub_group = group.get_child(sub_group_name)
        if not sub_group:
            sub_group = menus.MenuGroup(
                sub_group_name, None, group=group)

        item = sub_group.get_child(item_name)
        if item:
            logging.critical(
                'There is already a sub-menu item named "%s" registered in '
                'group %s subgroup %s.', item_name, group_name, sub_group_name)
            return

        if contents:
            action = action or group_name + '_' + item_name

        if action and not href:
            href = "dashboard?action={}".format(action)

        def combined_can_view(app_context):
            if action:
                # Current design disallows actions at the global level.
                # This might change in the future.
                if not app_context and not no_app_context:
                    return False

                # Check permissions in the dashboard
                if not cls.can_view(action):
                    return False

            # Additional custom visibility check
            if can_view and not can_view(app_context):
                return False

            return True

        item = menus.MenuItem(
            item_name, title, action=action, group=sub_group,
            can_view=combined_can_view, href=href, **kwargs)
        cls.actions_to_menu_items[action] = item

        if contents:
            cls.add_custom_get_action(action, handler=contents)
예제 #11
0
class GlobalAdminHandler(
        BaseAdminHandler, ApplicationHandler, ReflectiveRequestHandler):
    """Handler to present admin settings in global context."""

    # The binding URL for this handler
    URL = '/admin/global'

    # The URL used in relative addreses of this handler
    LINK_URL = '/admin/global'

    # List of functions which are used to generate content displayed at the top
    # of every dashboard page. Use this with caution, as it is extremely
    # invasive of the UX. Each function receives the handler as arg and returns
    # an object to be inserted into a Jinja template (e.g. a string, a safe_dom
    # Node or NodeList, or a jinja2.Markup).
    PAGE_HEADER_HOOKS = []

    # Isolate this class's actions list from its parents
    get_actions = []
    post_actions = []

    actions_to_menu_items = {}
    root_menu_group = menus.MenuGroup('admin', 'Global Admin')

    def format_title(self, text):
        return 'Course Builder > Admin > %s' % text

    @classmethod
    def make_menu(cls):
        cls.make_site_menu(cls.root_menu_group, placement=1000)
        dashboard.make_help_menu(cls.root_menu_group)

    @classmethod
    def disable(cls):
        super(GlobalAdminHandler, cls).disable()
        cls.root_menu_group.remove_all()
        cls.actions_to_menu_items = {}

    def get(self):
        action = self.request.get('action')

        if action:
            destination = '%s?action=%s' % (self.LINK_URL, action)
        else:
            destination = self.LINK_URL

        user = users.get_current_user()
        if not user:
            self.redirect(users.create_login_url(destination), normalize=False)
            return
        if not can_view_admin_action(action):
            if appengine_config.PRODUCTION_MODE:
                self.error(403)
            else:
                self.redirect(
                    users.create_login_url(destination), normalize=False)
            return

        # Force reload of properties. It's expensive, but admin deserves it!
        config.Registry.get_overrides(force_update=True)

        super(GlobalAdminHandler, self).get()

    def post(self):
        if not self.can_edit():
            self.redirect('/', normalize=False)
            return
        return super(GlobalAdminHandler, self).post()

    def get_template(self, template_name, dirs):
        """Sets up an environment and Gets jinja template."""
        dashboard_template_dir = os.path.join(
            appengine_config.BUNDLE_ROOT, 'modules', 'dashboard')
        return jinja_utils.get_template(
            template_name, dirs + [dashboard_template_dir], handler=self)

    def render_page(self, template_values, in_action=None):
        page_title = template_values['page_title']
        template_values['header_title'] = page_title
        template_values['page_headers'] = [
            hook(self) for hook in self.PAGE_HEADER_HOOKS]
        template_values['breadcrumbs'] = page_title

        current_action = (in_action or self.request.get('action')
            or self.default_action_for_current_permissions())
        current_menu_item = self.actions_to_menu_items.get(current_action)
        template_values['root_menu_group'] = self.root_menu_group
        template_values['current_menu_item'] = current_menu_item
        template_values['is_global_admin'] = True
        template_values['course_app_contexts'] = dashboard.get_visible_courses()

        template_values['gcb_course_base'] = '/'
        template_values['user_nav'] = safe_dom.NodeList().append(
            safe_dom.Text('%s | ' % users.get_current_user().email())
        ).append(
            safe_dom.Element(
                'a', href=users.create_logout_url(self.request.uri)
            ).add_text('Logout'))
        template_values[
            'page_footer'] = 'Page created on: %s' % datetime.datetime.now()
        template_values['coursebuilder_version'] = (
            os.environ['GCB_PRODUCT_VERSION'])
        template_values['application_id'] = app.get_application_id()
        template_values['application_version'] = (
            os.environ['CURRENT_VERSION_ID'])
        if not template_values.get('sections'):
            template_values['sections'] = []

        self.response.write(
            self.get_template('view.html', []).render(template_values))
예제 #12
0
class DashboardHandler(CourseHandler, FileManagerAndEditor,
                       LabelManagerAndEditor, QuestionGroupManagerAndEditor,
                       QuestionManagerAndEditor, ReflectiveRequestHandler,
                       RoleManagerAndEditor, UnitLessonEditor):
    """Handles all pages and actions required for managing a course."""

    # This dictionary allows the dashboard module to optionally nominate a
    # specific sub-tab within each major tab group as the default sub-tab to
    # open when first navigating to that major tab.  The default may be
    # explicitly specified here so that sub-tab registrations from other
    # modules do not inadvertently take over the first position due to order
    # of module registration.
    default_subtab_action = collections.defaultdict(lambda: None)
    get_actions = [
        'edit_settings', 'edit_unit_lesson', 'edit_unit', 'edit_link',
        'edit_lesson', 'edit_assessment', 'manage_asset', 'manage_text_asset',
        'import_course', 'add_mc_question', 'add_sa_question', 'edit_question',
        'add_question_group', 'edit_question_group', 'add_label', 'edit_label',
        'question_preview', 'add_role', 'edit_role', 'edit_custom_unit',
        'import_gift_questions', 'in_place_lesson_editor'
    ]
    # Requests to these handlers automatically go through an XSRF token check
    # that is implemented in ReflectiveRequestHandler.
    post_actions = [
        'create_or_edit_settings', 'add_unit', 'add_link', 'add_assessment',
        'add_lesson', 'set_draft_status', 'add_to_question_group',
        'clone_question', 'add_custom_unit'
    ]
    child_routes = [(AssessmentRESTHandler.URI, AssessmentRESTHandler),
                    (AssetItemRESTHandler.URI, AssetItemRESTHandler),
                    (FilesItemRESTHandler.URI, FilesItemRESTHandler),
                    (ImportCourseRESTHandler.URI, ImportCourseRESTHandler),
                    (LabelRestHandler.URI, LabelRestHandler),
                    (LessonRESTHandler.URI, LessonRESTHandler),
                    (LinkRESTHandler.URI, LinkRESTHandler),
                    (UnitLessonTitleRESTHandler.URI,
                     UnitLessonTitleRESTHandler),
                    (UnitRESTHandler.URI, UnitRESTHandler),
                    (McQuestionRESTHandler.URI, McQuestionRESTHandler),
                    (GiftQuestionRESTHandler.URI, GiftQuestionRESTHandler),
                    (SaQuestionRESTHandler.URI, SaQuestionRESTHandler),
                    (TextAssetRESTHandler.URI, TextAssetRESTHandler),
                    (QuestionGroupRESTHandler.URI, QuestionGroupRESTHandler),
                    (RoleRESTHandler.URI, RoleRESTHandler)]

    # List of functions which are used to generate content displayed at the top
    # of every dashboard page. Use this with caution, as it is extremely
    # invasive of the UX. Each function receives the handler as arg and returns
    # an object to be inserted into a Jinja template (e.g. a string, a safe_dom
    # Node or NodeList, or a jinja2.Markup).
    PAGE_HEADER_HOOKS = []

    # A list of hrefs for extra CSS files to be included in dashboard pages.
    # Files listed here by URL will be available on every Dashboard page.
    EXTRA_CSS_HREF_LIST = []

    # A list of hrefs for extra JS files to be included in dashboard pages.
    # Files listed here by URL will be available on every Dashboard page.
    EXTRA_JS_HREF_LIST = []

    # A list of template locations to be included in dashboard pages
    ADDITIONAL_DIRS = []

    # Dictionary that maps external permissions to their descriptions
    _external_permissions = {}
    # Dictionary that maps actions to permissions
    _action_to_permission = {}

    default_action = None
    _custom_get_actions = {}
    _custom_post_actions = {}

    # Create top level menu groups which other modules can register against.
    # I would do this in "register", but other modules register first.
    actions_to_menu_items = {}
    root_menu_group = menus.MenuGroup('dashboard', 'Dashboard')

    @classmethod
    def add_nav_mapping(cls, name, title, **kwargs):
        """Create a top level nav item."""
        group = cls.root_menu_group.get_child(name)
        if group is None:
            menu_cls = menus.MenuItem if kwargs.get(
                'href') else menus.MenuGroup
            menu_cls(name, title, group=cls.root_menu_group, **kwargs)

    @classmethod
    def get_nav_title(cls, action):
        item = cls.actions_to_menu_items.get(action)
        if item:
            return item.group.title + " > " + item.title
        else:
            return None

    @classmethod
    def has_action_permission(cls, app_context, action):
        return roles.Roles.is_user_allowed(
            app_context, custom_module,
            cls._action_to_permission.get('get_%s' % action, ''))

    @classmethod
    def add_sub_nav_mapping(cls,
                            group_name,
                            item_name,
                            title,
                            action=None,
                            contents=None,
                            can_view=None,
                            href=None,
                            **kwargs):
        """Create a second level nav item.

        Args:
            group_name: Name of an existing top level nav item to use as the
                parent
            item_name: A unique key for this item
            title: Human-readable label
            action: A unique operation ID for
            contents: A handler which will be added as a custom get-action on
                DashboardHandler

        """

        group = cls.root_menu_group.get_child(group_name)
        if group is None:
            logging.critical('The group %s does not exist', group_name)
            return

        item = group.get_child(item_name)
        if item:
            logging.critical(
                'There is already a sub-menu item named "%s" registered in '
                'group %s.', item_name, group_name)
            return

        if contents:
            action = action or group_name + '_' + item_name

        if action and not href:
            href = "dashboard?action={}".format(action)

        def combined_can_view(app_context):
            if action and not cls.has_action_permission(app_context, action):
                return False

            if can_view and not can_view(app_context):
                return False

            return True

        item = menus.MenuItem(item_name,
                              title,
                              action=action,
                              group=group,
                              can_view=combined_can_view,
                              href=href,
                              **kwargs)
        cls.actions_to_menu_items[action] = item

        if contents:
            cls.add_custom_get_action(action, handler=contents)

    @classmethod
    def add_custom_get_action(cls,
                              action,
                              handler=None,
                              in_action=None,
                              overwrite=False):
        if not action:
            logging.critical('Action not specified. Ignoring.')
            return

        if not handler:
            logging.critical('For action : %s handler can not be null.',
                             action)
            return

        if ((action in cls._custom_get_actions or action in cls.get_actions)
                and not overwrite):
            logging.critical(
                'action : %s already exists. Ignoring the custom get action.',
                action)
            return

        cls._custom_get_actions[action] = (handler, in_action)

    @classmethod
    def remove_custom_get_action(cls, action):
        if action in cls._custom_get_actions:
            cls._custom_get_actions.pop(action)

    @classmethod
    def add_custom_post_action(cls, action, handler, overwrite=False):
        if not handler or not action:
            logging.critical('Action or handler can not be null.')
            return

        if ((action in cls._custom_post_actions or action in cls.post_actions)
                and not overwrite):
            logging.critical(
                'action : %s already exists. Ignoring the custom get action.',
                action)
            return

        cls._custom_post_actions[action] = handler

    @classmethod
    def remove_custom_post_action(cls, action):
        if action in cls._custom_post_actions:
            cls._custom_post_actions.pop(action)

    @classmethod
    def get_child_routes(cls):
        """Add child handlers for REST."""
        return cls.child_routes

    def can_view(self, action):
        """Checks if current user has viewing rights."""
        return self.has_action_permission(self.app_context, action)

    def can_edit(self):
        """Checks if current user has editing rights."""
        return roles.Roles.is_course_admin(self.app_context)

    def default_action_for_current_permissions(self):
        """Set the default or first active navigation tab as default action."""
        item = self.root_menu_group.first_visible_item(self.app_context)
        if item:
            return item.action

    def get(self):
        """Enforces rights to all GET operations."""
        action = self.request.get('action')
        if not action:
            self.default_action = self.default_action_for_current_permissions()
            action = self.default_action

        if not self.can_view(action):
            self.redirect(self.app_context.get_slug())
            return

        if action in self._custom_get_actions:
            result = self._custom_get_actions[action][0](self)
            if result is None:
                return

            # The following code handles pages for actions that do not write out
            # their responses.

            template_values = {
                'page_title': self.format_title(self.get_nav_title(action)),
            }
            if isinstance(result, dict):
                template_values.update(result)
            else:
                template_values['main_content'] = result

            self.render_page(template_values)
            return

        # Force reload of properties. It is expensive, but admin deserves it!
        config.Registry.get_overrides(force_update=True)
        return super(DashboardHandler, self).get()

    def post(self):
        """Enforces rights to all POST operations."""
        if not self.can_edit():
            self.redirect(self.app_context.get_slug())
            return
        action = self.request.get('action')
        if action in self._custom_post_actions:
            # Each POST request must have valid XSRF token.
            xsrf_token = self.request.get('xsrf_token')
            if not crypto.XsrfTokenManager.is_xsrf_token_valid(
                    xsrf_token, action):
                self.error(403)
                return
            self.custom_post_handler()
            return

        return super(DashboardHandler, self).post()

    def get_template(self, template_name, dirs):
        """Sets up an environment and Gets jinja template."""
        return jinja_utils.get_template(template_name,
                                        dirs + [os.path.dirname(__file__)],
                                        handler=self)

    def get_alerts(self):
        alerts = []
        if not self.app_context.is_editable_fs():
            alerts.append('Read-only course.')
        if not self.app_context.now_available:
            alerts.append('The course is not publicly available.')
        return '\n'.join(alerts)

    def render_page(self, template_values, in_action=None):
        """Renders a page using provided template values."""
        template_values['header_title'] = template_values['page_title']
        template_values['page_headers'] = [
            hook(self) for hook in self.PAGE_HEADER_HOOKS
        ]
        template_values['course_title'] = self.app_context.get_title()

        current_action = (in_action or self.request.get('action')
                          or self.default_action_for_current_permissions())
        current_menu_item = self.actions_to_menu_items.get(current_action)
        template_values['root_menu_group'] = self.root_menu_group
        template_values['current_menu_item'] = current_menu_item
        template_values['app_context'] = self.app_context
        template_values['course_app_contexts'] = get_visible_courses()
        template_values['current_course'] = self.get_course()

        template_values['gcb_course_base'] = self.get_base_href(self)
        template_values['user_nav'] = safe_dom.NodeList().append(
            safe_dom.Text('%s | ' % users.get_current_user().email())).append(
                safe_dom.Element('a',
                                 href=users.create_logout_url(
                                     self.request.uri)).add_text('Logout'))
        template_values[
            'page_footer'] = 'Page created on: %s' % datetime.datetime.now()
        template_values['coursebuilder_version'] = (
            os.environ['GCB_PRODUCT_VERSION'])
        template_values['application_id'] = app_identity.get_application_id()
        template_values['application_version'] = (
            os.environ['CURRENT_VERSION_ID'])
        template_values['extra_css_href_list'] = self.EXTRA_CSS_HREF_LIST
        template_values['extra_js_href_list'] = self.EXTRA_JS_HREF_LIST
        if not template_values.get('sections'):
            template_values['sections'] = []

        self.response.write(
            self.get_template('view.html', []).render(template_values))

    def format_title(self, text):
        """Formats standard title with or without course picker."""
        ret = safe_dom.NodeList()
        cb_text = 'Course Builder '
        ret.append(safe_dom.Text(cb_text))
        ret.append(safe_dom.Entity('>'))
        ret.append(safe_dom.Text(' %s ' % self.app_context.get_title()))
        ret.append(safe_dom.Entity('>'))
        dashboard_text = ' Dashboard '
        ret.append(safe_dom.Text(dashboard_text))
        ret.append(safe_dom.Entity('>'))
        ret.append(safe_dom.Text(' %s' % text))
        return ret

    def get_question_preview(self):
        template_values = {}
        template_values['gcb_course_base'] = self.get_base_href(self)
        template_values['question'] = tags.html_to_safe_dom(
            '<question quid="%s">' % self.request.get('quid'), self)
        self.response.write(
            self.get_template('question_preview.html',
                              []).render(template_values))

    def custom_post_handler(self):
        """Edit Custom Unit Settings view."""
        action = self.request.get('action')
        self._custom_post_actions[action](self)

    def get_action_url(self, action, key=None, extra_args=None, fragment=None):
        args = {'action': action}
        if key:
            args['key'] = key
        if extra_args:
            args.update(extra_args)
        url = '/dashboard?%s' % urllib.urlencode(args)
        if fragment:
            url += '#' + fragment
        return self.canonicalize_url(url)

    def _render_roles_list(self):
        """Render roles list to HTML."""
        all_roles = RoleDAO.get_all()
        if all_roles:
            output = safe_dom.Element('ul')
            for role in sorted(all_roles, key=lambda r: r.name):
                li = safe_dom.Element('li')
                output.add_child(li)
                li.add_text(role.name).add_child(
                    dashboard_utils.create_edit_button(
                        'dashboard?action=edit_role&key=%s' % (role.id)))
        else:
            output = safe_dom.Element(
                'div', className='gcb-message').add_text('< none >')

        return output

    def _render_roles_view(self):
        """Renders course roles view."""
        actions = [{
            'id': 'add_role',
            'caption': 'Add Role',
            'href': self.get_action_url('add_role')
        }]
        sections = [{
            'description': messages.ROLES_DESCRIPTION,
            'actions': actions,
            'pre': self._render_roles_list()
        }]
        template_values = {
            'page_title': self.format_title('Roles'),
            'sections': sections,
        }
        return template_values

    @classmethod
    def map_action_to_permission(cls, action, permission):
        """Maps an action to a permission.

        Map a GET or POST action that goes through the dashboard to a
        permission to control which users have access. GET actions start with
        'get_' while post actions start with 'post_'.

        Example:
            The i18n module maps both the actions 'get_i18n_dashboard' and
            'get_i18_console' to the permission 'access_i18n_dashboard'.
            Users who have a role assigned with this permission are then allowed
            to perform these actions and thus access the translation tools.

        Args:
            action: a string specifying the action to map.
            permission: a string specifying to which permission the action maps.
        """
        cls._action_to_permission[action] = permission

    @classmethod
    def unmap_action_to_permission(cls, action):
        del cls._action_to_permission[action]

    @classmethod
    def add_external_permission(cls, permission_name, permission_description):
        """Adds extra permissions that will be registered by the Dashboard."""
        cls._external_permissions[permission_name] = permission_description

    @classmethod
    def remove_external_permission(cls, permission_name):
        del cls._external_permissions[permission_name]

    @classmethod
    def permissions_callback(cls, unused_app_context):
        return cls._external_permissions.iteritems()

    @classmethod
    def current_user_has_access(cls, app_context):
        return cls.root_menu_group.can_view(app_context, exclude_links=True)

    @classmethod
    def generate_dashboard_link(cls, app_context):
        if cls.current_user_has_access(app_context):
            return [('dashboard', 'Dashboard')]
        return []