コード例 #1
0
def list_files(app_context,
               subfolder,
               merge_local_files=False,
               all_paths=None):
    """Makes a list of files in a subfolder.

    Args:
        app_context: app_context.
        subfolder: string. Relative path of the subfolder to list.
        merge_local_files: boolean. If True, the returned list will
            contain files found on either the datastore filesystem or the
            read-only local filesystem. If a file is found on both, its
            datastore filesystem version will trump its local filesystem
            version.
        all_paths: list. A list of all file paths in the underlying file
            system.

    Returns:
        List of relative, normalized file path strings.
    """
    home = sites.abspath(app_context.get_home_folder(), '/')
    _paths = None
    if all_paths is not None:
        _paths = []
        for _path in all_paths:
            if _path.startswith(
                    sites.abspath(app_context.get_home_folder(), subfolder)):
                _paths.append(_path)
        _paths = set(_paths)
    else:
        _paths = set(
            app_context.fs.list(
                sites.abspath(app_context.get_home_folder(), subfolder)))

    if merge_local_files:
        local_fs = vfs.LocalReadOnlyFileSystem(logical_home_folder='/')
        _paths = _paths.union(
            set([
                os.path.join(appengine_config.BUNDLE_ROOT, path)
                for path in local_fs.list(subfolder[1:])
            ]))

    result = []
    for abs_filename in _paths:
        filename = os.path.relpath(abs_filename, home)
        result.append(vfs.AbstractFileSystem.normpath(filename))
    return sorted(result)
コード例 #2
0
ファイル: actions.py プロジェクト: barkinet/course-builder
 def __init__(self, environ=None, namespace=None, slug=None):
     self.environ = environ or {}
     self.namespace = namespace if namespace is not None else 'namespace'
     self.slug = slug if slug is not None else 'slug'
     self.fs = vfs.AbstractFileSystem(
         vfs.LocalReadOnlyFileSystem(logical_home_folder='/'))
コード例 #3
0
class FileManagerAndEditor(ApplicationHandler):
    """An editor for editing and managing files."""

    local_fs = vfs.LocalReadOnlyFileSystem(logical_home_folder='/')

    def _get_delete_url(self, base_url, key, xsrf_token_name):
        return '%s?%s' % (
            self.canonicalize_url(base_url),
            urllib.urlencode({
                'key': key,
                'xsrf_token': cgi.escape(
                    self.create_xsrf_token(xsrf_token_name)),
            }))

    def _get_normalized_base(self):
        """Gets base arg from URL and normalizes it for membership checks."""
        base = self.request.get('base')
        assert base
        base = strip_leading_and_trailing_slashes(base)
        assert base in ALLOWED_ASSET_UPLOAD_BASES
        return base

    def post_create_or_edit_settings(self):
        """Handles creation or/and editing of course.yaml."""
        assert is_editable_fs(self.app_context)

        # Check if course.yaml exists; create if not.
        fs = self.app_context.fs.impl
        course_yaml = fs.physical_to_logical('/course.yaml')
        if not fs.isfile(course_yaml):
            fs.put(course_yaml, vfs.string_to_stream(
                courses.EMPTY_COURSE_YAML % users.get_current_user().email()))

        self.redirect(self.get_action_url('edit_settings', key='/course.yaml'))

    def get_edit_settings(self):
        """Shows editor for course.yaml."""

        key = self.request.get('key')

        exit_url = self.canonicalize_url('/dashboard?action=settings')
        rest_url = self.canonicalize_url('/rest/files/item')
        form_html = oeditor.ObjectEditor.get_html_for(
            self,
            FilesItemRESTHandler.SCHEMA_JSON,
            FilesItemRESTHandler.SCHEMA_ANNOTATIONS_DICT,
            key, rest_url, exit_url,
            required_modules=FilesItemRESTHandler.REQUIRED_MODULES)

        template_values = {}
        template_values['page_title'] = self.format_title('Edit Settings')
        template_values['page_description'] = messages.EDIT_SETTINGS_DESCRIPTION
        template_values['main_content'] = form_html
        self.render_page(template_values)

    def get_add_asset(self):
        """Show an upload dialog for assets."""

        key = self._get_normalized_base()
        exit_url = self.canonicalize_url('/dashboard?action=assets')
        rest_url = self.canonicalize_url(
            AssetItemRESTHandler.URI)
        form_html = oeditor.ObjectEditor.get_html_for(
            self,
            AssetItemRESTHandler.SCHEMA_JSON,
            AssetItemRESTHandler.SCHEMA_ANNOTATIONS_DICT,
            key, rest_url, exit_url, save_method='upload', auto_return=True,
            required_modules=AssetItemRESTHandler.REQUIRED_MODULES,
            save_button_caption='Upload')

        template_values = {}
        template_values['page_title'] = self.format_title('Upload Asset')
        template_values['page_description'] = messages.UPLOAD_ASSET_DESCRIPTION
        template_values['main_content'] = form_html
        self.render_page(template_values)

    def get_delete_asset(self):
        """Show an review/delete page for assets."""

        uri = self.request.get('uri')

        exit_url = self.canonicalize_url('/dashboard?action=assets')
        rest_url = self.canonicalize_url(
            AssetUriRESTHandler.URI)
        delete_url = self._get_delete_url(
            FilesItemRESTHandler.URI, uri, 'delete-asset')
        form_html = oeditor.ObjectEditor.get_html_for(
            self,
            AssetUriRESTHandler.SCHEMA_JSON,
            AssetUriRESTHandler.SCHEMA_ANNOTATIONS_DICT,
            uri, rest_url, exit_url, save_method='',
            delete_url=delete_url, delete_method='delete')

        template_values = {}
        template_values['page_title'] = self.format_title('View Asset')
        template_values['main_content'] = form_html
        self.render_page(template_values)

    def get_manage_text_asset(self):
        """Show an edit/save/delete/revert form for a text asset."""
        assert is_editable_fs(self.app_context)
        uri = self.request.get('uri')
        assert uri

        asset = self.app_context.fs.impl.get(
            os.path.join(appengine_config.BUNDLE_ROOT, uri))
        assert asset
        asset_in_datastore_fs = not is_readonly_asset(asset)

        try:
            asset_in_local_fs = bool(self.local_fs.get(uri))
        except IOError:
            asset_in_local_fs = False

        exit_url = self.canonicalize_url('/dashboard?action=assets')
        rest_url = self.canonicalize_url(TextAssetRESTHandler.URI)

        delete_button_caption = 'Delete'
        delete_message = None
        delete_url = None

        if asset_in_datastore_fs:
            delete_message = 'Are you sure you want to delete %s?' % uri
            delete_url = self._get_delete_url(
                TextAssetRESTHandler.URI, uri,
                TextAssetRESTHandler.XSRF_TOKEN_NAME)

        if asset_in_local_fs:
            delete_message = (
                'Are you sure you want to restore %s to the original version? '
                'All your customizations will be lost.' % uri)
            delete_button_caption = 'Restore original'

        # Disable the save button if the payload is not text by setting method
        # to ''.
        save_method = 'put' if is_text_payload(asset.read()) else ''

        form_html = oeditor.ObjectEditor.get_html_for(
            self,
            TextAssetRESTHandler.SCHEMA.get_json_schema(),
            TextAssetRESTHandler.SCHEMA.get_schema_dict(),
            uri,
            rest_url,
            exit_url,
            delete_button_caption=delete_button_caption,
            delete_method='delete',
            delete_message=delete_message,
            delete_url=delete_url,
            required_modules=TextAssetRESTHandler.REQUIRED_MODULES,
            save_method=save_method,
        )
        self.render_page({
            'page_title': self.format_title('Edit ' + uri),
            'main_content': form_html,
        })
コード例 #4
0
class DashboardHandler(CourseSettingsHandler, FileManagerAndEditor,
                       UnitLessonEditor, QuestionManagerAndEditor,
                       QuestionGroupManagerAndEditor, AssignmentManager,
                       ApplicationHandler, ReflectiveRequestHandler):
    """Handles all pages and actions required for managing a course."""

    default_action = 'outline'
    get_actions = [
        default_action, 'assets', 'settings', 'analytics',
        'edit_basic_settings', 'edit_settings', 'edit_unit_lesson',
        'edit_unit', 'edit_link', 'edit_lesson', 'edit_assessment',
        'add_asset', 'delete_asset', 'manage_text_asset', 'import_course',
        'edit_assignment', 'add_mc_question', 'add_sa_question',
        'edit_question', 'add_question_group', 'edit_question_group'
    ]
    # Requests to these handlers automatically go through an XSRF token check
    # that is implemented in ReflectiveRequestHandler.
    post_actions = [
        'compute_student_stats', 'create_or_edit_settings', 'add_unit',
        'add_link', 'add_assessment', 'add_lesson',
        'edit_basic_course_settings', 'add_reviewer', 'delete_reviewer'
    ]

    local_fs = vfs.LocalReadOnlyFileSystem(logical_home_folder='/')

    @classmethod
    def get_child_routes(cls):
        """Add child handlers for REST."""
        return [(AssessmentRESTHandler.URI, AssessmentRESTHandler),
                (AssetItemRESTHandler.URI, AssetItemRESTHandler),
                (CourseSettingsRESTHandler.URI, CourseSettingsRESTHandler),
                (FilesItemRESTHandler.URI, FilesItemRESTHandler),
                (AssetItemRESTHandler.URI, AssetItemRESTHandler),
                (AssetUriRESTHandler.URI, AssetUriRESTHandler),
                (ImportActivityRESTHandler.URI, ImportActivityRESTHandler),
                (ImportCourseRESTHandler.URI, ImportCourseRESTHandler),
                (LessonRESTHandler.URI, LessonRESTHandler),
                (LinkRESTHandler.URI, LinkRESTHandler),
                (UnitLessonTitleRESTHandler.URI, UnitLessonTitleRESTHandler),
                (UnitRESTHandler.URI, UnitRESTHandler),
                (McQuestionRESTHandler.URI, McQuestionRESTHandler),
                (SaQuestionRESTHandler.URI, SaQuestionRESTHandler),
                (TextAssetRESTHandler.URI, TextAssetRESTHandler),
                (QuestionGroupRESTHandler.URI, QuestionGroupRESTHandler),
                (ExportAssessmentRESTHandler.URI, ExportAssessmentRESTHandler)]

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

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

    def get(self):
        """Enforces rights to all GET operations."""
        if not self.can_view():
            self.redirect(self.app_context.get_slug())
            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
        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 courses.is_editable_fs(self.app_context):
            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_top_nav(self):
        current_action = self.request.get('action')
        nav_mappings = [('', 'Outline'), ('assets', 'Assets'),
                        ('settings', 'Settings'), ('analytics', 'Analytics'),
                        ('edit_assignment', 'Peer Review')]
        nav = safe_dom.NodeList()
        for action, title in nav_mappings:

            class_name = 'selected' if action == current_action else ''
            action_href = 'dashboard?action=%s' % action
            nav.append(
                safe_dom.Element('a', href=action_href,
                                 className=class_name).add_text(title))

        if roles.Roles.is_super_admin():
            nav.append(safe_dom.Element('a', href='/admin').add_text('Admin'))

        nav.append(
            safe_dom.Element(
                'a',
                href='https://code.google.com/p/course-builder/wiki/Dashboard',
                target='_blank').add_text('Help'))

        return nav

    def render_page(self, template_values):
        """Renders a page using provided template values."""

        template_values['top_nav'] = self._get_top_nav()
        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'] = 'Created on: %s' % datetime.datetime.now()

        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."""
        title = self.app_context.get_environ()['course']['title']
        return safe_dom.NodeList().append(
            safe_dom.Text('Course Builder ')).append(
                safe_dom.Entity('>')).append(safe_dom.Text(
                    ' %s ' % title)).append(safe_dom.Entity('>')).append(
                        safe_dom.Text(' Dashboard ')).append(
                            safe_dom.Entity('>')).append(
                                safe_dom.Text(' %s' % text))

    def _get_edit_link(self, url):
        return safe_dom.NodeList().append(safe_dom.Text(' ')).append(
            safe_dom.Element('a', href=url).add_text('Edit'))

    def _get_availability(self, resource):
        if not hasattr(resource, 'now_available'):
            return safe_dom.Text('')
        if resource.now_available:
            return safe_dom.Text('')
        else:
            return safe_dom.NodeList().append(safe_dom.Text(' ')).append(
                safe_dom.Element('span', className='draft-label').add_text(
                    '(%s)' % unit_lesson_editor.DRAFT_TEXT))

    def render_course_outline_to_html(self):
        """Renders course outline to HTML."""
        course = courses.Course(self)
        if not course.get_units():
            return []

        is_editable = filer.is_editable_fs(self.app_context)

        lines = safe_dom.Element('ul', style='list-style: none;')
        for unit in course.get_units():
            if unit.type == verify.UNIT_TYPE_ASSESSMENT:
                li = safe_dom.Element('li').add_child(
                    safe_dom.Element('a',
                                     href='assessment?name=%s' % unit.unit_id,
                                     className='strong').add_text(
                                         unit.title)).add_child(
                                             self._get_availability(unit))
                if is_editable:
                    url = self.canonicalize_url(
                        '/dashboard?%s') % urllib.urlencode({
                            'action': 'edit_assessment',
                            'key': unit.unit_id
                        })
                    li.add_child(self._get_edit_link(url))
                lines.add_child(li)
                continue

            if unit.type == verify.UNIT_TYPE_LINK:
                li = safe_dom.Element('li').add_child(
                    safe_dom.Element('a', href=unit.href,
                                     className='strong').add_text(
                                         unit.title)).add_child(
                                             self._get_availability(unit))
                if is_editable:
                    url = self.canonicalize_url(
                        '/dashboard?%s') % urllib.urlencode({
                            'action': 'edit_link',
                            'key': unit.unit_id
                        })
                    li.add_child(self._get_edit_link(url))
                lines.add_child(li)
                continue

            if unit.type == verify.UNIT_TYPE_UNIT:
                li = safe_dom.Element('li').add_child(
                    safe_dom.Element('a',
                                     href='unit?unit=%s' % unit.unit_id,
                                     className='strong').add_text(
                                         'Unit %s - %s' %
                                         (unit.index, unit.title))).add_child(
                                             self._get_availability(unit))
                if is_editable:
                    url = self.canonicalize_url(
                        '/dashboard?%s') % urllib.urlencode({
                            'action': 'edit_unit',
                            'key': unit.unit_id
                        })
                    li.add_child(self._get_edit_link(url))

                ol = safe_dom.Element('ol')
                for lesson in course.get_lessons(unit.unit_id):
                    li2 = safe_dom.Element('li').add_child(
                        safe_dom.Element(
                            'a',
                            href='unit?unit=%s&lesson=%s' %
                            (unit.unit_id, lesson.lesson_id),
                        ).add_text(lesson.title)).add_child(
                            self._get_availability(lesson))
                    if is_editable:
                        url = self.get_action_url('edit_lesson',
                                                  key=lesson.lesson_id)
                        li2.add_child(self._get_edit_link(url))
                    ol.add_child(li2)
                li.add_child(ol)
                lines.add_child(li)
                continue

            raise Exception('Unknown unit type: %s.' % unit.type)

        return lines

    def get_outline(self):
        """Renders course outline view."""

        pages_info = [
            safe_dom.Element('a', href=self.canonicalize_url(
                '/announcements')).add_text('Announcements'),
            safe_dom.Element(
                'a', href=self.canonicalize_url('/course')).add_text('Course')
        ]

        outline_actions = []
        if filer.is_editable_fs(self.app_context):
            outline_actions.append({
                'id':
                'edit_unit_lesson',
                'caption':
                'Organize',
                'href':
                self.get_action_url('edit_unit_lesson')
            })
            all_units = courses.Course(self).get_units()
            if any([unit.type == verify.UNIT_TYPE_UNIT for unit in all_units]):
                outline_actions.append({
                    'id':
                    'add_lesson',
                    'caption':
                    'Add Lesson',
                    'action':
                    self.get_action_url('add_lesson'),
                    'xsrf_token':
                    self.create_xsrf_token('add_lesson')
                })
            outline_actions.append({
                'id':
                'add_unit',
                'caption':
                'Add Unit',
                'action':
                self.get_action_url('add_unit'),
                'xsrf_token':
                self.create_xsrf_token('add_unit')
            })
            outline_actions.append({
                'id':
                'add_link',
                'caption':
                'Add Link',
                'action':
                self.get_action_url('add_link'),
                'xsrf_token':
                self.create_xsrf_token('add_link')
            })
            outline_actions.append({
                'id':
                'add_assessment',
                'caption':
                'Add Assessment',
                'action':
                self.get_action_url('add_assessment'),
                'xsrf_token':
                self.create_xsrf_token('add_assessment')
            })
            if not courses.Course(self).get_units():
                outline_actions.append({
                    'id':
                    'import_course',
                    'caption':
                    'Import',
                    'href':
                    self.get_action_url('import_course')
                })

        data_info = self.list_files('/data/')

        sections = [{
            'title': 'Pages',
            'description': messages.PAGES_DESCRIPTION,
            'children': pages_info
        }, {
            'title': 'Course Outline',
            'description': messages.COURSE_OUTLINE_DESCRIPTION,
            'actions': outline_actions,
            'pre': self.render_course_outline_to_html()
        }, {
            'title': 'Data Files',
            'description': messages.DATA_FILES_DESCRIPTION,
            'children': data_info
        }]

        template_values = {}
        template_values['page_title'] = self.format_title('Outline')
        template_values['alerts'] = self._get_alerts()
        template_values['sections'] = sections
        self.render_page(template_values)

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

    def get_settings(self):
        """Renders course settings view."""

        yaml_actions = []
        basic_setting_actions = []

        # Basic course info.
        course_info = [
            'Course Title: %s' %
            self.app_context.get_environ()['course']['title'],
            'Context Path: %s' % self.app_context.get_slug(),
            'Datastore Namespace: %s' % self.app_context.get_namespace_name()
        ]

        # Course file system.
        fs = self.app_context.fs.impl
        course_info.append(('File System: %s' % fs.__class__.__name__))
        if fs.__class__ == vfs.LocalReadOnlyFileSystem:
            course_info.append(
                ('Home Folder: %s' %
                 sites.abspath(self.app_context.get_home_folder(), '/')))

        # Enable editing if supported.
        if filer.is_editable_fs(self.app_context):
            yaml_actions.append({
                'id':
                'edit_course_yaml',
                'caption':
                'Advanced Edit',
                'action':
                self.get_action_url('create_or_edit_settings'),
                'xsrf_token':
                self.create_xsrf_token('create_or_edit_settings')
            })
            yaml_actions.append({
                'id':
                'edit_basic_course_settings',
                'caption':
                'Edit',
                'action':
                self.get_action_url('edit_basic_course_settings'),
                'xsrf_token':
                self.create_xsrf_token('edit_basic_course_settings')
            })

        # course.yaml file content.
        yaml_info = []
        yaml_stream = self.app_context.fs.open(
            self.app_context.get_config_filename())
        if yaml_stream:
            yaml_lines = yaml_stream.read().decode('utf-8')
            for line in yaml_lines.split('\n'):
                yaml_info.append(line)
        else:
            yaml_info.append('< empty file >')

        # course_template.yaml file contents
        course_template_info = []
        course_template_stream = open(
            os.path.join(os.path.dirname(__file__),
                         '../../course_template.yaml'), 'r')
        if course_template_stream:
            course_template_lines = course_template_stream.read().decode(
                'utf-8')
            for line in course_template_lines.split('\n'):
                course_template_info.append(line)
        else:
            course_template_info.append('< empty file >')

        # Prepare template values.
        template_values = {}
        template_values['page_title'] = self.format_title('Settings')
        template_values['page_description'] = messages.SETTINGS_DESCRIPTION
        template_values['sections'] = [{
            'title': 'About the Course',
            'description': messages.ABOUT_THE_COURSE_DESCRIPTION,
            'actions': basic_setting_actions,
            'children': course_info
        }, {
            'title': 'Contents of course.yaml file',
            'description': messages.CONTENTS_OF_THE_COURSE_DESCRIPTION,
            'actions': yaml_actions,
            'children': yaml_info
        }, {
            'title': 'Contents of course_template.yaml file',
            'description': messages.COURSE_TEMPLATE_DESCRIPTION,
            'children': course_template_info
        }]

        self.render_page(template_values)

    def list_files(self, subfolder, merge_local_files=False):
        """Makes a list of files in a subfolder.

        Args:
            subfolder: string. Relative path of the subfolder to list.
            merge_local_files: boolean. If True, the returned list will
                contain files found on either the datastore filesystem or the
                read-only local filesystem. If a file is found on both, its
                datastore filesystem version will trump its local filesystem
                version.

        Returns:
            List of relative, normalized file path strings.
        """
        home = sites.abspath(self.app_context.get_home_folder(), '/')
        all_paths = set(
            self.app_context.fs.list(
                sites.abspath(self.app_context.get_home_folder(), subfolder)))

        if merge_local_files:
            all_paths = all_paths.union(
                set([
                    os.path.join(appengine_config.BUNDLE_ROOT, path)
                    for path in self.local_fs.list(subfolder[1:])
                ]))

        result = []
        for abs_filename in all_paths:
            filename = os.path.relpath(abs_filename, home)
            result.append(vfs.AbstractFileSystem.normpath(filename))
        return sorted(result)

    def list_and_format_file_list(self,
                                  title,
                                  subfolder,
                                  links=False,
                                  upload=False,
                                  prefix=None,
                                  caption_if_empty='< none >',
                                  edit_url_template=None,
                                  merge_local_files=False,
                                  sub_title=None):
        """Walks files in folders and renders their names in a section."""

        items = safe_dom.NodeList()
        count = 0
        for filename in self.list_files(subfolder,
                                        merge_local_files=merge_local_files):
            if prefix and not filename.startswith(prefix):
                continue
            li = safe_dom.Element('li')
            if links:
                li.add_child(
                    safe_dom.Element(
                        'a', href=urllib.quote(filename)).add_text(filename))
            else:
                li.add_text(filename)
            if (edit_url_template
                    and self.app_context.fs.impl.is_read_write()):
                edit_url = edit_url_template % urllib.quote(filename)
                li.add_child(safe_dom.Entity('&nbsp;')).add_child(
                    safe_dom.Element('a', href=edit_url).add_text('[Edit]'))
            count += 1
            items.append(li)

        output = safe_dom.NodeList()

        if filer.is_editable_fs(self.app_context) and upload:
            output.append(
                safe_dom.Element(
                    'a',
                    className='gcb-button gcb-pull-right',
                    href='dashboard?%s' % urllib.urlencode({
                        'action': 'add_asset',
                        'base': subfolder
                    })).add_text(
                        'Upload to ' +
                        filer.strip_leading_and_trailing_slashes(subfolder))
            ).append(
                safe_dom.Element('div',
                                 style='clear: both; padding-top: 2px;'))
        if title:
            h3 = safe_dom.Element('h3')
            if count:
                h3.add_text('%s (%s)' % (title, count))
            else:
                h3.add_text(title)
            output.append(h3)
        if sub_title:
            output.append(safe_dom.Element('blockquote').add_text(sub_title))
        if items:
            output.append(safe_dom.Element('ol').add_children(items))
        else:
            if caption_if_empty:
                output.append(
                    safe_dom.Element('blockquote').add_text(caption_if_empty))
        return output

    def list_questions(self):
        """Prepare a list of the question bank contents."""
        if not filer.is_editable_fs(self.app_context):
            return safe_dom.NodeList()

        output = safe_dom.NodeList().append(
            safe_dom.Element('a',
                             className='gcb-button gcb-pull-right',
                             href='dashboard?action=add_mc_question').
            add_text('Add Multiple Choice')).append(
                safe_dom.Element('a',
                                 className='gcb-button gcb-pull-right',
                                 href='dashboard?action=add_sa_question').
                add_text('Add Short Answer')).append(
                    safe_dom.Element(
                        'div', style='clear: both; padding-top: 2px;')).append(
                            safe_dom.Element('h3').add_text('Question Bank'))

        all_questions = QuestionDAO.get_all()
        if all_questions:
            ol = safe_dom.Element('ol')
            for question in all_questions:
                edit_url = 'dashboard?action=edit_question&key=%s' % question.id
                li = safe_dom.Element('li')
                li.add_text(question.description).add_child(
                    safe_dom.Entity('&nbsp;')).add_child(
                        safe_dom.Element('a',
                                         href=edit_url).add_text('[Edit]'))
                ol.add_child(li)
            output.append(ol)
        else:
            output.append(safe_dom.Element('blockquote').add_text('< none >'))

        return output

    def list_question_groups(self):
        """Prepare a list of question groups."""
        if not filer.is_editable_fs(self.app_context):
            return safe_dom.NodeList()

        all_questions = QuestionDAO.get_all()
        output = safe_dom.NodeList()
        if all_questions:
            output.append(
                safe_dom.Element('a',
                                 className='gcb-button gcb-pull-right',
                                 href='dashboard?action=add_question_group').
                add_text('Add Question Group')).append(
                    safe_dom.Element('div',
                                     style='clear: both; padding-top: 2px;'))
        output.append(safe_dom.Element('h3').add_text('Question Groups'))

        # TODO(jorr): Hook this into the datastore
        all_question_groups = QuestionGroupDAO.get_all()
        if all_question_groups:
            ol = safe_dom.Element('ol')
            for question_group in all_question_groups:
                edit_url = 'dashboard?action=edit_question_group&key=%s' % (
                    question_group.id)
                li = safe_dom.Element('li')
                li.add_text(question_group.description).add_child(
                    safe_dom.Entity('&nbsp;')).add_child(
                        safe_dom.Element('a',
                                         href=edit_url).add_text('[Edit]'))
                ol.add_child(li)
            output.append(ol)
        else:
            output.append(safe_dom.Element('blockquote').add_text('< none >'))

        return output

    def get_assets(self):
        """Renders course assets view."""
        def inherits_from(folder):
            return '< inherited from %s >' % folder

        text_asset_url_template = 'dashboard?action=manage_text_asset&uri=%s'

        items = safe_dom.NodeList().append(self.list_questions()).append(
            self.list_question_groups()).append(
                self.list_and_format_file_list('Assessments',
                                               '/assets/js/',
                                               links=True,
                                               prefix='assets/js/assessment-')
            ).append(
                self.list_and_format_file_list('Activities',
                                               '/assets/js/',
                                               links=True,
                                               prefix='assets/js/activity-')
            ).append(
                self.list_and_format_file_list(
                    'Images & Documents',
                    '/assets/img/',
                    links=True,
                    upload=True,
                    edit_url_template='dashboard?action=delete_asset&uri=%s',
                    caption_if_empty=inherits_from('/assets/img/'))).append(
                        self.list_and_format_file_list(
                            'Cascading Style Sheets',
                            '/assets/css/',
                            links=True,
                            upload=True,
                            edit_url_template=text_asset_url_template,
                            caption_if_empty=inherits_from('/assets/css/'),
                            merge_local_files=True)
                    ).append(
                        self.list_and_format_file_list(
                            'JavaScript Libraries',
                            '/assets/lib/',
                            links=True,
                            upload=True,
                            edit_url_template=text_asset_url_template,
                            caption_if_empty=inherits_from('/assets/lib/'),
                            merge_local_files=True)).append(
                                self.list_and_format_file_list(
                                    'View Templates',
                                    '/views/',
                                    upload=True,
                                    edit_url_template=text_asset_url_template,
                                    caption_if_empty=inherits_from('/views/'),
                                    merge_local_files=True))

        template_values = {}
        template_values['page_title'] = self.format_title('Assets')
        template_values['page_description'] = messages.ASSETS_DESCRIPTION
        template_values['main_content'] = items
        self.render_page(template_values)

    def get_markup_for_basic_analytics(self, job):
        """Renders markup for basic enrollment and assessment analytics."""
        subtemplate_values = {}
        errors = []
        stats_calculated = False
        update_message = safe_dom.Text('')

        if not job:
            update_message = safe_dom.Text(
                'Enrollment/assessment statistics have not been calculated '
                'yet.')
        else:
            if job.status_code == jobs.STATUS_CODE_COMPLETED:
                stats = transforms.loads(job.output)
                stats_calculated = True

                subtemplate_values['enrolled'] = stats['enrollment'][
                    'enrolled']
                subtemplate_values['unenrolled'] = (
                    stats['enrollment']['unenrolled'])

                scores = []
                total_records = 0
                for key, value in stats['scores'].items():
                    total_records += value[0]
                    avg = round(value[1] / value[0], 1) if value[0] else 0
                    scores.append({
                        'key': key,
                        'completed': value[0],
                        'avg': avg
                    })
                subtemplate_values['scores'] = scores
                subtemplate_values['total_records'] = total_records

                update_message = safe_dom.Text(
                    """
                    Enrollment and assessment statistics were last updated at
                    %s in about %s second(s).""" %
                    (job.updated_on.strftime(HUMAN_READABLE_TIME_FORMAT),
                     job.execution_time_sec))
            elif job.status_code == jobs.STATUS_CODE_FAILED:
                update_message = safe_dom.NodeList().append(
                    safe_dom.Text("""
                        There was an error updating enrollment/assessment
                        statistics. Here is the message:""")).append(
                        safe_dom.Element('br')).append(
                            safe_dom.Element('blockquote').add_child(
                                safe_dom.Element('pre').add_text('\n%s' %
                                                                 job.output)))
            else:
                update_message = safe_dom.Text(
                    'Enrollment and assessment statistics update started at %s'
                    ' and is running now. Please come back shortly.' %
                    job.updated_on.strftime(HUMAN_READABLE_TIME_FORMAT))

        subtemplate_values['stats_calculated'] = stats_calculated
        subtemplate_values['errors'] = errors
        subtemplate_values['update_message'] = update_message

        return jinja2.utils.Markup(
            self.get_template('basic_analytics.html',
                              [os.path.dirname(__file__)]).render(
                                  subtemplate_values, autoescape=True))

    def get_analytics(self):
        """Renders course analytics view."""
        template_values = {}
        template_values['page_title'] = self.format_title('Analytics')

        all_jobs_have_finished = True

        basic_analytics_job = ComputeStudentStats(self.app_context).load()
        stats_html = self.get_markup_for_basic_analytics(basic_analytics_job)
        if (basic_analytics_job and
                basic_analytics_job.status_code != jobs.STATUS_CODE_COMPLETED):
            all_jobs_have_finished = False

        for callback in DashboardRegistry.analytics_handlers:
            handler = callback()
            handler.app_context = self.app_context
            handler.request = self.request
            handler.response = self.response

            job = handler.stats_computer(self.app_context).load()
            stats_html += handler.get_markup(job)

            if job and job.status_code != jobs.STATUS_CODE_COMPLETED:
                all_jobs_have_finished = False

        template_values['main_content'] = jinja2.utils.Markup(
            self.get_template('analytics.html', [
                os.path.dirname(__file__)
            ]).render(
                {
                    'show_recalculate_button': all_jobs_have_finished,
                    'stats_html': stats_html,
                    'xsrf_token':
                    self.create_xsrf_token('compute_student_stats'),
                },
                autoescape=True))

        self.render_page(template_values)

    def post_compute_student_stats(self):
        """Submits a new student statistics calculation task."""
        job = ComputeStudentStats(self.app_context)
        job.submit()

        for callback in DashboardRegistry.analytics_handlers:
            job = callback().stats_computer(self.app_context)
            job.submit()

        self.redirect('/dashboard?action=analytics')
コード例 #5
0
class FileManagerAndEditor(ApplicationHandler):
    """An editor for editing and managing files."""

    local_fs = vfs.LocalReadOnlyFileSystem(logical_home_folder='/')

    def _get_delete_url(self, base_url, key, xsrf_token_name):
        return '%s?%s' % (self.canonicalize_url(base_url),
                          urllib.urlencode({
                              'key':
                              key,
                              'xsrf_token':
                              cgi.escape(
                                  self.create_xsrf_token(xsrf_token_name)),
                          }))

    def post_create_or_edit_settings(self):
        """Handles creation or/and editing of course.yaml."""
        create_course_file_if_not_exists(self)
        extra_args = {}
        for name in ('tab', 'tab_title'):
            value = self.request.get(name)
            if value:
                extra_args[name] = value
        self.redirect(
            self.get_action_url('edit_settings',
                                key='/course.yaml',
                                extra_args=extra_args))

    def get_edit_settings(self):
        """Shows editor for course.yaml."""

        key = self.request.get('key')
        tab = self.request.get('tab')
        exit_url = self.canonicalize_url('/dashboard?action=settings&tab=%s' %
                                         tab)
        rest_url = self.canonicalize_url('/rest/files/item')
        form_html = oeditor.ObjectEditor.get_html_for(
            self,
            FilesItemRESTHandler.SCHEMA_JSON,
            FilesItemRESTHandler.SCHEMA_ANNOTATIONS_DICT,
            key,
            rest_url,
            exit_url,
            required_modules=FilesItemRESTHandler.REQUIRED_MODULES)

        template_values = {}
        template_values['page_title'] = self.format_title('Edit Settings')
        template_values[
            'page_description'] = messages.EDIT_SETTINGS_DESCRIPTION
        template_values['main_content'] = form_html
        self.render_page(template_values, in_action='settings')

    def _is_displayable_asset(self, path):
        return any([path.startswith(name) for name in DISPLAYABLE_ASSET_BASES])

    def get_manage_asset(self):
        """Show an upload/delete dialog for assets."""

        key = self.request.get('key').lstrip('/').rstrip('/')
        if not _is_asset_in_allowed_bases(key):
            raise ValueError('Cannot add/edit asset with key "%s" ' % key +
                             'which is not under a valid asset path')
        fs = self.app_context.fs.impl

        delete_url = None
        delete_method = None
        delete_message = None
        auto_return = False
        if fs.isfile(fs.physical_to_logical(key)):
            delete_url = self._get_delete_url(FilesItemRESTHandler.URI, key,
                                              'delete-asset')
            delete_method = 'delete'
        else:
            # Sadly, since we don't know the name of the asset when we build
            # the form, the form can't update itself to show the uploaded
            # asset when the upload completes.  Rather than continue to
            # show a blank form, bring the user back to the assets list.
            auto_return = True

        if self._is_displayable_asset(key):
            json = AssetItemRESTHandler.DISPLAYABLE_SCHEMA_JSON
            ann = AssetItemRESTHandler.DISPLAYABLE_SCHEMA_ANNOTATIONS_DICT
        else:
            json = AssetItemRESTHandler.UNDISPLAYABLE_SCHEMA_JSON
            ann = AssetItemRESTHandler.UNDISPLAYABLE_SCHEMA_ANNOTATIONS_DICT

        tab_name = self.request.get('tab')
        exit_url = self.canonicalize_url(
            dashboard_utils.build_assets_url(tab_name))
        rest_url = self.canonicalize_url(AssetItemRESTHandler.URI)

        form_html = oeditor.ObjectEditor.get_html_for(
            self,
            json,
            ann,
            key,
            rest_url,
            exit_url,
            save_method='upload',
            save_button_caption='Upload',
            auto_return=auto_return,
            delete_url=delete_url,
            delete_method=delete_method,
            delete_message=delete_message,
            required_modules=AssetItemRESTHandler.REQUIRED_MODULES,
            additional_dirs=[
                os.path.join(dashboard_utils.RESOURCES_DIR, 'js')
            ])

        template_values = {}
        template_values['page_title'] = self.format_title('Manage Asset')
        template_values['page_description'] = messages.UPLOAD_ASSET_DESCRIPTION
        template_values['main_content'] = form_html
        self.render_page(template_values, 'assets', tab_name)

    def get_manage_text_asset(self):
        """Show an edit/save/delete/revert form for a text asset."""
        assert self.app_context.is_editable_fs()
        uri = self.request.get('uri')
        assert uri
        tab_name = self.request.get('tab')

        asset = self.app_context.fs.impl.get(
            os.path.join(appengine_config.BUNDLE_ROOT, uri))
        assert asset
        asset_in_datastore_fs = not is_readonly_asset(asset)

        try:
            asset_in_local_fs = bool(self.local_fs.get(uri))
        except IOError:
            asset_in_local_fs = False

        exit_url = self.canonicalize_url(
            dashboard_utils.build_assets_url(tab_name))
        rest_url = self.canonicalize_url(TextAssetRESTHandler.URI)

        delete_button_caption = 'Delete'
        delete_message = None
        delete_url = None

        if asset_in_datastore_fs:
            delete_message = 'Are you sure you want to delete %s?' % uri
            delete_url = self._get_delete_url(
                TextAssetRESTHandler.URI, uri,
                TextAssetRESTHandler.XSRF_TOKEN_NAME)

        if asset_in_local_fs:
            delete_message = (
                'Are you sure you want to restore %s to the original version? '
                'All your customizations will be lost.' % uri)
            delete_button_caption = 'Restore original'

        # Disable the save button if the payload is not text by setting method
        # to ''.
        save_method = 'put' if is_text_payload(asset.read()) else ''

        form_html = oeditor.ObjectEditor.get_html_for(
            self,
            TextAssetRESTHandler.SCHEMA.get_json_schema(),
            TextAssetRESTHandler.SCHEMA.get_schema_dict(),
            uri,
            rest_url,
            exit_url,
            delete_button_caption=delete_button_caption,
            delete_method='delete',
            delete_message=delete_message,
            delete_url=delete_url,
            required_modules=TextAssetRESTHandler.REQUIRED_MODULES,
            save_method=save_method,
        )
        self.render_page(
            {
                'page_title': self.format_title('Edit ' + uri),
                'main_content': form_html,
            }, 'assets', tab_name)
コード例 #6
0
ファイル: tests.py プロジェクト: dgerod/cb4oeu
 def after_create(unused_cls, instance):
     # pylint: disable-msg=protected-access
     instance._fs = vfs.LocalReadOnlyFileSystem(
         os.path.join(GeneratedCourse.data_home, 'data-vfs'),
         home_folder)