mirror of
https://github.com/pretix/pretix.git
synced 2026-05-04 15:04:03 +00:00
Add a maximum budget to vouchers (#1526)
* Data model changes * Fix test failures * Adjustments * Some tests and API support * Check when extending orders * Make things more deterministic, fix style * Do not apply negative discounts * Update price_before_voucher on item/subevent changes * Add tests for price_before_voucher in combination with free price * Fix InvoiceAddress.DoesNotExist
This commit is contained in:
@@ -3105,6 +3105,81 @@ def test_order_create_auto_pricing_reverse_charge_require_valid_vatid(token_clie
|
||||
assert p.tax_rate == Decimal('19.00')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_autopricing_voucher_budget_partially(token_client, organizer, event, item, quota, question,
|
||||
taxrule):
|
||||
with scopes_disabled():
|
||||
voucher = event.vouchers.create(price_mode="set", value=21.50, item=item, budget=Decimal('2.50'),
|
||||
max_usages=999)
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
res['positions'][0]['voucher'] = voucher.code
|
||||
del res['positions'][0]['price']
|
||||
del res['positions'][0]['positionid']
|
||||
res['positions'].append(res['positions'][0])
|
||||
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
print(resp.data)
|
||||
assert resp.status_code == 201
|
||||
with scopes_disabled():
|
||||
o = Order.objects.get(code=resp.data['code'])
|
||||
p = o.positions.first()
|
||||
p2 = o.positions.last()
|
||||
assert p.price == Decimal('21.50')
|
||||
assert p2.price == Decimal('22.00')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_autopricing_voucher_budget_full(token_client, organizer, event, item, quota, question, taxrule):
|
||||
with scopes_disabled():
|
||||
voucher = event.vouchers.create(price_mode="set", value=21.50, item=item, budget=Decimal('0.50'),
|
||||
max_usages=999)
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
res['positions'][0]['voucher'] = voucher.code
|
||||
del res['positions'][0]['price']
|
||||
del res['positions'][0]['positionid']
|
||||
res['positions'].append(res['positions'][0])
|
||||
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {'positions': [{}, {'voucher': ['The voucher has a remaining budget of 0.00, therefore a '
|
||||
'discount of 1.50 can not be given.']}]}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_voucher_budget_exceeded(token_client, organizer, event, item, quota, question, taxrule):
|
||||
with scopes_disabled():
|
||||
voucher = event.vouchers.create(price_mode="set", value=21.50, item=item, budget=Decimal('3.00'),
|
||||
max_usages=999)
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
res['positions'][0]['voucher'] = voucher.code
|
||||
res['positions'][0]['price'] = '19.00'
|
||||
del res['positions'][0]['positionid']
|
||||
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
print(resp.data)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {'positions': [{'voucher': ['The voucher has a remaining budget of 3.00, therefore a '
|
||||
'discount of 4.00 can not be given.']}]}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_voucher_price(token_client, organizer, event, item, quota, question):
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
|
||||
@@ -795,6 +795,31 @@ class VoucherTestCase(BaseQuotaTestCase):
|
||||
v = Voucher.objects.create(event=self.event, price_mode='percent', value=Decimal('23.00'))
|
||||
assert v.calculate_price(Decimal('100.00')) == Decimal('77.00')
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_calculate_price_max_discount(self):
|
||||
v = Voucher.objects.create(event=self.event, price_mode='subtract', value=Decimal('10.00'))
|
||||
assert v.calculate_price(Decimal('23.42'), max_discount=Decimal('5.00')) == Decimal('18.42')
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_calculate_budget_used(self):
|
||||
v = Voucher.objects.create(event=self.event, price_mode='sset', value=Decimal('20.00'))
|
||||
|
||||
order = Order.objects.create(
|
||||
status=Order.STATUS_PENDING, event=self.event,
|
||||
datetime=now() - timedelta(days=5), expires=now() + timedelta(days=5), total=46,
|
||||
)
|
||||
OrderPosition.objects.create(order=order, item=self.item1, voucher=v, price=Decimal('20.00'),
|
||||
price_before_voucher=Decimal('23.00'))
|
||||
assert v.budget_used() == Decimal('3.00')
|
||||
|
||||
order = Order.objects.create(
|
||||
status=Order.STATUS_PAID, event=self.event,
|
||||
datetime=now() - timedelta(days=5), expires=now() + timedelta(days=5), total=46,
|
||||
)
|
||||
OrderPosition.objects.create(order=order, item=self.item1, voucher=v, price=Decimal('20.00'),
|
||||
price_before_voucher=Decimal('23.00'))
|
||||
assert v.budget_used() == Decimal('6.00')
|
||||
|
||||
|
||||
class OrderTestCase(BaseQuotaTestCase):
|
||||
def setUp(self):
|
||||
|
||||
@@ -878,6 +878,36 @@ class OrderChangeManagerTests(TestCase):
|
||||
assert self.op1.tax_value == Decimal('3.67')
|
||||
assert self.op1.tax_rule == self.shirt.tax_rule
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_change_item_change_price_before_voucher(self):
|
||||
self.op1.voucher = self.event.vouchers.create(item=self.shirt, redeemed=1, price_mode='set', value='5.00')
|
||||
self.op1.price = Decimal('5.00')
|
||||
self.op1.price_before_voucher = Decimal('23.00')
|
||||
self.op1.save()
|
||||
p = self.op1.price
|
||||
self.ocm.change_item(self.op1, self.shirt, None)
|
||||
self.ocm.commit()
|
||||
self.op1.refresh_from_db()
|
||||
self.order.refresh_from_db()
|
||||
assert self.op1.item == self.shirt
|
||||
assert self.op1.price == p
|
||||
assert self.op1.price_before_voucher == Decimal('12.00')
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_change_item_change_price_before_voucher_minimum_value(self):
|
||||
self.op1.voucher = self.event.vouchers.create(item=self.shirt, redeemed=1, price_mode='set', value='20.00')
|
||||
self.op1.price = Decimal('20.00')
|
||||
self.op1.price_before_voucher = Decimal('23.00')
|
||||
self.op1.save()
|
||||
p = self.op1.price
|
||||
self.ocm.change_item(self.op1, self.shirt, None)
|
||||
self.ocm.commit()
|
||||
self.op1.refresh_from_db()
|
||||
self.order.refresh_from_db()
|
||||
assert self.op1.item == self.shirt
|
||||
assert self.op1.price == p
|
||||
assert self.op1.price_before_voucher == Decimal('20.00')
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_change_item_success(self):
|
||||
self.ocm.change_item(self.op1, self.shirt, None)
|
||||
|
||||
@@ -845,6 +845,69 @@ def test_order_extend_expired_quota_partial(client, env):
|
||||
assert o.status == Order.STATUS_EXPIRED
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_extend_expired_voucher_budget_ok(client, env):
|
||||
with scopes_disabled():
|
||||
o = Order.objects.get(id=env[2].id)
|
||||
o.expires = now() - timedelta(days=5)
|
||||
o.status = Order.STATUS_EXPIRED
|
||||
o.save()
|
||||
v = env[0].vouchers.create(
|
||||
code="foo", price_mode='subtract', value=Decimal('1.50'), budget=Decimal('1.50')
|
||||
)
|
||||
p = o.positions.first()
|
||||
p.voucher = v
|
||||
p.price_before_voucher = p.price
|
||||
p.price -= Decimal('1.50')
|
||||
p.save()
|
||||
|
||||
q = Quota.objects.create(event=env[0], size=100)
|
||||
q.items.add(env[3])
|
||||
newdate = (now() + timedelta(days=20)).strftime("%Y-%m-%d %H:%M:%S")
|
||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
response = client.post('/control/event/dummy/dummy/orders/FOO/extend', {
|
||||
'expires': newdate
|
||||
}, follow=True)
|
||||
assert b'alert-success' in response.content
|
||||
with scopes_disabled():
|
||||
o = Order.objects.get(id=env[2].id)
|
||||
assert o.status == Order.STATUS_PENDING
|
||||
assert v.budget_used() == Decimal('1.50')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_extend_expired_voucher_budget_fail(client, env):
|
||||
with scopes_disabled():
|
||||
o = Order.objects.get(id=env[2].id)
|
||||
o.expires = now() - timedelta(days=5)
|
||||
o.status = Order.STATUS_EXPIRED
|
||||
olddate = o.expires
|
||||
o.save()
|
||||
v = env[0].vouchers.create(
|
||||
code="foo", price_mode='subtract', value=Decimal('1.50'), budget=Decimal('0.00')
|
||||
)
|
||||
p = o.positions.first()
|
||||
p.voucher = v
|
||||
p.price_before_voucher = p.price
|
||||
p.price -= Decimal('1.50')
|
||||
p.save()
|
||||
|
||||
q = Quota.objects.create(event=env[0], size=100)
|
||||
q.items.add(env[3])
|
||||
newdate = (now() + timedelta(days=20)).strftime("%Y-%m-%d %H:%M:%S")
|
||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
response = client.post('/control/event/dummy/dummy/orders/FOO/extend', {
|
||||
'expires': newdate
|
||||
}, follow=True)
|
||||
assert b'alert-danger' in response.content
|
||||
assert b'The voucher "FOO" no longer has sufficient budget.' in response.content
|
||||
with scopes_disabled():
|
||||
o = Order.objects.get(id=env[2].id)
|
||||
assert o.expires.strftime("%Y-%m-%d %H:%M:%S") == olddate.strftime("%Y-%m-%d %H:%M:%S")
|
||||
assert o.status == Order.STATUS_EXPIRED
|
||||
assert v.budget_used() == Decimal('0.00')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_mark_paid_overdue_quota_blocked_by_waiting_list(client, env):
|
||||
with scopes_disabled():
|
||||
|
||||
@@ -1388,6 +1388,32 @@ class CartTest(CartTestMixin, TestCase):
|
||||
self.assertEqual(objs[0].item, self.ticket)
|
||||
self.assertIsNone(objs[0].variation)
|
||||
self.assertEqual(objs[0].price, Decimal('21.00'))
|
||||
self.assertEqual(objs[0].price_before_voucher, Decimal('23.00'))
|
||||
|
||||
def test_voucher_free_price_before_voucher_cap(self):
|
||||
with scopes_disabled():
|
||||
v = Voucher.objects.create(item=self.ticket, value=Decimal('10.00'), price_mode='percent', event=self.event)
|
||||
self.ticket.free_price = True
|
||||
self.ticket.save()
|
||||
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
'item_%d' % self.ticket.id: '1',
|
||||
'price_%d' % self.ticket.id: '41.00',
|
||||
'_voucher_code': v.code,
|
||||
}, follow=True)
|
||||
self.assertRedirects(response, '/%s/%s/?require_cookie=true' % (self.orga.slug, self.event.slug),
|
||||
target_status_code=200)
|
||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||
self.assertIn('Early-bird', doc.select('.cart .cart-row')[0].select('strong')[0].text)
|
||||
self.assertIn('1', doc.select('.cart .cart-row')[0].select('.count')[0].text)
|
||||
self.assertIn('41', doc.select('.cart .cart-row')[0].select('.price')[0].text)
|
||||
self.assertIn('41', doc.select('.cart .cart-row')[0].select('.price')[1].text)
|
||||
with scopes_disabled():
|
||||
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
|
||||
self.assertEqual(len(objs), 1)
|
||||
self.assertEqual(objs[0].item, self.ticket)
|
||||
self.assertIsNone(objs[0].variation)
|
||||
self.assertEqual(objs[0].price, Decimal('41.00'))
|
||||
self.assertEqual(objs[0].price_before_voucher, Decimal('41.00'))
|
||||
|
||||
def test_voucher_free_price_lower_bound(self):
|
||||
with scopes_disabled():
|
||||
@@ -1412,6 +1438,7 @@ class CartTest(CartTestMixin, TestCase):
|
||||
self.assertEqual(objs[0].item, self.ticket)
|
||||
self.assertIsNone(objs[0].variation)
|
||||
self.assertEqual(objs[0].price, Decimal('20.70'))
|
||||
self.assertEqual(objs[0].price_before_voucher, Decimal('23.00'))
|
||||
|
||||
def test_voucher_redemed(self):
|
||||
with scopes_disabled():
|
||||
@@ -2438,6 +2465,20 @@ class CartAddonTest(CartTestMixin, TestCase):
|
||||
assert cp2.expires > now()
|
||||
assert cp2.addon_to_id == cp1.pk
|
||||
|
||||
@classscope(attr='orga')
|
||||
def test_expand_expired_refresh_voucher(self):
|
||||
v = Voucher.objects.create(item=self.ticket, value=Decimal('20.00'), event=self.event, price_mode='set',
|
||||
valid_until=now() + timedelta(days=2), max_usages=999, redeemed=0)
|
||||
cp1 = CartPosition.objects.create(
|
||||
expires=now() - timedelta(minutes=10), item=self.ticket, price=Decimal('21.50'),
|
||||
event=self.event, cart_id=self.session_key, voucher=v
|
||||
)
|
||||
self.cm.extend_expired_positions()
|
||||
self.cm.commit()
|
||||
cp1.refresh_from_db()
|
||||
assert cp1.expires > now()
|
||||
assert cp1.price_before_voucher == Decimal('23.00')
|
||||
|
||||
|
||||
class CartBundleTest(CartTestMixin, TestCase):
|
||||
@scopes_disabled()
|
||||
@@ -2490,6 +2531,30 @@ class CartBundleTest(CartTestMixin, TestCase):
|
||||
assert cp.price == 23 - 1.5
|
||||
assert cp.addons.count() == 1
|
||||
assert cp.voucher == v
|
||||
assert cp.price_before_voucher == 23 - 1.5
|
||||
a = cp.addons.get()
|
||||
assert a.item == self.trans
|
||||
assert a.price == 1.5
|
||||
assert not a.voucher
|
||||
|
||||
@classscope(attr='orga')
|
||||
def test_discounted_voucher_on_base_product(self):
|
||||
v = self.event.vouchers.create(code="foo", item=self.ticket, price_mode='subtract', value=Decimal('1.50'))
|
||||
self.cm.add_new_items([
|
||||
{
|
||||
'item': self.ticket.pk,
|
||||
'variation': None,
|
||||
'voucher': v.code,
|
||||
'count': 1
|
||||
}
|
||||
])
|
||||
self.cm.commit()
|
||||
cp = CartPosition.objects.get(addon_to__isnull=True)
|
||||
assert cp.item == self.ticket
|
||||
assert cp.price == 23 - 1.5 - 1.5
|
||||
assert cp.addons.count() == 1
|
||||
assert cp.voucher == v
|
||||
assert cp.price_before_voucher == 23 - 1.5
|
||||
a = cp.addons.get()
|
||||
assert a.item == self.trans
|
||||
assert a.price == 1.5
|
||||
|
||||
@@ -3000,3 +3000,140 @@ class CheckoutSeatingTest(BaseCheckoutTestCase, TestCase):
|
||||
with self.assertRaises(OrderError):
|
||||
_perform_order(self.event, 'manual', [self.cp1.pk], 'admin@example.org', 'en', None, {}, 'web')
|
||||
assert not CartPosition.objects.filter(pk=self.cp1.pk).exists()
|
||||
|
||||
|
||||
class CheckoutVoucherBudgetTest(BaseCheckoutTestCase, TestCase):
|
||||
@scopes_disabled()
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.v = Voucher.objects.create(item=self.ticket, value=Decimal('21.50'), event=self.event, price_mode='set',
|
||||
valid_until=now() + timedelta(days=2), max_usages=999, redeemed=0)
|
||||
self.cp1 = CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
price_before_voucher=23, price=21.5, expires=now() + timedelta(minutes=10), voucher=self.v
|
||||
)
|
||||
self.cp2 = CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
price_before_voucher=23, price=21.5, expires=now() + timedelta(minutes=10), voucher=self.v
|
||||
)
|
||||
|
||||
@scopes_disabled()
|
||||
def test_no_budget(self):
|
||||
oid = _perform_order(self.event, 'manual', [self.cp1.pk, self.cp2.pk], 'admin@example.org', 'en', None, {},
|
||||
'web')
|
||||
o = Order.objects.get(pk=oid)
|
||||
op = o.positions.first()
|
||||
assert op.item == self.ticket
|
||||
assert op.price_before_voucher == Decimal('23.00')
|
||||
|
||||
@scopes_disabled()
|
||||
def test_budget_exceeded_for_second_order(self):
|
||||
self.v.budget = Decimal('1.50')
|
||||
self.v.save()
|
||||
oid = _perform_order(self.event, 'manual', [self.cp1.pk], 'admin@example.org', 'en', None, {},
|
||||
'web')
|
||||
o = Order.objects.get(pk=oid)
|
||||
op = o.positions.first()
|
||||
assert op.item == self.ticket
|
||||
|
||||
with self.assertRaises(OrderError):
|
||||
_perform_order(self.event, 'manual', [self.cp2.pk], 'admin@example.org', 'en', None, {},
|
||||
'web')
|
||||
self.cp2.refresh_from_db()
|
||||
assert self.cp2.price == Decimal('23.00')
|
||||
|
||||
@scopes_disabled()
|
||||
def test_budget_exceeded_between_positions(self):
|
||||
self.v.budget = Decimal('1.50')
|
||||
self.v.save()
|
||||
with self.assertRaises(OrderError):
|
||||
_perform_order(self.event, 'manual', [self.cp1.pk, self.cp2.pk], 'admin@example.org', 'en', None, {},
|
||||
'web')
|
||||
self.cp1.refresh_from_db()
|
||||
assert self.cp1.price == Decimal('21.50')
|
||||
self.cp2.refresh_from_db()
|
||||
assert self.cp2.price == Decimal('23.00')
|
||||
|
||||
@scopes_disabled()
|
||||
def test_budget_exceeded_in_first_position(self):
|
||||
self.v.budget = Decimal('1.00')
|
||||
self.v.save()
|
||||
with self.assertRaises(OrderError):
|
||||
_perform_order(self.event, 'manual', [self.cp1.pk, self.cp2.pk], 'admin@example.org', 'en', None, {},
|
||||
'web')
|
||||
self.cp1.refresh_from_db()
|
||||
assert self.cp1.price == Decimal('22.00')
|
||||
self.cp2.refresh_from_db()
|
||||
assert self.cp2.price == Decimal('23.00')
|
||||
|
||||
@scopes_disabled()
|
||||
def test_budget_exceeded_in_second_position(self):
|
||||
self.v.budget = Decimal('2.50')
|
||||
self.v.save()
|
||||
with self.assertRaises(OrderError):
|
||||
_perform_order(self.event, 'manual', [self.cp1.pk, self.cp2.pk], 'admin@example.org', 'en', None, {},
|
||||
'web')
|
||||
self.cp1.refresh_from_db()
|
||||
assert self.cp1.price == Decimal('21.50')
|
||||
self.cp2.refresh_from_db()
|
||||
assert self.cp2.price == Decimal('22.00')
|
||||
|
||||
@scopes_disabled()
|
||||
def test_budget_exceeded_during_price_change(self):
|
||||
self.v.budget = Decimal('2.50')
|
||||
self.v.value = Decimal('21.00')
|
||||
self.v.save()
|
||||
self.cp1.expires = now() - timedelta(hours=1)
|
||||
self.cp1.save()
|
||||
self.cp2.expires = now() - timedelta(hours=1)
|
||||
self.cp2.save()
|
||||
|
||||
with self.assertRaises(OrderError):
|
||||
_perform_order(self.event, 'manual', [self.cp1.pk, self.cp2.pk], 'admin@example.org', 'en', None, {},
|
||||
'web')
|
||||
self.cp1.refresh_from_db()
|
||||
assert self.cp1.price == Decimal('21.00')
|
||||
self.cp2.refresh_from_db()
|
||||
assert self.cp2.price == Decimal('22.50')
|
||||
|
||||
@scopes_disabled()
|
||||
def test_budget_exceeded_expired_cart(self):
|
||||
self.v.budget = Decimal('0.00')
|
||||
self.v.value = Decimal('21.00')
|
||||
self.v.save()
|
||||
self.cp1.expires = now() - timedelta(hours=1)
|
||||
self.cp1.save()
|
||||
self.cp2.expires = now() - timedelta(hours=1)
|
||||
self.cp2.save()
|
||||
|
||||
with self.assertRaises(OrderError):
|
||||
_perform_order(self.event, 'manual', [self.cp1.pk, self.cp2.pk], 'admin@example.org', 'en', None, {},
|
||||
'web')
|
||||
self.cp1.refresh_from_db()
|
||||
assert self.cp1.price == Decimal('23.00')
|
||||
self.cp2.refresh_from_db()
|
||||
assert self.cp2.price == Decimal('23.00')
|
||||
|
||||
@scopes_disabled()
|
||||
def test_budget_overbooked_expired_cart(self):
|
||||
self.v.budget = Decimal('1.50')
|
||||
self.v.value = Decimal('21.50')
|
||||
self.v.save()
|
||||
self.cp1.expires = now() - timedelta(hours=1)
|
||||
self.cp1.save()
|
||||
self.cp2.expires = now() - timedelta(hours=1)
|
||||
self.cp2.save()
|
||||
oid = _perform_order(self.event, 'manual', [self.cp1.pk], 'admin@example.org', 'en', None, {},
|
||||
'web')
|
||||
o = Order.objects.get(pk=oid)
|
||||
op = o.positions.first()
|
||||
|
||||
assert op.item == self.ticket
|
||||
self.v.budget = Decimal('1.00')
|
||||
self.v.save()
|
||||
|
||||
with self.assertRaises(OrderError):
|
||||
_perform_order(self.event, 'manual', [self.cp2.pk], 'admin@example.org', 'en', None, {},
|
||||
'web')
|
||||
self.cp2.refresh_from_db()
|
||||
assert self.cp2.price == Decimal('23.00')
|
||||
|
||||
Reference in New Issue
Block a user