Compare commits

..

No commits in common. "master" and "1.0.0" have entirely different histories.

67 changed files with 188 additions and 1460 deletions

View file

@ -9,7 +9,7 @@ This run with the [PyQt6](https://www.riverbankcomputing.com/software/pyqt/) and
To install the projet, you will need to install :
- Python >= 3.11 (preferably in a virtual environment with `python3 -m venv ./.venv/`)
- Python >= 3.11 (preferably in a virtual environment)
- Python dependencies (`python3 -m pip install -r ./requirements.txt`)
Or download one of the build in the [releases page](https://github.com/Faraphel/M1-Recherche/releases).

Binary file not shown.

View file

@ -1,43 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="en">
<context>
<name>LanguageSelection</name>
<message>
<location filename="..\..\source\ui\LanguageSelection.py" line="69" />
<source>SELECT YOUR LANGUAGE</source>
<translation>Select your language</translation>
</message>
<message>
<location filename="..\..\source\ui\LanguageSelection.py" line="70" />
<source>START</source>
<translation>Start</translation>
</message>
</context>
<context>
<context>
<name>SurveyEngine</name>
<message>
<location filename="..\..\source\ui\SurveyEngine.py" line="177" />
<source>WARNING</source>
<translation>Warning</translation>
<location filename="../../source/widget/SurveyEngine.py" line="177"/>
<source>WARNING</source>
<translation>Warning</translation>
</message>
</context>
<context>
</context>
<context>
<name>SurveyNavigation</name>
<message>
<location filename="..\..\source\ui\SurveyNavigation.py" line="19" />
<source>ABANDON</source>
<translation>Abandon</translation>
<location filename="../../source/widget/SurveyNavigation.py" line="19"/>
<source>ABANDON</source>
<translation>Abandon</translation>
</message>
<message>
<location filename="..\..\source\ui\SurveyNavigation.py" line="29" />
<source>SKIP</source>
<translation>Skip</translation>
<location filename="../../source/widget/SurveyNavigation.py" line="29"/>
<source>SKIP</source>
<translation>Skip</translation>
</message>
<message>
<location filename="..\..\source\ui\SurveyNavigation.py" line="38" />
<source>NEXT</source>
<translation>Next</translation>
<location filename="../../source/widget/SurveyNavigation.py" line="38"/>
<source>NEXT</source>
<translation>Next</translation>
</message>
</context>
</context>
<context>
<name>SurveyWindow</name>
<message>
<location filename="../../source/widget/SurveyWindow.py" line="16"/>
<source>SURVEY</source>
<translation>Survey</translation>
</message>
</context>
</TS>

Binary file not shown.

View file

@ -1,43 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="es">
<context>
<name>LanguageSelection</name>
<message>
<location filename="..\..\source\ui\LanguageSelection.py" line="69" />
<source>SELECT YOUR LANGUAGE</source>
<translation>Seleccione su idioma</translation>
</message>
<message>
<location filename="..\..\source\ui\LanguageSelection.py" line="70" />
<source>START</source>
<translation>Comenzar</translation>
</message>
</context>
<context>
<name>SurveyEngine</name>
<message>
<location filename="..\..\source\ui\SurveyEngine.py" line="177" />
<source>WARNING</source>
<translation>Atención</translation>
</message>
</context>
<context>
<name>SurveyNavigation</name>
<message>
<location filename="..\..\source\ui\SurveyNavigation.py" line="19" />
<source>ABANDON</source>
<translation>Abandonar</translation>
</message>
<message>
<location filename="..\..\source\ui\SurveyNavigation.py" line="29" />
<source>SKIP</source>
<translation>Pasar</translation>
</message>
<message>
<location filename="..\..\source\ui\SurveyNavigation.py" line="38" />
<source>NEXT</source>
<translation>Siguiente</translation>
</message>
</context>
</TS>

Binary file not shown.

View file

@ -1,43 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="fr_FR">
<context>
<name>LanguageSelection</name>
<message>
<location filename="..\..\source\ui\LanguageSelection.py" line="69" />
<source>SELECT YOUR LANGUAGE</source>
<translation>Sélectionner votre langue</translation>
</message>
<message>
<location filename="..\..\source\ui\LanguageSelection.py" line="70" />
<source>START</source>
<translation>Démarrer</translation>
</message>
</context>
<context>
<context>
<name>SurveyEngine</name>
<message>
<location filename="..\..\source\ui\SurveyEngine.py" line="177" />
<source>WARNING</source>
<translation>Attention</translation>
<location filename="../../source/widget/SurveyEngine.py" line="177"/>
<source>WARNING</source>
<translation>Avertissement</translation>
</message>
</context>
<context>
</context>
<context>
<name>SurveyNavigation</name>
<message>
<location filename="..\..\source\ui\SurveyNavigation.py" line="19" />
<source>ABANDON</source>
<translation>Abandonner</translation>
<location filename="../../source/widget/SurveyNavigation.py" line="19"/>
<source>ABANDON</source>
<translation>Abandonner</translation>
</message>
<message>
<location filename="..\..\source\ui\SurveyNavigation.py" line="29" />
<source>SKIP</source>
<translation>Passer</translation>
<location filename="../../source/widget/SurveyNavigation.py" line="29"/>
<source>SKIP</source>
<translation>Passer</translation>
</message>
<message>
<location filename="..\..\source\ui\SurveyNavigation.py" line="38" />
<source>NEXT</source>
<translation>Suivant</translation>
<location filename="../../source/widget/SurveyNavigation.py" line="38"/>
<source>NEXT</source>
<translation>Suivant</translation>
</message>
</context>
</context>
<context>
<name>SurveyWindow</name>
<message>
<location filename="../../source/widget/SurveyWindow.py" line="16"/>
<source>SURVEY</source>
<translation>Sondage</translation>
</message>
</context>
</TS>

BIN
assets/language/sp.qm Normal file

Binary file not shown.

38
assets/language/sp.ts Normal file
View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="es">
<context>
<name>SurveyEngine</name>
<message>
<location filename="../../source/widget/SurveyEngine.py" line="177"/>
<source>WARNING</source>
<translation>Atención</translation>
</message>
</context>
<context>
<name>SurveyNavigation</name>
<message>
<location filename="../../source/widget/SurveyNavigation.py" line="19"/>
<source>ABANDON</source>
<translation>Abandonar</translation>
</message>
<message>
<location filename="../../source/widget/SurveyNavigation.py" line="29"/>
<source>SKIP</source>
<translation>Pasar</translation>
</message>
<message>
<location filename="../../source/widget/SurveyNavigation.py" line="38"/>
<source>NEXT</source>
<translation>Siguiente</translation>
</message>
</context>
<context>
<name>SurveyWindow</name>
<message>
<location filename="../../source/widget/SurveyWindow.py" line="16"/>
<source>SURVEY</source>
<translation>Encuesta</translation>
</message>
</context>
</TS>

17
main.py
View file

@ -1,14 +1,29 @@
import sys
from PyQt6.QtCore import QTranslator, QLocale
from PyQt6.QtWidgets import QApplication
from source.ui import SurveyWindow
from source import translate
from source import assets_path
from source.widget import SurveyWindow
if __name__ == "__main__":
# create the application
application = QApplication(sys.argv)
# get the user language
local = QLocale()
language_code: str = local.languageToCode(local.language(), QLocale.LanguageCodeType.ISO639)
# apply the language on the translator
translate.set_language(language_code)
# load the translator to support multiple languages
translator = QTranslator()
application.installTranslator(translator)
translator.load(str(assets_path / f"language/{language_code}.qm"))
# create the window
window = SurveyWindow("./surveys.json")
window.show()

View file

@ -4,6 +4,4 @@ PyQt6-WebEngine
nextcord~=2.6.0
requests~=2.31.0
cx_freeze
matplotlib~=3.8.2
numpy~=1.26.3
cx_freeze

View file

@ -22,25 +22,16 @@ setup(
"build_exe": {
"include_msvcr": True,
"include_files": [
("./tools/", "./tools/"),
("./assets/", "./assets/"),
("./README.md", "./README.md"),
]
}
},
executables=[
Executable(
"main.py",
base=base,
target_name=__appname__,
icon=__icon_ico__
),
Executable(
"tools/web_replay/main.py",
base=base,
target_name="tools/web_replay/main",
icon=__icon_ico__
),
]
executables=[Executable(
"main.py",
base=base,
target_name=__appname__,
icon=__icon_ico__
)]
)

View file

@ -3,7 +3,7 @@ from pathlib import Path
assets_path = Path("./assets/")
__appname__ = "Survey Engine"
__version__ = (1,0,2)
__version__ = (1,0,0)
__str_version__ = ".".join(map(str, __version__))
__author__ = "Faraphel"
__mail__ = "rc60650@hotmail.com"

View file

@ -5,7 +5,6 @@ from typing import Optional
import nextcord
import requests
from PyQt6.QtCore import pyqtSignal
from PyQt6.QtWidgets import QApplication
result_path = Path("./results/")
@ -60,7 +59,6 @@ def upload_discord(
except Exception as exc:
if signal_warning is not None:
application = QApplication.instance()
signal_warning.emit(application.tr("COULD NOT UPLOAD THE DATA")) # NOQA: emit exist
signal_warning.emit("COULD NOT UPLOAD THE DATA") # NOQA: emit exist
else:
raise exc

View file

@ -4,7 +4,7 @@ from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QFont
from PyQt6.QtWidgets import QVBoxLayout, QLabel, QSpinBox
from source import translate, ui
from source import translate, widget
from source.survey.base import BaseSurvey
@ -47,7 +47,7 @@ class IntegerQuestion(BaseSurvey):
self._layout.addWidget(self.entry_response)
# navigation
self.navigation = ui.SurveyNavigation(signals=signals)
self.navigation = widget.SurveyNavigation(signals=signals)
self._layout.addWidget(self.navigation)
self.navigation.show_forward()

View file

@ -4,7 +4,7 @@ from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QFont
from PyQt6.QtWidgets import QVBoxLayout, QLabel
from source import translate, ui
from source import translate, widget
from source.survey.base import BaseSurvey
@ -43,7 +43,7 @@ class Text(BaseSurvey):
self.label_description.setAlignment(Qt.AlignmentFlag.AlignCenter)
# navigation
self.navigation = ui.SurveyNavigation(signals=signals)
self.navigation = widget.SurveyNavigation(signals=signals)
self._layout.addWidget(self.navigation)
self.navigation.show_forward() # always show forward

View file

@ -4,7 +4,7 @@ from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QFont
from PyQt6.QtWidgets import QVBoxLayout, QLabel, QTextEdit
from source import translate, ui
from source import translate, widget
from source.survey.base import BaseSurvey
@ -34,7 +34,7 @@ class TextQuestion(BaseSurvey):
self._layout.addWidget(self.entry_response)
# navigation
self.navigation = ui.SurveyNavigation(signals=signals)
self.navigation = widget.SurveyNavigation(signals=signals)
self._layout.addWidget(self.navigation)
self.navigation.show_forward()

View file

@ -5,7 +5,7 @@ from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QUrl, QEvent, QObject, QPointF
from PyQt6.QtGui import QFont, QMouseEvent, QResizeEvent, QKeyEvent
from PyQt6.QtWidgets import QLabel, QVBoxLayout, QSizePolicy
from source import translate, ui
from source import translate, widget
from source.survey.base import BaseSurvey
@ -14,20 +14,13 @@ class WebMission(BaseSurvey):
self,
title: translate.Translatable,
url: str,
initial_js: Optional[str] = None,
start_check_js: Optional[str] = None,
check_js: Optional[str] = None,
check_condition: Optional[str] = None,
skip_time: Optional[float] = None,
signals: dict[str, pyqtSignal] = None
):
super().__init__()
self.initial_js = initial_js if initial_js is not None else ""
self.start_check_js = start_check_js if start_check_js is not None else "true"
self.check_js = check_js if check_js is not None else "true"
self.check_condition = check_condition
self.default_url = url
self.skip_time = skip_time
self.signals = signals if signals is not None else {}
@ -54,38 +47,48 @@ class WebMission(BaseSurvey):
self.label_title.setFont(font_title)
# web page
self.browser = ui.Browser()
self.browser = widget.Browser()
self._layout.addWidget(self.browser)
self.browser.web.focusProxy().installEventFilter(self) # capture the event in eventFilter
self.browser.web.urlChanged.connect(self._on_url_changed) # NOQA: connect exist
self.browser.web.setUrl(QUrl(self.default_url))
self.browser.web.loadFinished.connect(self._initialise_js) # NOQA: connect exist
self.browser.web.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
# setup the timer for the check
self.timer_check = None
if self.check_condition is not None:
self.timer_check = QTimer()
self.timer_check.setInterval(100)
self.timer_check.timeout.connect(self.check) # NOQA: connect exist
# setup the timer for skipping the mission
self.timer_skip = None
if self.skip_time is not None:
self.timer_skip = QTimer()
self.timer_skip.setInterval(self.skip_time * 1000)
self.timer_skip.timeout.connect(self._on_time_skip) # NOQA: connect exist
# setup the events
self.browser.web.page().scrollPositionChanged.connect(self._on_scroll_position_changed)
# navigation
self.navigation = ui.SurveyNavigation(signals=signals)
self.navigation = widget.SurveyNavigation(signals=signals)
self._layout.addWidget(self.navigation)
# initialize the start time
self.start_time = time.time()
# initialise the timers
self.timer_start_check: Optional[QTimer] = None
self.timer_check: Optional[QTimer] = None
self.timer_skip: Optional[QTimer] = None
# check timer
if self.timer_check is not None:
# enable the timer
self.timer_check.start()
else:
self._success() # call directly the success method
# skip timer
if self.skip_time is not None:
self.timer_skip = QTimer()
self.timer_skip.setInterval(self.skip_time * 1000)
self.timer_skip.timeout.connect(self._allow_time_skip) # NOQA: connect exist
if self.timer_skip is not None:
# enable the timer for skipping the question
self.timer_skip.start()
@classmethod
@ -93,11 +96,7 @@ class WebMission(BaseSurvey):
return cls(
title=data["title"],
url=data.get("url"),
initial_js=data.get("initial_js"),
start_check_js=data.get("start_check_js"),
check_js=data.get("check_js"),
check_condition=data.get("check"),
skip_time=data.get("skip_time"),
signals=signals
@ -206,80 +205,30 @@ class WebMission(BaseSurvey):
url=self.browser.web.url().toString()
)
def _allow_time_skip(self):
def _on_time_skip(self):
# when the timer to allow skip have run out
if "skip" in self.signals:
self.navigation.show_skip()
# condition
def _preprocess_check(self, check: str) -> str:
"""
Preprocess a check
"""
check = check.replace("#LANGUAGE_CODE#", translate.get_language())
check = check.replace("#START_TIME#", str(self.start_time))
return check
def _initialise_js(self, ok: bool):
# prevent the event from being call a second time
self.browser.web.loadFinished.disconnect(self._initialise_js)
def callback(result: bool):
self.timer_start_check = QTimer()
self.timer_start_check.setInterval(100)
self.timer_start_check.timeout.connect(self._start_check) # NOQA: connect exist
self.timer_start_check.start()
# run the initial command
self.browser.web.page().runJavaScript(
self._preprocess_check(self.initial_js),
resultCallback=callback
)
def _start_check(self) -> None:
"""
Check if the real checking condition should start
"""
# if the check evaluated to True, enable the normal check
def callback(result: bool):
if result:
# stop this start check timer
self.timer_start_check.stop()
# create a new timer for the normal check
self.timer_check = QTimer()
self.timer_check.setInterval(100)
self.timer_check.timeout.connect(self._check) # NOQA: connect exist
self.timer_check.start()
# run the check
self.browser.web.page().runJavaScript(
self._preprocess_check(self.start_check_js),
resultCallback=callback
)
def _check(self) -> None:
def check(self) -> None:
"""
Check if the checking condition have been completed
"""
# if the check evaluated to True, call the success method
def callback(result: bool):
def check_callback(result: bool):
if result:
# stop this check timer
self.timer_check.stop()
# mark the test as successful
self._success()
# run the check
self.browser.web.page().runJavaScript(
self._preprocess_check(self.check_js),
resultCallback=callback
)
page = self.browser.web.page()
# preprocess the condition
condition = self.check_condition
condition = condition.replace("#LANGUAGE_CODE#", translate.get_language())
# run the condition
page.runJavaScript(condition, resultCallback=check_callback)
# data collection

View file

@ -5,7 +5,7 @@ from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QFont
from PyQt6.QtWidgets import QFrame, QVBoxLayout, QLabel, QButtonGroup, QLineEdit, QAbstractButton
from source import translate, ui
from source import translate, widget
from source.survey.base import BaseSurvey
@ -54,7 +54,7 @@ class BaseChoiceQuestion(BaseSurvey):
self.label_question.setFont(font_title)
# prepare navigation
self.navigation = ui.SurveyNavigation(signals=signals)
self.navigation = widget.SurveyNavigation(signals=signals)
# responses
self.frame_responses = QFrame()
@ -102,10 +102,6 @@ class BaseChoiceQuestion(BaseSurvey):
# add the navigation
self._layout.addWidget(self.navigation)
if not self.are_buttons_exclusive():
# if the buttons are not exclusive, allow to select no options
self.navigation.show_forward()
@classmethod
def from_dict(cls, data: dict[str, Any], signals: dict[str, pyqtSignal]) -> "BaseChoiceQuestion":
return cls(
@ -118,7 +114,7 @@ class BaseChoiceQuestion(BaseSurvey):
collected_data = {
"choices": {
choice_id: {
"checked": choice_data["button"].isChecked(),
"checked": choice_data["button"].isEnabled(),
"details": entry.text() if (entry := choice_data["entry"]) is not None else None
}
for choice_id, choice_data in self.buttons_responses.items()

View file

@ -1,77 +0,0 @@
import typing
from typing import Callable
from PyQt6.QtCore import Qt, QLocale
from PyQt6.QtWidgets import QWidget, QLabel, QVBoxLayout, QComboBox, QPushButton
from source import assets_path, translate, ui
class LanguageSelection(QWidget):
def __init__(self, parent: QWidget, after: Callable):
super().__init__(parent=parent)
self.after = after
# layout
self._layout = QVBoxLayout()
self.setLayout(self._layout)
# title
self.title = QLabel()
self._layout.addWidget(self.title)
self.title.setAlignment(Qt.AlignmentFlag.AlignCenter)
# language selection
self.select_language = QComboBox()
self._layout.addWidget(self.select_language)
language_default = QLocale().language() # get the default locale
for i, language_file in enumerate((assets_path / "language").glob("*.qm")):
# add every translated language to the selection
language_code: str = language_file.stem
language = QLocale.codeToLanguage(language_code, QLocale.LanguageCodeType.ISO639)
self.select_language.addItem(language.name, language_code)
# if the added language is the default one, select it directly
if language == language_default:
self.select_language.setCurrentIndex(i)
self.select_language.currentIndexChanged.connect(self.refresh_language) # NOQA: connect exist
# start button
self.button_start = QPushButton()
self._layout.addWidget(self.button_start)
self.button_start.clicked.connect(self.start) # NOQA: connect exist
# refresh the texts
self.refresh_language()
super().show()
def refresh_language(self):
language_code = self.select_language.currentData()
# load the correct translation in the window
window = typing.cast(ui.SurveyWindow, self.window())
window.translator.load(str(assets_path / f"language/{language_code}.qm"))
# apply the language on the custom translator
translate.set_language(language_code)
# refresh the texts
self.retranslate()
def retranslate(self):
self.title.setText(self.tr("SELECT YOUR LANGUAGE"))
self.button_start.setText(self.tr("START"))
def start(self) -> None:
# call the after event
self.after()
# delete the language selection
self.deleteLater()

View file

@ -1,34 +0,0 @@
from pathlib import Path
from PyQt6.QtCore import QTranslator
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import QMainWindow, QApplication
from source import ui, assets_path, __icon_png__, __appname__
icon_path = assets_path / "icon.png"
class SurveyWindow(QMainWindow):
def __init__(self, survey_path: Path | str):
super().__init__()
self.translator = QTranslator()
QApplication.instance().installTranslator(self.translator)
# window style
self.setWindowIcon(QIcon(__icon_png__))
self.setWindowTitle(__appname__)
# start by asking the user his language
self.language_selection = ui.LanguageSelection(
parent=self,
# after the language is selected, start the survey
after=lambda: self.setCentralWidget(ui.SurveyEngine.from_file(survey_path))
)
self.setCentralWidget(self.language_selection)
def quit(self):
# quit the application by closing and deleting the window
self.window().close()
self.window().deleteLater()

View file

@ -77,7 +77,7 @@ class Browser(QWidget):
# update the progress bar
self.progress.setValue(value)
def _load_finished(self, ok: bool):
def _load_finished(self):
# update the progress bar
self.progress.hide()
# refresh the navigation buttons

View file

@ -8,7 +8,7 @@ from typing import Optional
from PyQt6.QtCore import pyqtSignal
from PyQt6.QtWidgets import QVBoxLayout, QProgressBar, QWidget, QMessageBox
from source import translate, ui, save
from source import translate, widget, save
from source.survey.base import BaseSurvey
from source.survey import Empty, survey_get
from source.utils import compress
@ -93,7 +93,7 @@ class SurveyEngine(QWidget):
def _on_signal_abandon(self):
# on abandon, quit the survey
window = typing.cast(ui.SurveyWindow, self.window())
window = typing.cast(widget.SurveyWindow, self.window())
window.quit()
def _on_signal_skip(self):
@ -166,7 +166,7 @@ class SurveyEngine(QWidget):
)
# finally, close the window
window = typing.cast(ui.SurveyWindow, self.window())
window = typing.cast(widget.SurveyWindow, self.window())
window.quit()
# signals
@ -175,5 +175,5 @@ class SurveyEngine(QWidget):
QMessageBox.warning(
self,
self.tr("WARNING"),
message,
self.tr(message),
)

View file

@ -0,0 +1,23 @@
from pathlib import Path
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import QMainWindow
from source import widget, assets_path, __icon_png__
icon_path = assets_path / "icon.png"
class SurveyWindow(QMainWindow):
def __init__(self, survey_path: Path | str):
super().__init__()
self.setWindowIcon(QIcon(__icon_png__))
self.setWindowTitle(self.tr("SURVEY"))
self.setCentralWidget(widget.SurveyEngine.from_file(survey_path))
def quit(self):
# quit the application by closing and deleting the window
self.window().close()
self.window().deleteLater()

View file

@ -1,5 +1,4 @@
from .Browser import Browser
from .LanguageSelection import LanguageSelection
from .SurveyEngine import SurveyEngine
from .SurveyWindow import SurveyWindow
from .SurveyNavigation import SurveyNavigation

View file

View file

@ -1,21 +0,0 @@
import matplotlib.pyplot as plt
import numpy as np
from tools.statistics import extract, ressource
def analyse(datas: list[dict]) -> plt.Figure:
x = list(map(extract.age.extract, datas))
# prepare plotting
figure: plt.Figure = plt.figure()
axes = figure.add_subplot(1, 1, 1)
axes.set_title("Âge des personnes sondées")
# bar chart
bins = np.arange(min(x), max(x), 1)
axes.hist(x, bins=bins, edgecolor='black')
axes.set_xlabel("Âge")
axes.set_ylabel("Quantité")
return figure

View file

@ -1,42 +0,0 @@
from collections import defaultdict
import matplotlib.pyplot as plt
import numpy as np
from tools.statistics import extract
def analyse(datas: list[dict]) -> plt.Figure:
ages_completion: dict[int, int] = defaultdict(lambda: 0)
ages_count: dict[int, int] = defaultdict(lambda: 0)
for data in datas:
age = extract.age.extract(data)
ages_count[age] += 1
for survey, survey_data in data["surveys"].items():
# only scan survey mission
if not survey.startswith("mission-"):
continue
if extract.mission_completed.extract(data, survey):
ages_completion[age] += 1
x = list(ages_completion.keys())
y = (
np.array(list(ages_completion.values()))
/ np.array(list(ages_count.values()))
)
# prepare plotting
figure: plt.Figure = plt.figure()
axes = figure.add_subplot(1, 1, 1)
axes.set_title("Nombre moyen de mission complété par âge")
# bar chart
bins = np.arange(min(x), max(x), 1)
axes.hist(x, bins, weights=y, edgecolor="black")
axes.set_xlabel("Âge")
axes.set_ylabel("Complétion")
return figure

View file

@ -1,41 +0,0 @@
import matplotlib.pyplot as plt
import numpy as np
from tools.statistics import extract, ressource
def analyse(datas: list[dict]) -> plt.Figure:
experience_completion: dict[str, int] = dict.fromkeys(ressource.experience.choices, 0)
experience_count: dict[str, int] = dict.fromkeys(ressource.experience.choices, 0)
for data in datas:
experience = extract.experience.extract(data)
experience_count[experience] += 1
for survey, survey_data in data["surveys"].items():
# only scan survey mission
if not survey.startswith("mission-"):
continue
if extract.mission_completed.extract(data, survey):
experience_completion[experience] += 1
x = list(experience_completion.keys())
y = (
np.array(list(experience_completion.values()))
/ np.array(list(experience_count.values()))
)
# prepare plotting
figure: plt.Figure = plt.figure()
axes = figure.add_subplot(1, 1, 1)
axes.set_title("Nombre moyen de mission complété par opinion")
# bar chart
axes.bar(x, y, color=ressource.experience.colors, edgecolor='black')
axes.set_xticks(x)
axes.set_xticklabels(ressource.experience.labels)
axes.set_xlabel("Expérience")
axes.set_ylabel("Complétion")
return figure

View file

@ -1,41 +0,0 @@
import matplotlib.pyplot as plt
import numpy as np
from tools.statistics import extract, ressource
def analyse(datas: list[dict]) -> plt.Figure:
languages_completion: dict[str, int] = dict.fromkeys(ressource.language.choices, 0)
languages_count: dict[str, int] = dict.fromkeys(ressource.language.choices, 0)
for data in datas:
language = extract.language.extract(data)
languages_count[language] += 1
for survey, survey_data in data["surveys"].items():
# only scan survey mission
if not survey.startswith("mission-"):
continue
if extract.mission_completed.extract(data, survey):
languages_completion[language] += 1
x = list(languages_completion.keys())
y = (
np.array(list(languages_completion.values()))
/ np.array(list(languages_count.values()))
)
# prepare plotting
figure: plt.Figure = plt.figure()
axes = figure.add_subplot(1, 1, 1)
axes.set_title("Nombre moyen de mission complété par langue")
# bar chart
axes.bar(x, y, color=ressource.language.colors, edgecolor='black')
axes.set_xticks(x)
axes.set_xticklabels(ressource.language.labels)
axes.set_xlabel("Langue")
axes.set_ylabel("Complétion")
return figure

View file

@ -1,36 +0,0 @@
import matplotlib.pyplot as plt
from tools.statistics import extract, ressource
def analyse(datas: list[dict]) -> plt.Figure:
completions: dict[str] = dict.fromkeys(ressource.mission.choices, 0)
# TODO : couleur par mission
for data in datas:
for survey in data["surveys"].keys():
# only scan survey mission
if not survey.startswith("mission-"):
continue
if extract.mission_completed.extract(data, survey):
completions[survey] += 1
x = list(completions.keys())
y = list(completions.values())
# prepare plotting
figure: plt.Figure = plt.figure()
axes = figure.add_subplot(1, 1, 1)
axes.set_title("Nombre de mission total complété")
# bar chart
axes.bar(x, y, color=ressource.mission.colors, edgecolor='black')
axes.set_xlabel("Mission")
axes.set_ylabel("Complétion")
axes.set_xticks(x)
axes.set_xticklabels(ressource.mission.labels, rotation=45, ha="right")
figure.tight_layout()
return figure

View file

@ -1,45 +0,0 @@
import matplotlib.pyplot as plt
import numpy as np
from tools.statistics import extract, ressource
def analyse(datas: list[dict]) -> plt.Figure:
usage_completion: dict[str, int] = dict.fromkeys(ressource.usage.choices, 0)
usage_count: dict[str, int] = dict.fromkeys(ressource.usage.choices, 0)
for data in datas:
usage = next(filter(
lambda it: it[1]["checked"],
data["surveys"]["question-usage-steam"]["choices"].items()
))[0]
usage_count[usage] += 1
for survey in data["surveys"].keys():
# only scan survey mission
if not survey.startswith("mission-"):
continue
if extract.mission_completed.extract(data, survey):
usage_completion[usage] += 1
x = list(usage_completion.keys())
y = (
np.array(list(usage_completion.values()))
/ np.array(list(usage_count.values()))
)
# prepare plotting
figure: plt.Figure = plt.figure()
axes = figure.add_subplot(1, 1, 1)
axes.set_title("Nombre moyen de mission complété par habitude d'utilisation")
# bar chart
axes.bar(x, y, color=ressource.usage.colors, edgecolor='black')
axes.set_xticks(x)
axes.set_xticklabels(ressource.usage.labels)
axes.set_xlabel("Usage")
axes.set_ylabel("Complétion")
return figure

View file

@ -1,43 +0,0 @@
from collections import defaultdict
import matplotlib.pyplot as plt
import numpy as np
from tools.statistics import extract
def analyse(datas: list[dict]) -> plt.Figure:
ages_duration: dict[int, int] = defaultdict(lambda: 0)
ages_count: dict[int, int] = defaultdict(lambda: 0)
# TODO: affichage en minutes ?
for data in datas:
age = extract.age.extract(data)
ages_count[age] += 1
for survey in data["surveys"].keys():
# only scan survey mission
if not survey.startswith("mission-"):
continue
ages_duration[age] += extract.mission_duration.extract(data, survey)
x = list(ages_duration.keys())
y = (
np.array(list(ages_duration.values()))
/ np.array(list(ages_count.values()))
)
# prepare plotting
figure: plt.Figure = plt.figure()
axes = figure.add_subplot(1, 1, 1)
axes.set_title("Temps moyen passé par âge")
# bar chart
bins = np.arange(min(x), max(x), 1)
axes.hist(x, bins, weights=y, edgecolor="black")
axes.set_xlabel("Âge")
axes.set_ylabel("Durée")
return figure

View file

@ -1,41 +0,0 @@
import matplotlib.pyplot as plt
import numpy as np
from tools.statistics import extract, ressource
def analyse(datas: list[dict]) -> plt.Figure:
experience_duration: dict[str, int] = dict.fromkeys(ressource.experience.choices, 0)
experience_count: dict[str, int] = dict.fromkeys(ressource.experience.choices, 0)
for data in datas:
experience = extract.experience.extract(data)
experience_count[experience] += 1
for survey, survey_data in data["surveys"].items():
# only scan survey mission
if not survey.startswith("mission-"):
continue
if extract.mission_completed.extract(data, survey):
experience_duration[experience] += extract.mission_duration.extract(data, survey)
x = list(experience_duration.keys())
y = (
np.array(list(experience_duration.values()))
/ np.array(list(experience_count.values()))
)
# prepare plotting
figure: plt.Figure = plt.figure()
axes = figure.add_subplot(1, 1, 1)
axes.set_title("Temps moyen passé par opinion")
# bar chart
axes.bar(x, y, color=ressource.experience.colors, edgecolor='black')
axes.set_xticks(x)
axes.set_xticklabels(ressource.experience.labels)
axes.set_xlabel("Expérience")
axes.set_ylabel("Durée")
return figure

View file

@ -1,40 +0,0 @@
import matplotlib.pyplot as plt
import numpy as np
from tools.statistics import extract, ressource
def analyse(datas: list[dict]) -> plt.Figure:
languages_duration: dict[str, int] = dict.fromkeys(ressource.language.choices, 0)
languages_count: dict[str, int] = dict.fromkeys(ressource.language.choices, 0)
for data in datas:
language = extract.language.extract(data)
languages_count[language] += 1
for survey in data["surveys"].keys():
# only scan survey mission
if not survey.startswith("mission-"):
continue
languages_duration[language] += extract.mission_duration.extract(data, survey)
x = list(languages_duration.keys())
y = (
np.array(list(languages_duration.values()))
/ np.array(list(languages_count.values()))
)
# prepare plotting
figure: plt.Figure = plt.figure()
axes = figure.add_subplot(1, 1, 1)
axes.set_title("Temps moyen passé par langue")
# bar chart
axes.bar(x, y, color=ressource.language.colors, edgecolor='black')
axes.set_xticks(x)
axes.set_xticklabels(ressource.language.labels)
axes.set_xlabel("Langue")
axes.set_ylabel("Durée")
return figure

View file

@ -1,37 +0,0 @@
import matplotlib.pyplot as plt
import numpy as np
from tools.statistics import extract, ressource
def analyse(datas: list[dict]) -> plt.Figure:
durations: dict[str] = dict.fromkeys(ressource.mission.choices, 0)
# TODO : marqué en rouge la durée d'abandon ?
# TODO : couleur par mission
for data in datas:
for survey in data["surveys"].keys():
# only scan survey mission
if not survey.startswith("mission-"):
continue
durations[survey] += extract.mission_duration.extract(data, survey)
x = list(durations.keys())
y = np.array(list(durations.values())) / len(datas)
# prepare plotting
figure: plt.Figure = plt.figure()
axes = figure.add_subplot(1, 1, 1)
axes.set_title("Temps total passé par mission")
# bar chart
axes.bar(x, y, color=ressource.mission.colors, edgecolor='black')
axes.set_xlabel("Mission")
axes.set_ylabel("Durée")
axes.set_xticks(x)
axes.set_xticklabels(ressource.mission.labels, rotation=45, ha="right")
figure.tight_layout()
return figure

View file

@ -1,40 +0,0 @@
import matplotlib.pyplot as plt
import numpy as np
from tools.statistics import extract, ressource
def analyse(datas: list[dict]) -> plt.Figure:
usage_completion: dict[str, int] = dict.fromkeys(ressource.usage.choices, 0)
usage_count: dict[str, int] = dict.fromkeys(ressource.usage.choices, 0)
for data in datas:
usage = extract.usage.extract(data)
usage_count[usage] += 1
for survey in data["surveys"].keys():
# only scan survey mission
if not survey.startswith("mission-"):
continue
usage_completion[usage] += extract.mission_duration.extract(data, survey)
x = list(usage_completion.keys())
y = (
np.array(list(usage_completion.values()))
/ np.array(list(usage_count.values()))
)
# prepare plotting
figure: plt.Figure = plt.figure()
axes = figure.add_subplot(1, 1, 1)
axes.set_title("Temps moyen passé par habitude d'utilisation")
# bar chart
axes.bar(x, y, color=ressource.usage.colors, edgecolor='black')
axes.set_xticks(x)
axes.set_xticklabels(ressource.usage.labels)
axes.set_xlabel("Usage")
axes.set_ylabel("Durée")
return figure

View file

@ -1,30 +0,0 @@
from collections import Counter
import matplotlib.pyplot as plt
from tools.statistics import extract, ressource
def analyse(datas: list[dict]) -> plt.Figure:
experiences: dict[str, int] = dict.fromkeys(ressource.experience.choices, 0)
for data in datas:
experience = extract.experience.extract(data)
experiences[experience] += 1
counter = Counter(experiences)
x = list(counter.keys())
y = list(counter.values())
# prepare plotting
figure: plt.Figure = plt.figure()
axes = figure.add_subplot(1, 1, 1)
axes.set_title("Opinion des personnes sondées")
# bar chart
axes.bar(x, y, color=ressource.experience.colors, edgecolor='black')
axes.set_xticks(x)
axes.set_xticklabels(ressource.experience.labels)
axes.set_xlabel("Expérience")
axes.set_ylabel("Quantité")
return figure

View file

@ -1,28 +0,0 @@
import matplotlib.pyplot as plt
from tools.statistics import extract, ressource
def analyse(datas: list[dict]) -> plt.Figure:
missions = dict.fromkeys(ressource.mission.choices, 0)
for data in datas:
missions[extract.hardest_mission.extract(data)] += 1
x = list(missions.keys())
y = list(missions.values())
# prepare plotting
figure: plt.Figure = plt.figure()
axes = figure.add_subplot(1, 1, 1)
axes.set_title("Mission la plus difficile des personnes sondées")
# bar chart
axes.bar(x, y, color=ressource.mission.colors, edgecolor='black')
axes.set_xlabel("Mission")
axes.set_ylabel("Quantité")
axes.set_xticks(x)
axes.set_xticklabels(ressource.mission.labels, rotation=45, ha="right")
figure.tight_layout()
return figure

View file

@ -1,26 +0,0 @@
import matplotlib.pyplot as plt
from tools.statistics import extract, ressource
def analyse(datas: list[dict]) -> plt.Figure:
languages = dict.fromkeys(ressource.language.choices, 0)
for language in map(extract.language.extract, datas):
languages[language] += 1
x = list(languages.keys())
y = list(languages.values())
# prepare plotting
figure: plt.Figure = plt.figure()
axes = figure.add_subplot(1, 1, 1)
axes.set_title("Langue des personnes sondées")
# bar chart
axes.bar(x, y, color=ressource.language.colors, edgecolor='black')
axes.set_xticks(x)
axes.set_xticklabels(ressource.language.labels)
axes.set_xlabel("Langue")
axes.set_ylabel("Quantité")
return figure

View file

@ -1,26 +0,0 @@
import matplotlib.pyplot as plt
from tools.statistics import extract, ressource
def analyse(datas: list[dict]) -> plt.Figure:
usages: dict[str, int] = dict.fromkeys(ressource.usage.choices, 0)
for usage in map(extract.usage.extract, datas):
usages[usage] += 1
x = list(usages.keys())
y = list(usages.values())
# prepare plotting
figure: plt.Figure = plt.figure()
axes = figure.add_subplot(1, 1, 1)
axes.set_title("Habitude d'utilisation des personnes sondées")
# bar chart
axes.bar(x, y, color=ressource.usage.colors, edgecolor='black')
axes.set_xticks(x)
axes.set_xticklabels(ressource.usage.labels)
axes.set_xlabel("Usage")
axes.set_ylabel("Quantité")
return figure

View file

@ -1,7 +0,0 @@
from . import usage
from . import age
from . import mission_duration
from . import mission_completed
from . import experience
from . import hardest_mission
from . import language

View file

@ -1,2 +0,0 @@
def extract(data: dict) -> int:
return data["surveys"]["question-age"]["value"]

View file

@ -1,5 +0,0 @@
def extract(data: dict) -> str:
return next(filter(
lambda it: it[1]["checked"],
data["surveys"]["question-experience"]["choices"].items()
))[0]

View file

@ -1,5 +0,0 @@
def extract(data: dict) -> str:
return next(filter(
lambda it: it[1]["checked"],
data["surveys"]["question-hardest-mission"]["choices"].items()
))[0]

View file

@ -1,2 +0,0 @@
def extract(data: dict) -> str:
return data["language"]

View file

@ -1,9 +0,0 @@
def extract(data: dict, mission: str) -> bool:
events = data["surveys"][mission]["event"]
try:
checks = next(filter(lambda event: event["type"] == "check", events))
except StopIteration:
return False
else:
return True

View file

@ -1,2 +0,0 @@
def extract(data: dict, mission: str) -> float:
return data["surveys"][mission]["event"][-1]["time"]

View file

@ -1,5 +0,0 @@
def extract(data: dict) -> str:
return next(filter(
lambda it: it[1]["checked"],
data["surveys"]["question-usage-steam"]["choices"].items()
))[0]

View file

@ -1,70 +0,0 @@
from pathlib import Path
from matplotlib import pyplot as plt
from tools.statistics.analyse import (age, usage, completion_per_mission, duration_per_mission, completion_per_age,
completion_per_usage, duration_per_age, duration_per_usage,
completion_per_experience, duration_per_experience, experience, hardest_mission,
language, duration_per_language, completion_per_language)
plt.rcParams['text.usetex'] = True
if __name__ == "__main__":
from source.utils import compress
import matplotlib
matplotlib.use("pgf")
matplotlib.rcParams.update({
"pgf.texsystem": "pdflatex",
'font.family': 'serif',
'font.size': 11,
'text.usetex': True,
'pgf.rcfonts': False,
})
sondage_path = Path(r"./sondage/")
graph_path = Path(r"./graph/")
graph_path.mkdir(parents=True, exist_ok=True)
# read every peoples survey data
datas_all = [
compress.uncompress_data(file.read_bytes()) # decompress the data
for file in sondage_path.rglob("*.rsl")
]
# keep only the datas before the steam new year update
datas_steam_version_1 = list(filter(lambda data: data["time"] < 1704409200, datas_all))
# keep only the datas after the steam new year update
datas_steam_version_2 = list(filter(lambda data: data["time"] >= 1704409200, datas_all))
# regroup all the datas
datasets = {
"all": datas_all,
"version_1": datas_steam_version_1,
"version_2": datas_steam_version_2,
}
for datas_name, datas in datasets.items():
directory = graph_path / datas_name
directory.mkdir(parents=True, exist_ok=True)
age.analyse(datas).savefig(directory / "age.svg")
usage.analyse(datas).savefig(directory / "usage.svg")
experience.analyse(datas).savefig(directory / "experience.svg")
hardest_mission.analyse(datas).savefig(directory / "hardest_mission.svg")
language.analyse(datas).savefig(directory / "language.svg")
completion_per_mission.analyse(datas).savefig(directory / "completion_per_mission.svg")
completion_per_age.analyse(datas).savefig(directory / "completion_per_age.svg")
completion_per_usage.analyse(datas).savefig(directory / "completion_per_usage.svg")
completion_per_experience.analyse(datas).savefig(directory / "completion_per_experience.svg")
completion_per_language.analyse(datas).savefig(directory / "completion_per_language.svg")
duration_per_mission.analyse(datas).savefig(directory / "duration_per_mission.svg")
duration_per_age.analyse(datas).savefig(directory / "duration_per_age.svg")
duration_per_usage.analyse(datas).savefig(directory / "duration_per_usage.svg")
duration_per_experience.analyse(datas).savefig(directory / "duration_per_experience.svg")
duration_per_language.analyse(datas).savefig(directory / "duration_per_language.svg")

View file

@ -1,4 +0,0 @@
from . import experience
from . import mission
from . import usage
from . import language

View file

@ -1,3 +0,0 @@
choices = ["yes", "mixed", "no"]
labels = ["Oui", "Mitigé", "Non"]
colors = ["g", "y", "r"]

View file

@ -1,3 +0,0 @@
choices = ["fr", "en", "es"]
labels = ["Français", "Anglais", "Espagnol"]
colors = ["b", "g", "r"]

View file

@ -1,36 +0,0 @@
choices = [
"mission-language",
"mission-price",
"mission-community-hub",
"mission-game-page",
"mission-game-dlc",
"mission-actuality-new",
"mission-profile",
"mission-game-discussion",
"mission-gift-card",
"mission-workshop",
]
labels = [
"Changement de Langue",
"Filtre de Prix",
"Hub de la Communauté",
"Page de Jeu",
"Page de DLC",
"Page d'Actualité",
"Page de Profil",
"Page de Discussion",
"Carte Cadeaux",
"Workshop",
]
colors = [
"tab:blue",
"tab:orange",
"tab:green",
"tab:red",
"tab:olive",
"tab:purple",
"tab:brown",
"tab:pink",
"tab:cyan",
"tab:gray",
]

View file

@ -1,3 +0,0 @@
choices = ["always", "often", "sometime", "rarely", "never"]
labels = ["Toujours", "Souvent", "Parfois", "Rarement", "Jamais"]
colors = ["g", "c", "y", "m", "r"]

View file

@ -1,20 +0,0 @@
# Web-Replay
Allow you to replay the `web-mission` tasks that a user did while taking a survey.
# Installation
The installation is the same as the Survey-Engine. Refer to their installation step instead.
# Run
You must open a shell in the root directory (the `Survey-Engine` directory).
You can run the tool simply by using the command `python3 ./tools/web_replay/main.py`.
You will need to select a replay file `.rsl` and the mission you want to replay.
# Warning
The replay is in early development. Some features are missing and the replay is not very precise.
For now, key inputs don't seem to work correctly, so they are written on the bottom right instead.
Mouse movement are not perfect and mouse click don't seem to be recognised either.

View file

@ -1,17 +0,0 @@
import sys
from PyQt6.QtWidgets import QApplication
from ui import ReplayWindow
if __name__ == "__main__":
# create the application
application = QApplication(sys.argv)
# create the window
window = ReplayWindow("surveys.json")
window.show()
# start the application
application.exec()

View file

@ -1,63 +0,0 @@
from PyQt6.QtCore import pyqtSignal
from PyQt6.QtWidgets import QWidget, QLineEdit, QPushButton, QVBoxLayout, QFrame, QHBoxLayout, QComboBox, QFileDialog
class ReplayConfiguration(QWidget):
signal_start_replay = pyqtSignal([str, str])
def __init__(self, missions: list[str]):
super().__init__()
# layout
layout = QVBoxLayout()
self.setLayout(layout)
# replay path
self.frame_path = QFrame()
layout.addWidget(self.frame_path)
layout_path = QHBoxLayout()
self.frame_path.setLayout(layout_path)
self.entry_path = QLineEdit()
layout_path.addWidget(self.entry_path)
self.button_path = QPushButton()
layout_path.addWidget(self.button_path)
self.button_path.setText("...")
self.button_path.clicked.connect(self.select_replay) # NOQA: connect exist
# mission
self.frame_mission = QFrame()
layout.addWidget(self.frame_mission)
layout_mission = QHBoxLayout()
self.frame_mission.setLayout(layout_mission)
self.listbox_mission = QComboBox()
layout_mission.addWidget(self.listbox_mission)
for mission in missions:
self.listbox_mission.addItem(mission)
# button
self.button_confirm = QPushButton()
layout.addWidget(self.button_confirm)
self.button_confirm.setText("Replay")
self.button_confirm.clicked.connect(self._start_replay) # NOQA: connect exist
def select_replay(self):
# prompt the user for a file
file, raw_filetype = QFileDialog.getOpenFileName(
caption="Select the file to replay.",
filter="Replay (*.rsl)"
)
# if no file were selected, ignore
if not file:
return
self.entry_path.setText(file)
def _start_replay(self):
self.signal_start_replay.emit(self.entry_path.text(), self.listbox_mission.currentText()) # NOQA: emit

View file

@ -1,194 +0,0 @@
from datetime import datetime
from pathlib import Path
from typing import Callable
from PyQt6.QtCore import Qt, QUrl, QPointF, QTimer
from PyQt6.QtGui import QKeyEvent, QMouseEvent
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QApplication, QLabel
from source.utils import compress
from . import ReplayWebEngineView, ReplayNavigation
class ReplayEngine(QWidget):
"""
This widget allow to replay some event that occurred on a web page
"""
def __init__(self, replay_path: Path | str, mission: str):
super().__init__()
# load the replay
with open(replay_path, "rb") as file:
replay_data = compress.uncompress_data(file.read())
self.start_time = datetime.fromtimestamp(replay_data["time"])
self.replay_events = replay_data["surveys"][mission]["event"]
self.replay_index: int = 0
self.replay_time: float = 0
# layout
self._layout = QVBoxLayout()
self.setLayout(self._layout)
# cursor
self.cursor = QLabel(self)
self.cursor.setFixedSize(20, 20)
self.cursor.setStyleSheet("background-color: red; border-radius: 10px;")
# web
self.web = ReplayWebEngineView(self.start_time)
self._layout.addWidget(self.web, 1)
# information
self.navigation = ReplayNavigation()
self._layout.addWidget(self.navigation)
# event timer
self.timer = QTimer()
def run_event(self, event: dict, callback: Callable):
# TODO: check if click are done correctly, check position, if event are correct, ...
match event["type"]:
case "success":
# success event
print(f"success ! ({event['time']}s)")
callback()
case "url":
# changing url event
self.web.setUrl(QUrl(event["url"]))
# callback
self.web.loadFinished.connect(lambda ok: callback()) # NOQA: connect exist
case "resize":
# changing widget size event
w, h = event["size"]
zoom_factor: float = self.web.page().contentsSize().width() / w
self.web.setZoomFactor(zoom_factor)
# callback
callback()
case "keyboard_press":
# keyboard key pressed event
qevent = QKeyEvent(
QKeyEvent.Type.KeyPress,
event["key"],
Qt.KeyboardModifier.NoModifier
)
qevent.custom = True
QApplication.postEvent(self.web.focusProxy(), qevent)
# callback
callback()
case "keyboard_release":
# keyboard key released event
qevent = QKeyEvent(
QKeyEvent.Type.KeyRelease,
event["key"],
Qt.KeyboardModifier.NoModifier
)
qevent.custom = True
QApplication.postEvent(self.web.focusProxy(), qevent)
# callback
callback()
case "mouse_press":
# mouse pressed event
qevent = QMouseEvent(
QMouseEvent.Type.KeyPress,
QPointF(*event["position"]) / self.web.zoomFactor(),
Qt.MouseButton(event["button"]),
Qt.MouseButton.NoButton,
Qt.KeyboardModifier.NoModifier
)
qevent.custom = True
QApplication.postEvent(self.web.focusProxy(), qevent)
# callback
callback()
case "mouse_release":
# mouse pressed event
qevent = QMouseEvent(
QMouseEvent.Type.KeyRelease,
QPointF(*event["position"]) / self.web.zoomFactor(),
Qt.MouseButton(event["button"]),
Qt.MouseButton.NoButton,
Qt.KeyboardModifier.NoModifier
)
qevent.custom = True
QApplication.postEvent(self.web.focusProxy(), qevent)
# callback
callback()
case "mouse_move":
# mouse moved event
qevent = QMouseEvent(
QMouseEvent.Type.MouseMove,
QPointF(*event["position"]) / self.web.zoomFactor(),
Qt.MouseButton.NoButton,
Qt.MouseButton.NoButton,
Qt.KeyboardModifier.NoModifier
)
qevent.custom = True
QApplication.postEvent(self.web.focusProxy(), qevent)
# move the cursor
self.cursor.move(QPointF(*event["position"]).toPoint())
self.cursor.raise_()
# callback
callback()
case "scroll":
# scroll event
x, y = event["position"]
self.web.page().runJavaScript(
f"window.scrollTo({x}, {y});",
resultCallback=lambda result: callback()
)
def next(self):
# get event information
if self.replay_index >= len(self.replay_events):
return
event = self.replay_events[self.replay_index]
self.replay_time = event["time"]
self.replay_index = self.replay_index + 1
# set text
self.navigation.set_description(f"{event}")
# run the event
self.run_event(
event,
self._next_callback
)
def _next_callback(self):
# prevent the web loading to call this function again
try:
self.web.loadFinished.disconnect(self._next_callback) # NOQA: disconnect exist
except TypeError:
pass
# if there are still events after this one
if self.replay_index < len(self.replay_events):
# next event
next_event: dict = self.replay_events[self.replay_index]
next_time: float = next_event["time"]
# prepare the timer to play the event at the corresponding time
self.timer.singleShot(
round((next_time - self.replay_time) / 200),
self.next
)

View file

@ -1,19 +0,0 @@
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel
class ReplayNavigation(QWidget):
def __init__(self):
super().__init__()
# layout
layout = QVBoxLayout()
self.setLayout(layout)
# information of the replay
self._description = QLabel()
layout.addWidget(self._description)
self._description.setAlignment(Qt.AlignmentFlag.AlignCenter)
def set_description(self, text: str):
self._description.setText(text)

View file

@ -1,66 +0,0 @@
from datetime import datetime
from typing import Optional
from PyQt6.QtCore import QObject, QEvent, QUrl
from PyQt6.QtWebEngineWidgets import QWebEngineView
class ReplayWebEngineView(QWebEngineView):
def __init__(self, start_time: datetime):
super().__init__()
self.start_time = start_time
self._last_url: Optional[QUrl] = None
self.loadFinished.connect(self._initialize_proxy_event) # NOQA: connect exist
# event filter
def setUrl(self, url: QUrl, archive: bool = True) -> None:
if archive:
# get the archive.org link corresponding to that time
archive_time: str = self.start_time.strftime("%Y%m%d%H%M%S")
url = QUrl(f"https://web.archive.org/web/{archive_time}/{url.toString()}")
self._last_url = url
# call the super function with the archive url instead
super().setUrl(url)
# clean the archive header popup that will appear
self.loadFinished.connect(self._on_load_finished) # NOQA: connect exist
def eventFilter(self, obj: QObject, event: QEvent) -> bool:
match event.type():
# allow scroll events (they are created automatically)
case event.Type.Scroll:
pass
# allow timed events
case event.Type.Timer:
pass
# ignore all other events
case _:
if not getattr(event, "custom", False):
return True
return super().eventFilter(obj, event)
# events
def _initialize_proxy_event(self):
# make self.eventFilter intercept all focusProxy events
self.focusProxy().installEventFilter(self)
def _on_load_finished(self, ok: bool):
# prevent the event from being enabled another time
self.loadFinished.disconnect(self._on_load_finished) # NOQA: disconnect exist
if ok:
# hide archive.org header to avoid mouse movement being shifted
self.page().runJavaScript("document.getElementById('wm-ipp-base').style.display = 'none';")
self._initialize_proxy_event()
else:
self.setUrl(self._last_url, archive=False)

View file

@ -1,38 +0,0 @@
import json
from pathlib import Path
from PyQt6.QtWidgets import QMainWindow
from tools.web_replay.ui import ReplayConfiguration, ReplayEngine
class ReplayWindow(QMainWindow):
def __init__(self, survey_path: Path | str):
super().__init__()
# get the survey configuration
with open(survey_path, encoding="utf-8") as file:
survey_configuration: dict = json.load(file)
# get all the missions available in the survey that can be replayed
missions: list[str] = [
mission_id
for mission_id, mission in survey_configuration["surveys"].items()
if mission["type"] == "mission-web"
]
# decoration
self.setWindowTitle("Survey Engine - Web Replay")
# show the configuration
self.replay_engine = None
self.replay_configuration = ReplayConfiguration(missions=missions)
self.setCentralWidget(self.replay_configuration)
self.replay_configuration.signal_start_replay.connect(self.start_replay)
def start_replay(self, replay_path: str, mission: str):
# start the replay
self.replay_engine = ReplayEngine(replay_path, mission)
self.setCentralWidget(self.replay_engine)
self.showFullScreen()
self.replay_engine.next()

View file

@ -1,5 +0,0 @@
from .ReplayWebEngineView import ReplayWebEngineView
from .ReplayNavigation import ReplayNavigation
from .ReplayEngine import ReplayEngine
from .ReplayConfiguration import ReplayConfiguration
from .ReplayWindow import ReplayWindow