diff --git a/core/signals.py b/core/signals.py index 342532e7e96d887a2291b2e5778b68f501d46f19..95cdcc1a46c21bdc27e5af7389418f1f0fba212d 100644 --- a/core/signals.py +++ b/core/signals.py @@ -81,4 +81,3 @@ def set_comment_visibility_after_conflict(sender, instance, **kwargs): ) comments_on_the_same_line.update(visible_to_student=False) instance.visible_to_student = True - diff --git a/core/tests/test_subscription_assignment_service.py b/core/tests/test_subscription_assignment_service.py index 410dfca75c81b364ef3b4e813251a774daa33a65..79f0f14a6c4e9c2a83233e9372a8e2ed29a147c4 100644 --- a/core/tests/test_subscription_assignment_service.py +++ b/core/tests/test_subscription_assignment_service.py @@ -378,9 +378,10 @@ class TestApiEndpoints(APITestCase): "score": 23, "of_submission": response.data['submission']['pk'], "feedback_lines": { - 1: {"text": "< some string >"}, - 2: {"text": "< some string >"} - } + 1: {"text": "< some string >", "labels": []}, + 2: {"text": "< some string >", "labels": []} + }, + "labels": [], } ) self.assertEqual(status.HTTP_201_CREATED, response.status_code) diff --git a/frontend/src/components/feedback_labels/FeedbackLabelForm.vue b/frontend/src/components/feedback_labels/FeedbackLabelForm.vue index 237964288f5a279323517237d7c7cc9e2bc50e9a..9c984168a3c311b4f1d787ed3b87e752958b517b 100644 --- a/frontend/src/components/feedback_labels/FeedbackLabelForm.vue +++ b/frontend/src/components/feedback_labels/FeedbackLabelForm.vue @@ -1,13 +1,13 @@ <template> <v-layout wrap justify-start> <v-flex ml-3 xs9> - <v-text-field + <v-text-field id="label-name" label="Name" v-model="mutableName" /> </v-flex> <v-flex ml-3 xs9> - <v-textarea + <v-textarea id="label-description" label="Description" v-model="mutableDescription" placeholder="The description can be seen when hovering above the label" diff --git a/frontend/src/components/feedback_labels/FeedbackLabelUpdater.vue b/frontend/src/components/feedback_labels/FeedbackLabelUpdater.vue index 7b02cc513305cdb43c230b890966973a907efb40..f0074bd27010c229cf720a846a7452a8c6881a12 100644 --- a/frontend/src/components/feedback_labels/FeedbackLabelUpdater.vue +++ b/frontend/src/components/feedback_labels/FeedbackLabelUpdater.vue @@ -2,6 +2,7 @@ <v-layout wrap> <v-flex mx-2 xs12> <v-autocomplete + id="label-update-autocomplete" :items="feedbackLabels" item-text="name" item-value="pk" diff --git a/frontend/src/components/feedback_labels/FeedbackLabelsList.vue b/frontend/src/components/feedback_labels/FeedbackLabelsList.vue index db30f7e30ae7c403f77a77129dcef37fbfa7f132..8afcc0c32cc373a70df40e7729d2616a881c5b07 100644 --- a/frontend/src/components/feedback_labels/FeedbackLabelsList.vue +++ b/frontend/src/components/feedback_labels/FeedbackLabelsList.vue @@ -15,7 +15,7 @@ </v-toolbar> <v-tabs grow color="teal lighten-1" v-if="showDetail"> <v-tab>Create</v-tab> - <v-tab>Update</v-tab> + <v-tab id="update-label-section">Update</v-tab> <v-tab-item> <feedback-label-form/> </v-tab-item> diff --git a/frontend/src/components/feedback_labels/LabelSelector.vue b/frontend/src/components/feedback_labels/LabelSelector.vue index 1c4f6bb270e01e18c15899083357661f9644d319..37bb7e6dda399920864aac1fc60d7df4cb0901c8 100644 --- a/frontend/src/components/feedback_labels/LabelSelector.vue +++ b/frontend/src/components/feedback_labels/LabelSelector.vue @@ -5,6 +5,7 @@ <v-layout wrap> <v-flex ml-2 sm10> <v-autocomplete + id="label-add-autocomplete" :items="feedbackLabels" item-text="name" item-value="pk" diff --git a/frontend/src/components/submission_notes/SubmissionCorrection.vue b/frontend/src/components/submission_notes/SubmissionCorrection.vue index 58b9b8f55bf4f6083afe699eb40819a47db5f80f..6b3a3bf21ab03b74fea82ecc6ac44d96e40323ff 100644 --- a/frontend/src/components/submission_notes/SubmissionCorrection.vue +++ b/frontend/src/components/submission_notes/SubmissionCorrection.vue @@ -43,6 +43,7 @@ </tr> </template> <label-selector + id="feedback-label-selector" :assignedToFeedback="true" class="mt-1 elevation-1" slot="labels" diff --git a/frontend/src/components/submission_notes/base/CommentForm.vue b/frontend/src/components/submission_notes/base/CommentForm.vue index f768e78de4e02bb39305273af141956e84a8bc81..c4a8f5edfeeebd3c055b72d92af698d67c2c62d2 100644 --- a/frontend/src/components/submission_notes/base/CommentForm.vue +++ b/frontend/src/components/submission_notes/base/CommentForm.vue @@ -17,6 +17,7 @@ </v-flex> <v-flex lg10 md8 sm8 my-2> <label-selector + id="comment-label-selector" :assignedToFeedback="false" :lineNo="this.lineNo" :labelsUnchanged="labelsUnchanged" diff --git a/frontend/src/components/submission_notes/toolbars/ToggleFeedbackVisibilityButton.vue b/frontend/src/components/submission_notes/toolbars/ToggleFeedbackVisibilityButton.vue index 47aab7ca2d1a3a3a4bde3683b56f912c6af831a6..a579c31040c59f80957ed63cd04c4e65ff09dcdb 100644 --- a/frontend/src/components/submission_notes/toolbars/ToggleFeedbackVisibilityButton.vue +++ b/frontend/src/components/submission_notes/toolbars/ToggleFeedbackVisibilityButton.vue @@ -1,5 +1,5 @@ <template> - <v-btn flat color="info" @click="showFeedback = !showFeedback"> + <v-btn id="feedback-visibility-toggle" flat color="info" @click="showFeedback = !showFeedback"> <div v-if="showFeedback"> Hide Feedback</div> <div v-else> Show Feedback</div> </v-btn> diff --git a/functional_tests/test_export_modal.py b/functional_tests/test_export_modal.py index a323d1b997fa110f0787672ef6ced5127384bed2..e6af20036648a6088aa782c685f137db3e70722d 100644 --- a/functional_tests/test_export_modal.py +++ b/functional_tests/test_export_modal.py @@ -119,13 +119,10 @@ class ExportTestModal(LiveServerTestCase): def test_export_instance(self): self._login() fact.SubmissionFactory() - export_btn = self.browser.find_element_by_id('export-btn') - export_btn.click() - export_instance = self.browser.find_element_by_id('export-list1') - export_instance.click() + self.browser.find_element_by_id('export-btn').click() + self.browser.find_element_by_id('export-list1').click() instance_export_modal = self.browser.find_element_by_id('instance-export-modal') - instance_export_btn = instance_export_modal.find_element_by_id('instance-export-dl') - instance_export_btn.click() + instance_export_modal.find_element_by_id('instance-export-dl').click() WebDriverWait(self.browser, 10).until(expect_file_to_be_present(JSON_EXPORT_FILE)) with open(JSON_EXPORT_FILE) as f: data = json.load(f) diff --git a/functional_tests/test_feedback_creation.py b/functional_tests/test_feedback_creation.py index 042700d286848d7aa4f2388176dc5c36058d39f6..1e1bd33536ffbc25893408d982755ddea408e8f6 100644 --- a/functional_tests/test_feedback_creation.py +++ b/functional_tests/test_feedback_creation.py @@ -1,13 +1,12 @@ from django.test import LiveServerTestCase from selenium import webdriver -from selenium.webdriver.common.by import By -from selenium.common.exceptions import StaleElementReferenceException from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as ec from core.models import UserAccount, Submission, FeedbackComment from functional_tests.util import (login, create_browser, reset_browser_after_test, - subscriptions_loaded_cond) + go_to_subscription, wait_until_code_changes, + reconstruct_submission_code) from util import factory_boys as fact @@ -38,38 +37,6 @@ class UntestedParent: def _login(self): login(self.browser, self.live_server_url, self.username, self.password) - def _reconstruct_submission_code(self): - sub_table = self.browser.find_element_by_class_name('submission-table') - lines = sub_table.find_elements_by_tag_name('tr') - line_no_code_pairs = [ - (line.get_attribute('id'), - # call get_attribute here to get non normalized text - # https://github.com/SeleniumHQ/selenium/issues/2608 - line.find_element_by_class_name('code-cell-content').get_attribute('textContent')) - for line in lines - ] - line_no_code_pairs.sort(key=lambda x: x[0]) # sort by ids - code_lines = list(zip(*line_no_code_pairs))[1] - return '\n'.join(code_lines) - - # stage can be 'initial', 'validate', or 'conflict' - def _go_to_subscription(self, stage='initial', sub_type=None): - WebDriverWait(self.browser, 10).until(subscriptions_loaded_cond(self.browser)) - tasks = self.browser.find_element_by_name('subscription-list') - tab = tasks.find_element_by_xpath(f'//*[contains(text(), "{stage}")]') - tab.click() - sub_type = sub_type if sub_type is not None else self.sub_type - - sub_type_xpath = f'//*[contains(text(), "{sub_type.name}") ' \ - f'and not(contains(@class, "inactive"))]' - WebDriverWait(self.browser, 10).until( - ec.element_to_be_clickable((By.XPATH, sub_type_xpath)), - message="SubmissionType not clickable" - ) - sub_type_el = tasks.find_element_by_xpath(sub_type_xpath) - sub_type_el.click() - WebDriverWait(self.browser, 10).until(ec.url_contains('subscription')) - def write_comments_on_lines(self, line_comment_tuples): """ line_comment_tuples is an iterable containing tuples of (line_no, comment) where the line number starts at 1 @@ -87,14 +54,14 @@ class UntestedParent: def test_student_text_is_correctly_displayed(self): self._login() - self._go_to_subscription() - code = self._reconstruct_submission_code() + go_to_subscription(self) + code = reconstruct_submission_code(self) # query db for Submission with seen code, throws if not present and test fails Submission.objects.get(text=code) def test_submission_type_is_correctly_displayed(self): self._login() - self._go_to_subscription() + go_to_subscription(self) sub_type_el = self.browser.find_element_by_id('submission-type') title = sub_type_el.find_element_by_class_name('title') self.assertEqual( @@ -113,7 +80,7 @@ class UntestedParent: for submission in Submission.objects.all(): test = fact.TestFactory.create(submission=submission, annotation='This is a test') self._login() - self._go_to_subscription() + go_to_subscription(self) tests = self.browser.find_element_by_id('submission-tests') name_label = tests.find_element_by_name('test-name-label') name_label.click() @@ -123,32 +90,22 @@ class UntestedParent: WebDriverWait(self.browser, 10).until(ec.visibility_of(test_output)) self.assertEqual(test.annotation, test_output.text) - def wait_until_code_changes(self, code): - def condition(*args): - try: - # code might change during the call resulting in the exception - new_code = self._reconstruct_submission_code() - except StaleElementReferenceException: - return False - return code != new_code - return condition - def test_can_give_max_score(self): self._login() - self._go_to_subscription() - code = self._reconstruct_submission_code() + go_to_subscription(self) + code = reconstruct_submission_code(self) self.browser.find_element_by_id('score-full').click() submit_btn = self.browser.find_element_by_id('submit-feedback') submit_btn.click() WebDriverWait(self.browser, 10).until( - self.wait_until_code_changes(code) + wait_until_code_changes(self, code) ) submission_for_code = Submission.objects.get(text=code) self.assertEqual(self.sub_type.full_score, submission_for_code.feedback.score) def test_zero_score_without_warning_gives_error(self): self._login() - self._go_to_subscription() + go_to_subscription(self) self.browser.find_element_by_id('score-zero').click() submit_btn = self.browser.find_element_by_id('submit-feedback') submit_btn.click() @@ -157,19 +114,19 @@ class UntestedParent: def test_can_give_zero_score(self): self._login() - self._go_to_subscription() - code = self._reconstruct_submission_code() + go_to_subscription(self) + code = reconstruct_submission_code(self) self.browser.find_element_by_id('score-zero').click() self.write_comments_on_lines([(0, 'A comment')]) self.browser.find_element_by_id('submit-feedback').click() - WebDriverWait(self.browser, 10).until(self.wait_until_code_changes(code)) + WebDriverWait(self.browser, 10).until(wait_until_code_changes(self, code)) submission_for_code = Submission.objects.get(text=code) self.assertEqual(0, submission_for_code.feedback.score) def test_can_give_comments_and_decreased_score(self): self._login() - self._go_to_subscription() - code = self._reconstruct_submission_code() + go_to_subscription(self) + code = reconstruct_submission_code(self) # give half full score score_input = self.browser.find_element_by_id('score-input') @@ -184,7 +141,7 @@ class UntestedParent: submit_btn = self.browser.find_element_by_id('submit-feedback') submit_btn.click() WebDriverWait(self.browser, 10).until( - self.wait_until_code_changes(code) + wait_until_code_changes(self, code) ) submission_for_code = Submission.objects.get(text=code) self.assertEqual(self.sub_type.full_score // 2, submission_for_code.feedback.score) @@ -203,23 +160,23 @@ class UntestedParent: def test_can_skip_submission(self): self._login() - self._go_to_subscription() - code = self._reconstruct_submission_code() + go_to_subscription(self) + code = reconstruct_submission_code(self) self.browser.find_element_by_id('skip-submission').click() - WebDriverWait(self.browser, 10).until(self.wait_until_code_changes(code)) + WebDriverWait(self.browser, 10).until(wait_until_code_changes(self, code)) def test_can_validate_submission(self): self._login() - self._go_to_subscription() + go_to_subscription(self) def correct(): - code = self._reconstruct_submission_code() + code = reconstruct_submission_code(self) self.write_comments_on_lines([(0, 'A comment by me')]) self.browser.find_element_by_id('score-zero').click() self.browser.find_element_by_id('submit-feedback').click() return code code = correct() - WebDriverWait(self.browser, 10).until(self.wait_until_code_changes(code)) + WebDriverWait(self.browser, 10).until(wait_until_code_changes(self, code)) correct() sub_url = 'subscription/' + str(self.sub_type.pk) + '/ended' @@ -232,14 +189,14 @@ class UntestedParent: fact.UserAccountFactory(username=user_snd, password=password) login(self.browser, self.live_server_url, user_snd, password) - self._go_to_subscription(stage='validate') + go_to_subscription(self, stage='validate') self.write_comments_on_lines([(0, 'I disagree'), (1, 'Full points!')]) - code_final = self._reconstruct_submission_code() + code_final = reconstruct_submission_code(self) self.browser.find_element_by_id('score-full').click() self.browser.find_element_by_id('submit-feedback').click() - WebDriverWait(self.browser, 10).until(self.wait_until_code_changes(code_final)) - code_non_final = self._reconstruct_submission_code() + WebDriverWait(self.browser, 10).until(wait_until_code_changes(self, code_final)) + code_non_final = reconstruct_submission_code(self) self.browser.find_element_by_class_name('final-checkbox').click() self.browser.find_element_by_id('submit-feedback').click() @@ -254,8 +211,8 @@ class UntestedParent: fact.UserAccountFactory(username=user_rev, password=password, role=role) login(self.browser, self.live_server_url, user_rev, password) - self._go_to_subscription('conflict') - code = self._reconstruct_submission_code() + go_to_subscription(self, 'conflict') + code = reconstruct_submission_code(self) self.assertEqual(code, code_non_final) submission_for_code = Submission.objects.get(text=code_final) diff --git a/functional_tests/test_feedback_label_system.py b/functional_tests/test_feedback_label_system.py new file mode 100644 index 0000000000000000000000000000000000000000..44d63cce177cfbd6d4fcf60228050f02ca0bdda6 --- /dev/null +++ b/functional_tests/test_feedback_label_system.py @@ -0,0 +1,342 @@ +from django.test import LiveServerTestCase +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as ec + +from core.models import FeedbackLabel +from functional_tests.util import (login, create_browser, reset_browser_after_test, + query_returns_object, go_to_subscription, + reconstruct_submission_code, wait_until_code_changes) +from util import factory_boys as fact + + +class FeedbackLabelSystemTest(LiveServerTestCase): + browser: webdriver.Firefox = None + username = None + password = None + role = None + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.browser = create_browser() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + cls.browser.quit() + + def setUp(self): + super().setUp() + self.username = 'tut' + self.password = 'p' + fact.UserAccountFactory( + username=self.username, + password=self.password, + ) + self.sub_type = fact.SubmissionTypeFactory.create() + fact.SubmissionFactory.create_batch(2, type=self.sub_type) + + def tearDown(self): + reset_browser_after_test(self.browser, self.live_server_url) + + def _login(self): + login(self.browser, self.live_server_url, self.username, self.password) + + # creates a new label where colour_num is + # the index of the colour to click on the colour picker + def create_label(self, name, description, colour_num): + self.browser.find_element_by_id('label-name').send_keys(name) + self.browser.find_element_by_id('label-description').send_keys(description) + self.browser.find_elements_by_class_name('vc-compact-color-item')[colour_num].click() + self.browser.find_element_by_id('create-label-btn').click() + WebDriverWait(self.browser, 10).until(query_returns_object(FeedbackLabel, name=name)) + + # updates an already existing label with the given arguments + def update_label(self, old_name, new_name, description, colour_num): + self.browser.find_element_by_id('update-label-section').click() + WebDriverWait(self.browser, 2).until( + ec.element_to_be_clickable((By.ID, 'label-update-autocomplete')) + ) + self.browser.find_element_by_id('label-update-autocomplete').send_keys(old_name) + self.browser.find_element_by_link_text(old_name).click() + self.browser.find_element_by_xpath( + '//div[@class="v-window-item" and @style=""]//input[@id="label-name"]' + ).send_keys(new_name) + + self.browser.find_element_by_xpath( + '//div[@class="v-window-item" and @style=""]//textarea[@id="label-description"]' + ).send_keys(description) + + self.browser.find_elements_by_xpath( + '//div[@class="v-window-item" and @style=""]//li[@class="vc-compact-color-item"]' + )[colour_num].click() + self.browser.find_element_by_id('update-label-btn').click() + WebDriverWait(self.browser, 10).until( + query_returns_object(FeedbackLabel, name=old_name + new_name) + ) + + def assign_label_to_feedback(self, name): + self.browser.find_element_by_xpath( + '//div[@id="feedback-label-selector"]//input[@id="label-add-autocomplete"]' + ).send_keys(name) + self.browser.find_element_by_link_text(name).click() + + def remove_label_from_feedback(self, name): + self.browser.find_element_by_xpath( + f'//div[@id="feedback-label-selector"]//span[@class="v-chip__content" ' + f'and contains(text(), "{name}")]//div[@class="v-chip__close"]' + ).click() + + def assign_label_to_comment_line(self, line, name): + self.browser.find_element_by_xpath( + f'//div[@class="v-btn__content" and contains(text(), "{line}")]' + ).click() + self.browser.find_element_by_xpath( + '//div[@id="comment-label-selector"]//input[@id="label-add-autocomplete"]' + ).send_keys(name) + self.browser.find_element_by_link_text(name).click() + self.browser.find_element_by_id('submit-comment').click() + + def remove_label_from_comment_line(self, line, name): + self.browser.find_element_by_xpath( + f'//tr[@id="sub-line-{line}"]//span[contains(text(), "{name}")]' + '//div[@class="v-chip__close"]' + ).click() + + def test_can_create_label(self): + self._login() + label_name = 'test name' + label_desc = 'test description' + self.create_label(label_name, label_desc, 3) + created_label = FeedbackLabel.objects.get(name='test name') + + self.assertEqual(created_label.name, label_name) + self.assertEqual(created_label.description, label_desc) + + def test_can_not_create_duplicate_label(self): + self._login() + label_name = 'duplicate' + label_desc = 'duplicate test' + self.create_label(label_name, label_desc, 3) + self.create_label(label_name, label_desc, 3) + WebDriverWait(self.browser, 2).until( + ec.visibility_of_element_located((By.CLASS_NAME, 'notification-content')) + ) + notification = self.browser.find_element_by_class_name('notification-content') + + labels = FeedbackLabel.objects.all() + self.assertIn('already exists', notification.text) + self.assertEqual(len(labels), 1) + + def test_can_update_label(self): + self._login() + self.create_label('test', 'some desc', 1) + self.update_label('test', 'updated', 'updated desc', 3) + + label = FeedbackLabel.objects.get(name='testupdated') + + self.assertEqual(label.name, 'testupdated') + self.assertEqual(label.description, 'some descupdated desc') + + def test_can_assign_label_to_feedback_draft(self): + self._login() + self.create_label('test', 'some desc', 1) + go_to_subscription(self) + code = reconstruct_submission_code(self) + self.assign_label_to_feedback('test') + labels = self.browser.find_elements_by_xpath( + '//div[@id="feedback-label-selector"]//div[contains(text(), "WILL BE ADDED")]/..//*' + ) + self.assertGreater(len(labels), 1) + + self.browser.find_element_by_id('score-full').click() + self.browser.find_element_by_id('submit-feedback').click() + WebDriverWait(self.browser, 10).until( + wait_until_code_changes(self, code) + ) + + label = FeedbackLabel.objects.get(name='test') + self.assertEqual(len(label.feedback.all()), 1) + + def test_can_remove_label_from_feedback_draft(self): + self._login() + self.create_label('test', 'some desc', 1) + go_to_subscription(self) + self.assign_label_to_feedback('test') + self.remove_label_from_feedback('test') + labels = self.browser.find_elements_by_xpath( + '//div[@id="feedback-label-selector"]//div[contains(text(), "WILL BE ADDED")]/..//*' + ) + + self.assertEqual(len(labels), 1) + + def test_can_remove_label_from_submitted_feedback(self): + self._login() + self.create_label('test', 'some desc', 1) + go_to_subscription(self) + code = reconstruct_submission_code(self) + self.assign_label_to_feedback('test') + self.browser.find_element_by_id('score-full').click() + self.browser.find_element_by_id('submit-feedback').click() + WebDriverWait(self.browser, 10).until( + wait_until_code_changes(self, code) + ) + + # logs out user + reset_browser_after_test(self.browser, self.live_server_url) + + username = 'tut_snd' + password = 'p' + fact.UserAccountFactory(username=username, password=password) + login(self.browser, self.live_server_url, username, password) + + go_to_subscription(self, stage='validate') + code = reconstruct_submission_code(self) + self.remove_label_from_feedback('test') + removed = self.browser.find_elements_by_xpath( + '//div[@id="feedback-label-selector"]//div[contains(text(), "WILL BE REMOVED")]/..//*' + ) + self.assertGreater(len(removed), 1) + + self.browser.find_element_by_id('submit-feedback').click() + sub_url = 'subscription/' + str(self.sub_type.pk) + '/ended' + WebDriverWait(self.browser, 10).until(ec.url_contains(sub_url)) + label = FeedbackLabel.objects.get(name='test') + + self.assertEqual(len(label.feedback.all()), 0) + + def test_can_add_label_to_submitted_feedback(self): + self._login() + self.create_label('test', 'some test dec', 1) + self.create_label('add', 'add test dec', 4) + go_to_subscription(self) + code = reconstruct_submission_code(self) + self.assign_label_to_feedback('test') + self.browser.find_element_by_id('score-full').click() + self.browser.find_element_by_id('submit-feedback').click() + WebDriverWait(self.browser, 10).until( + wait_until_code_changes(self, code) + ) + + # logs out user + reset_browser_after_test(self.browser, self.live_server_url) + + username = 'tut_snd' + password = 'p' + fact.UserAccountFactory(username=username, password=password) + login(self.browser, self.live_server_url, username, password) + go_to_subscription(self, stage='validate') + + code = reconstruct_submission_code(self) + self.assign_label_to_feedback('add') + added = self.browser.find_elements_by_xpath( + '//div[@id="feedback-label-selector"]//div[contains(text(), "WILL BE ADDED")]/..//*' + ) + self.assertGreater(len(added), 1) + + self.browser.find_element_by_id('submit-feedback').click() + sub_url = 'subscription/' + str(self.sub_type.pk) + '/ended' + WebDriverWait(self.browser, 10).until(ec.url_contains(sub_url)) + label = FeedbackLabel.objects.get(name='add') + + self.assertEqual(len(label.feedback.all()), 1) + + def test_can_assign_label_to_comment(self): + self._login() + self.create_label('test', 'some desc', 1) + go_to_subscription(self) + code = reconstruct_submission_code(self) + comment_line = 1 + self.assign_label_to_comment_line(comment_line, 'test') + added = self.browser.find_elements_by_xpath( + f'//tr[@id="sub-line-{comment_line}"]//div[contains(text(), "WILL BE ADDED")]/..//*' + ) + self.assertGreater(len(added), 1) + + self.browser.find_element_by_id('score-full').click() + self.browser.find_element_by_id('submit-feedback').click() + WebDriverWait(self.browser, 10).until( + wait_until_code_changes(self, code) + ) + + label = FeedbackLabel.objects.get(name='test') + self.assertEqual(len(label.feedback_comments.all()), 1) + + def test_can_remove_label_from_submitted_comment(self): + self._login() + self.create_label('test', 'some desc', 1) + go_to_subscription(self) + code = reconstruct_submission_code(self) + comment_line = 1 + self.assign_label_to_comment_line(comment_line, 'test') + self.browser.find_element_by_id('score-full').click() + self.browser.find_element_by_id('submit-feedback').click() + WebDriverWait(self.browser, 10).until( + wait_until_code_changes(self, code) + ) + + # logs out user + reset_browser_after_test(self.browser, self.live_server_url) + + username = 'tut_snd' + password = 'p' + fact.UserAccountFactory(username=username, password=password) + login(self.browser, self.live_server_url, username, password) + + go_to_subscription(self, stage='validate') + code = reconstruct_submission_code(self) + self.browser.find_element_by_id('feedback-visibility-toggle').click() + self.remove_label_from_comment_line(comment_line, 'test') + removed = self.browser.find_elements_by_xpath( + '//div[@id="feedback-label-selector"]//div[contains(text(), "WILL BE REMOVED")]/..//*' + ) + self.assertGreaterEqual(len(removed), 1) + + self.browser.find_element_by_id('submit-feedback').click() + sub_url = 'subscription/' + str(self.sub_type.pk) + '/ended' + WebDriverWait(self.browser, 10).until(ec.url_contains(sub_url)) + label = FeedbackLabel.objects.get(name='test') + + # comment still exists but is now invisible + self.assertEqual(label.feedback_comments.all()[0].visible_to_student, False) + + def test_can_add_label_to_submitted_comment(self): + self._login() + self.create_label('test', 'some desc', 1) + self.create_label('add', 'add test desc', 4) + go_to_subscription(self) + code = reconstruct_submission_code(self) + comment_line = 1 + self.assign_label_to_comment_line(comment_line, 'test') + self.browser.find_element_by_id('score-full').click() + self.browser.find_element_by_id('submit-feedback').click() + WebDriverWait(self.browser, 10).until( + wait_until_code_changes(self, code) + ) + + # logs out user + reset_browser_after_test(self.browser, self.live_server_url) + + username = 'tut_snd' + password = 'p' + fact.UserAccountFactory(username=username, password=password) + login(self.browser, self.live_server_url, username, password) + + go_to_subscription(self, stage='validate') + code = reconstruct_submission_code(self) + self.browser.find_element_by_id('feedback-visibility-toggle').click() + self.assign_label_to_comment_line(comment_line, 'add') + added = self.browser.find_elements_by_xpath( + '//div[@id="feedback-label-selector"]//div[contains(text(), "WILL BE ADDED")]/..//*' + ) + self.assertGreaterEqual(len(added), 1) + + self.browser.find_element_by_id('submit-feedback').click() + sub_url = 'subscription/' + str(self.sub_type.pk) + '/ended' + WebDriverWait(self.browser, 10).until(ec.url_contains(sub_url)) + label = FeedbackLabel.objects.get(name='add') + + # comment still exists but is now invisible + self.assertEqual(len(label.feedback_comments.all()), 1) diff --git a/functional_tests/util.py b/functional_tests/util.py index 0cec31a72cbe2b775757dd0fce1d020a929b4ec1..be898df2ae65a531e4d8b32ea67e2a54c027d5cc 100644 --- a/functional_tests/util.py +++ b/functional_tests/util.py @@ -10,6 +10,8 @@ from selenium.webdriver.remote.webelement import WebElement from selenium.webdriver.support import expected_conditions as ec from selenium.webdriver.support.ui import WebDriverWait from selenium.common.exceptions import StaleElementReferenceException +from selenium.webdriver.common.by import By +from django.core.exceptions import ObjectDoesNotExist def create_browser() -> webdriver.Firefox: @@ -69,3 +71,60 @@ def subscriptions_loaded_cond(browser): pass return False return loaded + + +# returns a function that can be used as a callback for WebDriverWait +# the resulting functions returns True if the given query would return at least one object +def query_returns_object(model_class, **kwargs): + def query(*args): + try: + model_class.objects.get(**kwargs) + except ObjectDoesNotExist: + return False + return True + return query + + +# stage can be 'initial', 'validate', or 'conflict' +def go_to_subscription(self, stage='initial', sub_type=None): + WebDriverWait(self.browser, 10).until(subscriptions_loaded_cond(self.browser)) + tasks = self.browser.find_element_by_name('subscription-list') + tab = tasks.find_element_by_xpath(f'//*[contains(text(), "{stage}")]') + tab.click() + sub_type = sub_type if sub_type is not None else self.sub_type + + sub_type_xpath = f'//*[contains(text(), "{sub_type.name}") ' \ + f'and not(contains(@class, "inactive"))]' + WebDriverWait(self.browser, 10).until( + ec.element_to_be_clickable((By.XPATH, sub_type_xpath)), + message="SubmissionType not clickable" + ) + sub_type_el = tasks.find_element_by_xpath(sub_type_xpath) + sub_type_el.click() + WebDriverWait(self.browser, 10).until(ec.url_contains('subscription')) + + +def reconstruct_submission_code(self): + sub_table = self.browser.find_element_by_class_name('submission-table') + lines = sub_table.find_elements_by_tag_name('tr') + line_no_code_pairs = [ + (line.get_attribute('id'), + # call get_attribute here to get non normalized text + # https://github.com/SeleniumHQ/selenium/issues/2608 + line.find_element_by_class_name('code-cell-content').get_attribute('textContent')) + for line in lines + ] + line_no_code_pairs.sort(key=lambda x: x[0]) # sort by ids + code_lines = list(zip(*line_no_code_pairs))[1] + return '\n'.join(code_lines) + + +def wait_until_code_changes(self, code): + def condition(*args): + try: + # code might change during the call resulting in the exception + new_code = reconstruct_submission_code(self) + except StaleElementReferenceException: + return False + return code != new_code + return condition