def track_forum_event(request, event_name, course, obj, data, id_map=None): """ Send out an analytics event when a forum event happens. Works for threads, responses to threads, and comments on those responses. """ user = request.user data['id'] = obj.id commentable_id = data['commentable_id'] team = get_team(commentable_id) if team is not None: data.update(team_id=team.team_id) if id_map is None: id_map = get_cached_discussion_id_map(course, [commentable_id], user) if commentable_id in id_map: data['category_name'] = id_map[commentable_id]["title"] data['category_id'] = commentable_id data['url'] = request.META.get('HTTP_REFERER', '') data['user_forums_roles'] = [ role.name for role in user.roles.filter(course_id=course.id) ] data['user_course_roles'] = [ role.role for role in user.courseaccessrole_set.filter(course_id=course.id) ] tracker.emit(event_name, data)
def subsection_grade_calculated(subsection_grade): """ Emits an edx.grades.subsection.grade_calculated event with data from the passed subsection_grade. """ event_name = SUBSECTION_GRADE_CALCULATED context = contexts.course_context_from_course_id(subsection_grade.course_id) # TODO (AN-6134): remove this context manager with tracker.get_tracker().context(event_name, context): tracker.emit( event_name, { 'user_id': unicode(subsection_grade.user_id), 'course_id': unicode(subsection_grade.course_id), 'block_id': unicode(subsection_grade.usage_key), 'course_version': unicode(subsection_grade.course_version), 'weighted_total_earned': subsection_grade.earned_all, 'weighted_total_possible': subsection_grade.possible_all, 'weighted_graded_earned': subsection_grade.earned_graded, 'weighted_graded_possible': subsection_grade.possible_graded, 'first_attempted': unicode(subsection_grade.first_attempted), 'subtree_edited_timestamp': unicode(subsection_grade.subtree_edited_timestamp), 'event_transaction_id': unicode(get_event_transaction_id()), 'event_transaction_type': unicode(get_event_transaction_type()), 'visible_blocks_hash': unicode(subsection_grade.visible_blocks_id), } )
def set_cert_generation_enabled(course_key, is_enabled): """Enable or disable self-generated certificates for a course. There are two "switches" that control whether self-generated certificates are enabled for a course: 1) Whether the self-generated certificates feature is enabled. 2) Whether self-generated certificates have been enabled for this particular course. The second flag should be enabled *only* when someone has successfully generated example certificates for the course. This helps avoid configuration errors (for example, not having a template configured for the course installed on the workers). The UI for the instructor dashboard enforces this constraint. Arguments: course_key (CourseKey): The course identifier. Keyword Arguments: is_enabled (boolean): If provided, enable/disable self-generated certificates for this course. """ CertificateGenerationCourseSetting.set_enabled_for_course(course_key, is_enabled) cert_event_type = 'enabled' if is_enabled else 'disabled' event_name = '.'.join(['edx', 'certificate', 'generation', cert_event_type]) tracker.emit(event_name, { 'course_id': unicode(course_key), }) if is_enabled: log.info(u"Enabled self-generated certificates for course '%s'.", unicode(course_key)) else: log.info(u"Disabled self-generated certificates for course '%s'.", unicode(course_key))
def _track_certificate_events(request, context, course, user, user_certificate): """ Tracks web certificate view related events. """ badge = context['badge'] # Badge Request Event Tracking Logic if 'evidence_visit' in request.GET and badge: tracker.emit( 'edx.badge.assertion.evidence_visited', { 'user_id': user.id, 'course_id': unicode(course.id), 'enrollment_mode': badge.mode, 'assertion_id': badge.id, 'assertion_image_url': badge.data['image'], 'assertion_json_url': badge.data['json']['id'], 'issuer': badge.data['issuer'], } ) # track certificate evidence_visited event for analytics when certificate_user and accessing_user are different if request.user and request.user.id != user.id: emit_certificate_event('evidence_visited', user, unicode(course.id), course, { 'certificate_id': user_certificate.verify_uuid, 'enrollment_mode': user_certificate.mode, 'social_network': CertificateSocialNetworks.linkedin })
def set_s3_info(self, data, _suffix=''): """ Set required information of amazon web service for file uploading. Args: data: dict in JSON format data['aws_access_key']: Amazon Web Services access key data['aws_secret_key']: Amazon Web Services secret key data['bucketName']: Bucket name of your Amazon Web Services data['uploadedFileDir']: Directory for your upload files Returns: result['Success']: the boolean indicator for whether the setting is complete """ if not self.get_user_is_staff(): result = { 'error': 'Set S3 information without permission', 'Success': False } tracker.emit('set_s3_info', result) return result self.s3_configuration['aws_access_key'] = data['aws_access_key'] self.s3_configuration['aws_secret_key'] = data['aws_secret_key'] self.s3_configuration['bucketName'] = data['bucketName'] if data['uploadedFileDir'][len(data['uploadedFileDir']) - 1] == '/': self.s3_configuration['uploadedFileDir'] = data['uploadedFileDir'] else: self.s3_configuration['uploadedFileDir'] = (data['uploadedFileDir'] + '/') result = self.s3_configuration.copy() result['Success'] = True tracker.emit('set_s3_info', result) return result
def add_cohort(course_key, name, assignment_type): """ Add a cohort to a course. Raises ValueError if a cohort of the same name already exists. """ log.debug("Adding cohort %s to %s", name, course_key) if is_cohort_exists(course_key, name): raise ValueError(_("You cannot create two cohorts with the same name")) try: course = courses.get_course_by_id(course_key) except Http404: raise ValueError("Invalid course_key") cohort = CourseCohort.create( cohort_name=name, course_id=course.id, assignment_type=assignment_type ).course_user_group tracker.emit( "edx.cohort.creation_requested", {"cohort_name": cohort.name, "cohort_id": cohort.id} ) return cohort
def emit_event(self, event_name): """ Emits an event to explicitly track course enrollment and unenrollment. """ try: context = contexts.course_context_from_course_id(self.course_id) assert(isinstance(self.course_id, CourseKey)) data = { 'user_id': self.user.id, 'course_id': self.course_id.to_deprecated_string(), 'mode': self.mode, } with tracker.get_tracker().context(event_name, context): tracker.emit(event_name, data) if settings.FEATURES.get('SEGMENT_IO_LMS') and settings.SEGMENT_IO_LMS_KEY: tracking_context = tracker.get_tracker().resolve_context() analytics.track(self.user_id, event_name, { 'category': 'conversion', 'label': self.course_id.to_deprecated_string(), 'org': self.course_id.org, 'course': self.course_id.course, 'run': self.course_id.run, 'mode': self.mode, }, context={ 'Google Analytics': { 'clientId': tracking_context.get('client_id') } }) except: # pylint: disable=bare-except if event_name and self.course_id: log.exception('Unable to emit event %s for user %s and course %s', event_name, self.user.username, self.course_id)
def search(cls, query_params): default_params = {'page': 1, 'per_page': 20, 'course_id': query_params['course_id'], 'recursive': False, 'with_responses': True} params = merge_dict(default_params, strip_blank(strip_none(query_params))) if query_params.get('text'): url = cls.url(action='search') else: url = cls.url(action='get_all', params=extract(params, 'commentable_id')) if params.get('commentable_id'): del params['commentable_id'] response = perform_request( 'get', url, params, metric_tags=[u'course_id:{}'.format(query_params['course_id'])], metric_action='thread.search', paged_results=True ) if query_params.get('text'): search_query = query_params['text'] course_id = query_params['course_id'] group_id = query_params['group_id'] if 'group_id' in query_params else None requested_page = params['page'] total_results = response.get('total_results') corrected_text = response.get('corrected_text') # Record search result metric to allow search quality analysis. # course_id is already included in the context for the event tracker tracker.emit( 'edx.forum.searched', { 'query': search_query, 'corrected_text': corrected_text, 'group_id': group_id, 'page': requested_page, 'total_results': total_results, } ) log.info( u'forum_text_search query="{search_query}" corrected_text="{corrected_text}" course_id={course_id} group_id={group_id} page={requested_page} total_results={total_results}'.format( search_query=search_query, corrected_text=corrected_text, course_id=course_id, group_id=group_id, requested_page=requested_page, total_results=total_results ) ) return CommentClientPaginatedResult( collection=response.get('collection', []), page=response.get('page', 1), num_pages=response.get('num_pages', 1), thread_count=response.get('thread_count', 0), corrected_text=response.get('corrected_text', None) )
def emit_setting_changed_event(user, db_table, setting_name, old_value, new_value): """Emits an event for a change in a setting. Args: user (User): the user that this setting is associated with. db_table (str): the name of the table that we're modifying. setting_name (str): the name of the setting being changed. old_value (object): the value before the change. new_value (object): the new value being saved. Returns: None """ # Compute the maximum value length so that two copies can fit into the maximum event size # in addition to all the other fields recorded. max_value_length = settings.TRACK_MAX_EVENT / 4 serialized_old_value, old_was_truncated = _get_truncated_setting_value(old_value, max_length=max_value_length) serialized_new_value, new_was_truncated = _get_truncated_setting_value(new_value, max_length=max_value_length) truncated_values = [] if old_was_truncated: truncated_values.append("old") if new_was_truncated: truncated_values.append("new") tracker.emit( USER_SETTINGS_CHANGED_EVENT_NAME, { "setting": setting_name, "old": serialized_old_value, "new": serialized_new_value, "truncated": truncated_values, "user_id": user.id, "table": db_table, } )
def _emit_problem_submitted_event(kwargs): """ Emits a problem submitted event only if there is no current event transaction type, i.e. we have not reached this point in the code via a rescore or student state deletion. """ root_type = get_event_transaction_type() if not root_type: root_id = get_event_transaction_id() if not root_id: root_id = create_new_event_transaction_id() set_event_transaction_type(PROBLEM_SUBMITTED_EVENT_TYPE) tracker.emit( unicode(PROBLEM_SUBMITTED_EVENT_TYPE), { 'user_id': unicode(kwargs['user_id']), 'course_id': unicode(kwargs['course_id']), 'problem_id': unicode(kwargs['usage_id']), 'event_transaction_id': unicode(root_id), 'event_transaction_type': unicode(PROBLEM_SUBMITTED_EVENT_TYPE), 'weighted_earned': kwargs.get('weighted_earned'), 'weighted_possible': kwargs.get('weighted_possible'), } )
def emit_setting_changed_event(user, db_table, setting_name, old_value, new_value): """Emits an event for a change in a setting. Args: user (User): the user that this setting is associated with. db_table (str): the name of the table that we're modifying. setting_name (str): the name of the setting being changed. old_value (object): the value before the change. new_value (object): the new value being saved. Returns: None """ truncated_fields = truncate_fields(old_value, new_value) truncated_fields['setting'] = setting_name truncated_fields['user_id'] = user.id truncated_fields['table'] = db_table tracker.emit( USER_SETTINGS_CHANGED_EVENT_NAME, truncated_fields ) # Announce field change USER_FIELD_CHANGED.send(sender=None, user=user, table=db_table, setting=setting_name, old_value=old_value, new_value=new_value)
def add_user_to_cohort(cohort, username_or_email): """ Look up the given user, and if successful, add them to the specified cohort. Arguments: cohort: CourseUserGroup username_or_email: string. Treated as email if has '@' Returns: Tuple of User object and string (or None) indicating previous cohort Raises: User.DoesNotExist if can't find user. ValueError if user already present in this cohort. """ user = get_user_by_username_or_email(username_or_email) membership = CohortMembership(course_user_group=cohort, user=user) membership.save() tracker.emit( "edx.cohort.user_add_requested", { "user_id": user.id, "cohort_id": cohort.id, "cohort_name": cohort.name, "previous_cohort_id": membership.previous_cohort_id, "previous_cohort_name": membership.previous_cohort_name, } ) return (user, membership.previous_cohort_name)
def _emit_grade_calculated_event(grade): """ Emits an edx.grades.subsection.grade_calculated event with data from the passed grade. """ # TODO: remove this context manager after completion of AN-6134 event_name = u'edx.grades.subsection.grade_calculated' context = contexts.course_context_from_course_id(grade.course_id) with tracker.get_tracker().context(event_name, context): tracker.emit( event_name, { 'user_id': unicode(grade.user_id), 'course_id': unicode(grade.course_id), 'block_id': unicode(grade.usage_key), 'course_version': unicode(grade.course_version), 'weighted_total_earned': grade.earned_all, 'weighted_total_possible': grade.possible_all, 'weighted_graded_earned': grade.earned_graded, 'weighted_graded_possible': grade.possible_graded, 'first_attempted': unicode(grade.first_attempted), 'subtree_edited_timestamp': unicode(grade.subtree_edited_timestamp), 'event_transaction_id': unicode(get_event_transaction_id()), 'event_transaction_type': unicode(get_event_transaction_type()), 'visible_blocks_hash': unicode(grade.visible_blocks_id), } )
def track_forum_event(request, event_name, course, obj, data, id_map=None): """ Send out an analytics event when a forum event happens. Works for threads, responses to threads, and comments on those responses. """ user = request.user data['id'] = obj.id commentable_id = data['commentable_id'] if id_map is None: id_map = get_cached_discussion_id_map(course, [commentable_id], user) if commentable_id in id_map: data['category_name'] = id_map[commentable_id]["title"] data['category_id'] = commentable_id if len(obj.body) > TRACKING_MAX_FORUM_BODY: data['truncated'] = True else: data['truncated'] = False data['body'] = obj.body[:TRACKING_MAX_FORUM_BODY] data['url'] = request.META.get('HTTP_REFERER', '') data['user_forums_roles'] = [ role.name for role in user.roles.filter(course_id=course.id) ] data['user_course_roles'] = [ role.role for role in user.courseaccessrole_set.filter(course_id=course.id) ] tracker.emit(event_name, data)
def upload_csv_to_report_store(rows, csv_name, course_id, timestamp): """ Upload data as a CSV using ReportStore. Arguments: rows: CSV data in the following format (first column may be a header): [ [row1_colum1, row1_colum2, ...], ... ] csv_name: Name of the resulting CSV course_id: ID of the course """ report_store = ReportStore.from_config() report_store.store_rows( course_id, u"{course_prefix}_{csv_name}_{timestamp_str}.csv".format( course_prefix=course_filename_prefix_generator(course_id), csv_name=csv_name, timestamp_str=timestamp.strftime("%Y-%m-%d-%H%M") ), rows ) tracker.emit(REPORT_REQUESTED_EVENT_NAME, {"report_type": csv_name, })
def add_cohort(course_key, name): """ Add a cohort to a course. Raises ValueError if a cohort of the same name already exists. """ log.debug("Adding cohort %s to %s", name, course_key) if CourseUserGroup.objects.filter(course_id=course_key, group_type=CourseUserGroup.COHORT, name=name).exists(): raise ValueError(_("You cannot create two cohorts with the same name")) try: course = courses.get_course_by_id(course_key) except Http404: raise ValueError("Invalid course_key") cohort = CourseUserGroup.objects.create( course_id=course.id, group_type=CourseUserGroup.COHORT, name=name ) tracker.emit( "edx.cohort.creation_requested", {"cohort_name": cohort.name, "cohort_id": cohort.id} ) return cohort
def add_resource(self, data, _suffix=''): # pylint: disable=unused-argument """ Add a new resource entry. Args: data: dict in JSON format data[resource_content_field]: the resource to be added. Dictionary of description, etc. as defined above Returns: result: dict in JSON format result['error']: error message generated if the addition fails result[resource_content_field]: the content of the added resource """ # Construct new resource result = {} for field in self.resource_content_fields: result[field] = data[field] resource_id = stem_url(data['url']) self._check_redundant_resource(resource_id, 'add_resource', result) self._check_removed_resource(resource_id, 'add_resource', result) result['id'] = resource_id result['upvotes'] = 0 result['downvotes'] = 0 self.recommendations[resource_id] = dict(result) tracker.emit('add_resource', result) result["description"] = self._get_onetime_url(result["description"]) return result
def emit_event(self, event_name): """ Emits an event to explicitly track course enrollment and unenrollment. """ try: context = contexts.course_context_from_course_id(self.course_id) assert isinstance(self.course_id, CourseKey) data = {"user_id": self.user.id, "course_id": self.course_id.to_deprecated_string(), "mode": self.mode} with tracker.get_tracker().context(event_name, context): tracker.emit(event_name, data) if settings.FEATURES.get("SEGMENT_IO_LMS") and settings.SEGMENT_IO_LMS_KEY: tracking_context = tracker.get_tracker().resolve_context() analytics.track( self.user_id, event_name, { "category": "conversion", "label": self.course_id.to_deprecated_string(), "org": self.course_id.org, "course": self.course_id.course, "run": self.course_id.run, "mode": self.mode, }, context={"Google Analytics": {"clientId": tracking_context.get("client_id")}}, ) except: # pylint: disable=bare-except if event_name and self.course_id: log.exception( "Unable to emit event %s for user %s and course %s", event_name, self.user.username, self.course_id )
def _emit_event(name, context, data): """ Do the actual integration into the event-tracker """ try: if context: # try to parse out the org_id from the course_id if 'course_id' in context: try: course_key = CourseKey.from_string(context['course_id']) context['org_id'] = course_key.org except InvalidKeyError: # leave org_id blank pass with tracker.get_tracker().context(name, context): tracker.emit(name, data) else: # if None is passed in then we don't construct the 'with' context stack tracker.emit(name, data) except KeyError: # This happens when a default tracker has not been registered by the host application # aka LMS. This is normal when running unit tests in isolation. log.warning( 'Analytics tracker not properly configured. ' 'If this message appears in a production environment, please investigate' )
def user_track(request): """ Log when POST call to "event" URL is made by a user. GET or POST call should provide "event_type", "event", and "page" arguments. """ try: username = request.user.username except: username = "******" name = _get_request_value(request, 'event_type') data = _get_request_value(request, 'event', {}) page = _get_request_value(request, 'page') if isinstance(data, six.string_types) and len(data) > 0: try: data = json.loads(data) _add_user_id_for_username(data) except ValueError: pass context_override = contexts.course_context_from_url(page) context_override['username'] = username context_override['event_source'] = 'browser' context_override['page'] = page with eventtracker.get_tracker().context('edx.course.browser', context_override): eventtracker.emit(name=name, data=data) return HttpResponse('success')
def emit_team_event(event_name, course_key, event_data): """ Emit team events with the correct course id context. """ context = contexts.course_context_from_course_id(course_key) with tracker.get_tracker().context(event_name, context): tracker.emit(event_name, event_data)
def _cohort_added(sender, **kwargs): """Emits a tracking log event each time a cohort is created""" instance = kwargs["instance"] if kwargs["created"] and instance.group_type == CourseUserGroup.COHORT: tracker.emit( "edx.cohort.created", {"cohort_id": instance.id, "cohort_name": instance.name} )
def upload_screenshot(self, request, _suffix=''): # pylint: disable=unused-argument """ Upload a screenshot for an entry of resource as a preview (typically to S3 or filesystem). Args: request: HTTP POST request request.POST['file'].file: the file to be uploaded Returns: response: HTTP response response.body (response.responseText): name of the uploaded file We validate that this is a valid JPG, GIF, or PNG by checking magic number, mimetype, and extension all correspond. We also limit to 30MB. We save the file under its MD5 hash to (1) avoid name conflicts, (2) avoid race conditions and (3) save space. """ # Check invalid file types image_types = { 'jpeg': { 'extension': [".jpeg", ".jpg"], 'mimetypes': ['image/jpeg', 'image/pjpeg'], 'magic': ["ffd8"] }, 'png': { 'extension': [".png"], 'mimetypes': ['image/png'], 'magic': ["89504e470d0a1a0a"] }, 'gif': { 'extension': [".gif"], 'mimetypes': ['image/gif'], 'magic': ["474946383961", "474946383761"] } } file_type_error_msg = 'Please upload an image in GIF/JPG/PNG' result = self._check_upload_file( request, image_types, file_type_error_msg, 'upload_screenshot', 31457280 ) if isinstance(result, Response): return result try: content = request.POST['file'].file.read() file_id = hashlib.md5(content).hexdigest() file_name = (file_id + '.' + result) fhwrite = self.fs.open(file_name, "wb") fhwrite.write(content) fhwrite.close() except IOError: return self._raise_pyfs_error('upload_screenshot') response = Response() response.body = json.dumps({'file_name': str("fs://" + file_name)}) response.headers['Content-Type'] = 'application/json' tracker.emit('upload_screenshot', {'uploadedFileName': response.body}) response.status = 200 return response
def get_group_for_user(cls, course_key, user, user_partition, assign=True): """ Returns the group from the specified user position to which the user is assigned. If the user has not yet been assigned, a group will be randomly chosen for them if assign flag is True. """ partition_key = cls.key_for_partition(user_partition) group_id = course_tag_api.get_course_tag(user, course_key, partition_key) group = None if group_id is not None: # attempt to look up the presently assigned group try: group = user_partition.get_group(int(group_id)) except NoSuchUserPartitionGroupError: # jsa: we can turn off warnings here if this is an expected case. log.warn( u"group not found in RandomUserPartitionScheme: %r", { "requested_partition_id": user_partition.id, "requested_group_id": group_id, }, exc_info=True ) if group is None and assign and not course_tag_api.BulkCourseTags.is_prefetched(course_key): if not user_partition.groups: raise UserPartitionError('Cannot assign user to an empty user partition') # pylint: disable=fixme # TODO: had a discussion in arch council about making randomization more # deterministic (e.g. some hash). Could do that, but need to be careful not # to introduce correlation between users or bias in generation. group = cls.RANDOM.choice(user_partition.groups) # persist the value as a course tag course_tag_api.set_course_tag(user, course_key, partition_key, group.id) # emit event for analytics # FYI - context is always user ID that is logged in, NOT the user id that is # being operated on. If instructor can move user explicitly, then we should # put in event_info the user id that is being operated on. event_name = 'xmodule.partitions.assigned_user_to_partition' event_info = { 'group_id': group.id, 'group_name': group.name, 'partition_id': user_partition.id, 'partition_name': user_partition.name } # pylint: disable=fixme # TODO: Use the XBlock publish api instead with tracker.get_tracker().context(event_name, {}): tracker.emit( event_name, event_info, ) return group
def handle_vote(self, data, _suffix=''): # pylint: disable=unused-argument """ Add/Subtract a vote to a resource entry. Args: data: dict in JSON format data['id']: the ID of the resouce which was upvoted/downvoted data['event']: recommender_upvote or recommender_downvote Returns: result: dict in JSON format result['error']: error message generated if the process fails result['oldVotes']: original # of votes result['newVotes']: votes after this action result['toggle']: boolean indicator for whether the resource was switched from downvoted to upvoted """ resource_id = self._validate_resource(data['id'], data['event']) result = {} result['id'] = resource_id is_event_upvote = (data['event'] == 'recommender_upvote') result['oldVotes'] = (self.recommendations[resource_id]['upvotes'] - self.recommendations[resource_id]['downvotes']) upvoting_existing_upvote = is_event_upvote and resource_id in self.upvoted_ids downvoting_existing_downvote = not is_event_upvote and resource_id in self.downvoted_ids if upvoting_existing_upvote: # While the user is trying to upvote a resource which has been # upvoted, we restore the resource to unvoted self.upvoted_ids.remove(resource_id) self.recommendations[resource_id]['upvotes'] -= 1 elif downvoting_existing_downvote: # While the user is trying to downvote a resource which has # been downvoted, we restore the resource to unvoted self.downvoted_ids.remove(resource_id) self.recommendations[resource_id]['downvotes'] -= 1 elif is_event_upvote: # New upvote if resource_id in self.downvoted_ids: self.downvoted_ids.remove(resource_id) self.recommendations[resource_id]['downvotes'] -= 1 result['toggle'] = True self.upvoted_ids.append(resource_id) self.recommendations[resource_id]['upvotes'] += 1 else: # New downvote if resource_id in self.upvoted_ids: self.upvoted_ids.remove(resource_id) self.recommendations[resource_id]['upvotes'] -= 1 result['toggle'] = True self.downvoted_ids.append(resource_id) self.recommendations[resource_id]['downvotes'] += 1 result['newVotes'] = (self.recommendations[resource_id]['upvotes'] - self.recommendations[resource_id]['downvotes']) tracker.emit(data['event'], result) return result
def emit_course_goal_event(sender, instance, **kwargs): name = 'edx.course.goal.added' if kwargs.get('created', False) else 'edx.course.goal.updated' tracker.emit( name, { 'goal_key': instance.goal_key, } ) if settings.LMS_SEGMENT_KEY: update_google_analytics(name, instance.user.id)
def is_user_staff(self, _data, _suffix=''): """ Return whether the user is staff. Returns: is_user_staff: indicator for whether the user is staff """ result = {'is_user_staff': self.get_user_is_staff()} tracker.emit('is_user_staff', result) return result
def emit_course_goal_event(sender, instance, **kwargs): """Emit events for both tracking logs and for Segment.""" name = 'edx.course.goal.added' if kwargs.get('created', False) else 'edx.course.goal.updated' tracker.emit( name, { 'goal_key': instance.goal_key, } ) segment.track(instance.user.id, name)
def flag_resource(self, data, _suffix=''): # pylint: disable=unused-argument """ Flag (or unflag) an entry of problematic resource and give the reason. This shows in a list for staff to review. Args: data: dict in JSON format data['id']: the ID of the problematic resouce data['isProblematic']: the boolean indicator for whether the resource is being flagged or unflagged. Only flagging works. data['reason']: the reason why the user believes the resource is problematic Returns: result: dict in JSON format result['reason']: the new reason result['oldReason']: the old reason result['id']: the ID of the problematic resouce result['isProblematic']: the boolean indicator for whether the resource is now flagged """ result = {} result['id'] = data['id'] result['isProblematic'] = data['isProblematic'] result['reason'] = data['reason'] user_id = self.get_user_id() # If already flagged, update the reason for the flag if data['isProblematic']: # If already flagged, update the reason if data['id'] in self.flagged_ids: result['oldReason'] = self.flagged_reasons[ self.flagged_ids.index(data['id'])] self.flagged_reasons[ self.flagged_ids.index(data['id'])] = data['reason'] # Otherwise, flag it. else: self.flagged_ids.append(data['id']) self.flagged_reasons.append(data['reason']) if user_id not in self.flagged_accum_resources: self.flagged_accum_resources[user_id] = {} self.flagged_accum_resources[user_id][data['id']] = data['reason'] # Unflag resource. Currently unsupported. else: if data['id'] in self.flagged_ids: result['oldReason'] = self.flagged_reasons[ self.flagged_ids.index(data['id'])] result['reason'] = '' idx = self.flagged_ids.index(data['id']) del self.flagged_ids[idx] del self.flagged_reasons[idx] del self.flagged_accum_resources[user_id][data['id']] tracker.emit('flag_resource', result) return result
def search(cls, query_params): default_params = {"page": 1, "per_page": 20, "course_id": query_params["course_id"], "recursive": False} params = merge_dict(default_params, strip_blank(strip_none(query_params))) if query_params.get("text"): url = cls.url(action="search") else: url = cls.url(action="get_all", params=extract(params, "commentable_id")) if params.get("commentable_id"): del params["commentable_id"] response = perform_request( "get", url, params, metric_tags=[u"course_id:{}".format(query_params["course_id"])], metric_action="thread.search", paged_results=True, ) if query_params.get("text"): search_query = query_params["text"] course_id = query_params["course_id"] group_id = query_params["group_id"] if "group_id" in query_params else None requested_page = params["page"] total_results = response.get("total_results") corrected_text = response.get("corrected_text") # Record search result metric to allow search quality analysis. # course_id is already included in the context for the event tracker tracker.emit( "edx.forum.searched", { "query": search_query, "corrected_text": corrected_text, "group_id": group_id, "page": requested_page, "total_results": total_results, }, ) log.info( u'forum_text_search query="{search_query}" corrected_text="{corrected_text}" course_id={course_id} group_id={group_id} page={requested_page} total_results={total_results}'.format( search_query=search_query, corrected_text=corrected_text, course_id=course_id, group_id=group_id, requested_page=requested_page, total_results=total_results, ) ) return ( response.get("collection", []), response.get("page", 1), response.get("num_pages", 1), response.get("corrected_text"), )
def publish_event(event_name, result, **kwargs): """ Helper function to publish an event for analytics purposes """ event_data = { "location": unicode(location), "previous_count": previous_count, "result": result, "max_count": max_count, } event_data.update(kwargs) context = contexts.course_context_from_course_id( location.course_key) if user_id: context['user_id'] = user_id full_event_name = "edx.librarycontentblock.content.{}".format( event_name) with tracker.get_tracker().context(full_event_name, context): tracker.emit(full_event_name, event_data)
def _track_index_request(event_name, indexed_count, location=None): """Track content index requests. Arguments: location (str): The ID of content to be indexed. event_name (str): Name of the event to be logged. Returns: None """ data = { "indexed_count": indexed_count, 'category': 'courseware_index', } if location: data['location_id'] = location tracker.emit(event_name, data)
def set_client_configuration(self, data, _suffix=''): # pylint: disable=unused-argument """ Set the parameters for student-view, client side configurations. Args: data: dict in JSON format. Keys in data: disable_dev_ux: feature flag for any new UX under development which should not appear in prod entries_per_page: the number of resources in each page page_span: page range in pagination control intro_enable: Should we show the users a short usage tutorial the first time they see the XBlock? """ self.intro_enabled = data['intro_enable'] for key in ['disable_dev_ux', 'page_span', 'entries_per_page']: self.client_configuration[key] = data[key] tracker.emit('set_client_configuration', data) return {}
def _cohort_membership_changed(sender, **kwargs): """Emits a tracking log event each time cohort membership is modified""" def get_event_iter(user_id_iter, cohort_iter): """ Returns a dictionary containing a mashup of cohort and user information for the given lists """ return ({ "cohort_id": cohort.id, "cohort_name": cohort.name, "user_id": user_id } for user_id in user_id_iter for cohort in cohort_iter) action = kwargs["action"] instance = kwargs["instance"] pk_set = kwargs["pk_set"] reverse = kwargs["reverse"] if action == "post_add": event_name = "edx.cohort.user_added" elif action in ["post_remove", "pre_clear"]: event_name = "edx.cohort.user_removed" else: return if reverse: user_id_iter = [instance.id] if action == "pre_clear": cohort_iter = instance.course_groups.filter( group_type=CourseUserGroup.COHORT) else: cohort_iter = CourseUserGroup.objects.filter( pk__in=pk_set, group_type=CourseUserGroup.COHORT) else: cohort_iter = [ instance ] if instance.group_type == CourseUserGroup.COHORT else [] if action == "pre_clear": user_id_iter = (user.id for user in instance.users.all()) else: user_id_iter = pk_set for event in get_event_iter(user_id_iter, cohort_iter): tracker.emit(event_name, event)
def _send_assertion_created_event(self, user, assertion): """ Send an analytics event to record the creation of a badge assertion. """ tracker.emit( 'edx.badge.assertion.created', { 'user_id': user.id, 'badge_slug': assertion.badge_class.slug, 'badge_badgr_server_slug': assertion.badge_class.badgr_server_slug, 'badge_name': assertion.badge_class.display_name, 'issuing_component': assertion.badge_class.issuing_component, 'course_id': str(assertion.badge_class.course_id), 'enrollment_mode': assertion.badge_class.mode, 'assertion_id': assertion.id, 'assertion_image_url': assertion.image_url, 'assertion_json_url': assertion.assertion_url, 'issuer': assertion.data.get('issuer'), })
def _emit_event(kwargs): """ Emits a problem submitted event only if there is no current event transaction type, i.e. we have not reached this point in the code via a rescore or student state deletion. If the event transaction type has already been set and the transacation is a rescore, emits a problem rescored event. """ root_type = get_event_transaction_type() if not root_type: root_id = get_event_transaction_id() if not root_id: root_id = create_new_event_transaction_id() set_event_transaction_type(PROBLEM_SUBMITTED_EVENT_TYPE) tracker.emit( unicode(PROBLEM_SUBMITTED_EVENT_TYPE), { 'user_id': unicode(kwargs['user_id']), 'course_id': unicode(kwargs['course_id']), 'problem_id': unicode(kwargs['usage_id']), 'event_transaction_id': unicode(root_id), 'event_transaction_type': unicode(PROBLEM_SUBMITTED_EVENT_TYPE), 'weighted_earned': kwargs.get('weighted_earned'), 'weighted_possible': kwargs.get('weighted_possible'), }) if root_type in [GRADES_RESCORE_EVENT_TYPE, GRADES_OVERRIDE_EVENT_TYPE]: current_user = get_current_user() instructor_id = getattr(current_user, 'id', None) tracker.emit( unicode(GRADES_RESCORE_EVENT_TYPE), { 'course_id': unicode(kwargs['course_id']), 'user_id': unicode(kwargs['user_id']), 'problem_id': unicode(kwargs['usage_id']), 'new_weighted_earned': kwargs.get('weighted_earned'), 'new_weighted_possible': kwargs.get('weighted_possible'), 'only_if_higher': kwargs.get('only_if_higher'), 'instructor_id': unicode(instructor_id), 'event_transaction_id': unicode(get_event_transaction_id()), 'event_transaction_type': unicode(root_type), })
def password_reset(request): """ Attempts to send a password reset e-mail. """ password_reset_email_limiter = PasswordResetEmailRateLimiter() if password_reset_email_limiter.is_rate_limit_exceeded(request): AUDIT_LOG.warning("Password reset rate limit exceeded") return HttpResponse(_( "Your previous request is in progress, please try again in a few moments." ), status=403) form = PasswordResetFormNoActive(request.POST) if form.is_valid(): form.save(use_https=request.is_secure(), from_email=configuration_helpers.get_value( 'email_from_address', settings.DEFAULT_FROM_EMAIL), request=request) # When password change is complete, a "edx.user.settings.changed" event will be emitted. # But because changing the password is multi-step, we also emit an event here so that we can # track where the request was initiated. tracker.emit( SETTING_CHANGE_INITIATED, { "setting": "password", "old": None, "new": None, "user_id": request.user.id, }) destroy_oauth_tokens(request.user) else: # bad user? tick the rate limiter counter AUDIT_LOG.info("Bad password_reset user passed in.") password_reset_email_limiter.tick_request_counter(request) return JsonResponse({ 'success': True, 'value': render_to_string('registration/password_reset_done.html', {}), })
def _track_index_request(cls, event_name, category, indexed_count): """Track content index requests. Arguments: event_name (str): Name of the event to be logged. category (str): category of indexed items indexed_count (int): number of indexed items Returns: None """ data = { "indexed_count": indexed_count, 'category': category, } tracker.emit( event_name, data )
def emit_event(self, event_name): """ Emits an event to explicitly track course enrollment and unenrollment. """ try: context = contexts.course_context_from_course_id(self.course_id) data = { 'user_id': self.user.id, 'course_id': self.course_id, 'mode': self.mode, } with tracker.get_tracker().context(event_name, context): tracker.emit(event_name, data) except: # pylint: disable=bare-except if event_name and self.course_id: log.exception( 'Unable to emit event %s for user %s and course %s', event_name, self.user.username, self.course_id)
def course_grade_passed_first_time(user_id, course_id): """ Emits an event edx.course.grade.passed.first_time with data from the passed course_grade. """ event_name = COURSE_GRADE_PASSED_FIRST_TIME_EVENT_TYPE context = contexts.course_context_from_course_id(course_id) context_enterprise = get_enterprise_event_context(user_id, course_id) context.update(context_enterprise) # TODO (AN-6134): remove this context manager with tracker.get_tracker().context(event_name, context): tracker.emit( event_name, { 'user_id': str(user_id), 'course_id': str(course_id), 'event_transaction_id': str(get_event_transaction_id()), 'event_transaction_type': str(get_event_transaction_type()) } )
def emit_event(self, event_name): """ Emits an event to explicitly track course enrollment and unenrollment. """ try: context = contexts.course_context_from_course_id(self.course_id) assert (isinstance(self.course_id, CourseKey)) data = { 'user_id': self.user.id, 'course_id': self.course_id.to_deprecated_string(), 'mode': self.mode, } with tracker.get_tracker().context(event_name, context): tracker.emit(event_name, data) if settings.FEATURES.get( 'SEGMENT_IO_LMS') and settings.SEGMENT_IO_LMS_KEY: tracking_context = tracker.get_tracker().resolve_context() analytics.track( self.user_id, event_name, { 'category': 'conversion', 'label': self.course_id.to_deprecated_string(), 'org': self.course_id.org, 'course': self.course_id.course, 'run': self.course_id.run, 'mode': self.mode, }, context={ 'Google Analytics': { 'clientId': tracking_context.get('client_id') } }) except: # pylint: disable=bare-except if event_name and self.course_id: log.exception( 'Unable to emit event %s for user %s and course %s', event_name, self.user.username, self.course_id)
def accum_flagged_resource(self, _data, _suffix=''): # pylint: disable=unused-argument """ Accumulate the flagged resource ids and reasons from all students """ if not self.get_user_is_staff(): msg = self.ugettext('Tried to access flagged resources without staff permission') self._error_handler(msg, 'accum_flagged_resource') result = { 'flagged_resources': {} } for _, flagged_accum_resource_map in self.flagged_accum_resources.iteritems(): for resource_id in flagged_accum_resource_map: if resource_id in self.removed_recommendations: continue if resource_id not in result['flagged_resources']: result['flagged_resources'][resource_id] = [] if flagged_accum_resource_map[resource_id] != '': result['flagged_resources'][resource_id].append(flagged_accum_resource_map[resource_id]) tracker.emit('accum_flagged_resource', result) return result
def emit_setting_changed_event(user, db_table, setting_name, old_value, new_value): """Emits an event for a change in a setting. Args: user (User): the user that this setting is associated with. db_table (str): the name of the table that we're modifying. setting_name (str): the name of the setting being changed. old_value (object): the value before the change. new_value (object): the new value being saved. Returns: None """ truncated_fields = truncate_fields(old_value, new_value) truncated_fields['setting'] = setting_name truncated_fields['user_id'] = user.id truncated_fields['table'] = db_table tracker.emit(USER_SETTINGS_CHANGED_EVENT_NAME, truncated_fields)
def emit_certificate_event(event_name, user, course_id, course=None, event_data=None): """ Emits certificate event. """ event_name = '.'.join(['edx', 'certificate', event_name]) if course is None: course = modulestore().get_course(course_id, depth=0) context = { 'org_id': course.org, 'course_id': unicode(course_id) } data = { 'user_id': user.id, 'course_id': unicode(course_id), 'certificate_url': get_certificate_url(user.id, course_id) } event_data = event_data or {} event_data.update(data) with tracker.get_tracker().context(event_name, context): tracker.emit(event_name, event_data)
def track_share_redirect(request__unused, course_id, network, student_username): """ Tracks when a user downloads a badge for sharing. """ course_id = CourseLocator.from_string(course_id) assertion = get_object_or_404(BadgeAssertion, user__username=student_username, course_id=course_id) tracker.emit( 'edx.badge.assertion.shared', { 'course_id': unicode(course_id), 'social_network': network, 'assertion_id': assertion.id, 'assertion_json_url': assertion.data['json']['id'], 'assertion_image_url': assertion.image_url, 'user_id': assertion.user.id, 'enrollment_mode': assertion.mode, 'issuer': assertion.data['issuer'], }) return redirect(assertion.image_url)
def grade_updated(**kwargs): """ Emits the appropriate grade-related event after checking for which event-transaction is active. Emits a problem.submitted event only if there is no current event transaction type, i.e. we have not reached this point in the code via an outer event type (such as problem.rescored or score_overridden). """ root_type = get_event_transaction_type() if not root_type: root_id = get_event_transaction_id() if not root_id: root_id = create_new_event_transaction_id() set_event_transaction_type(PROBLEM_SUBMITTED_EVENT_TYPE) tracker.emit( str(PROBLEM_SUBMITTED_EVENT_TYPE), { 'user_id': str(kwargs['user_id']), 'course_id': str(kwargs['course_id']), 'problem_id': str(kwargs['usage_id']), 'event_transaction_id': str(root_id), 'event_transaction_type': str(PROBLEM_SUBMITTED_EVENT_TYPE), 'weighted_earned': kwargs.get('weighted_earned'), 'weighted_possible': kwargs.get('weighted_possible'), } ) elif root_type in [GRADES_RESCORE_EVENT_TYPE, GRADES_OVERRIDE_EVENT_TYPE]: current_user = get_current_user() instructor_id = getattr(current_user, 'id', None) tracker.emit( str(root_type), { 'course_id': str(kwargs['course_id']), 'user_id': str(kwargs['user_id']), 'problem_id': str(kwargs['usage_id']), 'new_weighted_earned': kwargs.get('weighted_earned'), 'new_weighted_possible': kwargs.get('weighted_possible'), 'only_if_higher': kwargs.get('only_if_higher'), 'instructor_id': str(instructor_id), 'event_transaction_id': str(get_event_transaction_id()), 'event_transaction_type': str(root_type), } ) elif root_type in [SUBSECTION_OVERRIDE_EVENT_TYPE]: tracker.emit( str(root_type), { 'course_id': str(kwargs['course_id']), 'user_id': str(kwargs['user_id']), 'problem_id': str(kwargs['usage_id']), 'only_if_higher': kwargs.get('only_if_higher'), 'override_deleted': kwargs.get('score_deleted', False), 'event_transaction_id': str(get_event_transaction_id()), 'event_transaction_type': str(root_type), } )
def endorse_resource(self, data, _suffix=''): # pylint: disable=unused-argument """ Endorse an entry of resource. This shows the students the resource has the staff seal of approval. Args: data: dict in JSON format data['id']: the ID of the resouce to be endorsed Returns: result: dict in JSON format result['error']: the error message generated when the endorsement fails result['id']: the ID of the resouce to be endorsed result['status']: endorse the resource or undo it """ # Auth+auth if not self.get_user_is_staff(): msg = self.ugettext('Endorse resource without permission') self._error_handler(msg, 'endorse_resource') resource_id = self._validate_resource(data['id'], 'endorse_resource') result = {} result['id'] = resource_id # Unendorse previously endorsed resource if resource_id in self.endorsed_recommendation_ids: result['status'] = 'undo endorsement' endorsed_index = self.endorsed_recommendation_ids.index( resource_id) del self.endorsed_recommendation_ids[endorsed_index] del self.endorsed_recommendation_reasons[endorsed_index] # Endorse new resource else: result['reason'] = data['reason'] result['status'] = 'endorsement' self.endorsed_recommendation_ids.append(resource_id) self.endorsed_recommendation_reasons.append(data['reason']) tracker.emit('endorse_resource', result) return result
def get_stats(request, course_id, problem_id): """ et stats for a single problem (LoncapaProblem). The problem id is passed has a GET paramater with 'problem_id' key. Args: course_id (str): The course id as string. """ start_time = time.time() store = modulestore() course_key = CourseKey.from_string(course_id) try: problem = utils.fetch_problem(store, course_key, problem_id) except ItemNotFoundError: raise Http404() problem_monitor = ProblemMonitor(problem) problem_monitor.get_student_answers() tracker.emit("course_dashboard.problem_stats.views.get_stats", {'task-time': time.time() - start_time}) return HttpResponse(problem_monitor.get_html())
def remove_resource(self, data, _suffix=''): """ Remove an entry of resource. This removes it from the student view, and prevents students from being able to add it back. Args: data: dict in JSON format data['id']: the ID of the resouce to be removed data['reason']: the reason why the resouce was removed Returns: result: dict in JSON format result['error']: the error message generated when the removal fails result['recommendation']: (Dict) the removed resource result['recommendation']['reason']: the reason why the resouce was removed """ # Auth+auth if not self.get_user_is_staff(): msg = self.ugettext( "You don't have the permission to remove this resource") self._error_handler(msg, 'remove_resource') resource_id = self._validate_resource(data['id'], 'remove_resource') # Grab a copy of the resource for the removed list # (swli: I reorganized the code a bit. First copy, then delete. This is more fault-tolerant) result = {} result['id'] = resource_id removed_resource = deepcopy(self.recommendations[resource_id]) removed_resource['reason'] = data['reason'] # Add it to removed resources and remove it from main resource list. self.removed_recommendations[resource_id] = removed_resource del self.recommendations[resource_id] # And return result['recommendation'] = removed_resource tracker.emit('remove_resource', result) return result
def get_client_configuration(self): """ Return the parameters for client-side configuration settings. Returns: disable_dev_ux: feature flag for any new UX under development which should not appear in prod entries_per_page: the number of resources in each page page_span: page range in pagination control intro: whether to take users through a short usage tutorial the first time they see the RecommenderXBlock is_user_staff: whether the user is staff """ result = self.client_configuration.copy() result['is_user_staff'] = self.get_user_is_staff() result['intro'] = not self.seen and self.intro_enabled if not self.seen: # Mark the user who interacted with the XBlock first time as seen, # in order not to show the usage tutorial in future. self.seen = True tracker.emit('get_client_configuration', result) return result
def subsection_grade_calculated(subsection_grade): """ Emits an edx.grades.subsection.grade_calculated event with data from the passed subsection_grade. """ event_name = SUBSECTION_GRADE_CALCULATED context = contexts.course_context_from_course_id( subsection_grade.course_id) # TODO (AN-6134): remove this context manager with tracker.get_tracker().context(event_name, context): tracker.emit( event_name, { 'user_id': unicode(subsection_grade.user_id), 'course_id': unicode(subsection_grade.course_id), 'block_id': unicode(subsection_grade.usage_key), 'course_version': unicode(subsection_grade.course_version), 'weighted_total_earned': subsection_grade.earned_all, 'weighted_total_possible': subsection_grade.possible_all, 'weighted_graded_earned': subsection_grade.earned_graded, 'weighted_graded_possible': subsection_grade.possible_graded, 'first_attempted': unicode(subsection_grade.first_attempted), 'subtree_edited_timestamp': unicode(subsection_grade.subtree_edited_timestamp), 'event_transaction_id': unicode(get_event_transaction_id()), 'event_transaction_type': unicode(get_event_transaction_type()), 'visible_blocks_hash': unicode(subsection_grade.visible_blocks_id), })
def _emit_grade_calculated_event(grade): """ Emits an edx.grades.subsection.grade_calculated event with data from the passed grade. """ # TODO: remove this context manager after completion of AN-6134 event_name = u'edx.grades.subsection.grade_calculated' context = contexts.course_context_from_course_id(grade.course_id) with tracker.get_tracker().context(event_name, context): tracker.emit( event_name, { 'user_id': unicode(grade.user_id), 'course_id': unicode(grade.course_id), 'block_id': unicode(grade.usage_key), 'course_version': unicode(grade.course_version), 'weighted_total_earned': grade.earned_all, 'weighted_total_possible': grade.possible_all, 'weighted_graded_earned': grade.earned_graded, 'weighted_graded_possible': grade.possible_graded, 'first_attempted': unicode(grade.first_attempted), 'subtree_edited_timestamp': unicode(grade.subtree_edited_timestamp), 'event_transaction_id': unicode(get_event_transaction_id()), 'event_transaction_type': unicode(get_event_transaction_type()), 'visible_blocks_hash': unicode(grade.visible_blocks_id), })
def upload_csv_to_report_store(rows, csv_name, course_id, timestamp): """ Upload data as a CSV using ReportStore. Arguments: rows: CSV data in the following format (first column may be a header): [ [row1_colum1, row1_colum2, ...], ... ] csv_name: Name of the resulting CSV course_id: ID of the course """ report_store = ReportStore.from_config() report_store.store_rows( course_id, u"{course_prefix}_{csv_name}_{timestamp_str}.csv".format( course_prefix=course_filename_prefix_generator(course_id), csv_name=csv_name, timestamp_str=timestamp.strftime("%Y-%m-%d-%H%M")), rows) tracker.emit(REPORT_REQUESTED_EVENT_NAME, { "report_type": csv_name, })
def course_grade_calculated(course_grade): """ Emits an edx.grades.course.grade_calculated event with data from the passed course_grade. """ event_name = COURSE_GRADE_CALCULATED context = contexts.course_context_from_course_id(course_grade.course_id) # TODO (AN-6134): remove this context manager with tracker.get_tracker().context(event_name, context): tracker.emit( event_name, { 'user_id': str(course_grade.user_id), 'course_id': str(course_grade.course_id), 'course_version': str(course_grade.course_version), 'percent_grade': course_grade.percent_grade, 'letter_grade': str(course_grade.letter_grade), 'course_edited_timestamp': str(course_grade.course_edited_timestamp), 'event_transaction_id': str(get_event_transaction_id()), 'event_transaction_type': str(get_event_transaction_type()), 'grading_policy_hash': str(course_grade.grading_policy_hash), } )
def _emit_grade_calculated_event(grade): """ Emits an edx.grades.subsection.grade_calculated event with data from the passed grade. """ tracker.emit( u'edx.grades.subsection.grade_calculated', { 'user_id': unicode(grade.user_id), 'course_id': unicode(grade.course_id), 'block_id': unicode(grade.usage_key), 'course_version': unicode(grade.course_version), 'weighted_total_earned': grade.earned_all, 'weighted_total_possible': grade.possible_all, 'weighted_graded_earned': grade.earned_graded, 'weighted_graded_possible': grade.possible_graded, 'first_attempted': unicode(grade.first_attempted), 'subtree_edited_timestamp': unicode(grade.subtree_edited_timestamp), 'event_transaction_id': unicode(get_event_transaction_id()), 'event_transaction_type': unicode(get_event_transaction_type()), 'visible_blocks_hash': unicode(grade.visible_blocks_id), } )
def add_cohort(course_key, name, assignment_type): """ Add a cohort to a course. Raises ValueError if a cohort of the same name already exists. """ log.debug("Adding cohort %s to %s", name, course_key) if is_cohort_exists(course_key, name): raise ValueError(_("You cannot create two cohorts with the same name")) try: course = courses.get_course_by_id(course_key) except Http404: raise ValueError("Invalid course_key") cohort = CourseCohort.create( cohort_name=name, course_id=course.id, assignment_type=assignment_type).course_user_group tracker.emit("edx.cohort.creation_requested", { "cohort_name": cohort.name, "cohort_id": cohort.id }) return cohort
def _emit_grade_calculated_event(grade): """ Emits an edx.grades.course.grade_calculated event with data from the passed grade. """ # TODO: remove this context manager after completion of AN-6134 event_name = u'edx.grades.course.grade_calculated' context = contexts.course_context_from_course_id(grade.course_id) with tracker.get_tracker().context(event_name, context): tracker.emit( event_name, { 'user_id': unicode(grade.user_id), 'course_id': unicode(grade.course_id), 'course_version': unicode(grade.course_version), 'percent_grade': grade.percent_grade, 'letter_grade': unicode(grade.letter_grade), 'course_edited_timestamp': unicode(grade.course_edited_timestamp), 'event_transaction_id': unicode(get_event_transaction_id()), 'event_transaction_type': unicode(get_event_transaction_type()), 'grading_policy_hash': unicode(grade.grading_policy_hash), } )
def set_cert_generation_enabled(course_key, is_enabled): """Enable or disable self-generated certificates for a course. There are two "switches" that control whether self-generated certificates are enabled for a course: 1) Whether the self-generated certificates feature is enabled. 2) Whether self-generated certificates have been enabled for this particular course. The second flag should be enabled *only* when someone has successfully generated example certificates for the course. This helps avoid configuration errors (for example, not having a template configured for the course installed on the workers). The UI for the instructor dashboard enforces this constraint. Arguments: course_key (CourseKey): The course identifier. Keyword Arguments: is_enabled (boolean): If provided, enable/disable self-generated certificates for this course. """ CertificateGenerationCourseSetting.set_enabled_for_course( course_key, is_enabled) cert_event_type = 'enabled' if is_enabled else 'disabled' event_name = '.'.join( ['edx', 'certificate', 'generation', cert_event_type]) tracker.emit(event_name, { 'course_id': unicode(course_key), }) if is_enabled: log.info(u"Enabled self-generated certificates for course '%s'.", unicode(course_key)) else: log.info(u"Disabled self-generated certificates for course '%s'.", unicode(course_key))
def task_track(request_info, task_info, event_type, event, page=None): """ Logs tracking information for events occuring within celery tasks. The `event_type` is a string naming the particular event being logged, while `event` is a dict containing whatever additional contextual information is desired. The `request_info` is a dict containing information about the original task request. Relevant keys are `username`, `ip`, `agent`, and `host`. While the dict is required, the values in it are not, so that {} can be passed in. In addition, a `task_info` dict provides more information about the current task, to be stored with the `event` dict. This may also be an empty dict. The `page` parameter is optional, and allows the name of the page to be provided. """ # supplement event information with additional information # about the task in which it is running. data = dict(event, **task_info) context_override = contexts.course_context_from_url(page) context_override.update({ 'username': request_info.get('username', 'unknown'), 'ip': request_info.get('ip', 'unknown'), 'agent': request_info.get('agent', 'unknown'), 'host': request_info.get('host', 'unknown'), 'event_source': 'task', 'page': page, }) with eventtracker.get_tracker().context('edx.course.task', context_override): eventtracker.emit(name=event_type, data=data)
def add_cohort(course_key, name): """ Add a cohort to a course. Raises ValueError if a cohort of the same name already exists. """ log.debug("Adding cohort %s to %s", name, course_key) if CourseUserGroup.objects.filter(course_id=course_key, group_type=CourseUserGroup.COHORT, name=name).exists(): raise ValueError(_("You cannot create two cohorts with the same name")) try: course = courses.get_course_by_id(course_key) except Http404: raise ValueError("Invalid course_key") cohort = CourseUserGroup.objects.create(course_id=course.id, group_type=CourseUserGroup.COHORT, name=name) tracker.emit("edx.cohort.creation_requested", { "cohort_name": cohort.name, "cohort_id": cohort.id }) return cohort