Repository: zulip/zulip-terminal
Pull request #1254 (2022-09-10 01:23:11)

lint-and-test: Add job to run docstring linting.
<!-- Please see https://github.com/zulip/zulip-terminal#contributor-guidelines ! -->

**What does this PR do?**  <!-- Overall description goes here -->

Adds the docstring linting from #1208 to CI, fully fixing #892.

**Tested?** <!-- Fine to leave some of these unchecked if this is a draft/work-in-progress -->
- [x] Manually


Patch:
diff --git a/zulipterminal/config/ui_mappings.py b/zulipterminal/config/ui_mappings.py
index 1618c2d..49871f0 100644
--- a/zulipterminal/config/ui_mappings.py
+++ b/zulipterminal/config/ui_mappings.py
@@ -65,3 +65,11 @@ STREAM_POST_POLICY = {
     3: "Only organization administrators, moderators and full members can send to this stream",
     4: "Only organization administrators and moderators can send to this stream",
 }
+
+EDIT_TOPIC_POLICY = {
+    1: "Only organization administrators, moderators, full members and members can edit topic",
+    2: "Only organization administrators can edit topic",
+    3: "Only organization administrators, moderators and full members can edit topic",
+    4: "Only organization administrators and moderators can edit topic",
+    5: "Any user can edit topic",
+}
diff --git a/zulipterminal/model.py b/zulipterminal/model.py
index 7f0a5cf..508c3cc 100644
--- a/zulipterminal/model.py
+++ b/zulipterminal/model.py
@@ -41,7 +41,11 @@ from zulipterminal.api_types import (
 )
 from zulipterminal.config.keys import primary_key_for_command
 from zulipterminal.config.symbols import STREAM_TOPIC_SEPARATOR
-from zulipterminal.config.ui_mappings import ROLE_BY_ID, StreamAccessType
+from zulipterminal.config.ui_mappings import (
+    EDIT_TOPIC_POLICY,
+    ROLE_BY_ID,
+    StreamAccessType,
+)
 from zulipterminal.helper import (
     Message,
     NamedEmojiData,
@@ -614,6 +618,83 @@ class Model:
 
         return response["result"] == "success"
 
+    def get_latest_message_in_topic(
+        self, stream_id: int, topic_name: str
+    ) -> Optional[Message]:
+        request: Dict[str, Any] = {
+            "anchor": 10000000000000000,
+            "num_before": 1,
+            "num_after": 0,
+            "narrow": [
+                {
+                    "operator": "stream",
+                    "operand": stream_id,
+                },
+                {"operator": "topic", "operand": topic_name},
+            ],
+        }
+        response = self.client.get_messages(request)
+        display_error_if_present(response, self.controller)
+        if response["messages"] != [] and len(response["messages"]) == 1:
+            return response["messages"][0]
+        else:
+            return None
+
+    def can_user_edit_topic(self) -> bool:
+        user_info = self.get_user_info(self.user_id)
+        if user_info is not None:
+            if not self.initial_data.get("realm_allow_message_editing"):
+                self.controller.report_error("Editing messages is disabled")
+                return False
+            role = user_info["role"]
+            if role <= 200:
+                return True
+            # ZFL >= 75, realm/ allow_community_topic_editing was replaced with
+            # edit_topic_policy
+            allow_community_topic_editing = self.initial_data.get(
+                "realm_allow_community_topic_editing", None
+            )
+            if allow_community_topic_editing is True:
+                return True
+            elif allow_community_topic_editing is False:
+                self.controller.report_error("Editing topic is disabled")
+                return False
+            else:
+                edit_topic_policy = self.initial_data.get("realm_edit_topic_policy")
+                if edit_topic_policy == 2:
+                    # For admins and owners only (not using user role)
+                    if role <= 200:
+                        return True
+                    else:
+                        self.controller.report_error(EDIT_TOPIC_POLICY[2])
+                        return False
+                elif edit_topic_policy == 4:
+                    # Check for moderators
+                    if role <= 300:
+                        return True
+                    else:
+                        self.controller.report_error(EDIT_TOPIC_POLICY[4])
+                        return False
+                elif edit_topic_policy == 3:
+                    # Check for 'full members'
+                    if role <= 400:
+                        return True
+                    else:
+                        self.controller.report_error(EDIT_TOPIC_POLICY[3])
+                        return False
+                elif edit_topic_policy == 1:
+                    # Check for members
+                    if role <= 400:
+                        return True
+                    else:
+                        self.controller.report_error(EDIT_TOPIC_POLICY[1])
+                        return False
+                else:  # edit_topic_policy == 5 (or None)
+                    # All users including guests
+                    return True
+        self.controller.report_error("User not found")
+        return False
+
     def generate_all_emoji_data(
         self, custom_emoji: Dict[str, RealmEmojiData]
     ) -> Tuple[NamedEmojiData, List[str]]:


Test patch:
diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml
index 8c61fa5..78ef1a4 100644
--- a/.github/workflows/lint-and-test.yml
+++ b/.github/workflows/lint-and-test.yml
@@ -100,6 +100,23 @@ jobs:
       - name: Run generate_hotkeys
         run: ./tools/generate_hotkeys.py --check-only
 
+  docstrings:
+    runs-on: ubuntu-latest
+    name: Lint - Docstrings linting & docs sync check
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          persist-credentials: false
+      - uses: actions/setup-python@v4
+        with:
+          python-version: 3.6
+          cache: 'pip'
+          cache-dependency-path: 'setup.py'
+      - name: Minimal install
+        run: pip install .
+      - name: Run lint-docstring
+        run: ./tools/lint-docstring
+
   pytest:
     strategy:
       # Not failing fast allows all matrix jobs to try & finish even if one fails early
diff --git a/tests/model/test_model.py b/tests/model/test_model.py
index b536eac..45886df 100644
--- a/tests/model/test_model.py
+++ b/tests/model/test_model.py
@@ -966,6 +966,251 @@ class TestModel:
         else:
             report_success.assert_not_called()
 
+    @pytest.mark.parametrize(
+        "response, return_value",
+        [
+            (
+                {
+                    "result": "success",
+                    "messages": [
+                        {
+                            "subject": "hi!",
+                            "stream_id": 1,
+                        }
+                    ],
+                },
+                {
+                    "subject": "hi!",
+                    "stream_id": 1,
+                },
+            ),
+            (
+                {
+                    "result": "success",
+                    "messages": [
+                        {
+                            "subject": "hi!",
+                            "stream_id": 1,
+                        },
+                        {
+                            "subject": "hi!",
+                            "stream_id": 1,
+                        },
+                    ],
+                },
+                None,
+            ),
+            ({"result": "some_failure", "messages": []}, None),
+            ({"result": "success", "messages": []}, None),
+        ],
+    )
+    def test_get_latest_message_in_topic(
+        self,
+        mocker,
+        model,
+        response,
+        return_value,
+        topic_name="hi!",
+        stream_id=1,
+    ):
+        self.client.get_messages = mocker.Mock(return_value=response)
+
+        result = model.get_latest_message_in_topic(stream_id, topic_name)
+
+        assert result == return_value
+        self.display_error_if_present.assert_called_once_with(response, self.controller)
+
+    @pytest.mark.parametrize(
+        "user_role, user_type",
+        [
+            (None, "_"),
+            ({"role": 100}, "_owner"),
+            ({"role": 200}, "_admin"),
+            ({"role": 300}, "_moderator"),
+            ({"role": 400}, "_member"),
+            ({"role": 600}, "_guest"),
+        ],
+    )
+    @pytest.mark.parametrize(
+        "realm_editing_settings, expected_response",
+        [
+            case(
+                {
+                    "allow_message_editing": False,
+                    "allow_community_topic_editing": True,
+                },
+                {
+                    "_": ["User not found", False],
+                    "_owner": ["Editing messages is disabled", False],
+                    "_admin": ["Editing messages is disabled", False],
+                    "_moderator": ["Editing messages is disabled", False],
+                    "_member": ["Editing messages is disabled", False],
+                    "_guest": ["Editing messages is disabled", False],
+                },
+                id="editing_msg_disabled:PreZulip4.0",
+            ),
+            case(
+                {
+                    "allow_message_editing": True,
+                    "allow_community_topic_editing": False,
+                },
+                {
+                    "_": ["User not found", False],
+                    "_owner": ["", True],
+                    "_admin": ["", True],
+                    "_moderator": ["Editing topic is disabled", False],
+                    "_member": ["Editing topic is disabled", False],
+                    "_guest": ["Editing topic is disabled", False],
+                },
+                id="editing_topic_disabled:PreZulip4.0",
+            ),
+            case(
+                {
+                    "allow_message_editing": True,
+                    "allow_community_topic_editing": True,
+                },
+                {
+                    "_": ["User not found", False],
+                    "_owner": ["", True],
+                    "_admin": ["", True],
+                    "_moderator": ["", True],
+                    "_member": ["", True],
+                    "_guest": ["", True],
+                },
+                id="editing_topic_and_msg_enabled:PreZulip4.0",
+            ),
+            case(
+                {"allow_message_editing": False, "edit_topic_policy": 1},
+                {
+                    "_": ["User not found", False],
+                    "_owner": ["Editing messages is disabled", False],
+                    "_admin": ["Editing messages is disabled", False],
+                    "_moderator": ["Editing messages is disabled", False],
+                    "_member": ["Editing messages is disabled", False],
+                    "_guest": ["Editing messages is disabled", False],
+                },
+                id="all_but_no_editing:Zulip4.0+:ZFL75",
+            ),
+            case(
+                {"allow_message_editing": True, "edit_topic_policy": 1},
+                {
+                    "_": ["User not found", False],
+                    "_owner": ["", True],
+                    "_admin": ["", True],
+                    "_moderator": ["", True],
+                    "_member": ["", True],
+                    "_guest": [
+                        "Only organization administrators, moderators, full members and members can edit topic",
+                        False,
+                    ],
+                },
+                id="members:Zulip4.0+:ZFL75",
+            ),
+            case(
+                {"allow_message_editing": True, "edit_topic_policy": 2},
+                {
+                    "_": ["User not found", False],
+                    "_owner": ["", True],
+                    "_admin": ["", True],
+                    "_moderator": [
+                        "Only organization administrators can edit topic",
+                        False,
+                    ],
+                    "_member": [
+                        "Only organization administrators can edit topic",
+                        False,
+                    ],
+                    "_guest": [
+                        "Only organization administrators can edit topic",
+                        False,
+                    ],
+                },
+                id="admins:Zulip4.0+:ZFL75",
+            ),
+            case(
+                {"allow_message_editing": True, "edit_topic_policy": 3},
+                {
+                    "_": ["User not found", False],
+                    "_owner": ["", True],
+                    "_admin": ["", True],
+                    "_moderator": ["", True],
+                    "_member": ["", True],
+                    "_guest": [
+                        "Only organization administrators, moderators and full members can edit topic",
+                        False,
+                    ],
+                },
+                id="full_members:Zulip4.0+:ZFL75",
+            ),
+            case(
+                {"allow_message_editing": True, "edit_topic_policy": 4},
+                {
+                    "_": ["User not found", False],
+                    "_owner": ["", True],
+                    "_admin": ["", True],
+                    "_moderator": ["", True],
+                    "_member": [
+                        "Only organization administrators and moderators can edit topic",
+                        False,
+                    ],
+                    "_guest": [
+                        "Only organization administrators and moderators can edit topic",
+                        False,
+                    ],
+                },
+                id="moderators:Zulip4.0+:ZFL75",
+            ),
+            case(
+                {"allow_message_editing": True, "edit_topic_policy": 5},
+                {
+                    "_": ["User not found", False],
+                    "_owner": ["", True],
+                    "_admin": ["", True],
+                    "_moderator": ["", True],
+                    "_member": ["", True],
+                    "_guest": ["", True],
+                },
+                id="guests:Zulip4.0+:ZFL75",
+            ),
+        ],
+    )
+    def test_can_user_edit_topic(
+        self,
+        mocker,
+        model,
+        initial_data,
+        user_role,
+        user_type,
+        realm_editing_settings,
+        expected_response,
+    ):
+        model.get_user_info = mocker.Mock(return_value=user_role)
+        model.initial_data = initial_data
+        initial_data["realm_allow_message_editing"] = realm_editing_settings[
+            "allow_message_editing"
+        ]
+        allow_community_topic_editing = realm_editing_settings.get(
+            "allow_community_topic_editing", None
+        )
+        edit_topic_policy = realm_editing_settings.get("edit_topic_policy", None)
+        if allow_community_topic_editing is None:
+            assert edit_topic_policy is not None
+            initial_data["realm_edit_topic_policy"] = edit_topic_policy
+        else:
+            assert edit_topic_policy is None
+            initial_data[
+                "realm_allow_community_topic_editing"
+            ] = allow_community_topic_editing
+        report_error = model.controller.report_error
+
+        result = model.can_user_edit_topic()
+
+        assert result == expected_response[user_type][1]
+        if result:
+            report_error.assert_not_called()
+        else:
+            report_error.assert_called_once_with(expected_response[user_type][0])
+
     # NOTE: This tests only getting next-unread, not a fixed anchor
     def test_success_get_messages(
         self,