Skip to content

Commit

Permalink
feat: Add mechanism to ask user to fill survey (#1125)
Browse files Browse the repository at this point in the history
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced a survey mechanism in PartSeg 0.15.3 to allow users to
participate in surveys supporting PartSeg development.
  - Added documentation for the new survey mechanism.

- **Documentation**
- Updated documentation to include details about the new survey feature
in the incomplete documentation section.
- Included `graphviz` in the build configuration for documentation
generation.

- **Chores**
- Updated the manifest file to exclude `survey_url.txt` during package
distribution.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
  • Loading branch information
Czaki authored Jul 9, 2024
1 parent b56a34c commit cc9d57b
Show file tree
Hide file tree
Showing 9 changed files with 209 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .readthedocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ build:
os: ubuntu-22.04
tools:
python: "3.11"
apt_packages:
- graphviz


# Optionally set the version of Python and requirements required to build your docs
Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,4 @@ exclude dist/**.swp
exclude cliff.toml
exclude runtime.txt
exclude .sourcery.yaml
exclude survey_url.txt
Binary file added docs/images/survey.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ The documentation is incomplete. Many utilities are undocumented.
interface-overview/interface-overview
state_store
error_reporting
survey_mechanism


Indices and tables
Expand Down
47 changes: 47 additions & 0 deletions docs/survey_mechanism.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
PartSeg surveys
===============

Since the partseg 0.15.3 release there is a new feature that allows
to ask users for fill a survey to help in the PartSeg development.

How it works
------------

Whole logic is implemented in ``PartSeg._launcher.check_survey`` module.

.. image:: images/survey.png
:width: 600
:alt: Survey message

1. Check if ignore file exist and is last modified less than 16 hours ago. If yes, do nothing.
2. Fetch data from https://raw.githubusercontent.com/4DNucleome/PartSeg/develop/survey_url.txt.
If file do not exists or is empty, do nothing.
Save fetched data as url to form.
3. Check if ignore file exist and its content is equal to fetched url. If yes, do nothing.
4. Display message to user with information that there is a survey available.

* If user click "Open survey" button, open browser with fetched url.
* If user click "Ignore" button, save fetched url to ignore file.
* If user click "Close" button, touch the ignore file to prevent showing the message again for 16 hours.


.. graphviz::

digraph flow {
"start" -> "check if already asked"

"check if already asked" -> "fetch if exist active survey" [label=No];
"check if already asked" -> "end" [label=Yes];
"fetch if exist active survey" -> "end" [label=No];
"fetch if exist active survey" -> "check if user decided to ignore this survey" [label=Yes]
"check if user decided to ignore this survey" -> "end" [label=Yes]
"check if user decided to ignore this survey" -> "show dialog with question" [label=No]
"show dialog with question" -> "end" [label=Close]
"show dialog with question" -> "open browser" [label="Open survey"]
"show dialog with question" -> "Save to not ask about this survey" [label="Ignore"]
"open browser" -> "end"
"Save to not ask about this survey" -> "end"
start [shape=Mdiamond];

end [shape=Msquare];
}
80 changes: 80 additions & 0 deletions package/PartSeg/_launcher/check_survey.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import os
import time
import urllib.error
import urllib.request
import webbrowser
from contextlib import suppress
from pathlib import Path

from qtpy.QtCore import Qt, QThread
from qtpy.QtWidgets import QMessageBox, QWidget
from superqt import ensure_main_thread

from PartSeg import state_store

IGNORE_DAYS = 21
IGNORE_FILE = "ignore_survey.txt"

IGNORE_FILE_PATH = Path(state_store.save_folder) / ".." / IGNORE_FILE


class SurveyMessageBox(QMessageBox):
def __init__(self, survey_url: str, parent: QWidget = None):
super().__init__(parent)
self.setWindowTitle("PartSeg survey")
self.setIcon(QMessageBox.Icon.Information)
self.setTextFormat(Qt.TextFormat.RichText)
self.setText(
f'Please fill the survey <a href="{survey_url}">{survey_url}</a> to <b>help us</b> improve PartSeg'
)
self._open_btn = self.addButton("Open survey", QMessageBox.ButtonRole.AcceptRole)
self._close_btn = self.addButton("Close", QMessageBox.ButtonRole.RejectRole)
self._ignore_btn = self.addButton("Ignore", QMessageBox.ButtonRole.DestructiveRole)
self.setEscapeButton(self._ignore_btn)
self.survey_url = survey_url

self._open_btn.clicked.connect(self._open_survey)
self._ignore_btn.clicked.connect(self._ignore_survey)

def _open_survey(self):
webbrowser.open(self.survey_url)

def _ignore_survey(self):
with IGNORE_FILE_PATH.open("w") as f_p:
f_p.write(self.survey_url)

def exec_(self):
result = super().exec_()
IGNORE_FILE_PATH.touch()
return result


class CheckSurveyThread(QThread):
"""Thread to check if there is new PartSeg release. Checks base on newest version available on pypi_
.. _PYPI: https://pypi.org/project/PartSeg/
"""

def __init__(self, survey_file_url="https://raw.githubusercontent.com/4DNucleome/PartSeg/develop/survey_url.txt"):
super().__init__()
self.survey_file_url = survey_file_url
self.survey_url = ""
self.finished.connect(self.show_version_info)

def run(self):
"""This function perform check if there is any active survey."""
if IGNORE_FILE_PATH.exists() and (time.time() - os.path.getmtime(IGNORE_FILE_PATH)) < 60 * 60 * 16:
return
with suppress(urllib.error.URLError), urllib.request.urlopen(self.survey_file_url) as r: # nosec # noqa: S310
self.survey_url = r.read().decode("utf-8").strip()

@ensure_main_thread
def show_version_info(self):
if os.path.exists(IGNORE_FILE_PATH):
with open(IGNORE_FILE_PATH) as f_p:
old_survey_link = f_p.read().strip()
if old_survey_link == self.survey_url:
return

if self.survey_url:
SurveyMessageBox(self.survey_url).exec_()
4 changes: 4 additions & 0 deletions package/PartSeg/launcher_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from contextlib import suppress
from functools import partial

from PartSeg._launcher.check_survey import CheckSurveyThread

multiprocessing.freeze_support()


Expand Down Expand Up @@ -127,6 +129,8 @@ def main(): # pragma: no cover
my_app.aboutToQuit.connect(wait_for_workers_to_quit)
check_version = CheckVersionThread()
check_version.start()
check_survey = CheckSurveyThread()
check_survey.start()
wind.show()
rc = my_app.exec_()
del wind # skipcq: PTC-W0043`
Expand Down
73 changes: 73 additions & 0 deletions package/tests/test_PartSeg/test_check_survey.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from io import BytesIO
from unittest.mock import MagicMock, Mock

import pytest

from PartSeg._launcher.check_survey import CheckSurveyThread, SurveyMessageBox


@pytest.fixture(autouse=True)
def _mock_file_path(monkeypatch, tmp_path):
monkeypatch.setattr("PartSeg._launcher.check_survey.IGNORE_FILE_PATH", tmp_path / "file1.txt")


@pytest.mark.usefixtures("qtbot")
def test_check_survey_thread(monkeypatch, tmp_path):
urlopen_mock = Mock(return_value=BytesIO(b"some data"))
monkeypatch.setattr("urllib.request.urlopen", urlopen_mock)
thr = CheckSurveyThread("test_url")

thr.run()
assert urlopen_mock.call_count == 1
assert thr.survey_url == "some data"

(tmp_path / "file1.txt").touch()
thr2 = CheckSurveyThread("test_url")
thr2.run()

assert urlopen_mock.call_count == 1
assert thr2.survey_url == ""


@pytest.mark.usefixtures("qtbot")
def test_thread_ignore_file_exist(monkeypatch, tmp_path):
message_mock = Mock()
monkeypatch.setattr("PartSeg._launcher.check_survey.SurveyMessageBox", message_mock)
with (tmp_path / "file1.txt").open("w") as f_p:
f_p.write("test_url")

thr = CheckSurveyThread("test_url2")
thr.survey_url = "test_url"

thr.show_version_info()
assert message_mock.call_count == 0


@pytest.mark.usefixtures("qtbot")
def test_call_survey_message_box(monkeypatch):
message_mock = MagicMock()
monkeypatch.setattr("PartSeg._launcher.check_survey.SurveyMessageBox", message_mock)
thr = CheckSurveyThread("test_url")
thr.survey_url = "test_url"
thr.show_version_info()
assert message_mock.call_count == 1


def test_survey_message_box(qtbot, monkeypatch, tmp_path):
web_open = Mock()
monkeypatch.setattr("PartSeg._launcher.check_survey.webbrowser.open", web_open)
monkeypatch.setattr("PartSeg._launcher.check_survey.QMessageBox.exec_", Mock(return_value=0))
msg = SurveyMessageBox("test_url")
qtbot.addWidget(msg)

msg._open_btn.click()
web_open.assert_called_once_with("test_url")

msg.exec_()
assert (tmp_path / "file1.txt").exists()
assert (tmp_path / "file1.txt").read_text() == ""

msg._ignore_btn.click()

assert (tmp_path / "file1.txt").exists()
assert (tmp_path / "file1.txt").read_text() == "test_url"
1 change: 1 addition & 0 deletions survey_url.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
https://forms.gle/CCnS7ccyPQUfKRgCA

0 comments on commit cc9d57b

Please sign in to comment.