Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR2: Add new preference and AnnouncementChecker class #1509

Open
wants to merge 35 commits into
base: shrivaths/changelog-announcement-1
Choose a base branch
from

Conversation

shrivaths16
Copy link
Contributor

@shrivaths16 shrivaths16 commented Sep 19, 2023

Description

Adding the appropriate preferences for the date of the last seen announcement and updating the app.py class to update the preference of saving the last seen announcement date. Further, creating a class AnnouncementChecker in web.py to check and update the announcement date when a new one is published.

This is the second PR of the stack that solves the discussion #1492

Types of changes

  • Bugfix
  • New feature
  • Refactor / Code style update (no logical changes)
  • Build / CI changes
  • Documentation Update
  • Other (explain)

Does this address any currently open issues?

Outside contributors checklist

  • Review the guidelines for contributing to this repository
  • Read and sign the CLA and add yourself to the authors list
  • Make sure you are making a pull request against the develop branch (not main). Also you should start your branch off develop
  • Add tests that prove your fix is effective or that your feature works
  • Add necessary documentation (if appropriate)

Thank you for contributing to SLEAP!

❤️

Summary by CodeRabbit

  • New Features

    • Introduced an announcement system in the app to notify users of the latest changes and updates.
    • Added a new section in the documentation for version 1.3.2 of SLEAP.
  • Documentation

    • Updated the bulletin document to include a summary of the latest version.
  • Automation

    • Implemented a workflow to automatically generate structured bulletin content in JSON format from markdown files.
  • User Preferences

    • Added new user preferences to track the last seen announcement date and content.

@codecov
Copy link

codecov bot commented Sep 19, 2023

Codecov Report

All modified and coverable lines are covered by tests ✅

❗ No coverage uploaded for pull request base (shrivaths/changelog-announcement-1@0c9b54d). Click here to learn what that means.

Additional details and impacted files
@@                          Coverage Diff                          @@
##             shrivaths/changelog-announcement-1    #1509   +/-   ##
=====================================================================
  Coverage                                      ?   73.39%           
=====================================================================
  Files                                         ?      134           
  Lines                                         ?    24069           
  Branches                                      ?        0           
=====================================================================
  Hits                                          ?    17666           
  Misses                                        ?     6403           
  Partials                                      ?        0           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@shrivaths16 shrivaths16 requested a review from roomrys September 28, 2023 07:06
@shrivaths16 shrivaths16 marked this pull request as ready for review September 28, 2023 16:25
@coderabbitai
Copy link

coderabbitai bot commented Sep 28, 2023

Walkthrough

The update introduces a system to inform users about new announcements in the SLEAP application. A Python script now generates a JSON file from a markdown bulletin, which the app reads to display recent updates. The GUI tracks the last seen announcement, ensuring users stay informed without repetition. The workflow automation ensures the JSON is updated before website deployment.

Changes

Files Summary
docs/bulletin.md Added a new section for SLEAP v1.3.2 including a description and links to previous versions.
docs/.../make_bulletin_json.py Introduced a script to convert markdown bulletin to JSON format.
sleap/gui/app.py, sleap/prefs.py Updated to check for new announcements, manage related preferences, and include announcement tracking.
sleap/gui/web.py Added AnnouncementChecker class to handle announcement updates and related functionality.
.github/workflows/website.yml Updated workflow to generate JSON bulletin before deployment.

🐇
"In the code's burrow, a whisper arose,
'New tales to share!' the software proposes.
With each update's sunbeam, our knowledge grows,
As the bulletin's story, like a river, flows." 🌱

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

Share

Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>.
    • Generate unit-tests for this file.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit tests for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai generate interesting stats about this repository from git and render them as a table.
    • @coderabbitai show all the console.log statements in this repository.
    • @coderabbitai read src/utils.ts and generate unit tests.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (invoked as PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger a review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai help to get help.

Additionally, you can add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.

CodeRabbit Configration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • The JSON schema for the configuration file is available here.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/coderabbit-overrides.v2.json

CodeRabbit Discord Community

Join our Discord Community to get help, request features, and share feedback.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Status

Actionable comments generated: 8

Commits Files that changed from the base of the PR and between 8ab2e6c and 0db2904.
Files selected for processing (5)
  • docs/bulletin.md (1 hunks)
  • docs/make_bulletin_json.py (1 hunks)
  • sleap/gui/app.py (4 hunks)
  • sleap/gui/web.py (2 hunks)
  • sleap/prefs.py (2 hunks)
Files skipped from review due to trivial changes (2)
  • docs/bulletin.md
  • sleap/prefs.py
Additional comments (Suppressed): 4
sleap/gui/app.py (4)
  • 67-70: The import statement has been updated to include the AnnouncementChecker class from sleap.gui.web. This is in line with the changes described in the PR summary.

  • 152-156: Two new state variables, "announcement last seen date" and "announcement", have been added. These are initialized with the corresponding values from preferences. Ensure that these preference keys are always present in the prefs object to avoid KeyError.

  • 166-167: An instance of the AnnouncementChecker class is created. This aligns with the PR summary which mentions the addition of this class for checking new announcements.

  • 218-222: The state variables "announcement last seen date" and "announcement" are saved back into the preferences before saving the preferences. This ensures that the latest announcement data is stored persistently.

docs/make_bulletin_json.py Outdated Show resolved Hide resolved
Comment on lines 7 to 24
def generate_json_file():
with open(input_md_file, 'r', encoding='utf-8') as md_file:
markdown_content = md_file.read()
bulletin_json = []
content = ''

for line in markdown_content.split('\n'):
if line.startswith('---'):
bulletin_json.append({'title': title, 'date': date, 'content':content})
content = ''
elif line.startswith('##'):
title = line[3:].strip()
elif line.startswith('_'):
date = line[1:len(line)-1].strip()
else:
content += (line + '\n')
# Append last section
bulletin_json.append({'title': title, 'date': date, 'content':content})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code assumes that the markdown file will always have a specific structure (i.e., sections start with ---, titles start with ##, and dates start with _). If the markdown file does not follow this structure, the script may fail or produce incorrect results. Consider adding error handling to ensure that the markdown file follows the expected structure.

else:
content += (line + '\n')
# Append last section
bulletin_json.append({'title': title, 'date': date, 'content':content})
Copy link

@coderabbitai coderabbitai bot Sep 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variables title and date are used before they are defined. This could lead to a NameError if the first line of the markdown file does not start with ## or _. Initialize these variables before the loop to avoid potential errors.

- content = ''
+ title = date = content = ''

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty easy preventative measure. Maybe we can set defaults and raise a warning if the parser never overrides the default value?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@roomrys, that's a great suggestion. We can initialize title and date with default values and then check if they remain unchanged after parsing the markdown file. If they do, we can raise a warning to alert the user. Here's how we could modify the code:

import json
import warnings

# Set the file paths
input_md_file = 'D:\TalmoLab\sleap\docs\bulletin.md'
output_json_file = 'D:\TalmoLab\sleap\docs\\bulletin.json'

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'):
        if 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 line.startswith('_'):
            date = line[1:len(line)-1].strip()
        else:
            content += (line + '\n')
    # Append last section   
    bulletin_json.append({'title': title, 'date': date, 'content':content})

    # Check if title or date were never updated from their default values
    if title == "DEFAULT_TITLE" or date == "DEFAULT_DATE":
        warnings.warn("The parser did not find a title or date in the markdown file.")

    with open(output_json_file, 'w', encoding='utf-8') as json_file:
        json.dump(bulletin_json, json_file, ensure_ascii=False, indent=4)

if __name__ == '__main__':
    generate_json_file()

This way, we can ensure that the script doesn't fail due to uninitialized variables and also alert the user if the markdown file is not formatted as expected.

elif line.startswith('##'):
title = line[3:].strip()
elif line.startswith('_'):
date = line[1:len(line)-1].strip()
Copy link

@coderabbitai coderabbitai bot Sep 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The date extraction logic assumes that the date is surrounded by underscores (_). If the date format changes or if there are additional underscores in the date string, this could lead to incorrect results. Consider using a more robust method for extracting the date, such as regular expressions.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We plan on sticking with this format, but perhaps we should add a test that the current bulletin.md changes can be parsed correctly. I'm curious, can you give an example of how you would parse the date?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@roomrys: Sure, I can provide an example using regular expressions which is a more robust way to parse the date. Here's how you could modify your code:

import re

# ...

for line in markdown_content.split('\n'):
    if line.startswith('---'):
        bulletin_json.append({'title': title, 'date': date, 'content':content})
        content = ''
    elif line.startswith('##'):
        title = line[3:].strip()
    else:
        match = re.search(r'_([^_]+)_', line)
        if match:
            date = match.group(1).strip()
        else:
            content += (line + '\n')

In this code, re.search(r'_([^_]+)_', line) looks for a pattern where there are any characters between two underscores. The parentheses in the regular expression create a group, and match.group(1) gets the contents of the first group. This will correctly extract the date even if there are additional underscores in the line.

For testing, you could create a separate function that takes a string as input and returns the parsed title, date, and content. Then, you can write tests that call this function with different inputs and check that the output is as expected.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically the same logic, but with regex which is more difficult to understand imo.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@roomrys: I understand your concern. Regular expressions can indeed be a bit difficult to grasp initially, but they offer a lot of flexibility and robustness when it comes to text parsing. In this case, the regular expression is used to ensure that the date extraction works correctly even if there are additional underscores in the line.

However, if you prefer to stick with the current approach, it's important to make sure that the format of the dates in the markdown file remains consistent. Any changes could potentially break the date extraction logic. As you suggested earlier, adding tests to verify that the parsing works correctly would be a good way to catch any issues early on.

If you have any other questions or need further clarification, feel free to ask!

sleap/gui/web.py Outdated


REPO_ID = "talmolab/sleap"
ANALYTICS_ENDPOINT = "https://analytics.sleap.ai/ping"
BULLETIN_JSON = "D:\TalmoLab\sleap\docs\\bulletin.json"
Copy link

@coderabbitai coderabbitai bot Sep 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path to the bulletin.json file is hardcoded. This could lead to issues if the file is not located at the specified path on a different machine or environment. Consider making this a configurable option, possibly through an environment variable or configuration file.

- BULLETIN_JSON = "D:\TalmoLab\sleap\docs\\bulletin.json"
+ BULLETIN_JSON = os.getenv('BULLETIN_JSON_PATH', default_path)

In the above code, default_path should be the default location of bulletin.json in your project structure. This way, you can override it with an environment variable when necessary.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is for testing purposes as we don't have the bulletin online yet, but to allow others (me) to test your branch as well, can we instead use relative paths to the location of BULLETIN_JSON?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@roomrys: Absolutely, using relative paths is a good idea for portability and ease of testing. Here's how you can modify the BULLETIN_JSON path to be relative to the current file's directory:

import os

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
BULLETIN_JSON = os.path.join(BASE_DIR, "..", "docs", "bulletin.json")

In this code, os.path.dirname(os.path.abspath(__file__)) gets the directory that the current Python script is in. The os.path.join() function then constructs a filepath by joining the base directory with the relative path to bulletin.json.

This way, regardless of where you run the script from, it will always correctly locate bulletin.json as long as the relative structure of the project remains the same.

sleap/gui/web.py Outdated
Comment on lines 159 to 171
def check_for_announcement(self) -> bool:
"""Returns if new announcements are available."""
try:
# Attempt to open the file in read mode
with open(self.bulletin_json_path, 'r', encoding='utf-8') as jsf:
# Load the JSON data into a Python data structure
data = json.load(jsf)
latest_data = data[0]

if latest_data['date'] != self.previous_announcement_date:
return True
except FileNotFoundError:
return False
Copy link

@coderabbitai coderabbitai bot Sep 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check_for_announcement method opens and reads the JSON file every time it's called. This could be inefficient if the method is called frequently. Consider reading the file once and storing the data in memory, then updating it as needed.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai This will eventually be a ping yielding an online JSON response, would that change your response? Otherwise, what would you suggest with this new information?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@roomrys If the JSON data is going to be fetched from an online source, then it's reasonable to fetch the data each time you need it. However, if the frequency of checking for announcements is high, you might still want to consider caching the response and updating it at regular intervals to reduce network traffic and latency.

Here's a simple way to implement caching:

import time

@attr.s(auto_attribs=True)
class AnnouncementChecker:
    """Checker for new announcements on the bulletin page of sleap."""

    bulletin_json_path: str = BULLETIN_JSON
    previous_announcement_date: str = prefs.prefs["announcement last seen date"]
    cache_expiry_time: int = 3600  # Cache expiry time in seconds (1 hour)
    _cache_timestamp: float = 0.0
    _cached_data: Optional[Dict] = None

    def _fetch_data(self) -> Dict:
        """Fetches data from the JSON file or online source."""
        with open(self.bulletin_json_path, 'r', encoding='utf-8') as jsf:
            return json.load(jsf)

    def _get_data(self) -> Dict:
        """Gets data from cache if valid, otherwise fetches and updates cache."""
        if self._cached_data is None or time.time() - self._cache_timestamp > self.cache_expiry_time:
            self._cached_data = self._fetch_data()
            self._cache_timestamp = time.time()
        return self._cached_data

    def check_for_announcement(self) -> bool:
        """Returns if new announcements are available."""
        try:
            data = self._get_data()
            latest_data = data[0]
            
            if latest_data['date'] != self.previous_announcement_date:
                return True
        except FileNotFoundError:
            return False

This code will fetch the data from the source only if it's not in the cache or if the cache has expired. The expiry time can be adjusted according to your needs.

sleap/gui/web.py Outdated
Comment on lines 173 to 185
def get_latest_announcement(self) -> Optional[Tuple[str, str]]:
"""Return latest announcements on the releases page not seen by user."""
success = self.check_for_announcement()
if success:
# Attempt to open the file in read mode
with open(self.bulletin_json_path, 'r', encoding='utf-8') as jsf:
# Load the JSON data into a Python data structure
data = json.load(jsf)
latest_data = data[0]

if latest_data['date'] != self.previous_announcement_date:
return (latest_data['date'], latest_data['content'])
return None
Copy link

@coderabbitai coderabbitai bot Sep 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The get_latest_announcement method also opens and reads the JSON file, duplicating the logic in check_for_announcement. To avoid repeating this operation, consider refactoring these methods to share the file reading and parsing logic.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a bad idea... maybe we can create a helper function just for reading the data and only use this reading function once in check_for_announcement where we can store the data in an attribute for access between both functions?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@roomrys: That's a great suggestion! By creating a helper function to read the JSON data, we can avoid duplicating code and improve efficiency. Here's how you might refactor the AnnouncementChecker class:

@attr.s(auto_attribs=True)
class AnnouncementChecker:
    """Checker for new announcements on the bulletin page of sleap."""

    bulletin_json_path: str = BULLETIN_JSON
    previous_announcement_date: str = prefs.prefs["announcement last seen date"]
    _latest_data: Optional[Dict[str, str]] = None

    def _read_bulletin_data(self) -> Dict[str, str]:
        """Reads the bulletin data from the JSON file."""
        try:
            with open(self.bulletin_json_path, 'r', encoding='utf-8') as jsf:
                data = json.load(jsf)
                return data[0]
        except FileNotFoundError:
            return {}

    def check_for_announcement(self) -> bool:
        """Returns if new announcements are available."""
        self._latest_data = self._read_bulletin_data()
        if self._latest_data and self._latest_data['date'] != self.previous_announcement_date:
            return True
        return False

    def get_latest_announcement(self) -> Optional[Tuple[str, str]]:
        """Return latest announcements on the releases page not seen by user."""
        if self._latest_data and self._latest_data['date'] != self.previous_announcement_date:
            return (self._latest_data['date'], self._latest_data['content'])
        return None

In this refactored version, _read_bulletin_data is a helper method that reads the JSON file and returns the latest data. This method is called once in check_for_announcement, and the result is stored in the _latest_data attribute. Then, get_latest_announcement can use this stored data without needing to read the file again.

Copy link
Contributor Author

@shrivaths16 shrivaths16 Oct 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case where are we calling the check_for_announcement function? Since we don't call the function in the get_latest_announcement, self._latest_data would be None always. So, can we just have one function that checks and returns an announcement if there is one? @coderabbitai do you suggest something else.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like humans are talking. I'll be quiet for now. If you need me again, just tag @coderabbitai in a new comment.

Copy link
Contributor Author

@shrivaths16 shrivaths16 Oct 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shrivaths16: You're correct. The check_for_announcement function needs to be called before get_latest_announcement to ensure that _latest_data is not None. If we want to simplify the interface and avoid this dependency, we could indeed merge these two functions into one. Here's how you might do it:

@attr.s(auto_attribs=True)
class AnnouncementChecker:
    """Checker for new announcements on the bulletin page of sleap."""

    bulletin_json_path: str = BULLETIN_JSON
    previous_announcement_date: str = prefs.prefs["announcement last seen date"]

    def _read_bulletin_data(self) -> Dict[str, str]:
        """Reads the bulletin data from the JSON file."""
        try:
            with open(self.bulletin_json_path, 'r', encoding='utf-8') as jsf:
                data = json.load(jsf)
                return data[0]
        except FileNotFoundError:
            return {}

    def get_latest_announcement(self) -> Optional[Tuple[str, str]]:
        """Return latest announcements on the releases page not seen by user."""
        latest_data = self._read_bulletin_data()
        if latest_data and latest_data['date'] != self.previous_announcement_date:
            return (latest_data['date'], latest_data['content'])
        return None

In this refactored version, get_latest_announcement calls _read_bulletin_data directly and checks if there's a new announcement. This way, there's no need to call check_for_announcement separately.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@roomrys does this work, or do we need to store the bulletin JSON file as an attribute after reading it?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this simplifies things, no need to have something (check_for_announcement) that we don't really need.

sleap/gui/web.py Outdated
Comment on lines 187 to 192
def update_announcement(self):
"""Update the last seen date of announcement in preferences."""
announcement = self.get_latest_announcement()
if announcement is not None:
prefs.prefs["announcement last seen date"] = announcement[0]
prefs.prefs["announcement"] = announcement[1]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The update_announcement method does not handle the case where the get_latest_announcement method returns None. If announcement is None, attempting to index into it will raise a TypeError. Add error handling to prevent this.

  def update_announcement(self):
      """Update the last seen date of announcement in preferences."""
      announcement = self.get_latest_announcement()
+     if announcement is None:
+         return
      prefs.prefs["announcement last seen date"] = announcement[0]
      prefs.prefs["announcement"] = announcement[1]

Copy link
Collaborator

@roomrys roomrys left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a test for the AnnouncementChecker class would be beneficial.

docs/bulletin.md Outdated Show resolved Hide resolved
elif line.startswith('##'):
title = line[3:].strip()
elif line.startswith('_'):
date = line[1:len(line)-1].strip()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We plan on sticking with this format, but perhaps we should add a test that the current bulletin.md changes can be parsed correctly. I'm curious, can you give an example of how you would parse the date?

else:
content += (line + '\n')
# Append last section
bulletin_json.append({'title': title, 'date': date, 'content':content})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty easy preventative measure. Maybe we can set defaults and raise a warning if the parser never overrides the default value?

sleap/gui/web.py Outdated


REPO_ID = "talmolab/sleap"
ANALYTICS_ENDPOINT = "https://analytics.sleap.ai/ping"
BULLETIN_JSON = "D:\TalmoLab\sleap\docs\\bulletin.json"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is for testing purposes as we don't have the bulletin online yet, but to allow others (me) to test your branch as well, can we instead use relative paths to the location of BULLETIN_JSON?

sleap/gui/web.py Outdated
Comment on lines 168 to 169
if latest_data['date'] != self.previous_announcement_date:
return True
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we instead convert both the latest_date['date'] and self.previous_announcement_date to a format that allows >, < comparison? I see this erroring when the self.previous_announcement_date > latest_date['date'] and still showing the announcement although it was already seen.

sleap/gui/web.py Outdated
Comment on lines 159 to 171
def check_for_announcement(self) -> bool:
"""Returns if new announcements are available."""
try:
# Attempt to open the file in read mode
with open(self.bulletin_json_path, 'r', encoding='utf-8') as jsf:
# Load the JSON data into a Python data structure
data = json.load(jsf)
latest_data = data[0]

if latest_data['date'] != self.previous_announcement_date:
return True
except FileNotFoundError:
return False
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai This will eventually be a ping yielding an online JSON response, would that change your response? Otherwise, what would you suggest with this new information?

sleap/gui/web.py Outdated
previous_announcement_date: str = prefs.prefs["announcement last seen date"]

def check_for_announcement(self) -> bool:
"""Returns if new announcements are available."""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically always intended to return since we catch the error.

Suggested change
"""Returns if new announcements are available."""
"""Returns True if new announcements are available, otherwise returns False."""

sleap/gui/web.py Outdated
Comment on lines 173 to 185
def get_latest_announcement(self) -> Optional[Tuple[str, str]]:
"""Return latest announcements on the releases page not seen by user."""
success = self.check_for_announcement()
if success:
# Attempt to open the file in read mode
with open(self.bulletin_json_path, 'r', encoding='utf-8') as jsf:
# Load the JSON data into a Python data structure
data = json.load(jsf)
latest_data = data[0]

if latest_data['date'] != self.previous_announcement_date:
return (latest_data['date'], latest_data['content'])
return None
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a bad idea... maybe we can create a helper function just for reading the data and only use this reading function once in check_for_announcement where we can store the data in an attribute for access between both functions?

sleap/gui/web.py Outdated
Comment on lines 191 to 192
prefs.prefs["announcement last seen date"] = announcement[0]
prefs.prefs["announcement"] = announcement[1]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The prefs.prefs variable should only be read in at opening and written to at closing the program. For intermediate states, we use MainWindow.state. The prefs.prefs variable stores the user preferences which are written to a file to be remembered each time the GUI is opened/closed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we directly use MainWindow.state["announcement last seen date"] or do we have to use it in some other way?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See other comment:

For initialization, we would need to pass in the instance of MainWindow to access its state attribute. For example, this is how we initialize the CommandContext:

sleap/sleap/gui/app.py

Lines 123 to 125 in ed77b49

self.commands = CommandContext(
state=self.state, app=self, update_callback=self.on_data_update
)

. Here, self is the instance of MainWindow, and this bit of code resides in MainWindow.__init__. We just won't know the value of prefs if you use it for a default for previous_announcement_date, I expect it will error as an unbound variable.

sleap/gui/web.py Outdated
"""Checker for new announcements on the bulletin page of sleap."""

bulletin_json_path: str = BULLETIN_JSON
previous_announcement_date: str = prefs.prefs["announcement last seen date"]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The prefs.prefs variable should only be read in at opening and written to at closing the program. For intermediate states, we use MainWindow.state. The prefs.prefs variable stores the user preferences which are written to a file to be remembered each time the GUI is opened/closed. Also, I don't think that this class would know what the prefs variable is if we ran it in some test code - maybe add both a fixture (for the bulletin.md) and a test that runs through all the methods in the announcement checker?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we directly use MainWindow.state["announcement last seen date"] or do we have to use it in some other way?

Copy link
Collaborator

@roomrys roomrys Oct 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For initialization, we would need to pass in the instance of MainWindow to access its state attribute. For example, this is how we initialize the CommandContext:

sleap/sleap/gui/app.py

Lines 123 to 125 in ed77b49

self.commands = CommandContext(
state=self.state, app=self, update_callback=self.on_data_update
)

. Here, self is the instance of MainWindow, and this bit of code resides in MainWindow.__init__. We just won't know the value of prefs if you use it for a default for previous_announcement_date and initialize AnnouncementChecker as AnnouncementChecker() with no keyword arguments passed in; I expect it will error as an unbound variable.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Status

Actionable comments generated: 5

Commits Files that changed from the base of the PR and between 0db2904 and 6eddcf0.
Files selected for processing (3)
  • docs/make_bulletin_json.py (1 hunks)
  • sleap/gui/app.py (4 hunks)
  • sleap/gui/web.py (2 hunks)
Additional comments (Suppressed): 5
sleap/gui/app.py (4)
  • 67-70: The import of AnnouncementChecker is new and seems to be part of the feature addition. Ensure that this class is implemented correctly and handles any potential exceptions, especially when reading from the JSON file.

  • 152-156: New state variables "announcement last seen date" and "announcement" are being initialized from preferences. Make sure these preferences are always available or have default values to prevent potential KeyError.

  • 166-169: An instance of AnnouncementChecker is created with the application itself as an argument. This could potentially lead to circular dependencies if not handled carefully. Also, ensure that the AnnouncementChecker class has proper error handling for any issues that might arise during its operation.

  • 220-224: The new preferences "announcement last seen date" and "announcement" are being saved. Ensure that these values are always in a savable format (e.g., serializable) to prevent any errors during saving.

sleap/gui/web.py (1)
  • 3-14: The new hunk introduces additional imports and defines two new global variables: BASE_DIR and BULLETIN_JSON. The BASE_DIR variable is used to get the directory of the current file, while BULLETIN_JSON is used to define the path to the bulletin JSON file. These changes seem appropriate for the new feature being introduced.

Comment on lines 1 to 6
import json
import os

# Set the file paths
input_md_file = os.path.join(os.path.dirname(__file__), 'bulletin.md')
output_json_file = os.path.join(os.path.dirname(__file__), 'bulletin.json')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script uses absolute paths to the markdown and JSON files. This could lead to issues if the script is run from a different directory or on a different machine where the file paths may not be the same. Consider using relative paths instead.

- input_md_file = os.path.join(os.path.dirname(__file__), 'bulletin.md')
- output_json_file = os.path.join(os.path.dirname(__file__), 'bulletin.json')
+ input_md_file = 'bulletin.md'
+ output_json_file = 'bulletin.json'

Comment on lines 19 to 24
if 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"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script assumes that each section in the markdown file starts with ---. If this format changes, the script might not correctly identify the start of a new section. Consider adding a comment to document this assumption or use a more robust method to identify section boundaries.

Comment on lines 26 to 30
title = line[3:].strip()
elif line.startswith('_'):
date = line[1:len(line)-1].strip()
else:
content += (line + '\n')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script assumes that the title of a section starts with ## and the date starts with _. If these formats change, the script might not correctly extract the title and date. Consider adding comments to document these assumptions or use a more robust method to extract the title and date.

Comment on lines 34 to 35
with open(output_json_file, 'w', encoding='utf-8') as json_file:
json.dump(bulletin_json, json_file, ensure_ascii=False, indent=4)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script writes the generated JSON data to a file without checking if the file already exists. If the file does exist, its contents will be overwritten. Consider checking if the file exists before writing to it and prompt the user for confirmation if it does.

+ if os.path.exists(output_json_file):
+     print(f"File {output_json_file} already exists. Overwrite? (y/n)")
+     response = input().strip().lower()
+     if response != 'y':
+         return
  with open(output_json_file, 'w', encoding='utf-8') as json_file:
      json.dump(bulletin_json, json_file, ensure_ascii=False, indent=4)

sleap/gui/web.py Outdated
Comment on lines 150 to 187
)


@attr.s(auto_attribs=True)
class AnnouncementChecker:
"""Checker for new announcements on the bulletin page of sleap."""

app: "MainWindow"
bulletin_json_path: str = BULLETIN_JSON
previous_announcement_date: str = app.state["announcement last seen date"]
_latest_data: Optional[Dict[str, str]] = None

def _read_bulletin_data(self) -> Dict[str, str]:
"""Reads the bulletin data from the JSON file."""
try:
with open(self.bulletin_json_path, 'r', encoding='utf-8') as jsf:
data = json.load(jsf)
return data[0]
except FileNotFoundError:
return {}

def get_latest_announcement(self) -> Optional[Tuple[str, str]]:
"""Return latest announcements on the releases page not seen by user."""
self._latest_datalatest_data = self._read_bulletin_data()
if self._latest_data and self._latest_data['date'] != self.previous_announcement_date:
return (self._latest_data['date'], self._latest_data['content'])
return None

def update_announcement(self):
"""Update the last seen date of announcement in preferences."""
announcement = self.get_latest_announcement()
if announcement is None:
return
self.app.state["announcement last seen date"] = announcement[0]
self.app.state["announcement"] = announcement[1]


def get_analytics_data() -> Dict[str, Any]:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new hunk introduces a new class AnnouncementChecker with methods to read bulletin data from a JSON file, retrieve the latest unseen announcement, and update the last seen date in preferences. However, there's an issue on line 173 where self._latest_datalatest_data should be self._latest_data.

- self._latest_datalatest_data = self._read_bulletin_data()
+ self._latest_data = self._read_bulletin_data()

Also, the attribute previous_announcement_date (line 159) is initialized using app.state["announcement last seen date"] which might raise a KeyError if the key doesn't exist in the app state. It would be safer to use app.state.get("announcement last seen date") which will return None if the key doesn't exist.

- previous_announcement_date: str = app.state["announcement last seen date"]
+ previous_announcement_date: str = app.state.get("announcement last seen date")

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Status

Actionable comments generated: 1

Commits Files that changed from the base of the PR and between 6eddcf0 and 3f99b8b.
Files selected for processing (1)
  • sleap/gui/web.py (2 hunks)
Additional comments (Suppressed): 1
sleap/gui/web.py (1)
  • 3-14: The new hunk introduces additional imports and constants. The json and os modules are imported, and two new constants BASE_DIR and BULLETIN_JSON are defined. These changes seem to be necessary for the new feature of tracking and displaying announcements.

sleap/gui/web.py Outdated
Comment on lines 150 to 185
)


@attr.s(auto_attribs=True)
class AnnouncementChecker:
"""Checker for new announcements on the bulletin page of sleap."""

app: "MainWindow"
bulletin_json_path: str = BULLETIN_JSON
previous_announcement_date: str = None
_latest_data: Optional[Dict[str, str]] = None

def _read_bulletin_data(self) -> Dict[str, str]:
"""Reads the bulletin data from the JSON file."""
try:
with open(self.bulletin_json_path, 'r', encoding='utf-8') as jsf:
data = json.load(jsf)
return data[0]
except FileNotFoundError:
return {}

def get_latest_announcement(self) -> Optional[Tuple[str, str]]:
"""Return latest announcements on the releases page not seen by user."""
self._latest_data = self._read_bulletin_data()
if self._latest_data and self._latest_data['date'] != self.previous_announcement_date:
return (self._latest_data['date'], self._latest_data['content'])
return None

def update_announcement(self):
"""Update the last seen date of announcement in preferences."""
announcement = self.get_latest_announcement()
if announcement is None:
return
self.app.state["announcement last seen date"] = announcement[0]
self.app.state["announcement"] = announcement[1]

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A new class AnnouncementChecker is introduced. This class has methods to read bulletin data from a JSON file, get the latest announcement not seen by the user, and update the last seen date of the announcement in preferences.

However, there's a potential issue with the get_latest_announcement method. It checks if the latest data's date is not equal to the previous announcement date. But it doesn't handle the case where the date key might not exist in _latest_data. This could lead to a KeyError.

Here's a suggested fix:

        self._latest_data = self._read_bulletin_data()
-       if self._latest_data and self._latest_data['date'] != self.previous_announcement_date:
+       if self._latest_data and 'date' in self._latest_data and self._latest_data['date'] != self.previous_announcement_date:
            return (self._latest_data['date'], self._latest_data['content'])
        return None

@coderabbitai
Copy link

coderabbitai bot commented Oct 12, 2023

CodeRabbit review skipped

By default, CodeRabbit only reviews PRs on the default branch. If you wish to have PRs reviewed on additional branches, you can configure that under the repository settings in the UI.

@shrivaths16 shrivaths16 requested a review from roomrys October 16, 2023 20:06
tests/gui/test_web.py Outdated Show resolved Hide resolved
sleap/gui/web.py Outdated Show resolved Hide resolved
tests/gui/test_web.py Outdated Show resolved Hide resolved
@shrivaths16 shrivaths16 requested a review from roomrys October 18, 2023 23:27
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Status

Actionable comments generated: 3

Configuration used: CodeRabbit UI

Commits Files that changed from the base of the PR and between 21ea789 and 3cc4188.
Files ignored due to filter (1)
  • tests/data/announcement_checker_bulletin/test_bulletin.json
Files selected for processing (2)
  • sleap/gui/web.py (2 hunks)
  • tests/gui/test_web.py (2 hunks)
Additional comments: 2
sleap/gui/web.py (1)
  • 154-207: The PR objective mentions adding new preferences to the Preferences class, but the provided code does not show any changes to Preferences. Please verify that the changes to Preferences are included in the PR or update the PR objective accordingly.
tests/gui/test_web.py (1)
  • 1-13: The imports for AnnouncementChecker, json, os, CommandContext, and Labels are correctly added in accordance with the PR objective and the summary.

Comment on lines 85 to 125
def test_announcementchecker(bulletin_json_path):

labels = Labels()
context = CommandContext.from_labels(labels=labels)
context.state = {}
context.state["announcement last seen date"] = "10/10/2023"
checker = AnnouncementChecker(
state=context.state, bulletin_json_path=bulletin_json_path
)

# Check if the announcement checker gets the correct date from the app
assert checker.previous_announcement_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"},
]
with open(bulletin_json_path, "w") as test_file:
json.dump(bulletin_data, test_file)

# Check if latest announcement is fetched
announcement = checker.get_latest_announcement()
assert announcement == ("title1", "10/12/2023", "New announcement")

# Check if announcement is updated
checker.update_announcement()
assert context.state["announcement last seen date"] == "10/12/2023"
assert context.state["announcement"] == "New announcement"

# Create dummy JSON file
bulletin_data = [
{"title": "title1", "date": "10/09/2023", "content": "New announcement"},
{"title": "title2", "date": "10/07/2023", "content": "Old Announcment"},
]
with open(bulletin_json_path, "w") as test_file:
json.dump(bulletin_data, test_file)

# Check to ensure no new announcement is created
announcement = checker.get_latest_announcement()
assert announcement == None
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test_announcementchecker function should include teardown code to remove the dummy JSON file created during the test to prevent any side effects on subsequent tests or when the tests are run in a different environment.

def test_announcementchecker(bulletin_json_path):
    # ... existing test code ...

    # Teardown code to remove the dummy JSON file
    os.remove(bulletin_json_path)

sleap/gui/web.py Outdated
Comment on lines 200 to 206
def update_announcement(self):
"""Update the last seen date of announcement in preferences."""
announcement = self.get_latest_announcement()
if announcement is None:
return
self.state["announcement last seen date"] = announcement[1]
self.state["announcement"] = announcement[2]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The update_announcement method assumes that announcement is a tuple with at least two elements. If get_latest_announcement returns a tuple with fewer elements or None, this will raise an IndexError. Consider adding a check to ensure that announcement has the expected structure before accessing its elements.

sleap/gui/web.py Outdated
Comment on lines 178 to 188
def new_announcement_available(self):
self._read_bulletin_data()
latest_date = datetime.strptime(self._latest_data["date"], "%m/%d/%Y")
previous_date = datetime.strptime(self.previous_announcement_date, "%m/%d/%Y")
if (
self._latest_data
and self.previous_announcement_date
and latest_date > previous_date
):
return True
return False
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new_announcement_available property does not handle the case where _latest_data is None, which can occur if the bulletin JSON file is not found or is empty. This will lead to a TypeError when attempting to parse None as a date. Consider adding a check to ensure that _latest_data is not None before attempting to parse dates.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Status

Actionable comments generated: 2

Configuration used: CodeRabbit UI

Commits Files that changed from the base of the PR and between 59f07ae and acdf130.
Files selected for processing (1)
  • sleap/prefs.py (2 hunks)
Additional comments: 1
sleap/prefs.py (1)
  • 29-36: > Note: This review was outside the patches, so it was mapped to the patch with the greatest overlap. Original lines [5-36]

The addition of new preferences to the Preferences class aligns with the PR objectives and the summary provided. Ensure that the default values for these preferences are appropriate for their intended use.

@@ -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,
Copy link

@coderabbitai coderabbitai bot Dec 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default value for "announcement last seen date" is set to False, which is not a date type. Consider using None or a specific date format if the intention is to store a date.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, do this.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@talmo I've updated the default value for "announcement last seen date" to None to reflect that it should store a date type. Here's the updated code snippet:

_defaults = {
    # ... other defaults ...
    "announcement last seen date": None,
    # ... other defaults ...
}

Please ensure to handle this None value appropriately in the code where the announcement last seen date preference is accessed or updated.

@@ -28,6 +29,8 @@
"node label size": 12,
"show non-visible nodes": True,
"share usage data": True,
"announcement last seen date": False,
"announcement": False,
Copy link

@coderabbitai coderabbitai bot Dec 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default value for "announcement" is set to False. If this preference is intended to store a string, consider using an empty string "" or None as a more appropriate default value.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, do this.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@talmo I've noted the change for the default value of "announcement" to be an empty string or None. Since I cannot commit code, here's the suggested update for the Preferences class defaults:

_defaults = {
    # ... other defaults ...
    "announcement last seen date": None,
    "announcement": "",
}

You can apply this change to the prefs.py file. If you need any further assistance, feel free to ask!

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Status

Actionable comments generated: 0

Configuration used: CodeRabbit UI

Commits Files that changed from the base of the PR and between acdf130 and 68cb64b.
Files selected for processing (1)
  • .github/workflows/website.yml (1 hunks)
Files skipped from review due to trivial changes (1)
  • .github/workflows/website.yml

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Status

Actionable comments generated: 0

Configuration used: CodeRabbit UI

Commits Files that changed from the base of the PR and between 68cb64b and 2c92712.
Files selected for processing (1)
  • .github/workflows/website.yml (2 hunks)
Additional comments: 2
.github/workflows/website.yml (2)
  • 12-12: Adding shrivaths/changelog-announcement-2 to the trigger list will allow the workflow to run when changes are pushed to this branch. This seems to be in line with the PR's objectives to support new features related to announcements.

  • 52-52: The addition of the python make_bulletin_json.py step is necessary to convert the bulletin content into JSON format as per the PR objectives. Ensure that the script make_bulletin_json.py is present in the repository and that it is designed to be run from the docs directory as the current working directory is changed to docs before running the script.

Verification successful

The script has successfully located the make_bulletin_json.py file within the docs directory of the repository. This confirms that the file is present where expected and supports the PR's objective to convert bulletin content into JSON format.

Scripts Executed

The following scripts were executed for the analysis:


Script:

#!/bin/bash
# Verify the presence of make_bulletin_json.py in the docs directory.
fd make_bulletin_json.py --exec test -f {}

Length of output: 82



Script:

#!/bin/bash
# Verify the presence of make_bulletin_json.py in the repository.
fd make_bulletin_json.py

Length of output: 53

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Status

Actionable comments generated: 1

Configuration used: CodeRabbit UI

Commits Files that changed from the base of the PR and between 2c92712 and 303a2d2.
Files selected for processing (1)
  • docs/make_bulletin_json.py (1 hunks)
Additional comments: 2
docs/make_bulletin_json.py (2)
  • 1-6: The script correctly imports necessary modules and sets file paths using os.path.join for better OS compatibility.

  • 42-43: The script execution condition is correctly placed to allow the function to be called when the script is run directly.

Comment on lines 9 to 43
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 line.startswith("_"):
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("bulletin.json", "w", encoding="utf-8") as json_file:
json.dump(bulletin_json, json_file, ensure_ascii=False, indent=4)


if __name__ == "__main__":
generate_json_file()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generate_json_file function is well-structured and follows a clear logic for parsing the Markdown file and generating JSON. However, there are a few points to consider:

  1. The script assumes a specific Markdown format. Ensure that the Markdown files follow this format strictly, or the script may not parse them correctly.
  2. The script does not handle the case where the Markdown file does not start with a title or a date. It may be beneficial to add error handling or checks for the expected format.
  3. The output JSON file path is hardcoded in the open function (line 38), which could be replaced with the output_json_file variable defined earlier for consistency and maintainability.
  4. There is no error handling for file operations which could be problematic if the files don't exist or there are permission issues.
- with open("bulletin.json", "w", encoding="utf-8") as json_file:
+ with open(output_json_file, "w", encoding="utf-8") as json_file:

Committable suggestion

IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
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 line.startswith("_"):
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("bulletin.json", "w", encoding="utf-8") as json_file:
json.dump(bulletin_json, json_file, ensure_ascii=False, indent=4)
if __name__ == "__main__":
generate_json_file()
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 line.startswith("_"):
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(output_json_file, "w", encoding="utf-8") as json_file:
json.dump(bulletin_json, json_file, ensure_ascii=False, indent=4)
if __name__ == "__main__":
generate_json_file()

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Status

Actionable comments generated: 0

Configuration used: CodeRabbit UI

Commits Files that changed from the base of the PR and between 303a2d2 and b708717.
Files selected for processing (1)
  • docs/make_bulletin_json.py (1 hunks)
Files skipped from review as they are similar to previous changes (1)
  • docs/make_bulletin_json.py

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Status

Actionable comments generated: 0

Configuration used: CodeRabbit UI

Commits Files that changed from the base of the PR and between b708717 and a679e61.
Files selected for processing (1)
  • .github/workflows/website.yml (3 hunks)
Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/website.yml

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Status

Actionable comments generated: 0

Configuration used: CodeRabbit UI

Commits Files that changed from the base of the PR and between a679e61 and 02757e6.
Files selected for processing (1)
  • docs/make_bulletin_json.py (1 hunks)
Files skipped from review as they are similar to previous changes (1)
  • docs/make_bulletin_json.py

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Status

Actionable comments generated: 3

Configuration used: CodeRabbit UI

Commits Files that changed from the base of the PR and between 02757e6 and 090e36f.
Files selected for processing (2)
  • sleap/gui/web.py (2 hunks)
  • tests/gui/test_web.py (2 hunks)
Additional comments: 2
sleap/gui/web.py (2)
  • 212-212: The get_analytics_data function collects various pieces of data for analytics purposes. It's important to ensure that no personally identifiable information (PII) is being collected and transmitted.
  • Verify that the data collected complies with privacy policies and user consent.
  • Ensure that the data is anonymized and cannot be used to identify individual users.
Verification successful

The get_analytics_data function in sleap/gui/web.py collects the following data:

  • SLEAP version
  • Python version
  • TensorFlow version
  • Conda environment name
  • Platform information

None of these data points are personally identifiable information (PII). They are related to the software environment and system specifications. Additionally, the function ping_analytics includes a note that it respects user preferences for data collection, which suggests that user consent is considered.

Based on the provided script output, it appears that the get_analytics_data function does not collect PII and is designed to comply with privacy policies.

Scripts Executed

The following scripts were executed for the analysis:


Script:

#!/bin/bash
# Verify that no PII is collected by the get_analytics_data function.
rg --type python $'get_analytics_data'

Length of output: 68



Script:

#!/bin/bash
# Correcting the file type and re-running the search for the get_analytics_data function.
rg --type py 'def get_analytics_data'

Length of output: 100



Script:

#!/bin/bash
# Examine the contents of the get_analytics_data function to ensure no PII is collected.
rg --type py --context 10 'def get_analytics_data'

Length of output: 1154



Script:

#!/bin/bash
# Requesting more context lines to view the complete get_analytics_data function.
rg --type py --context 30 'def get_analytics_data'

Length of output: 3146

* 212-212: The `ping_analytics` function sends analytics data to a remote endpoint. It uses a separate thread to perform the network request.
  • Ensure that the network request respects the user's preference settings regarding data collection.
  • Verify that the endpoint is secure and that data is transmitted over HTTPS.
  • Consider adding error handling for the thread's execution to catch any exceptions that may occur.
Verification successful

The verification process has confirmed that the ANALYTICS_ENDPOINT is set to "https://analytics.sleap.ai/ping" in the sleap/gui/web.py file, indicating that the endpoint uses HTTPS, which is a secure protocol.

Scripts Executed

The following scripts were executed for the analysis:


Script:

#!/bin/bash
# Verify that the analytics endpoint is secure and uses HTTPS.
rg --type python $'ANALYTICS_ENDPOINT'

Length of output: 68



Script:

#!/bin/bash
# Correcting the file type specifier and verifying that the analytics endpoint is secure and uses HTTPS.
rg --type py 'ANALYTICS_ENDPOINT'

Length of output: 158

Comment on lines 85 to 126
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_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

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test_announcementchecker function tests the AnnouncementChecker class. It checks the previous announcement date, the availability of new announcements, and the update of the latest announcement. The test uses hardcoded dates and dummy JSON data for announcements.

  • Ensure that the test covers all possible scenarios, including edge cases and failure modes.
  • The test should also mock external calls to avoid making actual HTTP requests during testing.
  • The use of hardcoded dates should be replaced with relative dates to make the tests less brittle over time.

is_announcement_available = checker.new_announcement_available()
assert is_announcement_available == False


This comment was marked as off-topic.

sleap/gui/web.py Outdated
Comment on lines 154 to 210
@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_content + "\n"
)
self.state["announcement"] = announcement_markdown
self.state["announcement last seen date"] = self.bulletin_json_data[0][
"date"
]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The search results do not show any sorting logic applied to bulletin_json_data after it's fetched. The code assumes the first item in the JSON data is the latest announcement without explicitly sorting the data. This could lead to incorrect behavior if the data is not already sorted by date in descending order at the source.

Analysis chain

The AnnouncementChecker class has been added to handle the checking and updating of announcements. It includes methods to check for bulletin data, determine if a new announcement is available, and update the latest announcement.

  • The previous comment about handling the case where the key "announcement last seen date" might not exist in self.state is still valid. This needs to be addressed to prevent a KeyError.
  • The check_for_bulletin_data method should handle potential exceptions from the requests.get call more gracefully, including handling non-200 HTTP responses.
  • The new_announcement_available method assumes the first announcement in the JSON data is the latest. This should be verified, or the data should be sorted to ensure this is the case.
  • The update_latest_announcement method updates the state with the latest announcement. Ensure that the state is saved persistently if required.

The script output confirms the line in sleap/gui/web.py where bulletin_json_data is set but does not verify if the JSON data is sorted by date. To ensure the latest announcement is correctly identified, we need to check if the data is sorted or if the code assumes the first item is the latest.

Scripts Executed

The following scripts were executed for the analysis:


Script:

#!/bin/bash
# Verify that the bulletin JSON data is sorted by date in descending order.
ast-grep --lang python --pattern $'self.bulletin_json_data = requests.get($_).json()'

Length of output: 187



Script:

#!/bin/bash
# Check if there's any sorting logic applied to the bulletin_json_data after it's fetched.
rg "bulletin_json_data" --context 5

Length of output: 4443

@shrivaths16 shrivaths16 requested a review from talmo January 13, 2024 05:40
Comment on lines 31 to 32
elif line.startswith("_"):
date = line[1 : len(line) - 1].strip()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably needs a stronger check, like a date format regexp or another more explicit indicator. There are lots of cases where we might start a line with "_" in the main content.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added a regex that checks if the entire line has only the date content and it is in a certain format.

@@ -28,6 +29,8 @@
"node label size": 12,
"show non-visible nodes": True,
"share usage data": True,
"announcement last seen date": False,
"announcement": False,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, do this.

@@ -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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, do this.

@talmo talmo changed the title PR2: Add new preference and AnnoucementChecker class PR2: Add new preference and AnnouncementChecker class Jan 17, 2024
@shrivaths16 shrivaths16 requested a review from talmo January 17, 2024 21:51
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Status

Actionable comments generated: 2

Configuration used: CodeRabbit UI

Commits Files that changed from the base of the PR and between 090e36f and c218b10.
Files selected for processing (3)
  • docs/make_bulletin_json.py (1 hunks)
  • sleap/gui/web.py (2 hunks)
  • tests/gui/test_web.py (2 hunks)
Files skipped from review as they are similar to previous changes (2)
  • sleap/gui/web.py
  • tests/gui/test_web.py
Additional comments: 2
docs/make_bulletin_json.py (2)
  • 9-9: Ensure the regex pattern for dates matches the expected format used in bulletin.md.
Verification successful

The regex pattern ^_(\d{2}/\d{2}/\d{4})_$ used in make_bulletin_json.py correctly matches the date format found in bulletin.md, as evidenced by the output from the rg command which found dates in the format 09/15/2023 and 09/10/2023.

Scripts Executed

The following scripts were executed for the analysis:


Script:

#!/bin/bash
# Verify that the date format in bulletin.md matches the regex pattern.
rg --type md "^_(\d{2}/\d{2}/\d{4})_$" docs/bulletin.md

Length of output: 83

* 44-45: The script is executed if it's the main module, which is standard practice.

Comment on lines +21 to +36
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"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loop processing the markdown content should handle edge cases, such as multiple consecutive newlines or markdown formatting within the content.

Consider adding additional logic to handle edge cases in markdown formatting.

Comment on lines +40 to +41
with open("_static/bulletin.json", "w") as json_file:
json.dump(bulletin_json, json_file, indent=4)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tip

Codebase Verification

The _static directory does exist within the docs directory of the repository. However, the script output did not provide any instances of build or deployment scripts referencing the _static directory, which means there is no evidence from the script output that such scripts are responsible for creating the _static directory or setting its permissions.

Given this information, the hardcoded path in the make_bulletin_json.py script should be updated to reflect the correct relative path from the script's location to the _static directory. Additionally, the absence of write permissions should be addressed, possibly by modifying the deployment process to ensure the directory is writable when the script is executed.

Analysis chain

The JSON file is written to a hardcoded path. Ensure this path is correct and the file has the proper permissions to be written to.

Scripts Executed

The following scripts were executed for the analysis:


Script:

#!/bin/bash
# Check if the _static directory exists and has write permissions.
if [ -w "_static/" ]; then
  echo "_static directory is writable."
else
  echo "_static directory is not writable or does not exist."
fi

Length of output: 136



Script:

#!/bin/bash
# Search for the _static directory within the repository
fd _static

# Check for build or deployment scripts that might create the _static directory
fd --extension sh --exec grep -H "_static" {}

Length of output: 74

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants