def get_stage_content_fragment(self, context, view='student_view'):
     final_grade = self.final_grade
     context_extension = {
         'final_grade': final_grade if final_grade is not None else _(u"N/A")
     }
     context_extension.update(context)
     return super(GradeDisplayStage, self).get_stage_content_fragment(context_extension, view)
    def persist_and_submit_file(self, activity, context, file_stream):
        """
        Saves uploaded files to their permanent location, sends them to submissions backend and emits submission events
        """
        uploaded_file = UploadFile(file_stream, self.upload_id, context)

        # Save the files first
        try:
            uploaded_file.save_file()
        except Exception as save_file_error:  # pylint: disable=broad-except
            original_message = save_file_error.message if hasattr(save_file_error, "message") else ""
            save_file_error.message = _("Error storing file {} - {}").format(uploaded_file.file.name, original_message)
            raise

        # It have been saved... note the submission
        try:
            uploaded_file.submit()
            # Emit analytics event...
            self.runtime.publish(
                self,
                self.SUBMISSION_RECEIVED_EVENT,
                {
                    "submission_id": uploaded_file.submission_id,
                    "filename": uploaded_file.file.name,
                    "content_id": activity.content_id,
                    "group_id": activity.workgroup.id,
                    "user_id": activity.user_id,
                }
            )
        except Exception as save_record_error:  # pylint: disable=broad-except
            original_message = save_record_error.message if hasattr(save_record_error, "message") else ""
            save_record_error.message = _("Error recording file information {} - {}").format(
                uploaded_file.file.name, original_message
            )
            raise

        # See if the xBlock Notification Service is available, and - if so -
        # dispatch a notification to the entire workgroup that a file has been uploaded
        # Note that the NotificationService can be disabled, so it might not be available
        # in the list of services
        notifications_service = self.runtime.service(self, 'notifications')
        if notifications_service:
            self.stage.fire_file_upload_notification(notifications_service)

        return uploaded_file
 def question_ids_values_provider(self):
     not_selected = {
         "display_name": _(u"--- Not selected ---"), "value": self.DEFAULT_QUESTION_ID_VALUE
     }
     question_values = [
         {"display_name": question.title, "value": question.question_id}
         for question in self.activity_questions
     ]
     return [not_selected] + question_values
 def validate_field_data(self, validation, data):
     super(GroupActivityXBlock, self).validate_field_data(validation, data)
     should_be_ints = ('weight', 'group_reviews_required_count', 'user_review_count')
     for field_name in should_be_ints:
         try:
             int(getattr(data, field_name))
         except (TypeError, ValueError):
             message = _(u"{field_name} must be integer, {field_value} given").format(
                 field_name=field_name, field_value=getattr(data, field_name)
             )
             validation.add(ValidationMessage(ValidationMessage.ERROR, message))
class BasicStage(SimpleCompletionStageMixin, BaseGroupActivityStage):

    display_name = String(
        display_name=DISPLAY_NAME_NAME,
        help=DISPLAY_NAME_HELP,
        scope=Scope.content,
        default=_(u"Text Stage")
    )

    CATEGORY = 'gp-v2-stage-basic'

    NAVIGATION_LABEL = _(u'Overview')
    STUDIO_LABEL = _(u"Text")

    def student_view(self, context):
        fragment = super(BasicStage, self).student_view(context)

        if self.can_mark_complete:
            self.mark_complete()

        return fragment
Exemple #6
0
class SubmissionsViewXBlock(ProjectNavigatorViewXBlockBase):
    """
    Submissions View - displays submissions grouped by Activity. Allows uploading new files and downloading
    earlier uploads
    """
    CATEGORY = "gp-v2-navigator-submissions"
    STUDIO_LABEL = _(u"Submissions View")
    STUDENT_VIEW_TITLE = _(u"Upload")
    type = ViewTypes.SUBMISSIONS
    icon = u"fa fa-upload"
    display_name_with_default = STUDIO_LABEL

    SORT_ORDER = 1

    template = "submissions_view.html"
    css_file = "submissions_view.css"
    js_file = "submissions_view.js"
    initialize_js_function = "GroupProjectNavigatorSubmissionsView"
    additional_js_files = ('public/js/vendor/jquery.ui.widget.js',
                           'public/js/vendor/jquery.fileupload.js',
                           'public/js/vendor/jquery.iframe-transport.js')

    @property
    def allow_admin_grader_access(self):
        return True

    def student_view(self, context):
        """
        Student view
        """
        activity_fragments = []
        for activity in self.navigator.group_project.activities:
            activity_fragment = activity.render("submissions_view", context)
            activity_fragments.append(activity_fragment)

        context = {
            'view': self,
            'activity_contents': [frag.content for frag in activity_fragments]
        }
        return self.render_student_view(context, activity_fragments)
Exemple #7
0
class AskTAViewXBlock(ProjectNavigatorViewXBlockBase):
    """
    Ask a TA view - displays  a form to send message to Teaching Assistant
    """
    CATEGORY = "gp-v2-navigator-ask-ta"
    STUDIO_LABEL = _(u"Ask a TA View")
    STUDENT_VIEW_TITLE = _(u"Ask a McKinsey TA")
    type = ViewTypes.ASK_TA
    selector_text = u"TA"
    display_name_with_default = STUDIO_LABEL

    SORT_ORDER = 4

    template = "ask_ta_view.html"
    css_file = "ask_ta_view.css"
    js_file = "ask_ta_view.js"
    initialize_js_function = "GroupProjectNavigatorAskTAView"

    @property
    def allow_admin_grader_access(self):
        return False

    @classmethod
    def is_view_type_available(cls):
        # TODO: LMS support - check if TAs are available at all
        return True

    def student_view(self, context):
        """
        Student view
        """
        img_url = self.runtime.local_resource_url(self,
                                                  "public/img/ask_ta.png")
        context = {
            'view': self,
            'course_id': self.course_id,
            'img_url': img_url
        }
        return self.render_student_view(context)
Exemple #8
0
class BaseGroupProjectResourceXBlock(BaseStageComponentXBlock,
                                     StudioEditableXBlockMixin,
                                     XBlockWithPreviewMixin):
    display_name = String(display_name=_(u"Display Name"),
                          help=_(U"This is a name of the resource"),
                          scope=Scope.settings,
                          default=_(u"Group Project V2 Resource"))

    description = String(display_name=_(u"Resource Description"),
                         scope=Scope.settings)

    editable_fields = ('display_name', 'description')

    def student_view(self, _context):  # pylint: disable=no-self-use
        return Fragment()

    def resources_view(self, context):
        fragment = Fragment()
        render_context = {'resource': self}
        render_context.update(context)
        fragment.add_content(
            loader.render_template(self.PROJECT_NAVIGATOR_VIEW_TEMPLATE,
                                   render_context))
        return fragment
class FeedbackDisplayBaseStage(SimpleCompletionStageMixin, BaseGroupActivityStage):
    NAVIGATION_LABEL = _(u'Review')
    FEEDBACK_DISPLAY_BLOCK_CATEGORY = None

    @property
    def feedback_display_blocks(self):
        return self.get_children_by_category(self.FEEDBACK_DISPLAY_BLOCK_CATEGORY)

    @lazy
    def required_questions(self):
        return [
            feedback_display.question_id for feedback_display in self.feedback_display_blocks
            if feedback_display.question and feedback_display.question.required
        ]

    def get_reviewer_ids(self):
        raise NotImplementedError(MUST_BE_OVERRIDDEN)

    def get_reviews(self):
        raise NotImplementedError(MUST_BE_OVERRIDDEN)

    def _make_review_keys(self, review_items):
        return [(self.real_user_id(item['reviewer']), item['question']) for item in review_items]

    def validate(self):
        violations = super(FeedbackDisplayBaseStage, self).validate()

        if not self.feedback_display_blocks:
            violations.add(ValidationMessage(
                ValidationMessage.ERROR,
                self._(messages.FEEDBACK_BLOCKS_ARE_MISSING).format(
                    class_name=self.__class__.__name__, stage_title=self._(self.display_name)
                )
            ))

        return violations

    def student_view(self, context):
        fragment = super(FeedbackDisplayBaseStage, self).student_view(context)

        # TODO: should probably check for all reviews to be ready
        if self.can_mark_complete:
            self.mark_complete()

        return fragment
Exemple #10
0
class GroupProjectGradeEvaluationDisplayXBlock(
        GroupProjectBaseFeedbackDisplayXBlock):
    CATEGORY = "gp-v2-group-assessment"
    STUDIO_LABEL = _(u"Grade Evaluation Display")

    @property
    def activity_questions(self):
        return self.stage.activity.peer_review_questions

    def get_feedback(self):
        all_feedback = self.project_api.get_workgroup_review_items_for_group(
            self.group_id,
            self.stage.activity_content_id,
        )
        return [
            item for item in all_feedback
            if item["question"] == self.question_id
        ]
Exemple #11
0
class PrivateDiscussionViewXBlock(ProjectNavigatorViewXBlockBase):
    CATEGORY = 'gp-v2-navigator-private-discussion'
    STUDIO_LABEL = _(u"Private Discussion View")
    type = ViewTypes.PRIVATE_DISCUSSION
    icon = 'fa fa-comment'
    skip_content = True  # there're no content in this view so far - it only shows discussion in a popup
    display_name_with_default = STUDIO_LABEL

    SORT_ORDER = 3

    js_file = "private_discussion_view.js"
    initialize_js_function = "GroupProjectPrivateDiscussionView"

    def _project_has_discussion(self):
        return self.navigator.group_project.has_child_of_category(
            DiscussionXBlockShim.CATEGORY)

    @property
    def is_view_available(self):
        return self._project_has_discussion()

    def selector_view(self, context):
        """
        Selector view - this view is used by GroupProjectNavigatorXBlock to render selector buttons
        """
        fragment = super(PrivateDiscussionViewXBlock,
                         self).selector_view(context)
        add_resource(self, 'javascript', self.JS_BASE + self.js_file, fragment)
        fragment.initialize_js(self.initialize_js_function)
        return fragment

    def validate(self):
        validation = super(PrivateDiscussionViewXBlock, self).validate()
        if not self._project_has_discussion():
            validation.add(
                ValidationMessage(
                    ValidationMessage.WARNING,
                    self._(messages.NO_DISCUSSION_IN_GROUP_PROJECT).format(
                        block_type=self._(self.STUDIO_LABEL))))

        return validation
class ProjectTeamXBlock(
        BaseStageComponentXBlock,
        XBlockWithPreviewMixin,
        NoStudioEditableSettingsMixin,
        StudioEditableXBlockMixin,
):
    CATEGORY = 'gp-v2-project-team'
    STUDIO_LABEL = _(u"Project Team")

    display_name_with_default = STUDIO_LABEL

    def student_view(self, context):
        fragment = Fragment()
        # Could be a TA not in the group.
        if self.stage.is_group_member:
            user_details = [
                self.stage.project_api.get_member_data(self.stage.user_id)
            ]
        else:
            user_details = []
        render_context = {
            'team_members': user_details + self.stage.team_members,
            'course_id': self.stage.course_id,
            'group_id': self.stage.workgroup.id
        }
        render_context.update(context)

        fragment.add_content(
            loader.render_django_template(
                "templates/html/components/project_team.html",
                render_context,
                i18n_service=self.i18n_service,
            ))
        add_resource(self, 'css', "public/css/components/project_team.css",
                     fragment)
        add_resource(self, 'javascript',
                     "public/js/components/project_team.js", fragment)
        fragment.initialize_js("ProjectTeamXBlock")
        return fragment
    def student_view(self, context):
        if self.question is None:
            return Fragment(messages.COMPONENT_MISCONFIGURED)

        raw_feedback = self.get_feedback()

        feedback = []
        for item in raw_feedback:
            feedback.append(html.escape(item['answer']))

        fragment = Fragment()
        title = self.question.assessment_title if self.question.assessment_title else self.question.title
        render_context = {'assessment': self, 'question_title': title, 'feedback': feedback}
        if self.show_mean:
            try:
                render_context['mean'] = "{0:.1f}".format(mean(feedback))
            except ValueError as exc:
                log.warn(exc)
                render_context['mean'] = _(u"N/A")

        render_context.update(context)
        fragment.add_content(loader.render_template("templates/html/components/review_assessment.html", render_context))
        return fragment
Exemple #14
0
 def display_name_with_default(self):
     return self.title or _(u"Review Question")
Exemple #15
0
class GroupProjectReviewQuestionXBlock(BaseStageComponentXBlock,
                                       StudioEditableXBlockMixin,
                                       XBlockWithPreviewMixin):
    CATEGORY = "gp-v2-review-question"
    STUDIO_LABEL = _(u"Review Question")

    @property
    def display_name_with_default(self):
        return self.title or _(u"Review Question")

    question_id = String(display_name=_(u"Question ID"),
                         default=UNIQUE_ID,
                         scope=Scope.content,
                         force_export=True)

    title = String(display_name=_(u"Question Text"),
                   default=_(u""),
                   scope=Scope.content)

    assessment_title = String(
        display_name=_(u"Assessment Question Text"),
        help=_(u"Overrides question title when displayed in assessment mode"),
        default=None,
        scope=Scope.content)

    question_content = String(display_name=_(u"Question Content"),
                              help=_(u"HTML control"),
                              default=_(u""),
                              scope=Scope.content,
                              multiline_editor="xml",
                              xml_node=True)

    required = Boolean(display_name=_(u"Required"),
                       default=False,
                       scope=Scope.content)

    grade = Boolean(
        display_name=_(u"Grading"),
        help=
        _(u"IF True, answers to this question will be used to calculate student grade for Group Project."
          ),
        default=False,
        scope=Scope.content)

    single_line = Boolean(
        display_name=_(u"Single Line"),
        help=_(
            u"If True question label and content will be displayed on single line, allowing for more compact layout."
            u"Only affects presentation."),
        default=False,
        scope=Scope.content)

    question_css_classes = String(
        display_name=_(u"CSS Classes"),
        help=
        _(u"CSS classes to be set on question element. Only affects presentation."
          ),
        scope=Scope.content)

    editable_fields = ("question_id", "title", "assessment_title",
                       "question_content", "required", "grade", "single_line",
                       "question_css_classes")
    has_author_view = True

    @lazy
    def stage(self):
        return self.get_parent()

    def render_content(self):
        try:
            answer_node = ElementTree.fromstring(self.question_content)
        except ElementTree.ParseError:
            message_tpl = "Exception when parsing question content for question {question_id}. Content is [{content}]."
            message_tpl.format(question_id=self.question_id,
                               content=self.question_content)
            log.exception(message_tpl)
            return ""

        answer_node.set('name', self.question_id)
        answer_node.set('id', self.question_id)
        current_class = answer_node.get('class')
        answer_classes = ['answer']
        if current_class:
            answer_classes.append(current_class)
        if self.single_line:
            answer_classes.append('side')
        if self.stage.is_closed:
            answer_node.set('disabled', 'disabled')
        else:
            answer_classes.append('editable')
        answer_node.set('class', ' '.join(answer_classes))

        return outer_html(answer_node)

    def student_view(self, context):
        question_classes = ["question"]
        if self.required:
            question_classes.append("required")
        if self.question_css_classes:
            question_classes.append(self.question_css_classes)

        fragment = Fragment()
        render_context = {
            'question': self,
            'question_classes': " ".join(question_classes),
            'question_content': self.render_content()
        }
        render_context.update(context)
        fragment.add_content(
            loader.render_template(
                "templates/html/components/review_question.html",
                render_context))
        return fragment

    def studio_view(self, context):
        fragment = super(GroupProjectReviewQuestionXBlock,
                         self).studio_view(context)

        # TODO: StudioEditableXBlockMixin should really support Codemirror XML editor
        add_resource(self, 'css', "public/css/components/question_edit.css",
                     fragment)
        add_resource(self, 'javascript',
                     "public/js/components/question_edit.js", fragment)
        fragment.initialize_js("GroupProjectQuestionEdit")
        return fragment

    def author_view(self, context):
        fragment = self.student_view(context)
        add_resource(self, 'css', "public/css/components/question_edit.css",
                     fragment)
        return fragment
Exemple #16
0
class PeerReviewStage(ReviewBaseStage):
    display_name = String(display_name=DISPLAY_NAME_NAME,
                          help=DISPLAY_NAME_HELP,
                          scope=Scope.content,
                          default=_(u"Peer Grading Stage"))

    CATEGORY = 'gp-v2-stage-peer-review'
    STAGE_CONTENT_TEMPLATE = 'templates/html/stages/peer_review.html'

    EXTERNAL_STATUSES_LABEL_MAPPING = {
        StageState.NOT_STARTED: _("Pending Review"),
        StageState.INCOMPLETE: _("Pending Review"),
        StageState.COMPLETED: _("Review Complete"),
    }
    DEFAULT_EXTERNAL_STATUS_LABEL = _("Unknown")

    STUDIO_LABEL = _(u"Peer Grading")

    REVIEW_ITEM_KEY = "workgroup"

    @property
    def allowed_nested_blocks(self):
        blocks = super(PeerReviewStage, self).allowed_nested_blocks
        blocks.extend([GroupSelectorXBlock])
        return blocks

    @property
    def allow_admin_grader_access(self):
        return True

    @property
    def is_graded_stage(self):
        return True

    @property
    def available_to_current_user(self):
        if not super(PeerReviewStage, self).available_to_current_user:
            return False

        if not self.is_admin_grader and self.activity.is_ta_graded:
            return False

        return True

    @property
    def can_mark_complete(self):
        if self.is_admin_grader:
            return True
        return super(PeerReviewStage, self).can_mark_complete

    @lazy
    def review_subjects(self):
        """
        Returns groups to review. May throw `class`: OutsiderDisallowedError
        :rtype: list[group_project_v2.project_api.dtos.WorkgroupDetails]
        """
        if self.is_admin_grader:
            return [self.workgroup]

        try:
            return self.get_review_subjects(self.user_id)
        except ApiError:
            log.exception(
                "Error obtaining list of groups to grade - assuming no groups to grade"
            )
            return []

    @property
    def review_groups(self):
        return self.review_subjects

    def get_review_subjects(self, user_id):
        """
        Gets a list of workgroups to review for selected user
        :param int user_id: User ID
        :return: list[WorkgroupDetails]
        """
        return self.project_api.get_workgroups_to_review(
            user_id, self.course_id, self.activity_content_id)

    def _get_review_items(self, review_groups, with_caching=False):
        """
        Gets review items for a list of groups
        :param collections.Iterable[group_project_v2.project_api.dtos.WorkgroupDetails] review_groups: Target groups
        :param bool with_caching:
            Underlying implementation uses get_workgroup_review_items_for_group for both cached and non-cached version.
            However, one of the users of this method (get_review_data) might benefit from caching,
            while the other (review_status) is affected by the issue outlined in get_workgroup_review_items_for_group
            comment (i.e. cached value is not updated when new feedback is posted).
            So, caching is conditionally enabled here to serve both users of this method as efficiently as possible.
        :return:
        """
        def do_get_items(group_id):
            if with_caching:
                return self._get_review_items_for_group(
                    self.project_api, group_id, self.activity_content_id)
            else:
                return self.project_api.get_workgroup_review_items_for_group(
                    group_id, self.activity_content_id)

        return list(
            itertools.chain.from_iterable(
                do_get_items(group.id) for group in review_groups))

    def review_status(self):
        review_subjects_ids = [group.id for group in self.review_groups]
        all_review_items = self._get_review_items(self.review_groups,
                                                  with_caching=False)
        review_items = [
            item for item in all_review_items
            if item['reviewer'] == self.anonymous_student_id
        ]

        return self._calculate_review_status(review_subjects_ids, review_items)

    def get_review_state(self, review_subject_id):
        review_items = self.project_api.get_workgroup_review_items(
            self.anonymous_student_id, review_subject_id,
            self.activity_content_id)
        return self._calculate_review_status([review_subject_id], review_items)

    def _get_ta_reviews(self, target_workgroup):
        review_items = self._get_review_items([target_workgroup],
                                              with_caching=True)

        grouped_items = defaultdict(list)
        for item in review_items:
            grouped_items[item['reviewer']].append(item)

        ta_reviews = {
            reviewer: items
            for reviewer, items in grouped_items.iteritems()
            if self.is_user_ta(self.real_user_id(reviewer), self.course_id)
        }
        return ta_reviews

    def get_external_group_status(self, group):
        """
        Calculates external group status for the Stage.
        For Peer Grading stage, external status means "have students in other groups provided grades to this group?"
        :param group_project_v2.project_api.dtos.WorkgroupDetails group: workgroup
        :rtype: StageState
        """
        if not self.activity.is_ta_graded:
            reviewer_ids = [
                user['id']
                for user in self.project_api.get_workgroup_reviewers(
                    group.id, self.activity_content_id)
            ]
            reviews_for_group = self._get_review_items([group],
                                                       with_caching=True)
            review_results = [
                self._calculate_review_status([group.id],
                                              self._get_reviews_by_user(
                                                  reviews_for_group,
                                                  reviewer_id))
                for reviewer_id in reviewer_ids
            ]
            # if review_results is empty (e.g. no reviewers are configured) all will return True, and any will return
            # False. It would result in a "broken" state (has_all, but not has_some) - it is cleared later
            has_some = any(status != ReviewState.NOT_STARTED
                           for status in review_results)
            has_all = all(status == ReviewState.COMPLETED
                          for status in review_results)
        else:
            ta_reviews = self._get_ta_reviews(group)
            review_results = [
                self._calculate_review_status([group.id], ta_review_items)
                for ta_review_items in ta_reviews.values()
            ]
            has_some = any(status != ReviewState.NOT_STARTED
                           for status in review_results)
            # any completed TA review counts as "stage completed"
            has_all = any(status == ReviewState.COMPLETED
                          for status in review_results)

        has_all = has_some and has_all  # has_all should never be True if has_some is False
        review_state = self.REVIEW_STATE_CONDITIONS.get((has_some, has_all))
        return self.STAGE_STATE_REVIEW_STATE_MAPPING.get(review_state)

    def get_review_data(self, user_id):
        """
        :param int user_id:
        :rtype: (set[int], dict)
        """
        review_subjects = self.get_review_subjects(user_id)
        review_items = self._get_review_items(review_subjects,
                                              with_caching=True)
        reviews_by_user = self._get_reviews_by_user(review_items, user_id)
        return set(group.id for group in review_subjects), reviews_by_user

    @staticmethod
    @memoize_with_expiration()
    def _get_review_items_for_group(project_api, workgroup_id,
                                    activity_content_id):
        return project_api.get_workgroup_review_items_for_group(
            workgroup_id, activity_content_id)

    def validate(self):
        violations = super(PeerReviewStage, self).validate()

        if not self.grade_questions:
            violations.add(
                ValidationMessage(
                    ValidationMessage.ERROR,
                    messages.GRADED_QUESTIONS_ARE_REQUIRED.format(
                        class_name=self.STUDIO_LABEL,
                        stage_title=self.display_name)))

        if not self.has_child_of_category(GroupSelectorXBlock.CATEGORY):
            violations.add(
                ValidationMessage(
                    ValidationMessage.ERROR,
                    messages.GROUP_SELECTOR_BLOCK_IS_MISSING.format(
                        class_name=self.STUDIO_LABEL,
                        stage_title=self.display_name,
                        group_selector_class_name=GroupSelectorXBlock.
                        STUDIO_LABEL)))

        return violations

    def get_stage_state(self):
        if not self.review_subjects:
            return StageState.NOT_STARTED

        return super(PeerReviewStage, self).get_stage_state()

    @XBlock.handler
    @groupwork_protected_handler
    @key_error_protected_handler
    @conversion_protected_handler
    def other_submission_links(self, request, _suffix=''):
        group_id = int(request.GET["group_id"])

        target_stages = [
            stage for stage in self.activity.stages if stage.submissions_stage
        ]

        submission_stage_contents = []
        for stage in target_stages:
            stage_fragment = stage.render('review_submissions_view',
                                          {'group_id': group_id})
            submission_stage_contents.append(stage_fragment.content)

        context = {
            'block': self,
            'submission_stage_contents': submission_stage_contents
        }
        html_output = loader.render_template(
            '/templates/html/stages/stages_group_review_other_team_submissions.html',
            context)

        return webob.response.Response(body=json.dumps({"html": html_output}))

    @XBlock.handler
    @groupwork_protected_handler
    @key_error_protected_handler
    def load_other_group_feedback(self, request, _suffix=''):
        group_id = int(request.GET["group_id"])
        feedback = self.project_api.get_workgroup_review_items(
            self.anonymous_student_id, group_id, self.activity_content_id)
        results = self._pivot_feedback(feedback)

        return webob.response.Response(body=json.dumps(results))

    def do_submit_review(self, submissions):
        group_id = int(submissions["review_subject_id"])
        del submissions["review_subject_id"]

        self.project_api.submit_workgroup_review_items(
            self.anonymous_student_id, group_id, self.activity_content_id,
            submissions)

        for question_id in self.grade_questions:
            if question_id in submissions:
                # Emit analytics event...
                self.runtime.publish(
                    self, "group_activity.received_grade_question_score", {
                        "question": question_id,
                        "answer": submissions[question_id],
                        "reviewer_id": self.anonymous_student_id,
                        "is_admin_grader": self.is_admin_grader,
                        "group_id": group_id,
                        "content_id": self.activity_content_id,
                    })

        self.activity.calculate_and_send_grade(group_id)
Exemple #17
0
class GroupProjectSubmissionXBlock(BaseStageComponentXBlock,
                                   ProjectAPIXBlockMixin,
                                   StudioEditableXBlockMixin,
                                   XBlockWithPreviewMixin):
    CATEGORY = "gp-v2-submission"
    STUDIO_LABEL = _(u"Submission")
    PROJECT_NAVIGATOR_VIEW_TEMPLATE = 'templates/html/components/submission_navigator_view.html'
    REVIEW_VIEW_TEMPLATE = 'templates/html/components/submission_review_view.html'

    display_name = String(display_name=_(u"Display Name"),
                          help=_(U"This is a name of the submission"),
                          scope=Scope.settings,
                          default=_(u"Group Project V2 Submission"))

    description = String(display_name=_(u"Submission Description"),
                         scope=Scope.settings)

    upload_id = String(
        display_name=_(u"Upload ID"),
        help=_(
            U"This string is used as an identifier for an upload. "
            U"Submissions sharing the same Upload ID will be updated simultaneously"
        ),
    )

    editable_fields = ('display_name', 'description', 'upload_id')

    SUBMISSION_RECEIVED_EVENT = "activity.received_submission"

    # TODO: Make configurable via XBlock settings
    DEFAULT_FILE_FILTERS = {
        "mime-types": (
            # Images
            "image/png",
            "image/jpeg",
            "image/tiff",
            # Excel
            "application/vnd.ms-excel",
            "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
            # Word
            "application/msword",
            "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
            # PowerPoint
            "application/vnd.ms-powerpoint",
            "application/vnd.openxmlformats-officedocument.presentationml.presentation",
            # PDF
            "application/pdf"),
        "extensions": (
            "png",
            "jpg",
            "jpeg",
            "tif",
            "tiff",
            "doc",
            "docx",
            "xls",
            "xlsx",
            "ppt",
            "pptx",
            "pdf",
        )
    }

    validator = FileTypeValidator(
        allowed_types=DEFAULT_FILE_FILTERS["mime-types"],
        allowed_extensions=[
            ".{}".format(ext) for ext in DEFAULT_FILE_FILTERS["extensions"]
        ])

    def get_upload(self, group_id):
        submission_map = self.project_api.get_latest_workgroup_submissions_by_id(
            group_id)
        submission_data = submission_map.get(self.upload_id, None)

        if submission_data is None:
            return None

        document_signed_url = make_s3_link_temporary(
            submission_data.get('workgroup'),
            submission_data['document_url'].split('/')[-2],
            submission_data['document_filename'],
            submission_data["document_url"])

        return SubmissionUpload(
            document_signed_url, submission_data["document_filename"],
            format_date(build_date_field(submission_data["modified"])),
            submission_data.get("user_details", None))

    @property
    def upload(self):
        return self.get_upload(self.stage.activity.workgroup.id)

    def student_view(self, _context):  # pylint: disable=no-self-use
        return Fragment()

    def submissions_view(self, context):
        fragment = Fragment()
        uploading_allowed = (
            self.stage.available_now
            and self.stage.is_group_member) or self.stage.is_admin_grader
        render_context = {
            'submission': self,
            'upload': self.upload,
            'disabled': not uploading_allowed
        }
        render_context.update(context)
        fragment.add_content(
            loader.render_template(self.PROJECT_NAVIGATOR_VIEW_TEMPLATE,
                                   render_context))
        add_resource(self, 'javascript', 'public/js/components/submission.js',
                     fragment)
        fragment.initialize_js("GroupProjectSubmissionBlock")
        return fragment

    def submission_review_view(self, context):
        group_id = context.get('group_id', self.stage.activity.workgroup.id)
        fragment = Fragment()
        render_context = {
            'submission': self,
            'upload': self.get_upload(group_id)
        }
        render_context.update(context)
        fragment.add_content(
            loader.render_template(self.REVIEW_VIEW_TEMPLATE, render_context))
        # NOTE: adding js/css likely won't work here, as the result of this view is added as an HTML to an existing DOM
        # element
        return fragment

    def _validate_upload(self, request):
        if not self.stage.available_now:
            template = messages.STAGE_NOT_OPEN_TEMPLATE if not self.stage.is_open else messages.STAGE_CLOSED_TEMPLATE
            # 422 = unprocessable entity
            return 422, {
                'result': 'error',
                'message': template.format(action=self.stage.STAGE_ACTION)
            }

        if not self.stage.is_group_member and not self.stage.is_admin_grader:
            # 403 - forbidden
            return 403, {
                'result': 'error',
                'message': messages.NON_GROUP_MEMBER_UPLOAD
            }

        try:
            self.validator(request.params[self.upload_id].file)
        except ValidationError as validationError:
            message = validationError.message % validationError.params
            # 400 - BAD REQUEST
            return 400, {'result': 'error', 'message': message}

        return None, None

    @XBlock.handler
    def upload_submission(self, request, _suffix=''):
        """
        Handles submission upload and marks stage as completed if all submissions in stage have uploads.
        :param request: HTTP request
        :param str _suffix:
        """
        failure_code, response_data = self._validate_upload(request)

        if failure_code is None and response_data is None:
            target_activity = self.stage.activity
            response_data = {
                "title":
                messages.SUCCESSFUL_UPLOAD_TITLE,
                "message":
                messages.SUCCESSFUL_UPLOAD_MESSAGE_TPL.format(
                    icon='fa fa-paperclip')
            }
            failure_code = 0
            try:
                context = {
                    "user_id": target_activity.user_id,
                    "group_id": target_activity.workgroup.id,
                    "project_api": self.project_api,
                    "course_id": target_activity.course_id
                }

                uploaded_file = self.persist_and_submit_file(
                    target_activity, context,
                    request.params[self.upload_id].file)

                response_data["submissions"] = {
                    uploaded_file.submission_id:
                    make_s3_link_temporary(
                        uploaded_file.group_id,
                        uploaded_file.sha1,
                        uploaded_file.file.name,
                        uploaded_file.file_url,
                    )
                }

                self.stage.check_submissions_and_mark_complete()
                response_data["new_stage_states"] = [
                    self.stage.get_new_stage_state_data()
                ]

                response_data[
                    'user_label'] = self.project_api.get_user_details(
                        target_activity.user_id).user_label
                response_data['submission_date'] = format_date(date.today())

            except Exception as exception:  # pylint: disable=broad-except
                log.exception(exception)
                failure_code = 500
                if isinstance(exception, ApiError):
                    failure_code = exception.code
                error_message = getattr(exception, "message",
                                        messages.UNKNOWN_ERROR)

                response_data.update({
                    "title":
                    messages.FAILED_UPLOAD_TITLE,
                    "message":
                    messages.FAILED_UPLOAD_MESSAGE_TPL.format(
                        error_goes_here=error_message)
                })

        response = webob.response.Response(body=json.dumps(response_data))
        if failure_code:
            response.status_code = failure_code

        return response

    def persist_and_submit_file(self, activity, context, file_stream):
        """
        Saves uploaded files to their permanent location, sends them to submissions backend and emits submission events
        """
        uploaded_file = UploadFile(file_stream, self.upload_id, context)

        # Save the files first
        try:
            uploaded_file.save_file()
        except Exception as save_file_error:  # pylint: disable=broad-except
            original_message = save_file_error.message if hasattr(
                save_file_error, "message") else ""
            save_file_error.message = _("Error storing file {} - {}").format(
                uploaded_file.file.name, original_message)
            raise

        # It have been saved... note the submission
        try:
            uploaded_file.submit()
            # Emit analytics event...
            self.runtime.publish(
                self, self.SUBMISSION_RECEIVED_EVENT, {
                    "submission_id": uploaded_file.submission_id,
                    "filename": uploaded_file.file.name,
                    "content_id": activity.content_id,
                    "group_id": activity.workgroup.id,
                    "user_id": activity.user_id,
                })
        except Exception as save_record_error:  # pylint: disable=broad-except
            original_message = save_record_error.message if hasattr(
                save_record_error, "message") else ""
            save_record_error.message = _(
                "Error recording file information {} - {}").format(
                    uploaded_file.file.name, original_message)
            raise

        # See if the xBlock Notification Service is available, and - if so -
        # dispatch a notification to the entire workgroup that a file has been uploaded
        # Note that the NotificationService can be disabled, so it might not be available
        # in the list of services
        notifications_service = self.runtime.service(self, 'notifications')
        if notifications_service:
            self.stage.fire_file_upload_notification(notifications_service)

        return uploaded_file
class SubmissionStage(BaseGroupActivityStage):
    display_name = String(display_name=DISPLAY_NAME_NAME,
                          help=DISPLAY_NAME_HELP,
                          scope=Scope.content,
                          default=_(u"Submission Stage"))

    CATEGORY = 'gp-v2-stage-submission'

    NAVIGATION_LABEL = _(u'Task')
    STUDIO_LABEL = _(u"Deliverable")

    EXTERNAL_STATUSES_LABEL_MAPPING = {
        StageState.NOT_STARTED: _("Pending Upload"),
        StageState.INCOMPLETE: _("Pending Upload"),
        StageState.COMPLETED: _("Uploaded"),
    }
    DEFAULT_EXTERNAL_STATUS_LABEL = _("Unknown")

    submissions_stage = True

    STAGE_ACTION = _(u"upload submission")

    @property
    def allowed_nested_blocks(self):
        blocks = super(SubmissionStage, self).allowed_nested_blocks
        blocks.extend(
            [SubmissionsStaticContentXBlock, GroupProjectSubmissionXBlock])
        return blocks

    @property
    def is_graded_stage(self):
        return True

    @property
    def submissions(self):
        """
        :rtype: collections.Iterable[GroupProjectSubmissionXBlock]
        """
        return self.get_children_by_category(
            GroupProjectSubmissionXBlock.CATEGORY)

    @property
    def is_upload_available(self):
        return self.submissions and self.is_open and not self.is_closed

    @property
    def has_submissions(self):
        return bool(
            self.submissions
        )  # explicitly converting to bool to indicate that it is bool property

    def validate(self):
        violations = super(SubmissionStage, self).validate()

        if not self.submissions:
            violations.add(
                ValidationMessage(
                    ValidationMessage.ERROR,
                    messages.SUBMISSIONS_BLOCKS_ARE_MISSING.format(
                        class_name=self.__class__.__name__,
                        stage_title=self.display_name)))

        return violations

    @property
    def has_some_submissions(self):
        return any(submission.upload is not None
                   for submission in self.submissions)

    @property
    def has_all_submissions(self):
        return all(submission.upload is not None
                   for submission in self.submissions)

    def check_submissions_and_mark_complete(self):
        if self.has_all_submissions:
            for user in self.workgroup.users:
                self.mark_complete(user.id)

    def get_stage_state(self):
        if self.has_all_submissions:
            return StageState.COMPLETED
        elif self.has_some_submissions:
            return StageState.INCOMPLETE
        else:
            return StageState.NOT_STARTED

    def _render_view(self, child_view, template, context):
        fragment = Fragment()

        submission_contents = []
        for submission in self.submissions:
            submission_fragment = submission.render(child_view, context)
            fragment.add_frag_resources(submission_fragment)
            submission_contents.append(submission_fragment.content)

        context = {'stage': self, 'submission_contents': submission_contents}
        fragment.add_content(loader.render_template(template, context))

        return fragment

    def review_submissions_view(self, context):
        # transparently passing group_id via context
        return self._render_view(
            'submission_review_view',
            "templates/html/stages/submissions_review_view.html", context)

    def get_users_completion(self, target_workgroups, target_users):
        """
        Returns sets of completed user ids and partially completed user ids
        :param collections.Iterable[group_project_v2.project_api.dtos.WorkgroupDetails] target_workgroups:
        :param collections.Iterable[group_project_v2.project_api.dtos.ReducedUserDetails] target_users:
        :rtype: (set[int], set[int])
        """
        completed_users = []
        partially_completed_users = []
        for group in target_workgroups:
            group_stage_state = self.get_external_group_status(group)
            workgroup_user_ids = [user.id for user in group.users]

            if group_stage_state == StageState.COMPLETED:
                completed_users.extend(workgroup_user_ids)
            if group_stage_state == StageState.INCOMPLETE:
                partially_completed_users.extend(workgroup_user_ids)

        return set(completed_users), set(
            partially_completed_users)  # removing duplicates - just in case

    def get_external_group_status(self, group):
        """
        Calculates external group status for the Stage.
        For Submissions stage, external status is the same as internal one: "have workgroup submitted all uploads?"
        :param group_project_v2.project_api.dtos.WorkgroupDetails group: workgroup
        :rtype: StageState
        """
        upload_ids = set(submission.upload_id
                         for submission in self.submissions)
        group_submissions = self.project_api.get_latest_workgroup_submissions_by_id(
            group.id)
        uploaded_submissions = set(group_submissions.keys())

        has_all = uploaded_submissions >= upload_ids
        has_some = bool(uploaded_submissions & upload_ids)

        if has_all:
            return StageState.COMPLETED
        elif has_some:
            return StageState.INCOMPLETE
        else:
            return StageState.NOT_STARTED
from group_project_v2.utils import gettext as _

# Generic messages
UNKNOWN_ERROR = _(u"Unknown error.")
COMPONENT_MISCONFIGURED = _(
    u"This component is misconfigured and can't be displayed. It needs to be fixed by the course authors."
)
STAGE_NOT_OPEN_TEMPLATE = _(u"Can't {action} as stage is not yet opened.")
STAGE_CLOSED_TEMPLATE = _(u"Can't {action} as stage is closed.")

# Group Project XBlock messages
NO_ACTIVITIES = _(u"This Group Project does not contain any activities.")
NO_PROJECT_NAVIGATOR = _(
    u"This Group Project V2 does not contain Project Navigator - please edit course outline "
    u"in Studio to include one."
)
NO_DISCUSSION = _(u"This Group Project V2 does not contain a discussion.")
MUST_CONTAIN_PROJECT_NAVIGATOR_BLOCK = _(u"Group Project must contain Project Navigator Block.")

# Group Activity XBlock messages
NO_STAGES = _(u"This Group Project Activity does not contain any stages.")
SHOULD_BE_INTEGER = _(u"{field_name} must be integer, {field_value} given.")
ASSIGNED_TO_GROUPS_LABEL = _(u"This project is assigned to {group_count} group(s)")  # no full stop (period) by design

# Project Navigator messages
MUST_CONTAIN_NAVIGATION_VIEW = _(u"Project Navigator must contain Navigation view.")
NO_DISCUSSION_IN_GROUP_PROJECT = _(
    u"Parent group project does not contain discussion XBlock - this {block_type} "
    u"will not function properly and will not be displayed to students."
)
 def url_name_caption(self):
     return _(u"url_name to link to this {project_navigator_view}:").format(
         project_navigator_view=self.display_name_with_default
     )
Exemple #21
0
class BaseGroupActivityStage(
        CommonMixinCollection,
        DashboardXBlockMixin,
        XBlockWithPreviewMixin,
        StageNotificationsMixin,
        XBlockWithUrlNameDisplayMixin,
        AdminAccessControlXBlockMixin,
        XBlock,
):
    open_date = DateTime(display_name=_(u"Open Date"),
                         help=_(u"Stage open date"),
                         scope=Scope.settings)

    close_date = DateTime(display_name=_(u"Close Date"),
                          help=_(u"Stage close date"),
                          scope=Scope.settings)

    hide_stage_label = Boolean(
        display_name=_(u"Hide stage type label"),
        help=_(u"If true, hides stage type label in Project Navigator"),
        scope=Scope.settings,
        default=True)

    editable_fields = ('display_name', 'open_date', 'close_date',
                       'hide_stage_label')
    has_children = True
    has_score = False  # TODO: Group project V1 are graded at activity level. Check if we need to follow that
    submissions_stage = False

    CATEGORY = None
    STAGE_WRAPPER_TEMPLATE = 'templates/html/stages/stage_wrapper.html'
    STAGE_CONTENT_TEMPLATE = 'templates/html/stages/default_view.html'

    NAVIGATION_LABEL = None
    STUDIO_LABEL = _(u"Stage")
    EXTERNAL_STATUSES_LABEL_MAPPING = {}
    DEFAULT_EXTERNAL_STATUS_LABEL = ""

    js_file = None
    js_init = None

    template_location = 'stages'

    @property
    def id(self):
        return self.scope_ids.usage_id

    @property
    def navigation_label(self):
        return self._(self.NAVIGATION_LABEL)

    @property
    def allowed_nested_blocks(self):  # pylint: disable=no-self-use
        """
        This property outputs an ordered dictionary of allowed nested XBlocks in form of block_category: block_caption.
        """
        blocks = [HtmlXBlockShim, GroupProjectResourceXBlock]
        if GroupProjectVideoResourceXBlock.is_available():
            blocks.append(GroupProjectVideoResourceXBlock)
        blocks.append(ProjectTeamXBlock)
        return blocks

    @lazy
    def activity(self):
        """
        :rtype: group_project_v2.group_project.GroupActivityXBlock
        """
        return self.get_parent()

    @property
    def allow_admin_grader_access(self):
        return False

    @property
    def content_id(self):
        return get_block_content_id(self)

    @property
    def activity_content_id(self):
        return self.activity.content_id

    @property
    def resources(self):
        return self.get_children_by_category(
            GroupProjectResourceXBlock.CATEGORY,
            GroupProjectVideoResourceXBlock.CATEGORY)

    @property
    def team_members(self):
        """
        Returns teammates to review. May throw `class`: OutsiderDisallowedError
        """
        if not self.is_group_member:
            return []

        try:
            result = []
            for team_member in self.workgroup.users:
                team_member_id = team_member.id
                if self.user_id == int(team_member_id):
                    continue
                result.append(self.project_api.get_member_data(team_member_id))
            return result
        except ApiError:
            return []

    @property
    def formatted_open_date(self):
        return format_date(self.open_date)

    @property
    def formatted_close_date(self):
        return format_date(self.close_date)

    @property
    def is_open(self):
        return (self.open_date is None) or (
            self.open_date <= datetime.utcnow().replace(tzinfo=pytz.UTC))

    @property
    def is_closed(self):
        # If this stage is being loaded for the purposes of a TA grading,
        # then we never close the stage - in this way a TA can impose any
        # action necessary even if it has been closed to the group members
        if not self.is_group_member:
            return False

        if self.close_date is None:
            return False

        # A stage is closed at the end of the closing day.

        return self.close_date + timedelta(
            days=1) <= datetime.utcnow().replace(tzinfo=pytz.UTC)

    @property
    def completed(self):
        return self.get_stage_state() == StageState.COMPLETED

    @property
    def available_now(self):
        return self.is_open and not self.is_closed

    @property
    def url_name_caption(self):
        return self._(messages.STAGE_URL_NAME_TEMPLATE).format(
            stage_name=self._(self.STUDIO_LABEL))

    @property
    def can_mark_complete(self):
        return self.available_now and self.is_group_member

    @property
    def is_graded_stage(self):  # pylint: disable=no-self-use
        """
        If a stage is graded it is shown as graded on the the main dashboard, this property also is used by default
        implementation of ``shown_on_detail_view``.

        :rtype: bool
        """
        return False

    @property
    def shown_on_detail_view(self):
        """
        If true details of this stage are shown on the dashboard detail view, by default it returns ``is_graded_stage``.

        :rtype: bool
        """
        return self.is_graded_stage

    @property
    def dashboard_details_view_url(self):
        return self.activity.dashboard_details_url()

    def is_current_stage(self, context):
        target_stage_id = context.get(
            Constants.CURRENT_STAGE_ID_PARAMETER_NAME, None)
        if not target_stage_id:
            return False
        return target_stage_id == self.id

    def _view_render(self, context, view='student_view'):
        stage_fragment = self.get_stage_content_fragment(context, view)

        fragment = Fragment()
        fragment.add_fragment_resources(stage_fragment)
        render_context = {
            'stage': self,
            'stage_content': stage_fragment.content,
            "ta_graded": self.activity.group_reviews_required_count
        }
        fragment.add_content(
            loader.render_django_template(
                self.STAGE_WRAPPER_TEMPLATE,
                render_context,
                i18n_service=self.i18n_service,
            ))
        if stage_fragment.js_init_fn:
            fragment.initialize_js(stage_fragment.js_init_fn)

        return fragment

    @groupwork_protected_view
    def student_view(self, context):
        return self._view_render(context)

    @groupwork_protected_view
    def author_preview_view(self, context):
        # if we use student_view or author_view Studio will wrap it in HTML that we don't want in the preview
        fragment = self._view_render(context, "preview_view")
        url_name_fragment = self.get_url_name_fragment(self.url_name_caption)
        fragment.add_content(url_name_fragment.content)
        fragment.add_fragment_resources(url_name_fragment)
        return fragment

    @groupwork_protected_view
    def author_edit_view(self, context):
        fragment = super(BaseGroupActivityStage,
                         self).author_edit_view(context)
        url_name_fragment = self.get_url_name_fragment(self.url_name_caption)
        fragment.add_content(url_name_fragment.content)
        fragment.add_fragment_resources(url_name_fragment)
        return fragment

    def render_children_fragments(self, context, view='student_view'):
        children_fragments = []
        for child in self._children:
            child_fragment = self._render_child_fragment(child, context, view)
            children_fragments.append(child_fragment)

        return children_fragments

    def get_stage_content_fragment(self, context, view='student_view'):
        fragment = Fragment()
        children_fragments = self.render_children_fragments(context, view=view)
        render_context = {
            'stage': self,
            'children_contents': [frag.content for frag in children_fragments]
        }

        for frag in children_fragments:
            fragment.add_fragment_resources(frag)

        render_context.update(context)
        fragment.add_content(
            loader.render_django_template(
                self.STAGE_CONTENT_TEMPLATE,
                render_context,
                i18n_service=self.i18n_service,
            ))

        if self.js_file:
            add_resource(self, 'javascript', self.js_file, fragment)

        if self.js_init:
            fragment.initialize_js(self.js_init)

        return fragment

    def mark_complete(self, user_id=None):
        user_id = user_id if user_id is not None else self.user_id
        self.runtime.publish(self, 'progress', {'user_id': user_id})

    def get_stage_state(self):
        raise NotImplementedError(MUST_BE_OVERRIDDEN)

    def get_dashboard_stage_state(self, target_workgroups, target_students):
        """
        :param collections.Iterable[group_project_v2.project_api.dtos.WorkgroupDetails] target_workgroups:
        :param collections.Iterable[group_project_v2.project_api.dtos.ReducedUserDetails] target_students:
        :return (str, dict[str, float]): Stage state and detailed stage stats as returned by `get_stage_stats`
        """
        state_stats = self.get_stage_stats(target_workgroups, target_students)
        if state_stats.get(StageState.COMPLETED, 0) == 1:
            stage_state = StageState.COMPLETED
        elif state_stats.get(StageState.INCOMPLETE, 0) > 0 or state_stats.get(
                StageState.COMPLETED, 0) > 0:
            stage_state = StageState.INCOMPLETE
        else:
            stage_state = StageState.NOT_STARTED

        return stage_state, state_stats

    def get_stage_stats(self, target_workgroups, target_students):  # pylint: disable=no-self-use
        """
        Calculates stage state stats for given workgroups and students
        :param collections.Iterable[group_project_v2.project_api.dtos.WorkgroupDetails] target_workgroups:
        :param collections.Iterable[group_project_v2.project_api.dtos.ReducedUserDetails] target_students:
        :return dict[str, float]:
            Percentage of students completed, partially completed and not started the stage as floats in range[0..1]
        """
        target_user_ids = set(user.id for user in target_students)
        if not target_user_ids:
            return {
                StageState.COMPLETED: None,
                StageState.INCOMPLETE: None,
                StageState.NOT_STARTED: None
            }

        target_user_count = float(len(target_user_ids))

        completed_users_ids, partially_completed_users_ids = self.get_users_completion(
            target_workgroups, target_students)
        log_format_data = dict(
            stage=self.display_name,
            target_users=target_user_ids,
            completed=completed_users_ids,
            partially_completed=partially_completed_users_ids)
        log.info(STAGE_STATS_LOG_TPL, log_format_data)

        completed_ratio = len(completed_users_ids
                              & target_user_ids) / target_user_count
        partially_completed_ratio = len(partially_completed_users_ids
                                        & target_user_ids) / target_user_count

        return {
            StageState.COMPLETED: completed_ratio,
            StageState.INCOMPLETE: partially_completed_ratio,
            StageState.NOT_STARTED:
            1 - completed_ratio - partially_completed_ratio
        }

    def get_users_completion(self, target_workgroups, target_users):
        """
        Returns sets of completed user ids and partially completed user ids
        :param collections.Iterable[group_project_v2.project_api.dtos.WorkgroupDetails] target_workgroups:
        :param collections.Iterable[group_project_v2.project_api.dtos.ReducedUserDetails] target_users:
        :rtype: (set[int], set[int])
        """
        raise NotImplementedError(MUST_BE_OVERRIDDEN)

    def get_external_group_status(self, group):  # pylint: disable=unused-argument, no-self-use
        """
        Calculates external group status for the Stage.
        Meaning of external status varies by Stage - see actual implementations docstrings.
        :param group_project_v2.project_api.dtos.WorkgroupDetails group: workgroup
        :rtype: StageState
        """
        return StageState.NOT_AVAILABLE

    def get_external_status_label(self, status):
        """
        Gets human-friendly label for external status.
        Label vary by stage, so consult with actual implementaiton for details
        :param StageState status: external stage status
        :rtype: str
        """
        return self.EXTERNAL_STATUSES_LABEL_MAPPING.get(
            status, self.DEFAULT_EXTERNAL_STATUS_LABEL)

    def navigation_view(self, context):
        """
        Renders stage content for navigation view
        :param dict context:
        :rtype: Fragment
        """
        fragment = Fragment()
        rendering_context = {
            'stage': self,
            'activity_id': self.activity.id,
            'stage_state': self.get_stage_state(),
            'block_link': get_link_to_block(self),
            'is_current_stage': self.is_current_stage(context)
        }
        rendering_context.update(context)
        fragment.add_content(
            loader.render_django_template(
                "templates/html/stages/navigation_view.html",
                rendering_context,
                i18n_service=self.i18n_service,
            ))
        return fragment

    @classmethod
    def make_human_stats(cls, stats):
        """
        Readies stats dictionary for presentation, by sorting it's contents, and converting
        ratios to percentages.
        """
        return OrderedDict([
            (StageState.get_human_name(stage),
             stats[stage] * 100 if stats[stage] is not None else None)
            for stage in (StageState.NOT_STARTED, StageState.INCOMPLETE,
                          StageState.COMPLETED)
        ])

    @AuthXBlockMixin.check_dashboard_access_for_current_user
    def dashboard_view(self, context):
        """
        Renders stage content for dashboard view.
        :param dict context:
        :rtype: Fragment
        """
        fragment = Fragment()

        target_workgroups = context.get(Constants.TARGET_WORKGROUPS)
        target_students = context.get(Constants.TARGET_STUDENTS)
        filtered_students = context.get(Constants.FILTERED_STUDENTS)

        students_to_display = [
            student for student in target_students
            if student.id not in filtered_students
        ]

        state, stats = self.get_dashboard_stage_state(target_workgroups,
                                                      students_to_display)
        human_stats = self.make_human_stats(stats)
        render_context = {
            'stage': self,
            'stats': human_stats,
            'stage_state': state,
            'ta_graded': self.activity.is_ta_graded
        }
        render_context.update(context)
        fragment.add_content(
            self.render_template('dashboard_view', render_context))
        return fragment

    @AuthXBlockMixin.check_dashboard_access_for_current_user
    def dashboard_detail_view(self, context):
        """
        Renders stage header for dashboard details view.
        :param dict context:
        :rtype: Fragment
        """
        fragment = Fragment()
        render_context = {
            'stage':
            self,
            'ta_graded':
            self.activity.is_ta_graded,
            'download_incomplete_emails_handler_url':
            self.get_incomplete_emails_handler_url()
        }
        fragment.add_content(
            self.render_template('dashboard_detail_view', render_context))
        return fragment

    def get_incomplete_emails_handler_url(self):
        base_url = self.runtime.handler_url(self.activity.project,
                                            'download_incomplete_list')
        query_params = {Constants.ACTIVATE_BLOCK_ID_PARAMETER_NAME: self.id}
        return base_url + '?' + urlencode(query_params)

    def get_new_stage_state_data(self):
        return {
            "activity_id": str(self.activity.id),
            "stage_id": str(self.id),
            "state": self.get_stage_state()
        }
Exemple #22
0
class ReviewBaseStage(BaseGroupActivityStage):
    NAVIGATION_LABEL = _(u'Task')

    visited = Boolean(default=False, scope=Scope.user_state)

    js_file = "public/js/stages/review_stage.js"
    js_init = "GroupProjectReviewStage"

    REVIEW_ITEM_KEY = None

    STAGE_ACTION = _(u"save feedback")

    # (has_some, has_all) -> ReviewState. have_all = True and have_some = False is obviously an error
    REVIEW_STATE_CONDITIONS = {
        (True, True): ReviewState.COMPLETED,
        (True, False): ReviewState.INCOMPLETE,
        (False, False): ReviewState.NOT_STARTED
    }

    # pretty much obvious mapping, but still it is useful to separate the two - more stage states could be theoretically
    # added, i.e. Open, Closed, etc. THose won't have a mapping to ReviewState
    STAGE_STATE_REVIEW_STATE_MAPPING = {
        ReviewState.COMPLETED: StageState.COMPLETED,
        ReviewState.INCOMPLETE: StageState.INCOMPLETE,
        ReviewState.NOT_STARTED: StageState.NOT_STARTED,
    }

    @property
    def allowed_nested_blocks(self):
        blocks = super(ReviewBaseStage, self).allowed_nested_blocks
        blocks.extend(
            [GradeRubricStaticContentXBlock, GroupProjectReviewQuestionXBlock])
        return blocks

    @property
    def review_subjects(self):
        raise NotImplementedError(MUST_BE_OVERRIDDEN)

    @property
    def questions(self):
        return self.get_children_by_category(
            GroupProjectReviewQuestionXBlock.CATEGORY)

    @property
    def required_questions(self):
        return [question for question in self.questions if question.required]

    @property
    def grade_questions(self):
        return [question for question in self.questions if question.grade]

    def validate(self):
        violations = super(ReviewBaseStage, self).validate()

        if not self.questions:
            violations.add(
                ValidationMessage(
                    ValidationMessage.ERROR,
                    messages.QUESTION_BLOCKS_ARE_MISSING.format(
                        class_name=self.__class__.__name__,
                        stage_title=self.display_name)))

        return violations

    def _convert_review_items_to_keys(self, review_items):
        empty_values = (None, '')
        return set(
            make_key(review_item[self.REVIEW_ITEM_KEY],
                     review_item["question"]) for review_item in review_items
            if review_item["answer"] not in empty_values)

    def _make_required_keys(self, items_to_grade):
        return set(
            make_key(item_id, question.question_id)
            for item_id in items_to_grade
            for question in self.required_questions)

    def _calculate_review_status(self, review_subject_ids, review_items):
        """
        Calculates review status for all reviewers listed in review_items collection
        :param collections.Iterable[int] review_subject_ids: Ids of review subjects (teammates or other groups)
        :param collections.Iterable[dict] review_items: Review feedback items
        :rtype: ReviewState
        """
        required_keys = self._make_required_keys(review_subject_ids)
        review_item_keys = self._convert_review_items_to_keys(review_items)
        has_all = bool(required_keys) and review_item_keys >= required_keys
        has_some = bool(review_item_keys & required_keys)

        return self.REVIEW_STATE_CONDITIONS.get((has_some, has_all))

    def get_stage_state(self):
        review_status = self.review_status()

        if not self.visited:
            return StageState.NOT_STARTED

        return self.STAGE_STATE_REVIEW_STATE_MAPPING[review_status]

    def _pivot_feedback(self, feedback):  # pylint: disable=no-self-use
        """
        Pivots the feedback to show question -> answer
        """
        return {pi['question']: pi['answer'] for pi in feedback}

    def get_users_completion(self, target_workgroups, target_users):
        """
        :param collections.Iterable[group_project_v2.project_api.dtos.WorkgroupDetails] target_workgroups:
        :param collections.Iterable[group_project_v2.project_api.dtos.ReducedUserDetails] target_users:
        :rtype: (set[int], set[int])
        """
        completed_users, partially_completed_users = set(), set()

        for user in target_users:
            review_subjects_ids, review_items = self.get_review_data(user.id)
            review_status = self._calculate_review_status(
                review_subjects_ids, review_items)

            if review_status == ReviewState.COMPLETED:
                completed_users.add(user.id)
            elif review_status == ReviewState.INCOMPLETE:
                partially_completed_users.add(user.id)

        return completed_users, partially_completed_users

    def get_review_data(self, user_id):
        """
        :param gint user_id:
        :rtype: (set[int], dict)
        """
        raise NotImplementedError(MUST_BE_OVERRIDDEN)

    def _get_reviews_by_user(self, review_items, user_id):
        return [
            item for item in review_items
            if self.real_user_id(item['reviewer']) == user_id
        ]

    @XBlock.json_handler
    @groupwork_protected_handler
    @key_error_protected_handler
    @conversion_protected_handler
    def submit_review(self, submissions, _context=''):
        # if admin grader - still allow providing grades even for non-TA-graded activities
        if self.is_admin_grader and not self.allow_admin_grader_access:
            return {'result': 'error', 'msg': messages.TA_GRADING_NOT_ALLOWED}

        if not self.available_now:
            reason = messages.STAGE_NOT_OPEN_TEMPLATE if not self.is_open else messages.STAGE_CLOSED_TEMPLATE
            return {
                'result': 'error',
                'msg': reason.format(action=self.STAGE_ACTION)
            }

        try:
            self.do_submit_review(submissions)

            if self.can_mark_complete and self.review_status(
            ) == ReviewState.COMPLETED:
                self.mark_complete()
        except ApiError as exception:
            log.exception(exception.message)
            return {'result': 'error', 'msg': exception.message}

        return {
            'result': 'success',
            'msg': messages.FEEDBACK_SAVED_MESSAGE,
            'new_stage_states': [self.get_new_stage_state_data()]
        }

    def do_submit_review(self, submissions):
        raise NotImplementedError(MUST_BE_OVERRIDDEN)

    def student_view(self, context):
        if self.can_mark_complete:
            self.visited = True

        return super(ReviewBaseStage, self).student_view(context)
 def display_name_with_default(self):
     if self.question:
         return _(u'Review Assessment for question "{question_title}"').format(question_title=self.question.title)
     else:
         return _(u"Review Assessment")
Exemple #24
0
class GroupActivityXBlock(CommonMixinCollection, DashboardXBlockMixin,
                          XBlockWithPreviewMixin, ActivityNotificationsMixin,
                          XBlock):
    """
    XBlock providing a group activity project for a group of students to collaborate upon
    """
    display_name = String(
        display_name=_(u"Display Name"),
        help=
        _(u"This name appears in the horizontal navigation at the top of the page."
          ),
        scope=Scope.settings,
        default=_(u"Group Project Activity"))

    weight = Float(
        display_name=_(u"Weight"),
        help=
        _(u"This is the maximum score that the user receives when he/she successfully completes the problem."
          ),
        scope=Scope.settings,
        default=100.0)

    group_reviews_required_count = Integer(
        display_name=_(u"Reviews Required Minimum"),
        help=_(
            u"The minimum number of group-reviews that should be applied to a set of submissions "
            u"(set to 0 to be 'TA Graded')"),
        scope=Scope.settings,
        default=3)

    user_review_count = Integer(
        display_name=_(u"User Reviews Required Minimum"),
        help=
        _(u"The minimum number of other-group reviews that an individual user should perform"
          ),
        scope=Scope.settings,
        default=1)

    due_date = DateTime(display_name=_(u"Due date"),
                        help=_(u"Activity due date"),
                        has_score=Scope.settings,
                        default=None)

    CATEGORY = "gp-v2-activity"
    STUDIO_LABEL = _(u"Group Project Activity")

    editable_fields = ("display_name", "weight",
                       "group_reviews_required_count", "user_review_count",
                       "due_date")
    has_score = True
    has_children = True

    template_location = 'activity'

    DASHBOARD_DETAILS_URL_KEY = 'dashboard_details_url'
    DEFAULT_DASHBOARD_DETAILS_URL_TPL = "/dashboard_details_view?activate_block_id={activity_id}"
    TA_REVIEW_URL_KEY = 'ta_review_url'
    DEFAULT_TA_REVIEW_URL_TPL = "ta_grading=true&activate_block_id={activate_block_id}&group_id={group_id}"

    @property
    def id(self):
        return self.scope_ids.usage_id

    def max_score(self):
        """
        Used for grading purposes:
            * As max grade for submitting grade event. See :method:`assign_grade_to_group`
            * As theoretical max score for grade calculation when grade is not yet available
        :rtype: Float
        """
        return self.weight

    @property
    def project(self):
        return self.get_parent()

    @property
    def content_id(self):
        return get_block_content_id(self)

    @property
    def is_ta_graded(self):
        return self.group_reviews_required_count == 0

    @property
    def allowed_nested_blocks(self):  # pylint: disable=no-self-use
        return [
            BasicStage, CompletionStage, SubmissionStage, TeamEvaluationStage,
            PeerReviewStage, EvaluationDisplayStage, GradeDisplayStage
        ]

    @property
    def stages(self):
        return self._children

    @property
    def available_stages(self):
        for stage in self.stages:
            if stage.available_to_current_user:
                yield stage

    @property
    def default_stage(self):
        def_stage = get_default_stage(self.available_stages)
        if def_stage:
            return def_stage
        else:
            return self.stages[0] if self.stages else None

    @property
    def questions(self):
        return list(self._chain_questions(self.stages, 'questions'))

    @property
    def grade_questions(self):
        return list(self._chain_questions(self.stages, 'grade_questions'))

    @property
    def team_evaluation_questions(self):
        stages = self.get_children_by_category(TeamEvaluationStage.CATEGORY)
        return list(self._chain_questions(stages, 'questions'))

    @property
    def peer_review_questions(self):
        stages = self.get_children_by_category(PeerReviewStage.CATEGORY)
        return list(self._chain_questions(stages, 'questions'))

    def dashboard_details_url(self):
        """
        Gets dashboard details view URL for current activity. If settings service is not available or does not provide
        URL template, default template is used.
        """
        template = self._get_setting(self.DASHBOARD_DETAILS_URL_KEY,
                                     self.DEFAULT_DASHBOARD_DETAILS_URL_TPL)

        return template.format(program_id=self.user_preferences.get(
            self.DASHBOARD_PROGRAM_ID_KEY, None),
                               course_id=self.course_id,
                               project_id=self.project.scope_ids.usage_id,
                               activity_id=self.id)

    def get_ta_review_link(self, group_id, target_block_id=None):
        target_block_id = target_block_id if target_block_id else self.id
        template = self._get_setting(self.TA_REVIEW_URL_KEY,
                                     self.DEFAULT_TA_REVIEW_URL_TPL)
        return template.format(course_id=self.course_id,
                               group_id=group_id,
                               activate_block_id=target_block_id)

    @staticmethod
    def _chain_questions(stages, question_type):
        return itertools.chain.from_iterable(
            getattr(stage, question_type, ()) for stage in stages)

    def get_stage_to_display(self, target_block_id):
        try:
            if target_block_id:
                stage = self.runtime.get_block(target_block_id)
                if self.get_child_category(
                        stage
                ) in STAGE_TYPES and stage.available_to_current_user:
                    return stage
        except (InvalidKeyError, KeyError, NoSuchUsage) as exc:
            log.exception(exc)

        return self.default_stage

    @groupwork_protected_view
    def student_view(self, context):
        """
        Player view, displayed to the student
        """
        fragment = Fragment()

        current_stage_id = context.get(
            Constants.CURRENT_STAGE_ID_PARAMETER_NAME, None)
        target_stage = self.get_stage_to_display(current_stage_id)

        if not target_stage:
            fragment.add_content(messages.NO_STAGES)
        else:
            stage_fragment = target_stage.render('student_view', context)
            fragment.add_frag_resources(stage_fragment)
            render_context = {
                'activity': self,
                'stage_content': stage_fragment.content,
            }
            render_context.update(context)
            fragment.add_content(
                self.render_template('student_view', render_context))

        return fragment

    @groupwork_protected_view
    def navigation_view(self, context):
        fragment = Fragment()

        children_context = {}
        children_context.update(context)

        stage_fragments = self._render_children('navigation_view',
                                                children_context,
                                                self.available_stages)
        stage_contents = [frag.content for frag in stage_fragments]
        fragment.add_frags_resources(stage_fragments)

        render_context = {'activity': self, 'stage_contents': stage_contents}
        fragment.add_content(
            self.render_template('navigation_view', render_context))

        return fragment

    @groupwork_protected_view
    def resources_view(self, context):
        fragment = Fragment()

        resources = [
            resource for stage in self.stages for resource in stage.resources
        ]
        has_resources = bool(resources)

        resource_fragments = self._render_children('resources_view', context,
                                                   resources)
        resource_contents = [frag.content for frag in resource_fragments]
        fragment.add_frags_resources(resource_fragments)

        render_context = {
            'activity': self,
            'resource_contents': resource_contents,
            'has_resources': has_resources
        }
        fragment.add_content(
            self.render_template('resources_view', render_context))

        return fragment

    @groupwork_protected_view
    def submissions_view(self, context):
        fragment = Fragment()

        submissions = [
            submission for stage in self.stages
            if isinstance(stage, SubmissionStage)
            for submission in stage.submissions
        ]
        has_submissions = bool(submissions)

        submission_fragments = self._render_children('submissions_view',
                                                     context, submissions)
        submission_contents = [frag.content for frag in submission_fragments]
        fragment.add_frags_resources(submission_fragments)

        render_context = {
            'activity': self,
            'submission_contents': submission_contents,
            'has_submissions': has_submissions
        }
        fragment.add_content(
            self.render_template('submissions_view', render_context))

        return fragment

    @groupwork_protected_view
    @AuthXBlockMixin.check_dashboard_access_for_current_user
    def dashboard_view(self, context):
        fragment = Fragment()

        children_context = context.copy()

        stage_fragments = self._render_children('dashboard_view',
                                                children_context, self.stages)
        stage_contents = [frag.content for frag in stage_fragments]
        fragment.add_frags_resources(stage_fragments)

        render_context = {'activity': self, 'stage_contents': stage_contents}
        fragment.add_content(
            self.render_template('dashboard_view', render_context))

        return fragment

    @groupwork_protected_view
    @AuthXBlockMixin.check_dashboard_access_for_current_user
    def dashboard_detail_view(self, context):
        fragment = Fragment()

        children_context = context.copy()

        target_workgroups = context.get(Constants.TARGET_WORKGROUPS)
        target_users = context.get(Constants.TARGET_STUDENTS)
        filtered_users = children_context[Constants.FILTERED_STUDENTS]

        stages = []
        stage_stats = {}
        for stage in self.stages:
            if not stage.is_graded_stage:
                continue
            stage_fragment = stage.render('dashboard_detail_view',
                                          children_context)
            stage_fragment.add_frag_resources(fragment)
            stages.append({"id": stage.id, 'content': stage_fragment.content})
            stage_stats[stage.id] = self._get_stage_completion_details(
                stage, target_workgroups, target_users)

        groups_data = self._build_groups_data(target_workgroups, stage_stats,
                                              filtered_users)
        visible_groups = [
            group for group in groups_data if group["group_visible"]
        ]

        render_context = {
            'activity':
            self,
            'StageState':
            StageState,
            'stages':
            stages,
            'stages_count':
            len(stages),
            'groups':
            visible_groups,
            'filtered_out_workgroups':
            len(groups_data) - len(visible_groups),
            'stage_cell_width_percent': (100 - 30) /
            float(len(stages)),  # 30% is reserved for first column
            'assigned_to_groups_label':
            messages.ASSIGNED_TO_GROUPS_LABEL.format(
                group_count=len(groups_data))
        }
        fragment.add_content(
            self.render_template('dashboard_detail_view', render_context))

        return fragment

    def _render_user(self, user, stage_stats, filtered_students):
        """
        :param group_project_v2.project_api.dtos.ReducedUserDetail user:
        :param dict[str, StageCompletionDetailsData] stage_stats: Stage completion statistics
        :param set[int] filtered_students:  users filtered out from view
        :return: dict
        """

        return {
            'id': user.id,
            'full_name': user.full_name,
            'email': user.email,
            'is_filtered_out': user.id in filtered_students,
            'stage_states': {
                stage_id: stage_data.user_stats.get(user.id,
                                                    StageState.UNKNOWN)
                for stage_id, stage_data in stage_stats.iteritems()
            },
            'groups_to_grade': {
                stage_id: [{
                    'id':
                    group.id,
                    'ta_grade_link':
                    self.get_ta_review_link(group.id, stage_id)
                } for group in stage_data.groups_to_grade.get(user.id, [])]
                for stage_id, stage_data in stage_stats.iteritems()
            }
        }

    def _render_workgroup(self, workgroup, stage_stats, filtered_students):
        """
        :param group_project_v2.project_api.dtos.WorkgroupDetails workgroup:
        :param dict[str, StageCompletionDetailsData] stage_stats: Stage completion statistics
        :param set[int] filtered_students:  users filtered out from view
        :return: dict
        """

        users = [
            self._render_user(user, stage_stats, filtered_students)
            for user in workgroup.users
        ]

        users.sort(key=itemgetter('is_filtered_out'))

        group_visible = any((not user['is_filtered_out'] for user in users))

        return {
            'id': workgroup.id,
            'ta_grade_link': self.get_ta_review_link(workgroup.id),
            'group_visible': group_visible,
            'stage_states': {
                stage_id: {
                    'internal_status':
                    stage_data.internal_group_status.get(
                        workgroup.id, StageState.UNKNOWN),
                    'external_status':
                    stage_data.external_group_status.get(
                        workgroup.id, StageState.NOT_AVAILABLE),
                    'external_status_label':
                    stage_data.external_group_status_label.get(
                        workgroup.id, ""),
                }
                for stage_id, stage_data in stage_stats.iteritems()
            },
            'users': users
        }

    def _build_groups_data(self, workgroups, stage_stats, filtered_users):
        """
        Converts WorkgroupDetails into dict expected by dashboard_detail_view template.

        :param collections.Iterable[group_project_v2.project_api.dtos.WorkgroupDetails] workgroups: Workgroups
        :param dict[str, StageCompletionDetailsData] stage_stats: Stage statistics - group-wise and user-wise completion
            data and groups_to_review.
        :param set[int] filtered_users: users filtered out from view - depending on actual view
            (dashboard or dashboard details) such students are either completely excluded, or included but diplayed
            differently
        :rtype: list[dict]
        :returns:
            List of dictionaries with the following format:
                * id - Group ID
                * stage_states - dictionary stage_id -> StateState
                * users - dictionary with the following format:
                    * id - User ID
                    * full_name - User full name
                    * email - user email
                    * stage_states - dictionary stage_id -> StageState
                    * groups_to_grade - dictionary stage_id -> list of groups to grade
        """
        return [
            self._render_workgroup(workgroup, stage_stats, filtered_users)
            for workgroup in workgroups
        ]

    @classmethod
    def _get_stage_completion_details(cls, stage, target_workgroups,
                                      target_students):
        """
        Gets stage completion stats from individual stage
        :param group_project_v2.stage.BaseGroupActivityStage stage: Get stage stats from this stage
        :param collections.Iterable[group_project_v2.project_api.dtos.WorkgroupDetails] target_workgroups:
        :param collections.Iterable[group_project_v2.project_api.dtos.ReducedUserDetails] target_students:
        :rtype: StageCompletionDetailsData
        :returns: Stage completion stats
        """
        completed_users, partially_completed_users = stage.get_users_completion(
            target_workgroups, target_students)
        user_stats = {}
        groups_to_grade = {}
        for user in target_students:
            state = StageState.NOT_STARTED
            if user.id in completed_users:
                state = StageState.COMPLETED
            elif user.id in partially_completed_users:
                state = StageState.INCOMPLETE
            user_stats[user.id] = state

            if isinstance(stage, PeerReviewStage):
                groups_to_grade[user.id] = stage.get_review_subjects(user.id)

        external_group_status, external_group_status_label, internal_group_status = cls._get_group_statuses(
            stage, target_workgroups, user_stats)

        return StageCompletionDetailsData(
            internal_group_status=internal_group_status,
            external_group_status=external_group_status,
            external_group_status_label=external_group_status_label,
            user_stats=user_stats,
            groups_to_grade=groups_to_grade)

    @classmethod
    def _get_group_statuses(cls, stage, target_workgroups, user_stats):
        internal_group_status, external_group_status, external_group_status_label = {}, {}, {}
        for group in target_workgroups:
            user_completions = [
                user_stats.get(user.id, StageState.UNKNOWN)
                for user in group.users
            ]
            student_review_state = StageState.NOT_STARTED
            if all(completion == StageState.COMPLETED
                   for completion in user_completions):
                student_review_state = StageState.COMPLETED
            elif any(completion != StageState.NOT_STARTED
                     for completion in user_completions):
                student_review_state = StageState.INCOMPLETE
            elif any(completion == StageState.UNKNOWN
                     for completion in user_completions):
                student_review_state = StageState.UNKNOWN
            internal_group_status[group.id] = student_review_state

            external_status = stage.get_external_group_status(group)
            external_group_status[group.id] = external_status
            external_group_status_label[
                group.id] = stage.get_external_status_label(external_status)
        return external_group_status, external_group_status_label, internal_group_status

    def mark_complete(self, user_id):
        self.runtime.publish(self, 'progress', {'user_id': user_id})

    def validate_field_data(self, validation, data):
        super(GroupActivityXBlock, self).validate_field_data(validation, data)
        should_be_ints = ('weight', 'group_reviews_required_count',
                          'user_review_count')
        for field_name in should_be_ints:
            try:
                int(getattr(data, field_name))
            except (TypeError, ValueError):
                message = _(
                    u"{field_name} must be integer, {field_value} given"
                ).format(field_name=field_name,
                         field_value=getattr(data, field_name))
                validation.add(
                    ValidationMessage(ValidationMessage.ERROR, message))

    def calculate_and_send_grade(self, group_id):
        grade_value = self.calculate_grade(group_id)
        if grade_value:
            self.assign_grade_to_group(group_id, grade_value)

            workgroup = self.project_api.get_workgroup_by_id(group_id)
            for user in workgroup.users:
                self.mark_complete(user.id)

    def assign_grade_to_group(self, group_id, grade_value):
        """
        Assigns grade to group, fires required events and notifications
        :param int group_id: Group ID
        :param float grade_value: Grade to assign
        :return:
        """
        self.project_api.set_group_grade(group_id, self.course_id,
                                         self.content_id, grade_value,
                                         self.max_score())
        # Emit analytics event...
        self.runtime.publish(
            self, "group_activity.final_grade", {
                "grade_value": grade_value,
                "group_id": group_id,
                "content_id": self.content_id,
            })
        notifications_service = self.runtime.service(self, 'notifications')
        if notifications_service:
            self.fire_grades_posted_notification(group_id,
                                                 notifications_service)

    def calculate_grade(self, group_id):  # pylint:disable=too-many-locals,too-many-branches
        review_item_data = self.project_api.get_workgroup_review_items_for_group(
            group_id, self.content_id)
        review_item_map = {
            make_key(review_item['question'],
                     self.real_user_id(review_item['reviewer'])):
            review_item['answer']
            for review_item in review_item_data
        }
        all_reviewer_ids = set([
            self.real_user_id(review_item['reviewer'])
            for review_item in review_item_data
        ])
        group_reviewer_ids = [
            user["id"] for user in self.project_api.get_workgroup_reviewers(
                group_id, self.content_id)
        ]
        admin_reviewer_ids = [
            reviewer_id for reviewer_id in all_reviewer_ids
            if reviewer_id not in group_reviewer_ids
        ]

        def get_user_grade_value_list(user_id):
            user_grades = []
            for question in self.grade_questions:
                user_value = review_item_map.get(
                    make_key(question.question_id, user_id), None)
                if user_value is None:
                    # if any are incomplete, we consider the whole set to be unusable
                    return None
                else:
                    user_grades.append(user_value)

            return user_grades

        admin_provided_grades = None
        if len(admin_reviewer_ids) > 0:
            admin_provided_grades = []
            # Only include complete admin gradesets
            admin_reviewer_grades = [
                arg for arg in [
                    get_user_grade_value_list(admin_id)
                    for admin_id in admin_reviewer_ids
                ] if arg
            ]
            admin_grader_count = len(admin_reviewer_grades)
            if admin_grader_count > 1:
                for idx in range(len(self.grade_questions)):
                    admin_provided_grades.append(
                        mean([adm[idx] for adm in admin_reviewer_grades]))
            elif admin_grader_count > 0:  # which actually means admin_grader_count == 1
                admin_provided_grades = admin_reviewer_grades[0]

        user_grades = {}
        if len(group_reviewer_ids) > 0:
            for reviewer_id in group_reviewer_ids:
                this_reviewers_grades = get_user_grade_value_list(reviewer_id)
                if this_reviewers_grades is None:
                    if admin_provided_grades:
                        this_reviewers_grades = admin_provided_grades
                    else:
                        return None
                user_grades[reviewer_id] = this_reviewers_grades
        elif admin_provided_grades:
            group_reviewer_ids = [self.user_id]
            user_grades[self.user_id] = admin_provided_grades
        else:
            return None

        # Okay, if we've got here we have a complete set of marks to calculate the grade
        reviewer_grades = [
            mean(user_grades[reviewer_id])
            for reviewer_id in group_reviewer_ids
            if len(user_grades[reviewer_id]) > 0
        ]
        group_grade = round(
            mean(reviewer_grades)) if len(reviewer_grades) > 0 else None

        return group_grade
Exemple #25
0
 def url_name_caption(self):
     return _(u"url_name to link to this {project_navigator_view}:").format(
         project_navigator_view=self.display_name_with_default)
class GroupProjectVideoResourceXBlock(BaseGroupProjectResourceXBlock):
    CATEGORY = "gp-v2-video-resource"
    STUDIO_LABEL = _(u"Video Resource")
    PROJECT_NAVIGATOR_VIEW_TEMPLATE = 'templates/html/components/video_resource.html'

    video_id = String(
        display_name=_(u"Ooyala/Brightcove content ID"),
        help=_(u"This is the Ooyala/Brightcove Content Identifier"),
        default="Q1eXg5NzpKqUUzBm5WTIb6bXuiWHrRMi",
        scope=Scope.content,
    )

    editable_fields = ('display_name', 'description', 'video_id')

    @classmethod
    def is_available(cls):
        return True  # TODO: restore conditional availability when switched to use actual Ooyala XBlock

    @classmethod
    def brightcove_account_id(cls):
        """
        Gets bcove account id from settings
        """
        xblock_settings = settings.XBLOCK_SETTINGS if hasattr(
            settings, "XBLOCK_SETTINGS") else {}
        return xblock_settings.get('OoyalaPlayerBlock',
                                   {}).get('BCOVE_ACCOUNT_ID')

    @property
    def video_type(self):
        """
        Checks if video_id belongs to Brightcove or Ooyala
        """
        try:
            # Brightcove IDs are numeric
            int(self.video_id)
            return 'brightcove'
        except (ValueError, TypeError):
            return 'ooyala'

    def resources_view(self, context):
        render_context = {
            'video_id': self.video_id,
            'player_type': self.video_type,
            'bc_account_id': self.brightcove_account_id(),
        }
        render_context.update(context)
        fragment = super(GroupProjectVideoResourceXBlock,
                         self).resources_view(render_context)
        fragment.add_javascript_url(
            url='//players.brightcove.net/{}/default_default/index.min.js'.
            format(self.brightcove_account_id()))
        return fragment

    def author_view(self, context):
        return self.resources_view(context)

    def validate_field_data(self, validation, data):
        if not data.video_id:
            validation.add(
                ValidationMessage(ValidationMessage.ERROR,
                                  self._(messages.MUST_CONTAIN_CONTENT_ID)))

        return validation
Exemple #27
0
class GroupProjectBaseFeedbackDisplayXBlock(BaseStageComponentXBlock,
                                            StudioEditableXBlockMixin,
                                            XBlockWithPreviewMixin,
                                            WorkgroupAwareXBlockMixin):
    DEFAULT_QUESTION_ID_VALUE = None

    NO_QUESTION_SELECTED = _(u"No question selected")
    QUESTION_NOT_FOUND = _(u"Selected question not found")
    QUESTION_ID_IS_NOT_UNIQUE = _(u"Question ID is not unique")

    question_id = String(display_name=_(u"Question ID"),
                         help=_(u"Question to be assessed"),
                         scope=Scope.content,
                         default=DEFAULT_QUESTION_ID_VALUE)

    show_mean = Boolean(
        display_name=_(u"Show Mean Value"),
        help=
        _(u"If True, converts review answers to numbers and calculates mean value"
          ),
        default=False,
        scope=Scope.content)

    editable_fields = ("question_id", "show_mean")
    has_author_view = True

    @property
    def activity_questions(self):
        raise NotImplementedError(MUST_BE_OVERRIDDEN)

    @property
    def display_name_with_default(self):
        if self.question:
            return _(u'Review Assessment for question "{question_title}"'
                     ).format(question_title=self.question.title)
        else:
            return _(u"Review Assessment")

    @lazy
    def question(self):
        matching_questions = [
            question for question in self.activity_questions
            if question.question_id == self.question_id
        ]
        if len(matching_questions) > 1:
            raise ValueError(self.QUESTION_ID_IS_NOT_UNIQUE)
        if not matching_questions:
            return None

        return matching_questions[0]

    @groupwork_protected_view
    def student_view(self, context):
        if self.question is None:
            return Fragment(messages.COMPONENT_MISCONFIGURED)

        raw_feedback = self.get_feedback()

        feedback = []
        for item in raw_feedback:
            feedback.append(html.escape(item['answer']))

        fragment = Fragment()
        title = self.question.assessment_title if self.question.assessment_title else self.question.title
        render_context = {
            'assessment': self,
            'question_title': title,
            'feedback': feedback
        }
        if self.show_mean:
            try:
                render_context['mean'] = "{0:.1f}".format(mean(feedback))
            except ValueError as exc:
                log.warn(exc)
                render_context['mean'] = _(u"N/A")

        render_context.update(context)
        fragment.add_content(
            loader.render_template(
                "templates/html/components/review_assessment.html",
                render_context))
        return fragment

    def validate(self):
        validation = super(GroupProjectBaseFeedbackDisplayXBlock,
                           self).validate()

        if not self.question_id:
            validation.add(
                ValidationMessage(ValidationMessage.ERROR,
                                  self.NO_QUESTION_SELECTED))

        if self.question_id and self.question is None:
            validation.add(
                ValidationMessage(ValidationMessage.ERROR,
                                  self.QUESTION_NOT_FOUND))

        return validation

    def author_view(self, context):
        if self.question:
            return self.student_view(context)

        fragment = Fragment()
        fragment.add_content(messages.QUESTION_NOT_SELECTED)
        return fragment

    def studio_view(self, context):
        # can't use values_provider as we need it to be bound to current block instance
        with FieldValuesContextManager(self, 'question_id',
                                       self.question_ids_values_provider):
            return super(GroupProjectBaseFeedbackDisplayXBlock,
                         self).studio_view(context)

    def question_ids_values_provider(self):
        not_selected = {
            "display_name": _(u"--- Not selected ---"),
            "value": self.DEFAULT_QUESTION_ID_VALUE
        }
        question_values = [{
            "display_name": question.title,
            "value": question.question_id
        } for question in self.activity_questions]
        return [not_selected] + question_values
from group_project_v2.utils import _


class StageState(object):
    NOT_STARTED = 'not_started'
    INCOMPLETE = 'incomplete'
    COMPLETED = 'completed'
    UNKNOWN = 'unknown'
    NOT_AVAILABLE = 'na'

    HUMAN_NAMES_MAP = {
        NOT_STARTED: _("Not started"),
        INCOMPLETE: _("Partially complete"),
        COMPLETED: _("Complete")
    }

    @classmethod
    def get_human_name(cls, state):
        return cls.HUMAN_NAMES_MAP.get(state)


class ReviewState(object):
    NOT_STARTED = 'not_started'
    INCOMPLETE = 'incomplete'
    COMPLETED = 'completed'


DISPLAY_NAME_NAME = _(u"Display Name")
DISPLAY_NAME_HELP = _(U"This is a name of the stage")
Exemple #29
0
 def display_name_with_default(self):
     if self.question:
         return _(u'Review Assessment for question "{question_title}"'
                  ).format(question_title=self.question.title)
     else:
         return _(u"Review Assessment")
Exemple #30
0
class GroupProjectNavigatorXBlock(ChildrenNavigationXBlockMixin,
                                  XBlockWithComponentsMixin,
                                  XBlockWithPreviewMixin,
                                  NoStudioEditableSettingsMixin,
                                  StudioContainerXBlockMixin, CompletionMixin,
                                  XBlock, I18NService):
    """
    XBlock that provides basic layout and switching between children XBlocks (views)
    Should only be added as a child to GroupProjectXBlock
    """
    CATEGORY = "gp-v2-navigator"
    STUDIO_LABEL = _(u"Group Project Navigator")
    INITIAL_VIEW = ViewTypes.NAVIGATION

    display_name_with_default = _(u"Group Project Navigator")

    editable = False
    has_score = False
    has_children = True

    @lazy
    def group_project(self):
        """
        Reference to parent XBlock
        """
        return self.get_parent()

    @property
    def allowed_nested_blocks(self):  # pylint: disable=no-self-use
        return [
            NestedXBlockSpec(NavigationViewXBlock, single_instance=True),
            NestedXBlockSpec(ResourcesViewXBlock, single_instance=True),
            NestedXBlockSpec(SubmissionsViewXBlock, single_instance=True),
            NestedXBlockSpec(AskTAViewXBlock, single_instance=True),
            NestedXBlockSpec(PrivateDiscussionViewXBlock,
                             single_instance=True),
        ]

    def _get_activated_view_type(self, target_block_id):
        try:
            if target_block_id:
                block = self.runtime.get_block(target_block_id)
                if self.get_child_category(
                        block) in PROJECT_NAVIGATOR_VIEW_TYPES:
                    return block.type
        except (InvalidKeyError, KeyError, NoSuchUsage) as exc:
            log.exception(exc)

        return ViewTypes.NAVIGATION

    def _sorted_child_views(self):
        all_views = []
        for child_id in self.children:
            view = self.runtime.get_block(child_id)
            if view.available_to_current_user and view.is_view_available:
                all_views.append(view)

        all_views.sort(key=lambda view_instance: view_instance.SORT_ORDER)
        return all_views

    @staticmethod
    def resource_string(path):
        """Handy helper for getting resources."""
        data = pkg_resources.resource_string(__name__, path)
        return data.decode("utf8")

    def get_translation_content(self):
        """
        Returns JS content containing translations for user's language.
        """
        try:
            return self.resource_string(
                'public/js/translations/{lang}/textjs.js'.format(
                    lang=utils.translation.to_locale(
                        utils.translation.get_language()), ))
        except IOError:
            return self.resource_string('public/js/translations/en/textjs.js')

    def student_view(self, context):
        """
        Student view
        """
        fragment = Fragment()
        children_items = []
        for view in self._sorted_child_views():
            item = {
                'id': str(view.scope_ids.usage_id).replace("/", ";_"),
                'type': view.type,
            }

            if not view.skip_content:
                child_fragment = view.render('student_view', context)
                item['content'] = child_fragment.content
                fragment.add_fragment_resources(child_fragment)
            else:
                item['content'] = ''

            if not view.skip_selector:
                child_selector_fragment = view.render('selector_view', context)
                item['selector'] = child_selector_fragment.content
                fragment.add_fragment_resources(child_selector_fragment)
            else:
                item['selector'] = ''

            children_items.append(item)

        activate_block_id = self.get_block_id_from_string(
            context.get('activate_block_id', None))

        js_parameters = {
            'selected_view': self._get_activated_view_type(activate_block_id)
        }

        fragment.add_content(
            loader.render_django_template(
                'templates/html/project_navigator/project_navigator.html',
                {'children': children_items},
                i18n_service=self.i18n_service,
            ))
        add_resource(self, 'css',
                     'public/css/project_navigator/project_navigator.css',
                     fragment)
        add_resource(self, 'javascript',
                     'public/js/project_navigator/project_navigator.js',
                     fragment)
        fragment.add_javascript(self.get_translation_content())
        fragment.initialize_js("GroupProjectNavigatorBlock", js_parameters)

        return fragment

    def author_preview_view(self, context):
        fragment = Fragment()
        children_contents = []
        for child in self._children:
            child_fragment = child.render('preview_view', context)
            fragment.add_fragment_resources(child_fragment)
            children_contents.append(child_fragment.content)

        fragment.add_content(
            loader.render_django_template(
                "templates/html/project_navigator/project_navigator_author_view.html",
                {
                    'navigator': self,
                    'children_contents': children_contents
                },
                i18n_service=self.i18n_service,
            ))
        add_resource(self, 'css',
                     'public/css/project_navigator/project_navigator.css',
                     fragment)
        return fragment

    def validate(self):
        validation = super(GroupProjectNavigatorXBlock, self).validate()

        if not self.has_child_of_category(NavigationViewXBlock.CATEGORY):
            validation.add(
                ValidationMessage(
                    ValidationMessage.ERROR,
                    self._(messages.MUST_CONTAIN_NAVIGATION_VIEW)))

        return validation
Exemple #31
0
class GroupProjectXBlock(CommonMixinCollection, DashboardXBlockMixin,
                         DashboardRootXBlockMixin, XBlock):
    display_name = String(display_name=_(u"Display Name"),
                          help=_(u"This is a name of the project"),
                          scope=Scope.settings,
                          default=_(u"Group Project V2"))

    CATEGORY = "gp-v2-project"
    REPORT_FILENAME = "group_project_{group_project_name}_stage_{stage_name}_incomplete_report_{timestamp}.csv"
    CSV_HEADERS = ['Name', 'Username', 'Email']
    CSV_TIMESTAMP_FORMAT = "%Y_%m_%d_%H_%M_%S"

    editable_fields = ('display_name', )
    has_score = False
    has_children = True

    template_location = "project"

    @staticmethod
    def _sanitize_context(context):
        """
        Context passed into views is :class:QueryDict instance, which has a particular feature of supporting multiple
        values for the same key. It is achieved at a cost of always using lists even for singular values. When `get` is
        invoked for the singular key it detects there's only one value and returns value, not the list itself

        The caveat is that if it is used with dict(context) or update(context), those singular values are returned as
        lists. This breaks the approach of passing context as intact as possible to children by making a copy and adding
        necessary children data. So, this method serves as a filter and converter for context coming from LMS.
        """

        if not context:
            return {}
        raw_client_id = context.get(
            Constants.CURRENT_CLIENT_FILTER_ID_PARAMETER_NAME)
        client_id = None
        if raw_client_id is not None and raw_client_id.strip():
            client_id = int(raw_client_id)
        return {
            Constants.ACTIVATE_BLOCK_ID_PARAMETER_NAME:
            context.get(Constants.ACTIVATE_BLOCK_ID_PARAMETER_NAME, None),
            Constants.CURRENT_CLIENT_FILTER_ID_PARAMETER_NAME:
            client_id
        }

    @property
    def allowed_nested_blocks(self):  # pylint: disable=no-self-use
        return [
            NestedXBlockSpec(GroupActivityXBlock),
            NestedXBlockSpec(GroupProjectNavigatorXBlock,
                             single_instance=True),
            NestedXBlockSpec(DiscussionXBlockShim, single_instance=True)
        ]

    @property
    def content_id(self):
        return get_block_content_id(self)

    @lazy
    def activities(self):
        all_children = [
            self.runtime.get_block(child_id) for child_id in self.children
        ]
        return [
            child for child in all_children
            if isinstance(child, GroupActivityXBlock)
        ]

    @lazy
    def navigator(self):
        return self.get_child_of_category(GroupProjectNavigatorXBlock.CATEGORY)

    @property
    def default_stage(self):
        default_stages = [
            activity.default_stage for activity in self.activities
        ]

        return get_default_stage(default_stages)

    @staticmethod
    def _render_child_fragment_with_fallback(child,
                                             context,
                                             fallback_message,
                                             view='student_view'):
        if child:
            log.debug("Rendering {child} with context: {context}".format(
                child=child.__class__.__name__,
                context=context,
            ))
            return child.render(view, context)
        else:
            return Fragment(fallback_message)

    @groupwork_protected_view
    def student_view(self, context):
        ctx = self._sanitize_context(context)

        fragment = Fragment()
        render_context = {
            'project': self,
            'course_id': self.course_id,
            'group_id': self.workgroup.id
        }

        render_context.update(context)

        def render_child_fragment(child,
                                  content_key,
                                  fallback_message,
                                  extra_context=None):
            """
            Renders child, appends child fragment resources to parent fragment and
            updates parent's rendering context
            """
            internal_context = dict(ctx)
            if extra_context:
                internal_context.update(extra_context)

            child_fragment = self._render_child_fragment_with_fallback(
                child, internal_context, fallback_message, 'student_view')
            fragment.add_frag_resources(child_fragment)
            render_context[content_key] = child_fragment.content

        target_block_id = self.get_block_id_from_string(
            ctx.get(Constants.ACTIVATE_BLOCK_ID_PARAMETER_NAME, None))
        target_stage = self.get_stage_to_display(target_block_id)

        child_context = {}
        if target_stage:
            child_context[
                Constants.CURRENT_STAGE_ID_PARAMETER_NAME] = target_stage.id

        # activity should be rendered first, as some stages might report completion in student-view - this way stage
        # PN sees updated state.
        target_activity = target_stage.activity if target_stage else None
        render_child_fragment(target_activity, 'activity_content',
                              messages.NO_ACTIVITIES, child_context)

        # TODO: project nav is slow, mostly due to navigation view. It might make sense to rework it into
        # asynchronously loading navigation and stage states.
        project_navigator = self.get_child_of_category(
            GroupProjectNavigatorXBlock.CATEGORY)
        render_child_fragment(project_navigator, 'project_navigator_content',
                              messages.NO_PROJECT_NAVIGATOR, child_context)

        discussion = self.get_child_of_category(DiscussionXBlockShim.CATEGORY)
        render_child_fragment(discussion, 'discussion_content',
                              messages.NO_DISCUSSION, child_context)

        fragment.add_content(
            self.render_template('student_view', render_context))

        add_resource(self, 'css', 'public/css/group_project.css', fragment)
        add_resource(self, 'css', 'public/css/group_project_common.css',
                     fragment)
        add_resource(self,
                     'css',
                     'public/css/vendor/font-awesome/font-awesome.css',
                     fragment,
                     via_url=True)
        add_resource(self, 'javascript', 'public/js/group_project.js',
                     fragment)
        add_resource(self, 'javascript', 'public/js/group_project_common.js',
                     fragment)
        fragment.initialize_js("GroupProjectBlock")
        return fragment

    @groupwork_protected_view
    @AuthXBlockMixin.check_dashboard_access_for_current_user
    def dashboard_view(self, context):
        fragment = Fragment()

        children_context = self._sanitize_context(context)
        self._add_students_and_workgroups_to_context(children_context)

        activity_fragments = self._render_children('dashboard_view',
                                                   children_context,
                                                   self.activities)
        activity_contents = [frag.content for frag in activity_fragments]
        fragment.add_frags_resources(activity_fragments)

        render_context = {
            'project': self,
            'activity_contents': activity_contents
        }
        fragment.add_content(
            self.render_template('dashboard_view', render_context))
        add_resource(self, 'css', 'public/css/group_project_common.css',
                     fragment)
        add_resource(self, 'css', 'public/css/group_project_dashboard.css',
                     fragment)
        add_resource(self,
                     'css',
                     'public/css/vendor/font-awesome/font-awesome.css',
                     fragment,
                     via_url=True)

        return fragment

    @groupwork_protected_view
    @AuthXBlockMixin.check_dashboard_access_for_current_user
    def dashboard_detail_view(self, context):
        ctx = self._sanitize_context(context)
        self._add_students_and_workgroups_to_context(ctx)

        fragment = Fragment()
        render_context = {
            'project': self,
            'course_id': self.course_id,
            'group_id': self.workgroup.id
        }

        render_context.update(ctx)

        target_block_id = self.get_block_id_from_string(
            ctx.get(Constants.ACTIVATE_BLOCK_ID_PARAMETER_NAME, None))
        target_activity = self._get_target_block(target_block_id)
        if target_activity is None and self.activities:
            target_activity = self.activities[0]

        activity_fragment = self._render_child_fragment_with_fallback(
            target_activity,
            ctx,
            messages.NO_ACTIVITIES,
            view='dashboard_detail_view')
        render_context['activity_content'] = activity_fragment.content
        fragment.add_frag_resources(activity_fragment)

        fragment.add_content(
            self.render_template('dashboard_detail_view', render_context))
        add_resource(self, 'css', 'public/css/group_project_common.css',
                     fragment)
        add_resource(self, 'css', 'public/css/group_project_dashboard.css',
                     fragment)
        add_resource(self,
                     'css',
                     'public/css/vendor/font-awesome/font-awesome.css',
                     fragment,
                     via_url=True)
        add_resource(self, 'javascript',
                     'public/js/group_project_dashboard_detail.js', fragment)

        fragment.initialize_js('GroupProjectBlockDashboardDetailsView')

        return fragment

    @XBlock.handler
    def download_incomplete_list(self, request, _suffix=''):
        target_stage_id = self.get_block_id_from_string(
            request.GET.get(Constants.ACTIVATE_BLOCK_ID_PARAMETER_NAME))
        target_stage = self._get_target_block(target_stage_id)

        if target_stage is None:
            return webob.response.Response(
                u"Stage {stage_id} not found".format(stage_id=target_stage_id),
                status=404)

        workgroups, users = self.get_workgroups_and_students()
        completed, _partially_completed = target_stage.get_users_completion(
            workgroups, users)

        users_to_export = [user for user in users if user.id not in completed]
        filename = self.REPORT_FILENAME.format(
            group_project_name=self.display_name,
            stage_name=target_stage.display_name,
            timestamp=datetime.utcnow().strftime(self.CSV_TIMESTAMP_FORMAT))

        return self.export_users(users_to_export, filename)

    @classmethod
    def export_users(cls, users_to_export, filename):
        response = webob.response.Response(charset='UTF-8',
                                           content_type="text/csv")
        response.headers[
            'Content-Disposition'] = 'attachment; filename="{filename}"'.format(
                filename=filename)
        user_data = [[user.full_name, user.username, user.email]
                     for user in users_to_export]
        export_to_csv(user_data, response, headers=cls.CSV_HEADERS)

        return response

    def validate(self):
        validation = super(GroupProjectXBlock, self).validate()

        if not self.has_child_of_category(
                GroupProjectNavigatorXBlock.CATEGORY):
            validation.add(
                ValidationMessage(
                    ValidationMessage.ERROR,
                    messages.MUST_CONTAIN_PROJECT_NAVIGATOR_BLOCK))

        return validation

    def _get_target_block(self, target_block_id):
        try:
            if target_block_id:
                return self.runtime.get_block(target_block_id)
        except (InvalidKeyError, KeyError, NoSuchUsage) as exc:
            log.exception(exc)

        return None

    def get_stage_to_display(self, target_block_id):
        target_block = self._get_target_block(target_block_id)
        if target_block is not None:
            if self.get_child_category(
                    target_block
            ) in STAGE_TYPES and target_block.available_to_current_user:
                return target_block
            if isinstance(target_block, GroupActivityXBlock):
                return target_block.default_stage

        default_stage = self.default_stage
        if default_stage:
            return default_stage

        if self.activities:
            return self.activities[0].get_stage_to_display(target_block_id)

        return None  # if there are no activities there's no stages as well - nothing we can really do
 def display_name_with_default(self):
     return self.title or _(u"Review Question")
Exemple #33
0
from group_project_v2.utils import gettext as _

# Generic messages
UNKNOWN_ERROR = _(u"Unknown error.")
COMPONENT_MISCONFIGURED = _(
    u"This component is misconfigured and can't be displayed. It needs to be fixed by the course authors."
)
STAGE_NOT_OPEN_TEMPLATE = _(u"Can't {action} as stage is not yet opened.")
STAGE_CLOSED_TEMPLATE = _(u"Can't {action} as stage is closed.")
USER_NOT_ACCESS_DASHBOARD = _(u"User can't access dashboard")
USER_NOT_ACCESS_GROUPWORK = _(u"User can't access this group work")

# Group Project XBlock messages
NO_ACTIVITIES = _(u"This Group Project does not contain any activities.")
NO_PROJECT_NAVIGATOR = _(
    u"This Group Project V2 does not contain Project Navigator - please edit course outline "
    u"in Studio to include one.")
NO_DISCUSSION = _(u"This Group Project V2 does not contain a discussion.")
MUST_CONTAIN_PROJECT_NAVIGATOR_BLOCK = _(
    u"Group Project must contain Project Navigator Block.")

# Group Activity XBlock messages
NO_STAGES = _(u"This Group Project Activity does not contain any stages.")
SHOULD_BE_INTEGER = _(u"{field_name} must be integer, {field_value} given.")
ASSIGNED_TO_GROUPS_LABEL = _(
    u"This project is assigned to {group_count} group(s)"
)  # no full stop (period) by design

# Project Navigator messages
MUST_CONTAIN_NAVIGATION_VIEW = _(
    u"Project Navigator must contain Navigation view.")
Exemple #34
0
class TeamEvaluationStage(ReviewBaseStage):
    display_name = String(display_name=DISPLAY_NAME_NAME,
                          help=DISPLAY_NAME_HELP,
                          scope=Scope.content,
                          default=_(u"Team Evaluation Stage"))

    CATEGORY = 'gp-v2-stage-team-evaluation'
    STAGE_CONTENT_TEMPLATE = 'templates/html/stages/team_evaluation.html'

    STUDIO_LABEL = _(u"Team Evaluation")

    REVIEW_ITEM_KEY = "user"

    @lazy
    def review_subjects(self):
        return [
            user for user in self.workgroup.users if user.id != self.user_id
        ]

    @property
    def allowed_nested_blocks(self):
        blocks = super(TeamEvaluationStage, self).allowed_nested_blocks
        blocks.extend([PeerSelectorXBlock])
        return blocks

    def review_status(self):
        review_subjects_ids = [user.id for user in self.review_subjects]
        all_review_items = self.project_api.get_peer_review_items_for_group(
            self.workgroup.id, self.activity_content_id)
        review_items = [
            item for item in all_review_items
            if item['reviewer'] == self.anonymous_student_id
        ]

        return self._calculate_review_status(review_subjects_ids, review_items)

    def get_review_data(self, user_id):
        """
        :param int user_id: User ID
        :rtype: (set[int], dict)
        """
        workgroup = self.project_api.get_user_workgroup_for_course(
            user_id, self.course_id)
        review_subjects_ids = set(user.id
                                  for user in workgroup.users) - {user_id}
        review_items = self._get_review_items_for_group(
            self.project_api, workgroup.id, self.activity_content_id)
        review_items_by_user = self._get_reviews_by_user(review_items, user_id)
        return review_subjects_ids, review_items_by_user

    def get_review_state(self, review_subject_id):
        review_items = self.project_api.get_peer_review_items(
            self.anonymous_student_id, review_subject_id, self.group_id,
            self.activity_content_id)
        return self._calculate_review_status([review_subject_id], review_items)

    @staticmethod
    @memoize_with_expiration()
    def _get_review_items_for_group(project_api, workgroup_id,
                                    activity_content_id):
        return project_api.get_peer_review_items_for_group(
            workgroup_id, activity_content_id)

    def validate(self):
        violations = super(TeamEvaluationStage, self).validate()

        # Technically, nothing prevents us from allowing graded peer review questions. The only reason why
        # they are considered not supported is that GroupActivityXBlock.calculate_grade does not
        # take them into account.
        if self.grade_questions:
            violations.add(
                ValidationMessage(
                    ValidationMessage.ERROR,
                    messages.GRADED_QUESTIONS_NOT_SUPPORTED.format(
                        class_name=self.STUDIO_LABEL,
                        stage_title=self.display_name)))

        if not self.has_child_of_category(PeerSelectorXBlock.CATEGORY):
            violations.add(
                ValidationMessage(
                    ValidationMessage.ERROR,
                    messages.PEER_SELECTOR_BLOCK_IS_MISSING.format(
                        class_name=self.STUDIO_LABEL,
                        stage_title=self.display_name,
                        peer_selector_class_name=PeerSelectorXBlock.
                        STUDIO_LABEL)))

        return violations

    @XBlock.handler
    @groupwork_protected_handler
    @key_error_protected_handler
    @conversion_protected_handler
    def load_peer_feedback(self, request, _suffix=''):
        peer_id = int(request.GET["peer_id"])
        feedback = self.project_api.get_peer_review_items(
            self.anonymous_student_id,
            peer_id,
            self.workgroup.id,
            self.activity_content_id,
        )
        results = self._pivot_feedback(feedback)

        return webob.response.Response(body=json.dumps(results))

    def do_submit_review(self, submissions):
        peer_id = int(submissions["review_subject_id"])
        del submissions["review_subject_id"]

        self.project_api.submit_peer_review_items(
            self.anonymous_student_id,
            peer_id,
            self.workgroup.id,
            self.activity_content_id,
            submissions,
        )