From 93e937983e9d0283233d54b8268935d09c6921fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez=20Mondrag=C3=B3n?= <16805946+edgarrmondragon@users.noreply.github.com> Date: Wed, 15 Nov 2023 13:27:49 -0600 Subject: [PATCH] feat: Support `userActivationSettings` parameter in RPC method `activate_survey` (#1026) * feat: Support `userActivationSettings` parameter in RPC method `activate_survey` * XFail for `develop` branch --- .../unreleased/Added-20231028-222932.yaml | 2 +- .../unreleased/Added-20231109-165350.yaml | 5 ++ .../unreleased/Fixed-20231109-165446.yaml | 5 ++ .flake8 | 2 +- .pre-commit-config.yaml | 6 +-- README.md | 8 ++-- poetry.lock | 15 +++--- pyproject.toml | 7 +-- src/citric/client.py | 19 ++++++-- src/citric/exceptions.py | 13 ++--- src/citric/method.py | 8 +++- src/citric/session.py | 1 - src/citric/types.py | 23 ++++++++- tests/test_client.py | 10 ++-- tests/test_integration.py | 47 ++++++++++++++++++- 15 files changed, 131 insertions(+), 40 deletions(-) create mode 100644 .changes/unreleased/Added-20231109-165350.yaml create mode 100644 .changes/unreleased/Fixed-20231109-165446.yaml diff --git a/.changes/unreleased/Added-20231028-222932.yaml b/.changes/unreleased/Added-20231028-222932.yaml index a6cf7dd6..24a5b41c 100644 --- a/.changes/unreleased/Added-20231028-222932.yaml +++ b/.changes/unreleased/Added-20231028-222932.yaml @@ -1,5 +1,5 @@ kind: Added -body: Support `DestSurveyID` parameter to RPC method `copy_survey` +body: Support `DestSurveyID` parameter in RPC method `copy_survey` time: 2023-10-28T22:29:32.491728-06:00 custom: Issue: "1016" diff --git a/.changes/unreleased/Added-20231109-165350.yaml b/.changes/unreleased/Added-20231109-165350.yaml new file mode 100644 index 00000000..9d7a453b --- /dev/null +++ b/.changes/unreleased/Added-20231109-165350.yaml @@ -0,0 +1,5 @@ +kind: Added +body: Support `userActivationSettings` parameter in RPC method `activate_survey` +time: 2023-11-09T16:53:50.378464-06:00 +custom: + Issue: "1026" diff --git a/.changes/unreleased/Fixed-20231109-165446.yaml b/.changes/unreleased/Fixed-20231109-165446.yaml new file mode 100644 index 00000000..f626a696 --- /dev/null +++ b/.changes/unreleased/Fixed-20231109-165446.yaml @@ -0,0 +1,5 @@ +kind: Fixed +body: Update `types.YesNo` to include inherited (`I`) values +time: 2023-11-09T16:54:46.179939-06:00 +custom: + Issue: "1026" diff --git a/.flake8 b/.flake8 index e62457cb..8da706a7 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,5 @@ [flake8] -select = DAR +select = DOC docstring_style=google max-line-length = 88 strictness = short diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0d61f7e4..901ec14b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,10 +47,10 @@ repos: hooks: - id: flake8 additional_dependencies: - - darglint==1.8.1 + - pydoclint==0.3.8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.6.1 + rev: v1.7.0 hooks: - id: mypy additional_dependencies: @@ -66,7 +66,7 @@ repos: - id: validate_manifest - repo: https://github.com/hadialqattan/pycln - rev: v2.3.0 + rev: v2.4.0 hooks: - id: pycln args: [--all] diff --git a/README.md b/README.md index abec4081..136dab1d 100644 --- a/README.md +++ b/README.md @@ -61,12 +61,12 @@ Python. | | **PostgreSQL** | **MySQL** | | - |:--: | :-: | +| 6.3.4 | ✅ | ✅ | +| 6.3.3 | ✅ | ✅ | | 6.3.1 | ✅ | ✅ | -| 6.3.0 | ✅ | ✅ | -| 6.2.11 | ✅ | ✅ | +| 5.6.44 | ✅ | ✅ | +| 5.6.43 | ✅ | ✅ | | 5.6.42 | ✅ | ✅ | -| 5.6.41 | ✅ | ✅ | -| 5.6.40 | ✅ | ✅ | ## Installation diff --git a/poetry.lock b/poetry.lock index 505e086e..d3ffb685 100644 --- a/poetry.lock +++ b/poetry.lock @@ -362,13 +362,13 @@ test = ["pytest (>=6)"] [[package]] name = "faker" -version = "20.0.0" +version = "20.0.3" description = "Faker is a Python package that generates fake data for you." optional = false python-versions = ">=3.8" files = [ - {file = "Faker-20.0.0-py3-none-any.whl", hash = "sha256:171b27ba106cf69e30a91ac471407c2362bd6af27738e2461dc441aeff5eed91"}, - {file = "Faker-20.0.0.tar.gz", hash = "sha256:df44b68b9d231e784f4bfe616d781576cfef9f0c5d9a17671bf84dc10d7b44d6"}, + {file = "Faker-20.0.3-py3-none-any.whl", hash = "sha256:88316cfa7c8be892433bb10b2f1c2a7ce97246e18712680547e2fb1c4bd03912"}, + {file = "Faker-20.0.3.tar.gz", hash = "sha256:f9af61c9223e1a3fd01ee2a48265352432f40a4fb21feb274d9d1d97b4943d75"}, ] [package.dependencies] @@ -1454,18 +1454,17 @@ six = "*" [[package]] name = "urllib3" -version = "2.0.7" +version = "2.1.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, - {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, + {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"}, + {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"}, ] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] diff --git a/pyproject.toml b/pyproject.toml index ec3f8722..6c35b90f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,11 +83,12 @@ target-version = "py38" [tool.ruff.lint] explicit-preview-rules = false ignore = [ - "ANN101", # missing-type-self - "DJ", # flake8-django - "FIX002", # line-contains-todo + "ANN101", # missing-type-self + "DJ", # flake8-django + "FIX002", # line-contains-todo "COM812", # missing-trailing-comma "ISC001", # single-line-implicit-string-concatenation + "D107", # undocumented-public-init ] preview = true select = [ diff --git a/src/citric/client.py b/src/citric/client.py index 2fe107e1..7ee213b5 100644 --- a/src/citric/client.py +++ b/src/citric/client.py @@ -123,7 +123,6 @@ def __init__( requests_session: requests.Session | None = None, auth_plugin: str = "Authdb", ) -> None: - """Create a LimeSurvey Python API client.""" self.__session = self.session_class( url, username, @@ -169,20 +168,34 @@ def get_fieldmap(self, survey_id: int) -> dict[str, t.Any]: """ return self.session.get_fieldmap(survey_id) - def activate_survey(self, survey_id: int) -> types.OperationStatus: + def activate_survey( + self, + survey_id: int, + *, + user_activation_settings: types.SurveyUserActivationSettings | None = None, + ) -> types.OperationStatus: """Activate a survey. Calls :rpc_method:`activate_survey`. Args: survey_id: ID of survey to be activated. + user_activation_settings: Optional user activation settings. Returns: Status and plugin feedback. .. versionadded:: 0.0.1 """ - return self.session.activate_survey(survey_id) + activation_settings = ( + { + key: "Y" if value else "N" + for key, value in user_activation_settings.items() + } + if user_activation_settings + else None + ) + return self.session.activate_survey(survey_id, activation_settings) def activate_tokens( self, diff --git a/src/citric/exceptions.py b/src/citric/exceptions.py index 132b9757..5c8621c1 100644 --- a/src/citric/exceptions.py +++ b/src/citric/exceptions.py @@ -8,14 +8,13 @@ class ResponseMismatchError(Exception): class LimeSurveyError(Exception): - """Basic exception raised by LimeSurvey.""" + """Basic exception raised by LimeSurvey. - def __init__(self, message: str) -> None: - """Create a generic error for the LimeSurvey RPC API. + Args: + message: Exception message. By default none, and a generic message is used. + """ - Args: - message: Exception message. By default none, and a generic message is used. - """ + def __init__(self, message: str) -> None: super().__init__(message) @@ -31,7 +30,6 @@ class RPCInterfaceNotEnabledError(LimeSurveyError): """RPC interface not enabled on LimeSurvey.""" def __init__(self) -> None: - """Create a new exception.""" super().__init__("RPC interface not enabled") @@ -39,7 +37,6 @@ class InvalidJSONResponseError(LimeSurveyError): """RPC interface maybe not enabled on LimeSurvey.""" def __init__(self) -> None: - """Create a new exception.""" msg = ( "Received a non-JSON response, verify that the JSON RPC interface is " "enabled in global settings" diff --git a/src/citric/method.py b/src/citric/method.py index f7c40d3f..67dca9ee 100644 --- a/src/citric/method.py +++ b/src/citric/method.py @@ -10,10 +10,14 @@ class Method(t.Generic[T]): - """RPC method.""" + """RPC method. + + Args: + caller: RPC caller function. + name: RPC method name. + """ def __init__(self, caller: t.Callable[[str], T], name: str) -> None: - """Instantiate an RPC method.""" self.__caller = caller self.__name = name diff --git a/src/citric/session.py b/src/citric/session.py index 4f076d58..3a410e1d 100644 --- a/src/citric/session.py +++ b/src/citric/session.py @@ -106,7 +106,6 @@ def __init__( requests_session: requests.Session | None = None, json_encoder: type[json.JSONEncoder] | None = None, ) -> None: - """Create a LimeSurvey RPC session.""" self.url = url self._session = requests_session or requests.session() self._session.headers["User-Agent"] = self.USER_AGENT diff --git a/src/citric/types.py b/src/citric/types.py index aac1080b..67c0930e 100644 --- a/src/citric/types.py +++ b/src/citric/types.py @@ -27,11 +27,12 @@ "RPCResponse", "SetQuotaPropertiesResult", "SurveyProperties", + "SurveyUserActivationSettings", "CPDBParticipantImportResult", ] Result: TypeAlias = t.Any -YesNo: TypeAlias = t.Literal["Y", "N"] +YesNo: TypeAlias = t.Literal["Y", "N", "I"] class FileUploadResult(t.TypedDict): @@ -495,3 +496,23 @@ class CPDBParticipantImportResult(t.TypedDict): ImportCount: int UpdateCount: int + + +class SurveyUserActivationSettings(t.TypedDict, total=False): + """User settings for survey activation. + + Keys: + anonymized: Whether the survey is anonymized. + datestamp: Whether the survey records dates. + ipaddr: Whether the survey records IP addresses. + ipanonymize: Whether the survey anonymizes IP addresses. + refurl: Whether the survey records referrer URLs. + savetimings: Whether the survey saves response timings. + """ + + anonymized: bool + datestamp: bool + ipaddr: bool + ipanonymize: bool + refurl: bool + savetimings: bool diff --git a/tests/test_client.py b/tests/test_client.py index 5e87f944..9167d4da 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -44,9 +44,6 @@ class MockSession(Session): "restrictToLanguages": "en fr es", } - def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: - """Create a mock session.""" - def rpc(self, method: str, *params: t.Any) -> dict[str, t.Any]: """Process a mock RPC call.""" return {"method": method, "params": [*params]} @@ -197,7 +194,12 @@ def client() -> t.Generator[Client, None, None]: def test_activate_survey(client: MockClient): """Test activate_survey client method.""" - assert_client_session_call(client, "activate_survey", 1) + assert_client_session_call( + client, + "activate_survey", + 1, + user_activation_settings=None, + ) def test_activate_tokens(client: MockClient): diff --git a/tests/test_integration.py b/tests/test_integration.py index 23a4a883..e7e43db0 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -197,7 +197,7 @@ def test_copy_survey_destination_id( pytest.mark.xfail( server_version < semver.VersionInfo.parse("6.4.0-dev"), reason=( - "The destination_survey_id parameter is only supported in LimeSurvey " + "The destination_survey_id parameter is not supported in LimeSurvey " f"{server_version} < 6.4.0" ), ), @@ -348,6 +348,51 @@ def test_activate_survey(client: citric.Client, survey_id: int): assert properties_after["active"] == "Y" +@pytest.mark.integration_test +def test_activate_survey_with_settings( + request: pytest.FixtureRequest, + client: citric.Client, + server_version: semver.VersionInfo, + survey_id: int, +): + """Test whether the survey gets activated with the requested settings.""" + min_version = (5, 6, 45) if server_version < (6, 0) else (6, 3, 5) + request.applymarker( + pytest.mark.xfail( + server_version < min_version or server_version.prerelease is not None, + reason=( + "The user_activation_settings parameter is not supported in LimeSurvey " + f"{server_version} < {'.'.join(str(v) for v in min_version)}" + ), + ), + ) + + properties_before = client.get_survey_properties( + survey_id, + ["active", "anonymized", "ipaddr"], + ) + assert properties_before["active"] == "N" + assert properties_before["anonymized"] == "N" + assert properties_before["ipaddr"] == "I" + + result = client.activate_survey( + survey_id, + user_activation_settings={ + "anonymized": True, + "ipaddr": False, + }, + ) + assert result["status"] == "OK" + + properties_after = client.get_survey_properties( + survey_id, + ["active", "anonymized", "ipaddr"], + ) + assert properties_after["active"] == "Y" + assert properties_after["anonymized"] == "Y" + assert properties_after["ipaddr"] == "N" + + @pytest.mark.integration_test def test_activate_tokens(client: citric.Client, survey_id: int): """Test whether the participants table gets activated."""