Replace unit page with the container page.
STUD-1754
This commit is contained in:
@@ -37,11 +37,6 @@ REQUIREJS_WAIT = {
|
||||
"jquery", "js/base", "js/models/course", "js/models/settings/advanced",
|
||||
"js/views/settings/advanced", "codemirror"],
|
||||
|
||||
# Individual Unit (editing)
|
||||
re.compile('^Individual Unit \|'): [
|
||||
"js/base", "coffee/src/views/unit",
|
||||
"coffee/src/views/module_edit"],
|
||||
|
||||
# Content - Outline
|
||||
# Note that calling your org, course number, or display name, 'course' will mess this up
|
||||
re.compile('^Course Outline \|'): [
|
||||
|
||||
@@ -174,8 +174,7 @@ def add_staff_markup(user, has_instructor_access, block, view, frag, context):
|
||||
|
||||
if is_studio_course and is_mongo_course:
|
||||
# build edit link to unit in CMS. Can't use reverse here as lms doesn't load cms's urls.py
|
||||
# reverse for contentstore.views.unit_handler
|
||||
edit_link = "//" + settings.CMS_BASE + '/unit/' + unicode(block.location)
|
||||
edit_link = "//" + settings.CMS_BASE + '/container/' + unicode(block.location)
|
||||
|
||||
# return edit link in rendered HTML for display
|
||||
return wrap_fragment(frag, render_to_string("edit_unit_link.html", {'frag_content': frag.content, 'edit_link': edit_link}))
|
||||
|
||||
@@ -171,7 +171,6 @@ class SplitTestModuleStudioTest(SplitTestModuleTest):
|
||||
Context for rendering the studio "author_view".
|
||||
"""
|
||||
return {
|
||||
'container_view': True,
|
||||
'reorderable_items': set(),
|
||||
'root_xblock': root_xblock,
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ class StudioEditableModuleTestCase(BaseVerticalModuleTest):
|
||||
"""
|
||||
reorderable_items = set()
|
||||
context = {
|
||||
'container_view': True,
|
||||
'reorderable_items': reorderable_items,
|
||||
'read_only': False,
|
||||
'root_xblock': self.vertical,
|
||||
|
||||
@@ -57,7 +57,7 @@ class VerticalModuleTestCase(BaseVerticalModuleTest):
|
||||
"""
|
||||
# Vertical shouldn't render children on the unit page
|
||||
context = {
|
||||
'container_view': False,
|
||||
'is_unit_page': True
|
||||
}
|
||||
html = self.module_system.render(self.vertical, AUTHOR_VIEW, context).content
|
||||
self.assertNotIn(self.test_html_1, html)
|
||||
@@ -66,7 +66,7 @@ class VerticalModuleTestCase(BaseVerticalModuleTest):
|
||||
# Vertical should render reorderable children on the container page
|
||||
reorderable_items = set()
|
||||
context = {
|
||||
'container_view': True,
|
||||
'is_unit_page': False,
|
||||
'reorderable_items': reorderable_items,
|
||||
}
|
||||
html = self.module_system.render(self.vertical, AUTHOR_VIEW, context).content
|
||||
|
||||
@@ -45,9 +45,13 @@ class VerticalModule(VerticalFields, XModule, StudioEditableModule):
|
||||
Renders the Studio preview view, which supports drag and drop.
|
||||
"""
|
||||
fragment = Fragment()
|
||||
root_xblock = context.get('root_xblock')
|
||||
is_root = root_xblock and root_xblock.location == self.location
|
||||
|
||||
# For the container page we want the full drag-and-drop, but for unit pages we want
|
||||
# a more concise version that appears alongside the "View =>" link.
|
||||
if context.get('container_view'):
|
||||
# a more concise version that appears alongside the "View =>" link-- unless it is
|
||||
# the unit page and the vertical being rendered is itself the unit vertical (is_root == True).
|
||||
if is_root or not context.get('is_unit_page'):
|
||||
self.render_children(context, fragment, can_reorder=True, can_add=True)
|
||||
return fragment
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ class ContainerPage(PageObject):
|
||||
"""
|
||||
Container page in Studio
|
||||
"""
|
||||
NAME_SELECTOR = 'a.navigation-current'
|
||||
NAME_SELECTOR = '.page-header-title'
|
||||
|
||||
def __init__(self, browser, locator):
|
||||
super(ContainerPage, self).__init__(browser)
|
||||
@@ -126,16 +126,8 @@ class ContainerPage(PageObject):
|
||||
def edit(self):
|
||||
"""
|
||||
Clicks the "edit" button for the first component on the page.
|
||||
|
||||
Same as the implementation in unit.py, unit and component pages will be merging.
|
||||
"""
|
||||
self.q(css='.edit-button').first.click()
|
||||
EmptyPromise(
|
||||
lambda: self.q(css='.xblock-studio_view').present,
|
||||
'Wait for the Studio editor to be present'
|
||||
).fulfill()
|
||||
|
||||
return self
|
||||
return _click_edit(self)
|
||||
|
||||
def add_missing_groups(self):
|
||||
"""
|
||||
@@ -164,7 +156,7 @@ class XBlockWrapper(PageObject):
|
||||
"""
|
||||
url = None
|
||||
BODY_SELECTOR = '.studio-xblock-wrapper'
|
||||
NAME_SELECTOR = '.header-details'
|
||||
NAME_SELECTOR = '.xblock-display-name'
|
||||
|
||||
def __init__(self, browser, locator):
|
||||
super(XBlockWrapper, self).__init__(browser)
|
||||
@@ -210,3 +202,33 @@ class XBlockWrapper(PageObject):
|
||||
@property
|
||||
def preview_selector(self):
|
||||
return self._bounded_selector('.xblock-student_view,.xblock-author_view')
|
||||
|
||||
def go_to_container(self):
|
||||
"""
|
||||
Open the container page linked to by this xblock, and return
|
||||
an initialized :class:`.ContainerPage` for that xblock.
|
||||
"""
|
||||
return ContainerPage(self.browser, self.locator).visit()
|
||||
|
||||
def edit(self):
|
||||
"""
|
||||
Clicks the "edit" button for this xblock.
|
||||
"""
|
||||
return _click_edit(self, self._bounded_selector)
|
||||
|
||||
@property
|
||||
def editor_selector(self):
|
||||
return '.xblock-studio_view'
|
||||
|
||||
|
||||
def _click_edit(page_object, bounded_selector=lambda(x): x):
|
||||
"""
|
||||
Click on the first edit button found and wait for the Studio editor to be present.
|
||||
"""
|
||||
page_object.q(css=bounded_selector('.edit-button')).first.click()
|
||||
EmptyPromise(
|
||||
lambda: page_object.q(css='.xblock-studio_view').present,
|
||||
'Wait for the Studio editor to be present'
|
||||
).fulfill()
|
||||
|
||||
return page_object
|
||||
|
||||
@@ -5,7 +5,7 @@ from bok_choy.page_object import PageObject
|
||||
from bok_choy.promise import EmptyPromise
|
||||
|
||||
from .course_page import CoursePage
|
||||
from .unit import UnitPage
|
||||
from .container import ContainerPage
|
||||
|
||||
|
||||
class CourseOutlineContainer(object):
|
||||
@@ -84,10 +84,10 @@ class CourseOutlineUnit(CourseOutlineChild):
|
||||
|
||||
def go_to(self):
|
||||
"""
|
||||
Open the unit page linked to by this unit link, and return
|
||||
an initialized :class:`.UnitPage` for that unit.
|
||||
Open the container page linked to by this unit link, and return
|
||||
an initialized :class:`.ContainerPage` for that unit.
|
||||
"""
|
||||
return UnitPage(self.browser, self.locator).visit()
|
||||
return ContainerPage(self.browser, self.locator).visit()
|
||||
|
||||
def is_browser_on_page(self):
|
||||
return self.q(css=self.BODY_SELECTOR).present
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
"""
|
||||
Unit page in Studio
|
||||
"""
|
||||
|
||||
from bok_choy.page_object import PageObject
|
||||
from bok_choy.promise import EmptyPromise, Promise
|
||||
|
||||
from . import BASE_URL
|
||||
from .container import ContainerPage
|
||||
|
||||
|
||||
class UnitPage(PageObject):
|
||||
"""
|
||||
Unit page in Studio
|
||||
"""
|
||||
|
||||
NAME_SELECTOR = '#unit-display-name-input'
|
||||
|
||||
def __init__(self, browser, unit_locator):
|
||||
super(UnitPage, self).__init__(browser)
|
||||
self.unit_locator = unit_locator
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
"""URL to the pages UI in a course."""
|
||||
return "{}/unit/{}".format(BASE_URL, self.unit_locator)
|
||||
|
||||
def is_browser_on_page(self):
|
||||
|
||||
def _is_finished_loading():
|
||||
# Wait until all components have been loaded
|
||||
number_of_leaf_xblocks = len(self.q(css='{} .xblock-author_view,.xblock-student_view'.format(Component.BODY_SELECTOR)).results)
|
||||
is_done = len(self.q(css=Component.BODY_SELECTOR).results) == number_of_leaf_xblocks
|
||||
return (is_done, is_done)
|
||||
|
||||
# First make sure that an element with the view-unit class is present on the page,
|
||||
# and then wait to make sure that the xblocks are all there
|
||||
return (
|
||||
self.q(css='body.view-unit').present and
|
||||
Promise(_is_finished_loading, 'Finished rendering the xblocks in the unit.').fulfill()
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.q(css=self.NAME_SELECTOR).attrs('value')[0]
|
||||
|
||||
@property
|
||||
def components(self):
|
||||
"""
|
||||
Return a list of components loaded on the unit page.
|
||||
"""
|
||||
return self.q(css=Component.BODY_SELECTOR).map(
|
||||
lambda el: Component(self.browser, el.get_attribute('data-locator'))).results
|
||||
|
||||
def edit_draft(self):
|
||||
"""
|
||||
Started editing a draft of this unit.
|
||||
"""
|
||||
EmptyPromise(
|
||||
lambda: self.q(css='.create-draft').present,
|
||||
'Wait for edit draft link to be present'
|
||||
).fulfill()
|
||||
|
||||
self.q(css='.create-draft').first.click()
|
||||
|
||||
EmptyPromise(
|
||||
lambda: self.q(css='.editing-draft-alert').present,
|
||||
'Wait for draft mode to be activated'
|
||||
).fulfill()
|
||||
|
||||
def set_unit_visibility(self, visibility):
|
||||
"""
|
||||
Set unit visibility state
|
||||
|
||||
Arguments:
|
||||
visibility (str): private or public
|
||||
|
||||
"""
|
||||
self.q(css='select[name="visibility-select"] option[value="{}"]'.format(visibility)).first.click()
|
||||
self.wait_for_ajax()
|
||||
|
||||
selector = '.edit-button'
|
||||
if visibility == 'private':
|
||||
check_func = lambda: self.q(css=selector).visible
|
||||
elif visibility == 'public':
|
||||
check_func = lambda: not self.q(css=selector).visible
|
||||
|
||||
EmptyPromise(check_func, 'Unit Visibility is {}'.format(visibility)).fulfill()
|
||||
|
||||
|
||||
COMPONENT_BUTTONS = {
|
||||
'advanced_tab': '.editor-tabs li.inner_tab_wrap:nth-child(2) > a',
|
||||
'save_settings': '.action-save',
|
||||
}
|
||||
|
||||
|
||||
class Component(PageObject):
|
||||
"""
|
||||
A PageObject representing an XBlock child on the Studio UnitPage (including
|
||||
the editing controls).
|
||||
"""
|
||||
url = None
|
||||
BODY_SELECTOR = '.component'
|
||||
NAME_SELECTOR = '.component-header'
|
||||
|
||||
def __init__(self, browser, locator):
|
||||
super(Component, self).__init__(browser)
|
||||
self.locator = locator
|
||||
|
||||
def is_browser_on_page(self):
|
||||
return self.q(css='{}[data-locator="{}"]'.format(self.BODY_SELECTOR, self.locator)).present
|
||||
|
||||
def _bounded_selector(self, selector):
|
||||
"""
|
||||
Return `selector`, but limited to this particular `CourseOutlineChild` context
|
||||
"""
|
||||
return '{}[data-locator="{}"] {}'.format(
|
||||
self.BODY_SELECTOR,
|
||||
self.locator,
|
||||
selector
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
titles = self.q(css=self._bounded_selector(self.NAME_SELECTOR)).text
|
||||
if titles:
|
||||
return titles[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def preview_selector(self):
|
||||
return self._bounded_selector('.xblock-author_view,.xblock-student_view')
|
||||
|
||||
def edit(self):
|
||||
"""
|
||||
Clicks the "edit" button for the first component on the page.
|
||||
|
||||
Same as the implementation in unit.py, unit and component pages will be merging.
|
||||
"""
|
||||
self.q(css=self._bounded_selector('.edit-button')).first.click()
|
||||
EmptyPromise(
|
||||
lambda: self.q(css='.xblock-studio_view').present,
|
||||
'Wait for the Studio editor to be present'
|
||||
).fulfill()
|
||||
|
||||
return self
|
||||
|
||||
@property
|
||||
def editor_selector(self):
|
||||
return '.xblock-studio_view'
|
||||
|
||||
def go_to_container(self):
|
||||
"""
|
||||
Open the container page linked to by this component, and return
|
||||
an initialized :class:`.ContainerPage` for that xblock.
|
||||
"""
|
||||
return ContainerPage(self.browser, self.locator).visit()
|
||||
|
||||
def _click_button(self, button_name):
|
||||
"""
|
||||
Click on a button as specified by `button_name`
|
||||
|
||||
Arguments:
|
||||
button_name (str): button name
|
||||
|
||||
"""
|
||||
self.q(css=COMPONENT_BUTTONS[button_name]).first.click()
|
||||
self.wait_for_ajax()
|
||||
|
||||
def open_advanced_tab(self):
|
||||
"""
|
||||
Click on Advanced Tab.
|
||||
"""
|
||||
self._click_button('advanced_tab')
|
||||
|
||||
def save_settings(self):
|
||||
"""
|
||||
Click on settings Save button.
|
||||
"""
|
||||
self._click_button('save_settings')
|
||||
|
||||
def go_to_group_configuration_page(self):
|
||||
"""
|
||||
Go to the Group Configuration used by the component.
|
||||
"""
|
||||
self.q(css=self._bounded_selector('span.message-text a')).first.click()
|
||||
|
||||
@property
|
||||
def group_configuration_link_name(self):
|
||||
"""
|
||||
Get Group Configuration name from link.
|
||||
"""
|
||||
return self.q(css=self._bounded_selector('span.message-text a')).first.text[0]
|
||||
@@ -73,7 +73,7 @@ class XBlockAcidBase(WebAppTest):
|
||||
subsection = self.outline.section('Test Section').subsection('Test Subsection')
|
||||
unit = subsection.toggle_expand().unit('Test Unit').go_to()
|
||||
|
||||
acid_block = AcidView(self.browser, unit.components[0].preview_selector)
|
||||
acid_block = AcidView(self.browser, unit.xblocks[0].preview_selector)
|
||||
self.validate_acid_block_preview(acid_block)
|
||||
|
||||
def test_acid_block_editor(self):
|
||||
@@ -85,9 +85,7 @@ class XBlockAcidBase(WebAppTest):
|
||||
subsection = self.outline.section('Test Section').subsection('Test Subsection')
|
||||
unit = subsection.toggle_expand().unit('Test Unit').go_to()
|
||||
|
||||
unit.edit_draft()
|
||||
|
||||
acid_block = AcidView(self.browser, unit.components[0].edit().editor_selector)
|
||||
acid_block = AcidView(self.browser, unit.xblocks[0].edit().editor_selector)
|
||||
self.assertTrue(acid_block.init_fn_passed)
|
||||
self.assertTrue(acid_block.resource_url_passed)
|
||||
self.assertTrue(acid_block.scope_passed('content'))
|
||||
@@ -141,15 +139,11 @@ class XBlockAcidParentBase(XBlockAcidBase):
|
||||
self.outline.visit()
|
||||
subsection = self.outline.section('Test Section').subsection('Test Subsection')
|
||||
unit = subsection.toggle_expand().unit('Test Unit').go_to()
|
||||
container = unit.components[0].go_to_container()
|
||||
container = unit.xblocks[0].go_to_container()
|
||||
|
||||
acid_block = AcidView(self.browser, container.xblocks[0].preview_selector)
|
||||
self.validate_acid_block_preview(acid_block)
|
||||
|
||||
@skip('This will fail until the container page supports editing')
|
||||
def test_acid_block_editor(self):
|
||||
super(XBlockAcidParentBase, self).test_acid_block_editor()
|
||||
|
||||
|
||||
class XBlockAcidEmptyParentTest(XBlockAcidParentBase):
|
||||
"""
|
||||
@@ -212,7 +206,6 @@ class XBlockAcidChildTest(XBlockAcidParentBase):
|
||||
|
||||
self.user = course_fix.user
|
||||
|
||||
|
||||
@skip('This will fail until we fix support of children in pure XBlocks')
|
||||
def test_acid_block_preview(self):
|
||||
super(XBlockAcidChildTest, self).test_acid_block_preview()
|
||||
|
||||
@@ -34,17 +34,16 @@ class ContainerBase(StudioCourseTest):
|
||||
self.course_info['run']
|
||||
)
|
||||
|
||||
def go_to_container_page(self, make_draft=False):
|
||||
def go_to_nested_container_page(self):
|
||||
"""
|
||||
Go to the test container page.
|
||||
|
||||
If make_draft is true, the unit page (accessed on way to container page) will be put into draft mode.
|
||||
Go to the nested container page.
|
||||
"""
|
||||
unit = self.go_to_unit_page(make_draft)
|
||||
container = unit.components[0].go_to_container()
|
||||
unit = self.go_to_unit_page()
|
||||
# The 0th entry is the unit page itself.
|
||||
container = unit.xblocks[1].go_to_container()
|
||||
return container
|
||||
|
||||
def go_to_unit_page(self, make_draft=False):
|
||||
def go_to_unit_page(self):
|
||||
"""
|
||||
Go to the test unit page.
|
||||
|
||||
@@ -52,10 +51,7 @@ class ContainerBase(StudioCourseTest):
|
||||
"""
|
||||
self.outline.visit()
|
||||
subsection = self.outline.section('Test Section').subsection('Test Subsection')
|
||||
unit = subsection.toggle_expand().unit('Test Unit').go_to()
|
||||
if make_draft:
|
||||
unit.edit_draft()
|
||||
return unit
|
||||
return subsection.toggle_expand().unit('Test Unit').go_to()
|
||||
|
||||
def verify_ordering(self, container, expected_orderings):
|
||||
"""
|
||||
@@ -83,13 +79,13 @@ class ContainerBase(StudioCourseTest):
|
||||
"""
|
||||
Perform the supplied action and then verify the resulting ordering.
|
||||
"""
|
||||
container = self.go_to_container_page(make_draft=True)
|
||||
container = self.go_to_nested_container_page()
|
||||
action(container)
|
||||
|
||||
self.verify_ordering(container, expected_ordering)
|
||||
|
||||
# Reload the page to see that the change was persisted.
|
||||
container = self.go_to_container_page()
|
||||
container = self.go_to_nested_container_page()
|
||||
self.verify_ordering(container, expected_ordering)
|
||||
|
||||
|
||||
@@ -101,9 +97,9 @@ class NestedVerticalTest(ContainerBase):
|
||||
Sets up a course structure with nested verticals.
|
||||
"""
|
||||
self.container_title = ""
|
||||
self.group_a = "Expand or Collapse\nGroup A"
|
||||
self.group_b = "Expand or Collapse\nGroup B"
|
||||
self.group_empty = "Expand or Collapse\nGroup Empty"
|
||||
self.group_a = "Group A"
|
||||
self.group_b = "Group B"
|
||||
self.group_empty = "Group Empty"
|
||||
self.group_a_item_1 = "Group A Item 1"
|
||||
self.group_a_item_2 = "Group A Item 2"
|
||||
self.group_b_item_1 = "Group B Item 1"
|
||||
@@ -360,13 +356,13 @@ class EditContainerTest(NestedVerticalTest):
|
||||
"""
|
||||
Test the "edit" button on a container appearing on the unit page.
|
||||
"""
|
||||
unit = self.go_to_unit_page(make_draft=True)
|
||||
component = unit.components[0]
|
||||
unit = self.go_to_unit_page()
|
||||
component = unit.xblocks[1]
|
||||
self.modify_display_name_and_verify(component)
|
||||
|
||||
def test_edit_container_on_container_page(self):
|
||||
"""
|
||||
Test the "edit" button on a container appearing on the container page.
|
||||
"""
|
||||
container = self.go_to_container_page(make_draft=True)
|
||||
container = self.go_to_nested_container_page()
|
||||
self.modify_display_name_and_verify(container)
|
||||
|
||||
@@ -57,7 +57,7 @@ class SplitTestMixin(object):
|
||||
|
||||
def verify_add_missing_groups_button_not_present(self, container):
|
||||
"""
|
||||
Checks that the "add missing gorups" button/link is not present.
|
||||
Checks that the "add missing groups" button/link is not present.
|
||||
"""
|
||||
def missing_groups_button_not_present():
|
||||
button_present = container.missing_groups_button_present()
|
||||
@@ -105,9 +105,9 @@ class SplitTest(ContainerBase, SplitTestMixin):
|
||||
|
||||
Returns the container page.
|
||||
"""
|
||||
unit = self.go_to_unit_page(make_draft=True)
|
||||
unit = self.go_to_unit_page()
|
||||
add_advanced_component(unit, 0, 'split_test')
|
||||
container = self.go_to_container_page()
|
||||
container = self.go_to_nested_container_page()
|
||||
container.edit()
|
||||
component_editor = ComponentEditorView(self.browser, container.locator)
|
||||
component_editor.set_select_value_and_save('Group Configuration', 'Configuration alpha,beta')
|
||||
@@ -119,16 +119,16 @@ class SplitTest(ContainerBase, SplitTestMixin):
|
||||
],
|
||||
},
|
||||
})
|
||||
return self.go_to_container_page()
|
||||
return self.go_to_nested_container_page()
|
||||
|
||||
def test_create_and_select_group_configuration(self):
|
||||
"""
|
||||
Tests creating a split test instance on the unit page, and then
|
||||
assigning the group configuration.
|
||||
"""
|
||||
unit = self.go_to_unit_page(make_draft=True)
|
||||
unit = self.go_to_unit_page()
|
||||
add_advanced_component(unit, 0, 'split_test')
|
||||
container = self.go_to_container_page()
|
||||
container = self.go_to_nested_container_page()
|
||||
container.edit()
|
||||
component_editor = ComponentEditorView(self.browser, container.locator)
|
||||
component_editor.set_select_value_and_save('Group Configuration', 'Configuration alpha,beta')
|
||||
@@ -136,14 +136,14 @@ class SplitTest(ContainerBase, SplitTestMixin):
|
||||
|
||||
# Switch to the other group configuration. Must navigate again to the container page so
|
||||
# that there is only a single "editor" on the page.
|
||||
container = self.go_to_container_page()
|
||||
container = self.go_to_nested_container_page()
|
||||
container.edit()
|
||||
component_editor = ComponentEditorView(self.browser, container.locator)
|
||||
component_editor.set_select_value_and_save('Group Configuration', 'Configuration 0,1,2')
|
||||
self.verify_groups(container, ['Group 0', 'Group 1', 'Group 2'], ['alpha', 'beta'])
|
||||
|
||||
# Reload the page to make sure the groups were persisted.
|
||||
container = self.go_to_container_page()
|
||||
container = self.go_to_nested_container_page()
|
||||
self.verify_groups(container, ['Group 0', 'Group 1', 'Group 2'], ['alpha', 'beta'])
|
||||
|
||||
@skip("This fails periodically where it fails to trigger the add missing groups action.Dis")
|
||||
@@ -161,7 +161,7 @@ class SplitTest(ContainerBase, SplitTestMixin):
|
||||
self.verify_groups(container, ['alpha', 'gamma'], ['beta'])
|
||||
|
||||
# Reload the page to make sure the groups were persisted.
|
||||
container = self.go_to_container_page()
|
||||
container = self.go_to_nested_container_page()
|
||||
self.verify_groups(container, ['alpha', 'gamma'], ['beta'])
|
||||
|
||||
@skip("Disabling as this fails intermittently. STUD-2003")
|
||||
|
||||
Reference in New Issue
Block a user