diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index e33c9df3e..c5a0ba568 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -9,6 +9,7 @@ on: - main - develop - shrivaths/changelog-announcement-1 + - shrivaths/changelog-announcement-2 paths: - "docs/**" - "README.rst" @@ -48,6 +49,7 @@ jobs: run: | cd docs python make_api_doctree.py + python make_bulletin_json.py make html - name: Deploy (sleap.ai) @@ -66,4 +68,5 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} publish_branch: gh-pages publish_dir: docs/build/html - destination_dir: develop \ No newline at end of file + destination_dir: develop + keep_files: true \ No newline at end of file diff --git a/docs/make_bulletin_json.py b/docs/make_bulletin_json.py new file mode 100644 index 000000000..3c4789ba6 --- /dev/null +++ b/docs/make_bulletin_json.py @@ -0,0 +1,45 @@ +import json +import re +from pathlib import Path + +# Set the file path to the markdown file +input_md_file = Path(__file__).resolve().parent / "bulletin.md" + +# Regex for date +date_pattern = r'^_(\d{2}/\d{2}/\d{4})_$' + +def generate_json_file(): + with open(input_md_file, "r", encoding="utf-8") as md_file: + markdown_content = md_file.read() + bulletin_json = [] + content = "" + + # Initialize title and date with default values + title = "DEFAULT_TITLE" + date = "DEFAULT_DATE" + + for line in markdown_content.split("\n"): + # Skip if the line begins with # + if line.startswith("# "): + continue + elif line.startswith("---"): + bulletin_json.append({"title": title, "date": date, "content": content}) + content = "" + # Reset title and date to their default values after each section + title = "DEFAULT_TITLE" + date = "DEFAULT_DATE" + elif line.startswith("## "): + title = line[3:].strip() + elif re.match(date_pattern, line): + date = line[1 : len(line) - 1].strip() + else: + content += line + "\n" + # Append last section + bulletin_json.append({"title": title, "date": date, "content": content}) + + with open("_static/bulletin.json", "w") as json_file: + json.dump(bulletin_json, json_file, indent=4) + + +if __name__ == "__main__": + generate_json_file() diff --git a/sleap/gui/app.py b/sleap/gui/app.py index 41d696f0c..053e1007b 100644 --- a/sleap/gui/app.py +++ b/sleap/gui/app.py @@ -69,7 +69,7 @@ from sleap.gui.overlays.tracks import TrackListOverlay, TrackTrailOverlay from sleap.gui.shortcuts import Shortcuts from sleap.gui.state import GuiState -from sleap.gui.web import ReleaseChecker, ping_analytics +from sleap.gui.web import ReleaseChecker, AnnouncementChecker, ping_analytics from sleap.gui.widgets.docks import ( InstancesDock, SkeletonDock, @@ -158,6 +158,8 @@ def __init__( self.state["share usage data"] = prefs["share usage data"] self.state["skeleton_preview_image"] = None self.state["skeleton_description"] = "No skeleton loaded yet" + self.state["announcement last seen date"] = prefs["announcement last seen date"] + self.state["announcement"] = prefs["announcement"] if no_usage_data: self.state["share usage data"] = False self.state["clipboard_track"] = None @@ -168,6 +170,7 @@ def __init__( self.state.connect("show non-visible nodes", self.plotFrame) self.release_checker = ReleaseChecker() + self.announcement_checker = AnnouncementChecker(state=self.state) if self.state["share usage data"]: ping_analytics() @@ -223,6 +226,8 @@ def closeEvent(self, event): prefs["color predicted"] = self.state["color predicted"] prefs["trail shade"] = self.state["trail_shade"] prefs["share usage data"] = self.state["share usage data"] + prefs["announcement last seen date"] = self.state["announcement last seen date"] + prefs["announcement"] = self.state["announcement"] # Save preferences. prefs.save() diff --git a/sleap/gui/web.py b/sleap/gui/web.py index d753ea16e..98f664d56 100644 --- a/sleap/gui/web.py +++ b/sleap/gui/web.py @@ -3,11 +3,16 @@ import attr import pandas as pd import requests -from typing import List, Dict, Any +from typing import List, Dict, Any, Optional, Tuple +import json +import os +from datetime import datetime REPO_ID = "talmolab/sleap" ANALYTICS_ENDPOINT = "https://analytics.sleap.ai/ping" +# TODO: Change the Bulletin URL to the main website before deploying +BULLETIN_JSON_ENDPOINT = "https://sleap.ai/develop/docs/_static/bulletin.json" @attr.s(auto_attribs=True) @@ -146,6 +151,71 @@ def get_release(self, version: str) -> Release: ) +@attr.s(auto_attribs=True) +class AnnouncementChecker: + """Checker for new announcements on the bulletin page of sleap.""" + + state: "GuiState" + _previous_announcement_date: str = None + bulletin_json_data: Optional[List[Dict[str, str]]] = None + json_data_url: str = BULLETIN_JSON_ENDPOINT + checked: bool = attr.ib(default=False, init=False) + + @property + def previous_announcement_date(self): + _previous_announcement_date = self.state["announcement last seen date"] + return _previous_announcement_date + + def check_for_bulletin_data(self) -> Optional[List[Dict]]: + """Reads the bulletin data from the JSON file endpoint.""" + try: + self.checked = True + self.bulletin_json_data = requests.get(self.json_data_url).json() + except (requests.ConnectionError, requests.Timeout): + self.bulletin_json_data = None + + def new_announcement_available(self) -> bool: + """Check if latest announcement is available.""" + if not self.checked: + self.check_for_bulletin_data() + if self.bulletin_json_data: + if self.previous_announcement_date: + latest_date = datetime.strptime( + self.bulletin_json_data[0]["date"], "%m/%d/%Y" + ) + previous_date = datetime.strptime( + self.previous_announcement_date, "%m/%d/%Y" + ) + if latest_date > previous_date: + return True + else: + return False + else: + return True + else: + return False + + def update_latest_announcement(self) -> Optional[Tuple[str, str, str]]: + """Return latest announcements on the releases page not seen by user.""" + if self.new_announcement_available(): + announcement_markdown = "" + for announcement in self.bulletin_json_data: + announcement_content = "\n".join(announcement["content"].split("\n")) + announcement_markdown += ( + "## " + + announcement["title"] + + "\n" + + announcement["date"] + + "\n" + + announcement_content + + "\n" + ) + self.state["announcement"] = announcement_markdown + self.state["announcement last seen date"] = self.bulletin_json_data[0][ + "date" + ] + + def get_analytics_data() -> Dict[str, Any]: """Gather data to be transmitted to analytics backend.""" import os diff --git a/sleap/prefs.py b/sleap/prefs.py index 3d5a2113e..17a49e029 100644 --- a/sleap/prefs.py +++ b/sleap/prefs.py @@ -5,6 +5,7 @@ """ from sleap import util +from datetime import date class Preferences(object): @@ -28,6 +29,8 @@ class Preferences(object): "node label size": 12, "show non-visible nodes": True, "share usage data": True, + "announcement last seen date": False, + "announcement": False, } _filename = "preferences.yaml" diff --git a/tests/conftest.py b/tests/conftest.py index 1b2294682..0f6c388b1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,3 +11,4 @@ from tests.fixtures.datasets import * from tests.fixtures.videos import * from tests.fixtures.models import * +from tests.fixtures.announcements import * diff --git a/tests/data/announcement_checker_bulletin/test_bulletin.json b/tests/data/announcement_checker_bulletin/test_bulletin.json new file mode 100644 index 000000000..a4b2e60c8 --- /dev/null +++ b/tests/data/announcement_checker_bulletin/test_bulletin.json @@ -0,0 +1 @@ +[{"title": "title1", "date": "10/09/2023", "content": "New announcement"}, {"title": "title2", "date": "10/07/2023", "content": "Old Announcment"}] \ No newline at end of file diff --git a/tests/fixtures/announcements.py b/tests/fixtures/announcements.py new file mode 100644 index 000000000..969c7df43 --- /dev/null +++ b/tests/fixtures/announcements.py @@ -0,0 +1,8 @@ +import pytest + +TEST_BULLETIN_JSON = "tests/data/announcement_checker_bulletin/test_bulletin.json" + + +@pytest.fixture +def bulletin_json_path(): + return TEST_BULLETIN_JSON diff --git a/tests/gui/test_web.py b/tests/gui/test_web.py index cf6bf45ec..c3c1fc397 100644 --- a/tests/gui/test_web.py +++ b/tests/gui/test_web.py @@ -1,6 +1,16 @@ import pandas as pd -from sleap.gui.web import ReleaseChecker, Release, get_analytics_data, ping_analytics +from sleap.gui.web import ( + ReleaseChecker, + Release, + AnnouncementChecker, + get_analytics_data, + ping_analytics, +) import pytest +import json +import os +from sleap.gui.commands import CommandContext +from sleap.io.dataset import Labels def test_release_from_json(): @@ -72,6 +82,55 @@ def test_release_checker(): assert checker.releases[1] != rls_test +def test_announcementchecker(): + labels = Labels() + context = CommandContext.from_labels(labels=labels) + context.state = {} + context.state["announcement last seen date"] = "10/10/2023" + # Create dummy JSON file to check + bulletin_data = [ + {"title": "title1", "date": "10/12/2023", "content": "New announcement"}, + {"title": "title2", "date": "10/07/2023", "content": "Old Announcment"}, + ] + checker = AnnouncementChecker(state=context.state, bulletin_json_data=bulletin_data) + checker.checked = True + # Check if the announcement checker gets the correct date from the app + assert checker.previous_announcement_date == "10/10/2023" + + # Check if latest announcement is fetched + is_announcement_available = checker.new_announcement_available() + assert is_announcement_available == True + + # Concatenate the bulletin content to check updated announcement text + announcement_markdown = "" + for announcement in bulletin_data: + announcement_content = "\n".join(announcement["content"].split("\n")) + announcement_markdown += ( + "## " + + announcement["title"] + + "\n" + + announcement["date"] + + "\n" + + announcement_content + + "\n" + ) + + # Check if announcement is updated + checker.update_latest_announcement() + assert context.state["announcement last seen date"] == "10/12/2023" + assert context.state["announcement"] == announcement_markdown + + # Create another dummy JSON file + bulletin_data = [ + {"title": "title1", "date": "10/09/2023", "content": "New announcement"}, + {"title": "title2", "date": "10/07/2023", "content": "Old Announcment"}, + ] + checker.bulletin_json_data = bulletin_data + # Check to ensure no new announcement is created + is_announcement_available = checker.new_announcement_available() + assert is_announcement_available == False + + def test_get_analytics_data(): analytics_data = get_analytics_data() assert "platform" in analytics_data