From 3ec1615c41edb69694b8b34ce5b173e31f386228 Mon Sep 17 00:00:00 2001 From: Thibault Date: Thu, 16 May 2024 22:27:14 +0200 Subject: [PATCH 01/16] feat: add links in event description --- .dockerignore | 1 + .gitignore | 2 + Readme.md | 11 +++-- app.py | 32 +++++++++++- calendar_connector/calendar_converter.py | 41 +++++++++++++--- calendar_connector/consts.py | 18 +++++-- calendar_connector/cryptography.py | 24 +++++++++ .../{exceptions.py => custom_exceptions.py} | 5 ++ calendar_connector/database/__init__.py | 4 ++ calendar_connector/database/create_tables.py | 5 ++ calendar_connector/database/db_connector.py | 7 +++ calendar_connector/database/user.py | 37 ++++++++++++++ calendar_connector/datetime_utils.py | 4 ++ calendar_connector/event_convertor.py | 16 ++++-- calendar_connector/event_utils/__init__.py | 0 calendar_connector/event_utils/description.py | 49 +++++++++++++++++-- calendar_connector/event_utils/summary.py | 2 +- calendar_connector/html/auto_close.html | 10 ++++ calendar_connector/presence_updater.py | 12 +++++ calendar_connector/sporteasy_connector.py | 35 +++++++++++++ requirements.txt | 2 + test/test_get_calendar_text.py | 2 +- 22 files changed, 295 insertions(+), 24 deletions(-) create mode 100644 calendar_connector/cryptography.py rename calendar_connector/{exceptions.py => custom_exceptions.py} (52%) create mode 100644 calendar_connector/database/__init__.py create mode 100644 calendar_connector/database/create_tables.py create mode 100644 calendar_connector/database/db_connector.py create mode 100644 calendar_connector/database/user.py create mode 100644 calendar_connector/event_utils/__init__.py create mode 100644 calendar_connector/html/auto_close.html create mode 100644 calendar_connector/presence_updater.py diff --git a/.dockerignore b/.dockerignore index 2e903ec..90d7459 100644 --- a/.dockerignore +++ b/.dockerignore @@ -164,3 +164,4 @@ data_*.txt test/ docs/ +database.db diff --git a/.gitignore b/.gitignore index 7d71158..77d8cf8 100644 --- a/.gitignore +++ b/.gitignore @@ -161,3 +161,5 @@ cython_debug/ *.ics data_*.txt +database.db + diff --git a/Readme.md b/Readme.md index 9e6ac75..948f041 100644 --- a/Readme.md +++ b/Readme.md @@ -1,4 +1,4 @@ -[![Github Container Pulls](https://img.shields.io/badge/Github%20Container%20Pulls-960-blue +[![Github Container Pulls](https://img.shields.io/badge/Github%20Container%20Pulls-1K-blue )](https://github.com/tbmc/sporteasy-calendar-connector/pkgs/container/sporteasy-calendar-connector) [![Docker Pulls](https://img.shields.io/docker/pulls/tbmc/sporteasy-calendar-connector)](https://hub.docker.com/r/tbmc/sporteasy-calendar-connector) @@ -71,10 +71,15 @@ You can use mine, but at your own risk. :warning: Data in base64 are not ciphered. `` -https://sporteasy-calendar-connector.tbmc.ovh?data={base64Data} +https://sporteasy-calendar-connector.tbmc.ovh/api?data={base64Data} +`` + +You can add a parameter `disable_save_login` to disable saving of logins and password, but it deactivates links in event description to set present or absent. Without saving logins, it can not connect to SportEasy servers. + +`` +https://sporteasy-calendar-connector.tbmc.ovh/api?data={base64Data}&disable_save_login=True `` ## Info SportEasy block IPs from server providers, so you should have a domestic IP. - diff --git a/app.py b/app.py index b2c83c1..e6f34f9 100644 --- a/app.py +++ b/app.py @@ -5,9 +5,15 @@ import flask from flask import send_from_directory +from werkzeug.exceptions import BadRequestKeyError + from calendar_connector.calendar_converter import CalendarConverter +from calendar_connector.consts import route_change_presence from calendar_connector.data_decoder import decode_data from calendar_connector.sporteasy_connector import SporteasyConnector +from calendar_connector.database.user import generate_links_data +from calendar_connector.custom_exceptions import BadTokenException +from calendar_connector.presence_updater import set_presence_to_event logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", @@ -33,8 +39,12 @@ def request_handler() -> flask.Response: ip = flask.request.remote_addr logging.info(f"New incoming request from {ip=} and {username=}") + url_root = flask.request.url_root + disable_save_login = flask.request.args.get("disable_save_login") is not None calendar_converter = CalendarConverter() - calendar_text = calendar_converter.get_calendar_text(username, password, team_id) + calendar_text = calendar_converter.get_calendar_text( + username, password, not disable_save_login, url_root, team_id + ) return flask.Response( calendar_text, @@ -43,6 +53,26 @@ def request_handler() -> flask.Response: ) +@app.route(route_change_presence) +def change_my_presence() -> flask.Response: + try: + team_id = flask.request.args["team_id"] + event_id = flask.request.args["event_id"] + user_id = flask.request.args["user_id"] + token = flask.request.args["token"] + presence = flask.request.args["presence"].lower() == "yes" + except BadRequestKeyError as e: + return flask.Response("Parameter missing", status=500) + + hash_token = generate_links_data(event_id, user_id) + if token != hash_token: + raise BadTokenException() + + set_presence_to_event(int(team_id), int(event_id), int(user_id), presence) + + return flask.send_file("calendar_connector/html/auto_close.html") + + @app.route("/api/list-teams") def list_teams() -> flask.Response: if flask.request.method == "OPTIONS": diff --git a/calendar_connector/calendar_converter.py b/calendar_connector/calendar_converter.py index fd861b5..14778f0 100644 --- a/calendar_connector/calendar_converter.py +++ b/calendar_connector/calendar_converter.py @@ -1,10 +1,14 @@ +from typing import Optional, cast + from icalendar import Calendar, vText +from calendar_connector.database.user import save_user from calendar_connector.datetime_utils import ( - get_current_datetime, + get_formated_current_time, ) from calendar_connector.env import load_env_data from calendar_connector.event_convertor import event_to_calendar_event +from calendar_connector.event_utils.description import GenerateLinksData from calendar_connector.sporteasy_connector import SporteasyConnector @@ -13,9 +17,26 @@ def __init__(self) -> None: self.connector = SporteasyConnector() def get_calendar_text( - self, username: str, password: str, team_id: str | None = None + self, + username: str, + password: str, + save_login: bool, + url_root: str, + team_id: Optional[str] = None, ) -> str: self.connector.login(username, password) + + links_data: Optional[GenerateLinksData] = None + if save_login: + user = save_user(username, password) + links_data = GenerateLinksData( + cast(int, user.id), + cast(str, username), + cast(str, password), + cast(str, user.salt), + url_root, + ) + teams = self.connector.list_teams() cal = Calendar() @@ -28,8 +49,10 @@ def get_calendar_text( cal.add("x-wr-calname", "SportEasy Calendar") cal.add("x-wr-timezone", "Europe/Paris") - formatted_time = get_current_datetime().strftime("%Y-%m-%d %H:%M:%S") - cal.add("x-wr-caldesc", f"SportEasy Calendar | Last sync: {formatted_time}") + cal.add( + "x-wr-caldesc", + f"SportEasy Calendar | Last sync: {get_formated_current_time()}", + ) cal.add("REFRESH-INTERVAL;VALUE=DURATION", "PT8H") cal.add("X-PUBLISHED-TTL", "PT8H") @@ -43,7 +66,11 @@ def get_calendar_text( continue events = self.connector.list_events(current_team_id) for event in events: - cal.add_component(event_to_calendar_event(team_name, event)) + cal.add_component( + event_to_calendar_event( + current_team_id, team_name, event, links_data + ) + ) text_calendar: str = cal.to_ical().decode("utf-8").strip() @@ -54,7 +81,9 @@ def main() -> None: username, password, team_id = load_env_data() calendar_converter = CalendarConverter() - calendar_content = calendar_converter.get_calendar_text(username, password, team_id) + calendar_content = calendar_converter.get_calendar_text( + username, password, False, "http://localhost:5000/", team_id + ) with open("./test.ics", "w", encoding="utf-8") as f: f.write(calendar_content) diff --git a/calendar_connector/consts.py b/calendar_connector/consts.py index 45b40a1..a18ef08 100644 --- a/calendar_connector/consts.py +++ b/calendar_connector/consts.py @@ -1,10 +1,20 @@ from typing import Any import pytz -url_authenticate = "https://api.sporteasy.net/v2.1/account/authenticate/" -url_list_teams = "https://api.sporteasy.net/v2.1/me/teams/" -url_list_seasons = "https://api.sporteasy.net/v2.1/teams/{team_id}/seasons/" -url_list_events = "https://api.sporteasy.net/v2.1/teams/{team_id}/events/" +url_base = "https://api.sporteasy.net/v2.1" +url_authenticate = f"{url_base}/account/authenticate/" +url_csrf = f"{url_base}/account/csrf/" +url_me = f"{url_base}/me/" +url_list_teams = f"{url_base}/me/teams/" + +url_team_id_base = url_base + "/teams/{team_id}" +url_list_seasons = f"{url_team_id_base}/seasons/" +url_list_events = f"{url_team_id_base}/events/" + +url_event_base = url_team_id_base + "/events/{event_id}" +url_put_event_presence = url_event_base + "/profiles/{profile_id}/" + +route_change_presence = "/api/change_my_presence" PLAYED_WORDS = "played", "present", "available" EVENT_TYPE = dict[str, int | str | Any | list[Any] | dict[str, Any]] diff --git a/calendar_connector/cryptography.py b/calendar_connector/cryptography.py new file mode 100644 index 0000000..3e56125 --- /dev/null +++ b/calendar_connector/cryptography.py @@ -0,0 +1,24 @@ +import string +import random +import hashlib + +_alphabet = string.printable + + +def generate_salt() -> str: + chars: list[str] = [] + for i in range(random.randint(30, 50)): + chars.append(random.choice(_alphabet)) + return "".join(chars) + + +def hash_string(s: str) -> str: + m = hashlib.sha3_256(s.encode("utf-8")) + return m.hexdigest() + + +def generate_hash( + event_id: str, user_id: int | str, username: str, password: str, salt: str +) -> str: + hashed = hash_string(f"{event_id}:{user_id}:{username}:{password}:{salt}") + return hashed diff --git a/calendar_connector/exceptions.py b/calendar_connector/custom_exceptions.py similarity index 52% rename from calendar_connector/exceptions.py rename to calendar_connector/custom_exceptions.py index f73db06..535d6ef 100644 --- a/calendar_connector/exceptions.py +++ b/calendar_connector/custom_exceptions.py @@ -1,3 +1,8 @@ class AttributeNotFoundException(Exception): def __init__(self, name: str) -> None: super().__init__(f"{name} is not found") + + +class BadTokenException(Exception): + def __init__(self) -> None: + super().__init__(f"Your token is not valid") diff --git a/calendar_connector/database/__init__.py b/calendar_connector/database/__init__.py new file mode 100644 index 0000000..8b20264 --- /dev/null +++ b/calendar_connector/database/__init__.py @@ -0,0 +1,4 @@ +from calendar_connector.database.db_connector import db + +db.connect(reuse_if_open=True) +database = db diff --git a/calendar_connector/database/create_tables.py b/calendar_connector/database/create_tables.py new file mode 100644 index 0000000..1fdcd4d --- /dev/null +++ b/calendar_connector/database/create_tables.py @@ -0,0 +1,5 @@ +from calendar_connector.database.db_connector import db +from calendar_connector.database.user import User + +if __name__ == "__main__": + db.create_tables([User]) diff --git a/calendar_connector/database/db_connector.py b/calendar_connector/database/db_connector.py new file mode 100644 index 0000000..4d3f09d --- /dev/null +++ b/calendar_connector/database/db_connector.py @@ -0,0 +1,7 @@ +from pathlib import Path + +from peewee import SqliteDatabase + +database_path = Path(__file__).parent.parent.parent / "database.db" + +db = SqliteDatabase(database_path) diff --git a/calendar_connector/database/user.py b/calendar_connector/database/user.py new file mode 100644 index 0000000..cb2f4ff --- /dev/null +++ b/calendar_connector/database/user.py @@ -0,0 +1,37 @@ +from typing import cast + +from peewee import Model, PrimaryKeyField, CharField + +from calendar_connector.cryptography import generate_salt, generate_hash +from calendar_connector.database import db + + +class User(Model): + id = PrimaryKeyField() + username = CharField(unique=True, max_length=256, null=False) + password = CharField(max_length=256, null=False) + salt = CharField(max_length=51, null=False) + + class Meta: + database = db + + +def save_user(username: str, password: str) -> User: + already_existing_user = list(User.select().where(User.username == username)) + + if len(already_existing_user) == 0: + user = User(username=username, password=password, salt=generate_salt()) + user.save() + return user + + return cast(User, already_existing_user[0]) + + +def get_username_password(user_id: int) -> tuple[str, str]: + user = User.select().where(User.id == user_id).get() + return user.username, user.password + + +def generate_links_data(event_id: str, user_id: str) -> str: + user = User.select().where(User.id == user_id).get() + return generate_hash(event_id, user.id, user.username, user.password, user.salt) diff --git a/calendar_connector/datetime_utils.py b/calendar_connector/datetime_utils.py index f45ecff..1707d09 100644 --- a/calendar_connector/datetime_utils.py +++ b/calendar_connector/datetime_utils.py @@ -8,3 +8,7 @@ def get_current_timestamp() -> int: def get_current_datetime() -> datetime.datetime: return datetime.datetime.now() + + +def get_formated_current_time() -> str: + return get_current_datetime().strftime("%Y-%m-%d %H:%M:%S") diff --git a/calendar_connector/event_convertor.py b/calendar_connector/event_convertor.py index 2c2b396..a8ec470 100644 --- a/calendar_connector/event_convertor.py +++ b/calendar_connector/event_convertor.py @@ -1,24 +1,32 @@ from datetime import datetime -from typing import Any, cast +from typing import Any, cast, Optional from icalendar import Event from calendar_connector.consts import EVENT_TYPE from calendar_connector.datetime_utils import get_current_timestamp from calendar_connector.event_utils.date import extract_event_dates -from calendar_connector.event_utils.description import extract_event_description +from calendar_connector.event_utils.description import ( + extract_event_description, + GenerateLinksData, +) from calendar_connector.event_utils.location import extract_event_location from calendar_connector.event_utils.summary import extract_event_summary from calendar_connector.normalize import normalize -def event_to_calendar_event(team_name: str, event_data: EVENT_TYPE) -> Event: +def event_to_calendar_event( + team_id: int, + team_name: str, + event_data: EVENT_TYPE, + links_data: Optional[GenerateLinksData], +) -> Event: event = Event() event.add("uid", str(event_data["id"]) + f"@sporteasy.net") extract_event_location(event_data, event) extract_event_dates(event_data, event) extract_event_summary(event_data, event, team_name) - extract_event_description(event_data, event) + extract_event_description(team_id, event_data, event, links_data) event.add("class", "PUBLIC") current_timestamp = get_current_timestamp() diff --git a/calendar_connector/event_utils/__init__.py b/calendar_connector/event_utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/calendar_connector/event_utils/description.py b/calendar_connector/event_utils/description.py index eb550f8..0e6e8d4 100644 --- a/calendar_connector/event_utils/description.py +++ b/calendar_connector/event_utils/description.py @@ -1,9 +1,21 @@ -from typing import cast +import dataclasses +from typing import cast, Optional from icalendar import Event -from calendar_connector.consts import EVENT_TYPE, ORDER_PRESENT +from calendar_connector.consts import EVENT_TYPE, ORDER_PRESENT, route_change_presence +from calendar_connector.datetime_utils import get_formated_current_time from calendar_connector.event_utils.score import extract_scores +from calendar_connector.cryptography import generate_hash + + +@dataclasses.dataclass +class GenerateLinksData: + user_id: int + username: str + password: str + salt: str + url_root: str def _extract_attendee_description(event_data: EVENT_TYPE) -> str: @@ -34,12 +46,41 @@ def _extract_attendee_description(event_data: EVENT_TYPE) -> str: return attendee -def extract_event_description(event_data: EVENT_TYPE, event: Event) -> None: +def _generate_response_links( + team_id: int, event_id: int, data: GenerateLinksData +) -> str: + hashed = generate_hash( + str(event_id), data.user_id, data.username, data.password, data.salt + ) + url = ( + f"{data.url_root}{route_change_presence[1:]}" + f"?team_id={team_id}&event_id={event_id}" + f"&user_id={data.user_id}&token={hashed}" + ) + yes = f'YES' + no = f'NO' + + return f"{yes} | {no}" + + +def extract_event_description( + team_id: int, + event_data: EVENT_TYPE, + event: Event, + links_data: Optional[GenerateLinksData], +) -> None: description = "" score = extract_scores(event_data) if score is not None: description += f"{score}\n" attendee = _extract_attendee_description(event_data) - description += attendee + description += f"{attendee}\n" + + if links_data: + event_id = cast(int, event_data["id"]) + response_links = _generate_response_links(team_id, event_id, links_data) + description += f"{response_links}\n" + + description += f"\n\nLast sync: {get_formated_current_time()}\n" event.add("description", description.strip()) diff --git a/calendar_connector/event_utils/summary.py b/calendar_connector/event_utils/summary.py index 66bd35a..4f0b758 100644 --- a/calendar_connector/event_utils/summary.py +++ b/calendar_connector/event_utils/summary.py @@ -3,7 +3,7 @@ from icalendar import Event from calendar_connector.consts import EVENT_TYPE, MY_PRESENCE -from calendar_connector.exceptions import AttributeNotFoundException +from calendar_connector.custom_exceptions import AttributeNotFoundException from calendar_connector.normalize import normalize diff --git a/calendar_connector/html/auto_close.html b/calendar_connector/html/auto_close.html new file mode 100644 index 0000000..ecf2419 --- /dev/null +++ b/calendar_connector/html/auto_close.html @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/calendar_connector/presence_updater.py b/calendar_connector/presence_updater.py new file mode 100644 index 0000000..63717d3 --- /dev/null +++ b/calendar_connector/presence_updater.py @@ -0,0 +1,12 @@ +from calendar_connector.sporteasy_connector import SporteasyConnector +from calendar_connector.database.user import get_username_password + + +def set_presence_to_event( + team_id: int, event_id: int, user_id: int, presence: bool +) -> None: + username, password = get_username_password(user_id) + connector = SporteasyConnector() + connector.login(username, password) + + connector.put_presence_status(team_id, event_id, presence) diff --git a/calendar_connector/sporteasy_connector.py b/calendar_connector/sporteasy_connector.py index 605481f..78f99e6 100644 --- a/calendar_connector/sporteasy_connector.py +++ b/calendar_connector/sporteasy_connector.py @@ -1,3 +1,5 @@ +from typing import cast + import requests from calendar_connector.consts import ( @@ -5,6 +7,9 @@ url_list_teams, EVENT_TYPE, url_list_events, + url_put_event_presence, + url_me, + url_csrf, ) from calendar_connector.normalize import normalize @@ -39,3 +44,33 @@ def list_events(self, team_id: int) -> list[EVENT_TYPE]: ) data: list[EVENT_TYPE] = response.json()["results"] return data + + def get_profile_id(self) -> int: + response = self.session_requests.get(url_me) + profile_id = cast(int, response.json()["id"]) + return profile_id + + def get_csrf_token(self, team_id: int) -> tuple[str, str]: + response = self.session_requests.get(url_csrf) + csrf = response.json()["csrf_token"] + + teams = self.session_requests.get(url_list_teams).json()["results"] + current_team = [t for t in teams if t["id"] == team_id][0] + web_url = current_team["web_url"] # type: ignore + + return csrf, web_url + + def put_presence_status( + self, team_id: int, event_id: int, is_present: bool + ) -> None: + profile_id = self.get_profile_id() + csrf, web_url = self.get_csrf_token(team_id) + formatted_url = url_put_event_presence.format( + team_id=team_id, event_id=event_id, profile_id=profile_id + ) + result = self.session_requests.put( + formatted_url, + data={"attendance_status": "present" if is_present else "absent"}, + headers={"X-Csrftoken": csrf, "Referer": web_url}, + ) + # print(result) diff --git a/requirements.txt b/requirements.txt index 2b1a6bf..5324f3e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,5 @@ pytz==2023.3 types-pytz==2023.3.0.0 Flask==2.3.2 gunicorn==20.1.0 +peewee==3.17.5 +mypy==1.10.0 diff --git a/test/test_get_calendar_text.py b/test/test_get_calendar_text.py index 6b6408f..d238625 100644 --- a/test/test_get_calendar_text.py +++ b/test/test_get_calendar_text.py @@ -46,7 +46,7 @@ def test_get_calendar_text( ) converter = calendar_connector.calendar_converter.CalendarConverter() - calendar_text = converter.get_calendar_text("username", "password", "1") + calendar_text = converter.get_calendar_text("username", "password", False, "1") result = replace_unwanted_lines(calendar_text) assert result == expected_calendar From 3f0a3af7127227ee7052c9cd7dfb70099799d170 Mon Sep 17 00:00:00 2001 From: Thibault Date: Thu, 16 May 2024 22:30:51 +0200 Subject: [PATCH 02/16] ci --- .github/workflows/build_image.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build_image.yml b/.github/workflows/build_image.yml index 94714b2..b7e618f 100644 --- a/.github/workflows/build_image.yml +++ b/.github/workflows/build_image.yml @@ -7,6 +7,9 @@ on: paths-ignore: - 'docs' - '**.md' + pull_request: + branches: + - "main" jobs: build_and_publish_image: From 697ae13c065aae5cacce8622a4574eb0716fdee4 Mon Sep 17 00:00:00 2001 From: Thibault Date: Thu, 16 May 2024 23:02:50 +0200 Subject: [PATCH 03/16] ci --- .github/workflows/build_image.yml | 19 ------------------ .github/workflows/test.yml | 32 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/build_image.yml b/.github/workflows/build_image.yml index b7e618f..b2c8192 100644 --- a/.github/workflows/build_image.yml +++ b/.github/workflows/build_image.yml @@ -7,9 +7,6 @@ on: paths-ignore: - 'docs' - '**.md' - pull_request: - branches: - - "main" jobs: build_and_publish_image: @@ -23,22 +20,6 @@ jobs: uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Install requirements - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - - - name: Run Black check - run: black --check . - - - name: Run Pytest - run: pytest - name: Login to Docker Hub uses: docker/login-action@v3 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..91ae0ed --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,32 @@ +name: test + +on: + push: + branches: + - "main" + paths-ignore: + - 'docs' + - '**.md' + pull_request: + branches: + - "main" + +jobs: + test_and_lint: + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Install requirements + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run Black check + run: black --check . + - name: Run Pytest + run: pytest From ef3c97f575bdadf031249d1959f99b081f781aed Mon Sep 17 00:00:00 2001 From: Thibault Date: Thu, 16 May 2024 23:40:01 +0200 Subject: [PATCH 04/16] test --- calendar_connector/event_utils/description.py | 2 +- test/data/expected_calendar.ics | 5 +++-- test/test_get_calendar_text.py | 4 +++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/calendar_connector/event_utils/description.py b/calendar_connector/event_utils/description.py index 0e6e8d4..562fd1a 100644 --- a/calendar_connector/event_utils/description.py +++ b/calendar_connector/event_utils/description.py @@ -81,6 +81,6 @@ def extract_event_description( response_links = _generate_response_links(team_id, event_id, links_data) description += f"{response_links}\n" - description += f"\n\nLast sync: {get_formated_current_time()}\n" + description += f"\nLast sync: {get_formated_current_time()}\n" event.add("description", description.strip()) diff --git a/test/data/expected_calendar.ics b/test/data/expected_calendar.ics index b9ef671..17ce721 100644 --- a/test/data/expected_calendar.ics +++ b/test/data/expected_calendar.ics @@ -19,7 +19,8 @@ UID:1@sporteasy.net SEQUENCE:173512350 CLASS:PUBLIC CREATED:20200101T010101Z -DESCRIPTION:Présents: 8\, En attente: 7\, Non retenu: 0\, Absents: 2 +DESCRIPTION:Présents: 8\, En attente: 7\, Non retenu: 0\, Absents: 2\n\nL + ast sync: 2024-12-25 10:45:00 LAST-MODIFIED:20200101T010101Z LOCATION:rue de la Paix\, 75000 Paris\, France STATUS:TENTATIVE @@ -34,7 +35,7 @@ UID:2@sporteasy.net SEQUENCE:173512350 CLASS:PUBLIC CREATED:20200101T010101Z -DESCRIPTION: +DESCRIPTION:Last sync: 2024-12-25 10:45:00 LAST-MODIFIED:20200101T010101Z LOCATION:rue de la Paix\, 75000 Paris\, France TRANSP:OPAQUE diff --git a/test/test_get_calendar_text.py b/test/test_get_calendar_text.py index d238625..9b8fa63 100644 --- a/test/test_get_calendar_text.py +++ b/test/test_get_calendar_text.py @@ -46,7 +46,9 @@ def test_get_calendar_text( ) converter = calendar_connector.calendar_converter.CalendarConverter() - calendar_text = converter.get_calendar_text("username", "password", False, "1") + calendar_text = converter.get_calendar_text( + "username", "password", False, "http://localhost:5000/", "1" + ) result = replace_unwanted_lines(calendar_text) assert result == expected_calendar From 730528e11c623ad27dcf6b352a5632af1a7c5249 Mon Sep 17 00:00:00 2001 From: Thibault Date: Fri, 17 May 2024 17:43:25 +0200 Subject: [PATCH 05/16] front: add parameter to disable_save_login --- web-app/src/lib/GenerateUrl/LoginForm.svelte | 33 ++++++++++++++++++-- web-app/src/lib/GenerateUrl/ShowUrl.svelte | 6 ++-- web-app/src/lib/GenerateUrl/store.ts | 3 +- web-app/src/lib/i18n/locales/en.json | 6 ++-- web-app/src/lib/i18n/locales/fr.json | 6 ++-- 5 files changed, 44 insertions(+), 10 deletions(-) diff --git a/web-app/src/lib/GenerateUrl/LoginForm.svelte b/web-app/src/lib/GenerateUrl/LoginForm.svelte index 288efc8..c5bdd3d 100644 --- a/web-app/src/lib/GenerateUrl/LoginForm.svelte +++ b/web-app/src/lib/GenerateUrl/LoginForm.svelte @@ -2,7 +2,12 @@ import { t } from 'svelte-i18n'; import { dataToRequestParam } from '$lib/GenerateUrl/dataToRequestParam.js'; - import { dataParams, fetchTeamsGet, fetchTeamsIsLoading } from '$lib/GenerateUrl/store.js'; + import { + dataParamsStore, + disableSaveLoginStore, + fetchTeamsGet, + fetchTeamsIsLoading + } from '$lib/GenerateUrl/store.js'; import TwoTextComponent from '$lib/UI/TwoTextComponent.svelte'; let usernameTranslation = $t('generateUrl.form.username'); @@ -16,6 +21,8 @@ let invalidUsername: boolean | null = null; let invalidPassword: boolean | null = null; + let disableSaveLogin = false; + function clicked() { invalidUsername = username === ''; invalidPassword = password === ''; @@ -32,7 +39,8 @@ function generateUrl() { if (username !== '' && password !== '') { const data = dataToRequestParam(username, password, teamId); - dataParams.set(data); + dataParamsStore.set(data); + disableSaveLoginStore.set(disableSaveLogin) twoText.animate(); } @@ -67,6 +75,7 @@

{$t('generateUrl.warning.credentialsRequired')}

+ + + + + + + +
+ +

+ {$t('generateUrl.form.disableSaveLoginExtra')} +

+
diff --git a/web-app/src/lib/GenerateUrl/ShowUrl.svelte b/web-app/src/lib/GenerateUrl/ShowUrl.svelte index a12d4d8..c3cd2bc 100644 --- a/web-app/src/lib/GenerateUrl/ShowUrl.svelte +++ b/web-app/src/lib/GenerateUrl/ShowUrl.svelte @@ -1,11 +1,11 @@ -{#if $dataParams !== ''} +{#if $dataParamsStore !== ''}
{$t('generateUrl.urlGenerated')} diff --git a/web-app/src/lib/GenerateUrl/store.ts b/web-app/src/lib/GenerateUrl/store.ts index 3f10f8a..0f633e1 100644 --- a/web-app/src/lib/GenerateUrl/store.ts +++ b/web-app/src/lib/GenerateUrl/store.ts @@ -2,7 +2,8 @@ import { writable } from 'svelte/store'; import { dataToRequestParam } from './dataToRequestParam'; import { browser, dev } from '$app/environment'; -export const dataParams = writable(''); +export const dataParamsStore = writable(''); +export const disableSaveLoginStore = writable(false); export const fetchTeamsIsLoading = writable(false); export const fetchTeamsData = writable([]); diff --git a/web-app/src/lib/i18n/locales/en.json b/web-app/src/lib/i18n/locales/en.json index ed1ec22..ef72eb0 100644 --- a/web-app/src/lib/i18n/locales/en.json +++ b/web-app/src/lib/i18n/locales/en.json @@ -2,7 +2,7 @@ "generateUrl": { "header": "Generate Sporteasy Calendar Converter URL", "warning": { - "logins": "You are about to enter your Sporteasy credentials. Potentially I could collect them.", + "logins": "You are about to enter your Sporteasy credentials. Your logins are saved.", "repository": "You can host it yourself using", "credentialsRequired": "Your credentials are required to access your Sporteasy calendar." }, @@ -12,7 +12,9 @@ "teamId": "Team ID (optional)", "buttonReset": "Reset", "buttonListTeams": "List teams", - "buttonGenerate": "Generate" + "buttonGenerate": "Generate", + "disableSaveLogin": "Deactivate savings of your login details", + "disableSaveLoginExtra": "If you deactivate saving of your login details, you can not have links in event description which allow to respond directly from your calendar" }, "listTeams": { "title": "Team list", diff --git a/web-app/src/lib/i18n/locales/fr.json b/web-app/src/lib/i18n/locales/fr.json index 65f6cbc..0849307 100644 --- a/web-app/src/lib/i18n/locales/fr.json +++ b/web-app/src/lib/i18n/locales/fr.json @@ -2,7 +2,7 @@ "generateUrl": { "header": "Générer l'URL Sporteasy Calendar Converter", "warning": { - "logins": "Vous êtes sur le point de rentrer vos identifiants pour SportEasy. Potentiellement je pourrais les récupérer.", + "logins": "Vous êtes sur le point de rentrer vos identifiants pour SportEasy. Les indentifiants sont enregistrés (sans disable_save_login).", "repository": "Vous pouvez l'héberger vous même en utilisant le", "credentialsRequired": "Vos identifiants sont nécessaires pour accéder à votre calendrier Sporteasy." }, @@ -12,7 +12,9 @@ "teamId": "Id de l'équipe (optionnel)", "buttonReset": "Réinitiliser", "buttonListTeams": "Lister les équipes", - "buttonGenerate": "Générer" + "buttonGenerate": "Générer", + "disableSaveLogin": "Désactiver la sauvegarde de vos identifiants", + "disableSaveLoginExtra": "En désactivant la sauvegarde de vos identifiants, ça désactive la possibilité d'avoir des liens dans la description de l'évènement qui permettent de répondre directement depuis votre calendrier." }, "listTeams": { "title": "Liste des équipes", From 8cdf9f22bd89efd46c2e58d3f089feaabc480269 Mon Sep 17 00:00:00 2001 From: Thibault Date: Fri, 17 May 2024 18:29:52 +0200 Subject: [PATCH 06/16] front: display url style --- web-app/src/lib/GenerateUrl/ColoredUrl.svelte | 29 +++++++++++++++++++ web-app/src/lib/GenerateUrl/ShowUrl.svelte | 11 +++++-- web-app/src/lib/GenerateUrl/store.ts | 10 +------ 3 files changed, 39 insertions(+), 11 deletions(-) create mode 100644 web-app/src/lib/GenerateUrl/ColoredUrl.svelte diff --git a/web-app/src/lib/GenerateUrl/ColoredUrl.svelte b/web-app/src/lib/GenerateUrl/ColoredUrl.svelte new file mode 100644 index 0000000..8224a82 --- /dev/null +++ b/web-app/src/lib/GenerateUrl/ColoredUrl.svelte @@ -0,0 +1,29 @@ + + +{origin}/api?{disableSaveLogin ? '&' : ''}data={data} + + diff --git a/web-app/src/lib/GenerateUrl/ShowUrl.svelte b/web-app/src/lib/GenerateUrl/ShowUrl.svelte index c3cd2bc..885baf9 100644 --- a/web-app/src/lib/GenerateUrl/ShowUrl.svelte +++ b/web-app/src/lib/GenerateUrl/ShowUrl.svelte @@ -3,9 +3,10 @@ import { dataParamsStore, disableSaveLoginStore, getOrigin } from './store.js'; import TwoTextComponent from '$lib/UI/TwoTextComponent.svelte'; + import ColoredUrl from '$lib/GenerateUrl/ColoredUrl.svelte'; const origin = getOrigin(); - $: url = `${origin}?${$disableSaveLoginStore ? 'disable_save_login=true&' : ''}data=${$dataParamsStore}`; + $: url = `${origin}/api?${$disableSaveLoginStore ? 'disable_save_login=true&' : ''}data=${$dataParamsStore}`; let twoText: TwoTextComponent; @@ -23,7 +24,13 @@
- {url} + + +