def get_users(self, course_id, user_id=None): """ Gets the users for a given target. Result is returned in the form of a queryset, and may contain duplicates. """ staff_qset = CourseStaffRole(course_id).users_with_role() instructor_qset = CourseInstructorRole(course_id).users_with_role() staff_instructor_qset = (staff_qset | instructor_qset) enrollment_query = models.Q(is_active=True, courseenrollment__course_id=course_id, courseenrollment__is_active=True) enrollment_qset = User.objects.filter(enrollment_query) if self.target_type == SEND_TO_MYSELF: if user_id is None: raise ValueError( "Must define self user to send email to self.") user = User.objects.filter(id=user_id) return use_read_replica_if_available(user) elif self.target_type == SEND_TO_STAFF: return use_read_replica_if_available(staff_instructor_qset) elif self.target_type == SEND_TO_LEARNERS: return use_read_replica_if_available( enrollment_qset.exclude(id__in=staff_instructor_qset)) elif self.target_type == SEND_TO_COHORT: return self.cohorttarget.cohort.users.filter( id__in=enrollment_qset) # lint-amnesty, pylint: disable=no-member elif self.target_type == SEND_TO_TRACK: return use_read_replica_if_available( User.objects.filter( models.Q(courseenrollment__mode=self.coursemodetarget. track.mode_slug) & enrollment_query)) else: raise ValueError(f"Unrecognized target type {self.target_type}")
def _paginate_users(self, course_key, course_enrollment_filter=None, related_models=None, annotations=None): """ Args: course_key (CourseLocator): The course to retrieve grades for. course_enrollment_filter: Optional list of Q objects to pass to `CourseEnrollment.filter()`. related_models: Optional list of related models to join to the CourseEnrollment table. annotations: Optional dict of fields to add to the queryset via annotation Returns: A list of users, pulled from a paginated queryset of enrollments, who are enrolled in the given course. """ queryset = CourseEnrollment.objects if annotations: queryset = queryset.annotate(**annotations) filter_args = [Q(course_id=course_key) & Q(is_active=True)] filter_args.extend(course_enrollment_filter or []) enrollments_in_course = use_read_replica_if_available( queryset.filter(*filter_args)) if related_models: enrollments_in_course = enrollments_in_course.select_related( *related_models) paged_enrollments = self.paginate_queryset(enrollments_in_course) retlist = [] for enrollment in paged_enrollments: enrollment.user.enrollment_mode = enrollment.mode retlist.append(enrollment.user) return retlist
def handle(self, *args, **options): """ Handler for the command It filters approved Software Secure Photo Verifications and then sets the correct expiration_date """ batch_size = options['batch_size'] sleep_time = options['sleep_time'] query = SoftwareSecurePhotoVerification.objects.filter(status='approved').order_by() sspv = use_read_replica_if_available(query) if not sspv.count(): logger.info("No approved entries found in SoftwareSecurePhotoVerification") return update_verification_ids = [] update_verification_count = 0 for verification in sspv: # The expiration date should not be higher than 365 days # past the `updated_at` field, so only update those entries if verification.expiration_date > ( verification.updated_at + timedelta(days=settings.VERIFY_STUDENT["DAYS_GOOD_FOR"]) ): update_verification_ids.append(verification.pk) update_verification_count += 1 if update_verification_count == batch_size: self.bulk_update(update_verification_ids) update_verification_count = 0 update_verification_ids = [] time.sleep(sleep_time) if update_verification_ids: self.bulk_update(update_verification_ids)
def get_users(self, course_id, user_id=None): """ Gets the users for a given target. Result is returned in the form of a queryset, and may contain duplicates. """ staff_qset = CourseStaffRole(course_id).users_with_role() instructor_qset = CourseInstructorRole(course_id).users_with_role() staff_instructor_qset = (staff_qset | instructor_qset) enrollment_query = models.Q(is_active=True, courseenrollment__course_id=course_id, courseenrollment__is_active=True) enrollment_qset = User.objects.filter(enrollment_query) # filter out learners from the message who are no longer active in the course-run based on last login last_login_eligibility_period = settings.BULK_COURSE_EMAIL_LAST_LOGIN_ELIGIBILITY_PERIOD if last_login_eligibility_period and isinstance( last_login_eligibility_period, int): cutoff = datetime.now() - relativedelta( months=last_login_eligibility_period) enrollment_qset = enrollment_qset.exclude(last_login__lte=cutoff) if self.target_type == SEND_TO_MYSELF: if user_id is None: raise ValueError( "Must define self user to send email to self.") user = User.objects.filter(id=user_id) return use_read_replica_if_available(user) elif self.target_type == SEND_TO_STAFF: return use_read_replica_if_available(staff_instructor_qset) elif self.target_type == SEND_TO_LEARNERS: return use_read_replica_if_available( enrollment_qset.exclude(id__in=staff_instructor_qset)) elif self.target_type == SEND_TO_COHORT: return self.cohorttarget.cohort.users.filter( id__in=enrollment_qset) # lint-amnesty, pylint: disable=no-member elif self.target_type == SEND_TO_TRACK: return use_read_replica_if_available( User.objects.filter( models.Q(courseenrollment__mode=self.coursemodetarget. track.mode_slug) & enrollment_query)) else: raise ValueError(f"Unrecognized target type {self.target_type}")
def handle(self, *args, **options): """ Handle the send save for later reminder emails. """ reminder_email_threshold_date = datetime.now() - timedelta( days=settings.SAVE_FOR_LATER_REMINDER_EMAIL_THRESHOLD) saved_courses_ids = SavedCourse.objects.filter( reminder_email_sent=False, modified__lt=reminder_email_threshold_date).values_list('id', flat=True) total = saved_courses_ids.count() batch_size = max(1, options.get('batch_size')) num_batches = ((total - 1) / batch_size + 1) if total > 0 else 0 for batch_num in range(int(num_batches)): reminder_email_sent_ids = [] start = batch_num * batch_size end = min(start + batch_size, total) - 1 saved_courses_batch_ids = list(saved_courses_ids)[start:end + 1] query = SavedCourse.objects.filter( id__in=saved_courses_batch_ids).order_by('-modified') saved_courses = use_read_replica_if_available(query) for saved_course in saved_courses: user = User.objects.filter(email=saved_course.email).first() course_overview = CourseOverview.get_from_id( saved_course.course_id) course_data = { 'course': course_overview, 'send_to_self': None, 'user_id': saved_course.user_id, 'org_img_url': saved_course.org_img_url, 'marketing_url': saved_course.marketing_url, 'weeks_to_complete': saved_course.weeks_to_complete, 'min_effort': saved_course.min_effort, 'max_effort': saved_course.max_effort, 'type': 'course', 'reminder': True, 'braze_event': USER_SEND_SAVE_FOR_LATER_REMINDER_EMAIL, } if user: enrollment = CourseEnrollment.get_enrollment( user, saved_course.course_id) if enrollment: continue email_sent = send_email(saved_course.email, course_data) if email_sent: reminder_email_sent_ids.append(saved_course.id) else: logging.info( "Unable to send reminder email to {user} for {course} course" .format(user=str(saved_course.email), course=str(saved_course.course_id))) SavedCourse.objects.filter(id__in=reminder_email_sent_ids).update( reminder_email_sent=True)
def _get_users_queryset(self, initial_days): """ initial_days: numbers of days to go back from today :return: users queryset """ start_date = datetime.now().date() - timedelta(initial_days) end_date = datetime.now().date() - timedelta(1) self.stdout.write(f'Getting users from {start_date} to {end_date}') users_qs = User.objects.filter( date_joined__date__gte=start_date, date_joined__date__lte=end_date).order_by('id') return use_read_replica_if_available(users_qs)
def handle(self, *args, **options): """ Handle the send save for later reminder emails. """ reminder_email_threshold_date = datetime.now() - timedelta( days=settings.SAVE_FOR_LATER_REMINDER_EMAIL_THRESHOLD) saved_program_ids = SavedProgram.objects.filter( reminder_email_sent=False, modified__lt=reminder_email_threshold_date).values_list('id', flat=True) total = saved_program_ids.count() batch_size = max(1, options.get('batch_size')) num_batches = ((total - 1) / batch_size + 1) if total > 0 else 0 for batch_num in range(int(num_batches)): reminder_email_sent_ids = [] start = batch_num * batch_size end = min(start + batch_size, total) - 1 saved_program_batch_ids = list(saved_program_ids)[start:end + 1] query = SavedProgram.objects.filter( id__in=saved_program_batch_ids).order_by('-modified') saved_programs = use_read_replica_if_available(query) for saved_program in saved_programs: user = User.objects.filter(email=saved_program.email).first() program = get_programs(uuid=saved_program.program_uuid) if program: program_data = { 'program': program, 'send_to_self': None, 'user_id': saved_program.user_id, 'type': 'program', 'reminder': True, 'braze_event': USER_SEND_SAVE_FOR_LATER_REMINDER_EMAIL, } try: if user and get_program_enrollment( program_uuid=saved_program.program_uuid, user=user): continue except ObjectDoesNotExist: pass email_sent = send_email(saved_program.email, program_data) if email_sent: reminder_email_sent_ids.append(saved_program.id) else: logging.info( "Unable to send reminder email to {user} for {program} program" .format(user=str(saved_program.email), program=str(saved_program.program_uuid))) SavedProgram.objects.filter(id__in=reminder_email_sent_ids).update( reminder_email_sent=True)
def _get_enrollments_queryset(self, start_index, end_index): """ Args: start_index: start index or None end_index: end index or None Returns: EnterpriseCourseEnrollments Queryset """ self.stdout.write( u'Getting enrollments from {start} to {end} index (as per command params)' .format(start=start_index or 'start', end=end_index or 'end')) enrollments_qs = EnterpriseCourseEnrollment.objects.filter( source__isnull=True).order_by('id')[start_index:end_index] return use_read_replica_if_available(enrollments_qs)
def handle(self, *args, **options): """ Handler for the command It filters approved Software Secure Photo Verification and then for each distinct user it finds the most recent approved verification and set its expiration_date """ batch_size = options['batch_size'] sleep_time = options['sleep_time'] query = SoftwareSecurePhotoVerification.objects.filter( status='approved').order_by() sspv = use_read_replica_if_available(query) if not sspv.count(): logger.info( "No approved entries found in SoftwareSecurePhotoVerification") return distinct_user_ids = set() update_verification_ids = [] update_verification_count = 0 for verification in sspv: if verification.user_id not in distinct_user_ids: distinct_user_ids.add(verification.user_id) recent_verification = self.find_recent_verification( sspv, verification.user_id) if recent_verification.expiry_date: update_verification_ids.append(recent_verification.pk) update_verification_count += 1 if update_verification_count == batch_size: self.bulk_update(update_verification_ids) update_verification_count = 0 update_verification_ids = [] time.sleep(sleep_time) if update_verification_ids: self.bulk_update(update_verification_ids)
def handle(self, *args, **options): """ Handler for the command It creates batches of expired Software Secure Photo Verification and sends it to send_verification_expiry_email that used edx_ace to send email to these learners """ default_emails = settings.VERIFICATION_EXPIRY_EMAIL['DEFAULT_EMAILS'] resend_days = settings.VERIFICATION_EXPIRY_EMAIL['RESEND_DAYS'] days = settings.VERIFICATION_EXPIRY_EMAIL['DAYS_RANGE'] batch_size = options['batch_size'] sleep_time = options['sleep_time'] dry_run = options['dry_run'] if default_emails <= 0: raise CommandError( u'DEFAULT_EMAILS must be a positive integer. If you do not wish to send emails ' u'use --dry-run flag instead.') end_date = now().replace(hour=0, minute=0, second=0, microsecond=0) # If email was sent and user did not re-verify then this date will be used as the criteria for resending email date_resend_days_ago = end_date - timedelta(days=resend_days) start_date = end_date - timedelta(days=days) # Adding an order_by() clause will override the class meta ordering as we don't need ordering here query = SoftwareSecurePhotoVerification.objects.filter( Q(status='approved') & ( Q(expiration_date__isnull=False) & (Q(expiration_date__gte=start_date, expiration_date__lt=end_date) | Q(expiry_email_date__lte=date_resend_days_ago)) | # Account for old entries still using `expiry_date` rather than`expiration_date` # (this will be deprecated) Q(expiry_date__isnull=False) & (Q(expiry_date__gte=start_date, expiry_date__lt=end_date) | Q( expiry_email_date__lte=date_resend_days_ago)))).order_by() sspv = use_read_replica_if_available(query) total_verification = sspv.count() if not total_verification: logger.info( u"No approved expired entries found in SoftwareSecurePhotoVerification for the " u"date range {} - {}".format(start_date.date(), now().date())) return logger.info( u"For the date range {} - {}, total Software Secure Photo verification filtered are {}" .format(start_date.date(), now().date(), total_verification)) batch_verifications = [] email_config = { 'resend_days': resend_days, 'dry_run': dry_run, 'default_emails': default_emails } success = True for verification in sspv: user = verification.user if self.user_has_valid_verification(user): continue if not verification.expiry_email_date or verification.expiry_email_date <= date_resend_days_ago: batch_verifications.append(verification) if len(batch_verifications) == batch_size: success = self.send_verification_expiry_email( batch_verifications, email_config) and success time.sleep(sleep_time) batch_verifications = [] # If selected verification in batch are less than batch_size if batch_verifications: success = self.send_verification_expiry_email( batch_verifications, email_config) and success if not success: raise CommandError( 'One or more email attempts failed. Search for "Could not send" above.' )