Merge pull request #27505 from edx/AA-759
[AA-759] feat: Add flag for AA-759 streak celebration discount experiment
This commit is contained in:
@@ -9,6 +9,8 @@ from django.urls import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from lms.djangoapps.course_home_api.mixins import VerifiedModeSerializerMixin
|
||||
|
||||
|
||||
class CourseTabSerializer(serializers.Serializer):
|
||||
"""
|
||||
@@ -27,7 +29,7 @@ class CourseTabSerializer(serializers.Serializer):
|
||||
return request.build_absolute_uri(tab.link_func(self.context.get('course'), reverse))
|
||||
|
||||
|
||||
class CourseHomeMetadataSerializer(serializers.Serializer):
|
||||
class CourseHomeMetadataSerializer(VerifiedModeSerializerMixin, serializers.Serializer):
|
||||
"""
|
||||
Serializer for the Course Home Course Metadata
|
||||
"""
|
||||
|
||||
@@ -3,6 +3,7 @@ Tests for the Course Home Course Metadata API in the Course Home API
|
||||
"""
|
||||
|
||||
import ddt
|
||||
import mock
|
||||
from django.urls import reverse
|
||||
|
||||
from edx_toggles.toggles.testutils import override_waffle_flag
|
||||
@@ -15,6 +16,8 @@ from lms.djangoapps.courseware.toggles import (
|
||||
from common.djangoapps.student.models import CourseEnrollment
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from lms.djangoapps.course_home_api.tests.utils import BaseCourseHomeTests
|
||||
from lms.djangoapps.experiments.testutils import override_experiment_waffle_flag
|
||||
from lms.djangoapps.experiments.utils import STREAK_DISCOUNT_EXPERIMENT_FLAG
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@@ -74,6 +77,9 @@ class CourseHomeMetadataTests(BaseCourseHomeTests):
|
||||
def test_streak_data_in_response(self):
|
||||
""" Test that metadata endpoint returns data for the streak celebration """
|
||||
CourseEnrollment.enroll(self.user, self.course.id, 'audit')
|
||||
response = self.client.get(self.url, content_type='application/json')
|
||||
celebrations = response.json()['celebrations']
|
||||
assert 'streak_length_to_celebrate' in celebrations
|
||||
with override_experiment_waffle_flag(STREAK_DISCOUNT_EXPERIMENT_FLAG, active=True):
|
||||
with mock.patch('common.djangoapps.student.models.UserCelebration.perform_streak_updates', return_value=3):
|
||||
response = self.client.get(self.url, content_type='application/json')
|
||||
celebrations = response.json()['celebrations']
|
||||
assert celebrations['streak_length_to_celebrate'] == 3
|
||||
assert celebrations['streak_discount_experiment_enabled'] is True
|
||||
|
||||
@@ -11,6 +11,7 @@ from opaque_keys.edx.keys import CourseKey # lint-amnesty, pylint: disable=reim
|
||||
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
|
||||
from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser
|
||||
from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser
|
||||
from openedx.core.djangoapps.courseware_api.utils import get_celebrations_dict
|
||||
from openedx.core.djangoapps.courseware_api.views import CoursewareMeta
|
||||
|
||||
from common.djangoapps.student.models import CourseEnrollment, UserCelebration
|
||||
@@ -81,17 +82,15 @@ class CourseHomeMetadataView(RetrieveAPIView):
|
||||
|
||||
username = request.user.username if request.user.username else None
|
||||
course = course_detail(request, request.user.username, course_key)
|
||||
user_is_enrolled = CourseEnrollment.is_enrolled(request.user, course_key_string)
|
||||
browser_timezone = request.query_params.get('browser_timezone', None)
|
||||
celebrations = {
|
||||
'streak_length_to_celebrate': UserCelebration.perform_streak_updates(
|
||||
request.user, course_key, browser_timezone
|
||||
)
|
||||
}
|
||||
enrollment = CourseEnrollment.get_enrollment(request.user, course_key_string)
|
||||
user_is_enrolled = bool(enrollment)
|
||||
|
||||
courseware_meta = CoursewareMeta(course_key, request, request.user.username)
|
||||
can_load_courseware = courseware_meta.is_microfrontend_enabled_for_user()
|
||||
|
||||
browser_timezone = self.request.query_params.get('browser_timezone', None)
|
||||
celebrations = get_celebrations_dict(request.user, enrollment, course, browser_timezone)
|
||||
|
||||
data = {
|
||||
'course_id': course.id,
|
||||
'username': username,
|
||||
@@ -108,5 +107,7 @@ class CourseHomeMetadataView(RetrieveAPIView):
|
||||
}
|
||||
context = self.get_serializer_context()
|
||||
context['course'] = course
|
||||
context['course_overview'] = course
|
||||
context['enrollment'] = enrollment
|
||||
serializer = self.get_serializer_class()(data, context=context)
|
||||
return Response(serializer.data)
|
||||
|
||||
@@ -11,10 +11,15 @@ from urllib.parse import urlencode
|
||||
import ddt
|
||||
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from lms.djangoapps.courseware.utils import is_mode_upsellable
|
||||
from openedx.features.course_experience.url_helpers import get_courseware_url, ExperienceOption
|
||||
from common.djangoapps.student.tests.factories import AdminFactory, CourseEnrollmentFactory, UserFactory
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
|
||||
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls
|
||||
|
||||
from .field_overrides import OverrideModulestoreFieldData
|
||||
@@ -289,3 +294,40 @@ class FieldOverrideTestMixin:
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
OverrideModulestoreFieldData.provider_classes = None
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class CoursewareUtilsTests(SharedModuleStoreTestCase):
|
||||
"""
|
||||
Tests of the courseware utils file
|
||||
"""
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.course = CourseFactory.create()
|
||||
self.user = UserFactory.create()
|
||||
|
||||
@ddt.data(
|
||||
(CourseMode.HONOR, True),
|
||||
(CourseMode.PROFESSIONAL, False),
|
||||
(CourseMode.VERIFIED, False),
|
||||
(CourseMode.AUDIT, True),
|
||||
(CourseMode.NO_ID_PROFESSIONAL_MODE, False),
|
||||
(CourseMode.CREDIT_MODE, False),
|
||||
(CourseMode.MASTERS, False),
|
||||
(CourseMode.EXECUTIVE_EDUCATION, False),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_is_mode_upsellable(self, mode, is_upsellable):
|
||||
"""
|
||||
Test if this is a mode that is upsellable
|
||||
"""
|
||||
CourseModeFactory.create(mode_slug=mode, course_id=self.course.id)
|
||||
if mode == CourseMode.CREDIT_MODE:
|
||||
CourseModeFactory.create(mode_slug=CourseMode.VERIFIED, course_id=self.course.id)
|
||||
enrollment = CourseEnrollmentFactory(
|
||||
is_active=True,
|
||||
mode=mode,
|
||||
course_id=self.course.id,
|
||||
user=self.user
|
||||
)
|
||||
assert is_mode_upsellable(self.user, enrollment) is is_upsellable
|
||||
|
||||
@@ -35,6 +35,36 @@ def verified_upgrade_deadline_link(user, course=None, course_id=None):
|
||||
return EcommerceService().upgrade_url(user, course_id)
|
||||
|
||||
|
||||
def is_mode_upsellable(user, enrollment, course=None):
|
||||
"""
|
||||
Return whether the user is enrolled in a mode that can be upselled to another mode,
|
||||
usually audit upselled to verified.
|
||||
The partition code allows this function to more accurately return results for masquerading users.
|
||||
|
||||
Arguments:
|
||||
user (:class:`.AuthUser`): The user from the request.user property
|
||||
enrollment (:class:`.CourseEnrollment`): The enrollment under consideration.
|
||||
course (:class:`.ModulestoreCourse`): Optional passed in modulestore course.
|
||||
If provided, it is expected to correspond to `enrollment.course.id`.
|
||||
If not provided, the course will be loaded from the modulestore.
|
||||
We use the course to retrieve user partitions when calculating whether
|
||||
the upgrade link will be shown.
|
||||
"""
|
||||
partition_service = PartitionService(enrollment.course.id, course=course)
|
||||
enrollment_track_partition = partition_service.get_user_partition(ENROLLMENT_TRACK_PARTITION_ID)
|
||||
group = partition_service.get_group(user, enrollment_track_partition)
|
||||
current_mode = None
|
||||
if group:
|
||||
try:
|
||||
current_mode = [
|
||||
mode.get('slug') for mode in settings.COURSE_ENROLLMENT_MODES.values() if mode['id'] == group.id
|
||||
].pop()
|
||||
except IndexError:
|
||||
pass
|
||||
upsellable_mode = not current_mode or current_mode in CourseMode.UPSELL_TO_VERIFIED_MODES
|
||||
return upsellable_mode
|
||||
|
||||
|
||||
def can_show_verified_upgrade(user, enrollment, course=None):
|
||||
"""
|
||||
Return whether this user can be shown upgrade message.
|
||||
@@ -51,20 +81,8 @@ def can_show_verified_upgrade(user, enrollment, course=None):
|
||||
"""
|
||||
if enrollment is None:
|
||||
return False # this got accidentally flipped in 2017 (commit 8468357), but leaving alone to not switch again
|
||||
partition_service = PartitionService(enrollment.course.id, course=course)
|
||||
enrollment_track_partition = partition_service.get_user_partition(ENROLLMENT_TRACK_PARTITION_ID)
|
||||
group = partition_service.get_group(user, enrollment_track_partition)
|
||||
current_mode = None
|
||||
if group:
|
||||
try:
|
||||
current_mode = [
|
||||
mode.get('slug') for mode in settings.COURSE_ENROLLMENT_MODES.values() if mode['id'] == group.id
|
||||
].pop()
|
||||
except IndexError:
|
||||
pass
|
||||
upgradable_mode = not current_mode or current_mode in CourseMode.UPSELL_TO_VERIFIED_MODES
|
||||
|
||||
if not upgradable_mode:
|
||||
if not is_mode_upsellable(user, enrollment, course):
|
||||
return False
|
||||
|
||||
upgrade_deadline = enrollment.upgrade_deadline
|
||||
|
||||
@@ -11,6 +11,7 @@ from crum import get_current_request
|
||||
from edx_django_utils.cache import RequestCache
|
||||
|
||||
from common.djangoapps.track import segment
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from lms.djangoapps.experiments.stable_bucketing import stable_bucketing_hash_group
|
||||
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
|
||||
|
||||
@@ -263,6 +264,21 @@ class ExperimentWaffleFlag(CourseWaffleFlag):
|
||||
# Mark that we've recorded this bucketing, so that we don't do it again this session
|
||||
request.session[session_key] = True
|
||||
|
||||
# Temporary event for AA-759 experiment
|
||||
if course_key and self._experiment_name == 'AA-759':
|
||||
modes_dict = CourseMode.modes_for_course_dict(course_id=course_key, include_expired=False)
|
||||
verified_mode = modes_dict.get('verified', None)
|
||||
if verified_mode:
|
||||
segment.track(
|
||||
user_id=user.id,
|
||||
event_name='edx.bi.experiment.AA759.bucketed',
|
||||
properties={
|
||||
'course_id': course_key,
|
||||
'bucket': bucket,
|
||||
'sku': verified_mode.sku,
|
||||
}
|
||||
)
|
||||
|
||||
return self._cache_bucket(experiment_name, bucket)
|
||||
|
||||
def is_enabled(self, course_key=None):
|
||||
|
||||
@@ -17,6 +17,7 @@ from common.djangoapps.student.models import CourseEnrollment
|
||||
from lms.djangoapps.commerce.utils import EcommerceService
|
||||
from lms.djangoapps.courseware.access import has_staff_access_to_preview_mode
|
||||
from lms.djangoapps.courseware.utils import can_show_verified_upgrade, verified_upgrade_deadline_link
|
||||
from lms.djangoapps.experiments.flags import ExperimentWaffleFlag
|
||||
from openedx.core.djangoapps.catalog.utils import get_programs
|
||||
from openedx.core.djangoapps.django_comment_common.models import Role
|
||||
from openedx.core.djangoapps.schedules.models import Schedule
|
||||
@@ -73,6 +74,22 @@ UPSELL_TRACKING_FLAG = LegacyWaffleFlag(
|
||||
)
|
||||
# TODO END: Clean up as part of REV-1205 (End)
|
||||
|
||||
# .. toggle_name: streak_celebration.AA-759
|
||||
# .. toggle_implementation: ExperimentWaffleFlag
|
||||
# .. toggle_default: False
|
||||
# .. toggle_description: This experiment flag enables an engagement discount incentive message.
|
||||
# .. toggle_warnings: This flag depends on the streak celebration feature being enabled
|
||||
# .. toggle_use_cases: temporary
|
||||
# .. toggle_creation_date: 2021-05-05
|
||||
# .. toggle_target_removal_date: 2021-07-05
|
||||
# .. toggle_tickets: https://openedx.atlassian.net/browse/AA-759
|
||||
STREAK_DISCOUNT_EXPERIMENT_FLAG = ExperimentWaffleFlag(
|
||||
LegacyWaffleFlagNamespace(name='streak_celebration'),
|
||||
'discount_experiment_AA759',
|
||||
__name__,
|
||||
use_course_aware_bucketing=False
|
||||
)
|
||||
|
||||
|
||||
def check_and_get_upgrade_link_and_date(user, enrollment=None, course=None):
|
||||
"""
|
||||
|
||||
@@ -14,6 +14,8 @@ from django.contrib.auth import get_user_model
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from edx_toggles.toggles.testutils import override_waffle_flag
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
|
||||
from lms.djangoapps.certificates.api import get_certificate_url
|
||||
from lms.djangoapps.certificates.tests.factories import (
|
||||
GeneratedCertificateFactory, LinkedInAddToProfileConfigurationFactory
|
||||
@@ -27,6 +29,8 @@ from lms.djangoapps.courseware.toggles import (
|
||||
REDIRECT_TO_COURSEWARE_MICROFRONTEND,
|
||||
COURSEWARE_MICROFRONTEND_SPECIAL_EXAMS,
|
||||
)
|
||||
from lms.djangoapps.experiments.testutils import override_experiment_waffle_flag
|
||||
from lms.djangoapps.experiments.utils import STREAK_DISCOUNT_EXPERIMENT_FLAG
|
||||
from lms.djangoapps.verify_student.services import IDVerificationService
|
||||
from common.djangoapps.student.models import (
|
||||
CourseEnrollment, CourseEnrollmentCelebration
|
||||
@@ -105,6 +109,14 @@ class CourseApiTestViews(BaseCoursewareTests, MasqueradeMixin):
|
||||
)
|
||||
cls.store.update_item(cls.course, cls.user.id)
|
||||
LinkedInAddToProfileConfigurationFactory.create()
|
||||
CourseModeFactory(course_id=cls.course.id, mode_slug=CourseMode.AUDIT)
|
||||
CourseModeFactory(
|
||||
course_id=cls.course.id,
|
||||
mode_slug=CourseMode.VERIFIED,
|
||||
expiration_datetime=datetime(3028, 1, 1),
|
||||
min_price=149,
|
||||
sku='ABCD1234',
|
||||
)
|
||||
|
||||
@ddt.data(
|
||||
(True, None, ACCESS_DENIED),
|
||||
@@ -285,9 +297,12 @@ class CourseApiTestViews(BaseCoursewareTests, MasqueradeMixin):
|
||||
def test_streak_data_in_response(self):
|
||||
""" Test that metadata endpoint returns data for the streak celebration """
|
||||
CourseEnrollment.enroll(self.user, self.course.id, 'audit')
|
||||
response = self.client.get(self.url, content_type='application/json')
|
||||
celebrations = response.json()['celebrations']
|
||||
assert 'streak_length_to_celebrate' in celebrations
|
||||
with override_experiment_waffle_flag(STREAK_DISCOUNT_EXPERIMENT_FLAG, active=True):
|
||||
with mock.patch('common.djangoapps.student.models.UserCelebration.perform_streak_updates', return_value=3):
|
||||
response = self.client.get(self.url, content_type='application/json')
|
||||
celebrations = response.json()['celebrations']
|
||||
assert celebrations['streak_length_to_celebrate'] == 3
|
||||
assert celebrations['streak_discount_experiment_enabled'] is True
|
||||
|
||||
@ddt.data(
|
||||
(False, False),
|
||||
|
||||
@@ -5,8 +5,40 @@ Courseware API Mixins.
|
||||
from babel.numbers import get_currency_symbol
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from common.djangoapps.student.models import CourseEnrollmentCelebration, UserCelebration
|
||||
from lms.djangoapps.courseware.utils import can_show_verified_upgrade, verified_upgrade_deadline_link
|
||||
from lms.djangoapps.experiments.utils import STREAK_DISCOUNT_EXPERIMENT_FLAG
|
||||
from openedx.features.course_duration_limits.access import get_user_course_expiration_date
|
||||
from openedx.features.discounts.applicability import can_show_streak_discount_experiment_coupon
|
||||
|
||||
|
||||
def get_celebrations_dict(user, enrollment, course, browser_timezone):
|
||||
"""
|
||||
Returns a dict of celebrations that should be performed.
|
||||
"""
|
||||
if not enrollment:
|
||||
return {
|
||||
'first_section': False,
|
||||
'streak_length_to_celebrate': None,
|
||||
'streak_discount_experiment_enabled': False,
|
||||
}
|
||||
|
||||
streak_length_to_celebrate = UserCelebration.perform_streak_updates(
|
||||
user, course.id, browser_timezone
|
||||
)
|
||||
celebrations = {
|
||||
'first_section': CourseEnrollmentCelebration.should_celebrate_first_section(enrollment),
|
||||
'streak_length_to_celebrate': streak_length_to_celebrate,
|
||||
'streak_discount_experiment_enabled': False,
|
||||
}
|
||||
|
||||
# We only want to bucket people into the AA-759 experiment if they are going to see the streak celebration
|
||||
if streak_length_to_celebrate:
|
||||
# We only want to bucket people into the AA-759 experiment
|
||||
# if the course has not ended, is upgradeable and the user is not an enterprise learner
|
||||
if can_show_streak_discount_experiment_coupon(user, course):
|
||||
celebrations['streak_discount_experiment_enabled'] = STREAK_DISCOUNT_EXPERIMENT_FLAG.is_enabled()
|
||||
return celebrations
|
||||
|
||||
|
||||
def serialize_upgrade_info(user, course_overview, enrollment):
|
||||
|
||||
@@ -46,6 +46,7 @@ from lms.djangoapps.verify_student.services import IDVerificationService
|
||||
from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser
|
||||
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin
|
||||
from openedx.core.lib.courses import get_course_by_id
|
||||
from openedx.core.djangoapps.courseware_api.utils import get_celebrations_dict
|
||||
from openedx.core.djangoapps.programs.utils import ProgramProgressMeter
|
||||
from openedx.features.course_experience import DISPLAY_COURSE_SOCK_FLAG
|
||||
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
|
||||
@@ -54,8 +55,7 @@ from openedx.features.discounts.utils import generate_offer_data
|
||||
from common.djangoapps.student.models import (
|
||||
CourseEnrollment,
|
||||
CourseEnrollmentCelebration,
|
||||
LinkedInAddToProfileConfiguration,
|
||||
UserCelebration
|
||||
LinkedInAddToProfileConfiguration
|
||||
)
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.search import path_to_location
|
||||
@@ -90,6 +90,7 @@ class CoursewareMeta:
|
||||
self.original_user_is_staff = has_access(self.request.user, 'staff', self.overview).has_access
|
||||
self.original_user_is_global_staff = self.request.user.is_staff
|
||||
self.course_key = course_key
|
||||
self.course = get_course_by_id(self.course_key)
|
||||
self.course_masquerade, self.effective_user = setup_masquerade(
|
||||
self.request,
|
||||
course_key,
|
||||
@@ -209,15 +210,11 @@ class CoursewareMeta:
|
||||
@property
|
||||
def celebrations(self):
|
||||
"""
|
||||
Returns a list of celebrations that should be performed.
|
||||
Returns a dict of celebrations that should be performed.
|
||||
"""
|
||||
browser_timezone = self.request.query_params.get('browser_timezone', None)
|
||||
return {
|
||||
'first_section': CourseEnrollmentCelebration.should_celebrate_first_section(self.enrollment_object),
|
||||
'streak_length_to_celebrate': UserCelebration.perform_streak_updates(
|
||||
self.effective_user, self.course_key, browser_timezone
|
||||
),
|
||||
}
|
||||
celebrations = get_celebrations_dict(self.effective_user, self.enrollment_object, self.course, browser_timezone)
|
||||
return celebrations
|
||||
|
||||
@property
|
||||
def user_has_passing_grade(self):
|
||||
|
||||
@@ -19,6 +19,7 @@ from edx_toggles.toggles import LegacyWaffleFlag, LegacyWaffleFlagNamespace
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from common.djangoapps.entitlements.models import CourseEntitlement
|
||||
from lms.djangoapps.courseware.utils import is_mode_upsellable
|
||||
from lms.djangoapps.experiments.models import ExperimentData
|
||||
from lms.djangoapps.experiments.stable_bucketing import stable_bucketing_hash_group
|
||||
from openedx.features.discounts.models import DiscountPercentageConfig, DiscountRestrictionConfig
|
||||
@@ -82,6 +83,41 @@ def get_discount_expiration_date(user, course):
|
||||
return discount_expiration_date
|
||||
|
||||
|
||||
def can_show_streak_discount_experiment_coupon(user, course):
|
||||
"""
|
||||
Check whether this combination of user and course
|
||||
can receive the AA-759 experiment discount.
|
||||
"""
|
||||
# Course end date needs to be in the future
|
||||
if course.has_ended():
|
||||
return False
|
||||
|
||||
# Course needs to have a non-expired verified mode
|
||||
modes_dict = CourseMode.modes_for_course_dict(course=course, include_expired=False)
|
||||
if 'verified' not in modes_dict:
|
||||
return False
|
||||
|
||||
# Learner needs to be in an upgradeable mode
|
||||
try:
|
||||
enrollment = CourseEnrollment.objects.get(
|
||||
user=user,
|
||||
course=course.id,
|
||||
)
|
||||
except CourseEnrollment.DoesNotExist:
|
||||
return False
|
||||
|
||||
if not is_mode_upsellable(user, enrollment):
|
||||
return False
|
||||
|
||||
# We can't import this at Django load time within the openedx tests settings context
|
||||
from openedx.features.enterprise_support.utils import is_enterprise_learner
|
||||
# Don't give discount to enterprise users
|
||||
if is_enterprise_learner(user):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def can_receive_discount(user, course, discount_expiration_date=None):
|
||||
"""
|
||||
Check all the business logic about whether this combination of user and course
|
||||
|
||||
Reference in New Issue
Block a user