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

Owasp.org post preview #1043

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Draft

Owasp.org post preview #1043

wants to merge 12 commits into from

Conversation

Dishant1804
Copy link
Collaborator

@Dishant1804 Dishant1804 commented Mar 7, 2025

Resolves #994

  • Post model (Fields: title, date, author, author_image, url)
  • sync_posts command
  • GraphQL endpoint to fetch recent posts (recent 5 posts by date)
  • Front end with responsive design
  • Tests for frontend and backend

Copy link
Contributor

coderabbitai bot commented Mar 7, 2025

Summary by CodeRabbit

  • New Features

    • Added a GraphQL interface to query and display recent OWASP blog posts.
    • Enabled dynamic synchronization of blog posts from an external GitHub repository.
    • Introduced a structured model for blog posts with detailed metadata.
    • Improved URL generation for blog posts.
    • Enhanced the admin interface for post management with effective search and display options.
  • Chores

    • Added an automation target for blog post synchronization.
  • Tests

    • Implemented new tests to validate synchronization and core blog post functionalities.

Walkthrough

The pull request introduces multiple new functionalities for managing OWASP blog posts. It includes the addition of a GraphQL API for accessing post data, a management command to synchronize posts from a GitHub repository, and the creation of a Post model with associated fields and methods. Additionally, an admin interface class is provided for managing posts within the Django admin, along with comprehensive test suites to ensure the reliability of these new features.

Changes

Files Change Summary
backend/.../graphql/nodes/post.py
backend/.../graphql/queries/post.py
Added a GraphQL node (PostNode) and query (PostQuery) for exposing and retrieving post data.
backend/.../management/commands/owasp_sync_posts.py
backend/Makefile
Introduced a management command to sync posts from GitHub and a Makefile target (owasp-sync-posts) to execute it.
backend/.../migrations/0023_post.py
backend/.../models/post.py
Created the Post model with fields like author_name, author_image_url, published_at, title, and url, including methods for bulk saving, updating, and retrieval.
backend/.../admin.py Added the PostAdmin class and registered the Post model to enhance the Django admin interface.
backend/.../tests/owasp_sync_posts_test.py
backend/.../tests/owasp/models/post_test.py
Implemented test suites to validate the functionality of the Post model and the sync command.
backend/.../common/constants.py
backend/.../common/utils.py
Added a constant OWASP_BLOG_URL and a utility function get_blog_url(path) for constructing blog URLs.

Suggested reviewers

  • arkid15r
✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 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>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • 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 testing code 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 gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

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 using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@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.

Actionable comments posted: 3

🧹 Nitpick comments (6)
backend/apps/owasp/graphql/nodes/post.py (1)

6-6: Minor: Consider removing extra blank line

There's an extra blank line that could be removed for better code style consistency.

backend/apps/owasp/migrations/0023_post.py (1)

25-25: Consider adding an index for the date field

Since you're querying recent posts (based on the implementation in the query file), adding an index to the date field could improve query performance.

- ("date", models.DateTimeField(verbose_name="Publication date")),
+ ("date", models.DateTimeField(db_index=True, verbose_name="Publication date")),
backend/apps/owasp/management/commands/owasp_sync_posts.py (2)

31-34: Improve URL transformation logic.

The current URL transformation might not correctly map raw content URLs to repository URLs if file paths are complex. Consider a more robust approach to ensure consistent URL structure.

-                    url = download_url.replace(
-                        "https://raw.githubusercontent.com/OWASP/owasp.github.io/main",
-                        "https://github.com/OWASP/owasp.github.io/tree/main",
-                    )
+                    # Extract path components to construct a more reliable URL
+                    if "raw.githubusercontent.com" in download_url:
+                        path_parts = download_url.split("/")
+                        if len(path_parts) >= 5:  # Has enough parts to contain org/repo/branch
+                            org = path_parts[3]
+                            repo = path_parts[4]
+                            branch = path_parts[5]
+                            file_path = "/".join(path_parts[6:])
+                            url = f"https://github.com/{org}/{repo}/blob/{branch}/{file_path}"
+                        else:
+                            url = download_url.replace(
+                                "https://raw.githubusercontent.com/OWASP/owasp.github.io/main",
+                                "https://github.com/OWASP/owasp.github.io/blob/main",
+                            )
+                    else:
+                        url = download_url

41-41: Add command output and logging.

The command doesn't provide any feedback about its execution. Adding output would help track progress and confirm success.

+        self.stdout.write(self.style.SUCCESS(f"Processing {len(files)} potential post files"))
+        
+        # After processing all files
+        self.stdout.write(self.style.SUCCESS(f"Found {len(posts)} valid posts"))
+        
         Post.bulk_save(posts, fields=["title", "date", "author", "author_image", "url"])
+        
+        self.stdout.write(self.style.SUCCESS(f"Successfully synced {len(posts)} posts from OWASP GitHub repository"))
backend/apps/owasp/models/post.py (2)

30-33: Make recent_posts method more flexible.

The current method is hardcoded to return exactly 5 posts. Consider making it more flexible by adding a parameter for the number of posts to return and potentially more ordering options.

     @staticmethod
-    def recent_posts():
+    def recent_posts(limit=5, ordering="-date"):
         """Get recent posts."""
-        return Post.objects.all().order_by("-date")[:5]
+        return Post.objects.all().order_by(ordering)[:limit]

8-33: Add indexes for performance optimization.

Since this model will likely be queried by date frequently, consider adding an index on the date field to improve query performance, especially as the number of posts grows.

     class Meta:
         db_table = "owasp_post"
         verbose_name_plural = "Posts"
+        indexes = [
+            models.Index(fields=['-date'], name='post_date_idx'),
+        ]
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9b54e63 and 7807c1a.

📒 Files selected for processing (6)
  • backend/apps/owasp/graphql/nodes/post.py (1 hunks)
  • backend/apps/owasp/graphql/queries/post.py (1 hunks)
  • backend/apps/owasp/management/commands/owasp_sync_posts.py (1 hunks)
  • backend/apps/owasp/migrations/0023_post.py (1 hunks)
  • backend/apps/owasp/models/__init__.py (1 hunks)
  • backend/apps/owasp/models/post.py (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (5)
  • GitHub Check: CodeQL (python)
  • GitHub Check: CodeQL (javascript-typescript)
  • GitHub Check: Run frontend unit tests
  • GitHub Check: Run backend tests
  • GitHub Check: Run frontend e2e tests
🔇 Additional comments (5)
backend/apps/owasp/models/__init__.py (1)

1-1: Import looks good!

This import statement correctly exposes the Post model at the package level, following Python's common pattern for module organization.

backend/apps/owasp/graphql/nodes/post.py (1)

1-19: Node implementation looks clean and well-structured

The PostNode class is properly implemented, with clear docstrings and appropriate field definitions. The structure follows good practices for GraphQL schema definition in Django.

backend/apps/owasp/migrations/0023_post.py (1)

11-33: Migration looks well-structured

The Post model migration is properly defined with appropriate field types and constraints.

backend/apps/owasp/graphql/queries/post.py (1)

15-17:

✅ Verification successful

Verify the implementation of Post.recent_posts

The resolver accesses Post.recent_posts directly, which suggests it's a property or class method on the Post model. Ensure this property:

  1. Returns the expected number of posts (5, as specified in the docstring)
  2. Returns them in the correct order (most recent first)
  3. Is implemented efficiently (e.g., with proper database queries)

Without seeing the Post model implementation, I can't verify these details.


🏁 Script executed:

#!/bin/bash
# Check the implementation of recent_posts in the Post model
fd -t f "post.py" backend/apps/owasp/models/ -x cat

Length of output: 1030


Reviewed Post.recent_posts implementation – No changes required

  • The recent_posts static method in the Post model correctly orders posts in descending order by date and slices the queryset to return only 5 posts.
  • It meets the specifications outlined in the resolver’s docstring: returning the 5 most recent posts efficiently via the Django ORM.
backend/apps/owasp/models/post.py (1)

8-19: Consider adding content field to store post content.

The current model stores metadata about posts but doesn't have a field for the actual content. This might be intentional if content is always fetched from GitHub, but consider adding a TextField for content if you want to cache it locally.

If the content is intended to be stored locally to reduce GitHub API calls, you could add:

     title = models.CharField(verbose_name="Title")
     url = models.URLField(verbose_name="URL", unique=True)
+    content = models.TextField(
+        verbose_name="Post content",
+        blank=True,
+        help_text="The markdown content of the post"
+    )

Is the content intentionally excluded, or would storing it locally be beneficial for your use case?

Copy link
Contributor

@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.

Actionable comments posted: 4

🧹 Nitpick comments (9)
backend/Makefile (3)

101-103: Consider updating the echo message for consistency.

The new owasp-sync-posts target follows the same pattern as other commands, but unlike most other commands, it doesn't include an echo message to inform users about what's happening.

 owasp-sync-posts:
+	@echo "Synchronizing OWASP posts"
 	@CMD="python manage.py owasp_sync_posts" $(MAKE) exec-backend-command-it

101-103: Consider using non-interactive mode for automation.

The command uses exec-backend-command-it which is interactive. However, most other similar commands in the Makefile use the non-interactive exec-backend-command. Unless this command specifically requires user input, consider using the non-interactive version for better automation in CI/CD pipelines.

 owasp-sync-posts:
-	@CMD="python manage.py owasp_sync_posts" $(MAKE) exec-backend-command-it
+	@CMD="python manage.py owasp_sync_posts" $(MAKE) exec-backend-command

101-139: Consider adding the new command to the update-data target.

The new owasp-sync-posts command isn't included in the update-data target, which contains other data synchronization commands. Consider adding it to ensure posts are synchronized when running the full data update process.

 update-data: \
 	github-update-owasp-organization \
 	owasp-scrape-chapters \
 	owasp-scrape-committees \
 	owasp-scrape-projects \
 	github-update-project-related-repositories \
 	owasp-aggregate-projects \
 	owasp-update-events \
+	owasp-sync-posts \
 	owasp-update-sponsors
backend/apps/owasp/migrations/0023_post.py (1)

22-30: Consider adding an index on published_at field.

Since the recent_posts query will filter by the published_at field to get the most recent posts, adding an index would improve query performance.

 ("nest_updated_at", models.DateTimeField(auto_now=True)),
 ("author_name", models.CharField(max_length=100, verbose_name="Author name")),
 (
     "author_image_url",
     models.URLField(blank=True, default="", verbose_name="Author image URL"),
 ),
-("published_at", models.DateTimeField(verbose_name="Publication date")),
+("published_at", models.DateTimeField(db_index=True, verbose_name="Publication date")),
 ("title", models.CharField(max_length=200, verbose_name="Title")),
 ("url", models.URLField(verbose_name="URL")),
backend/apps/owasp/management/commands/owasp_sync_posts.py (1)

34-37: URL construction might be fragile.

The current URL replacement assumes a specific format that might break if the repository structure changes. Consider extracting just the filename from the URL and constructing a more robust blog URL.

-                    url = download_url.replace(
-                        "https://raw.githubusercontent.com/OWASP/owasp.github.io/main/_posts/",
-                        "https://owasp.org/blog/",
-                    )
+                    # Extract just the filename from the URL for more robust construction
+                    filename = repository_file.get("name")
+                    # Remove .md extension and construct the blog URL
+                    blog_path = filename[:-3] if filename.endswith('.md') else filename
+                    url = f"https://owasp.org/blog/{blog_path}"
backend/apps/owasp/graphql/queries/post.py (2)

15-17: Update docstring to reflect the limit parameter.

The docstring for resolve_recent_posts mentions returning "the 5 most recent posts", but the function accepts a limit parameter that can be different from 5.

-    def resolve_recent_posts(root, info, limit):
-        """Return the 5 most recent posts."""
+    def resolve_recent_posts(root, info, limit):
+        """Return the most recent posts, limited by the specified count (default: 5)."""
         return Post.recent_posts[:limit]

10-17: Consider adding error handling and pagination.

The GraphQL query lacks error handling and pagination for fetching posts. For a production API, consider adding proper error handling and pagination to handle large datasets efficiently.

Here's an improved implementation with error handling and pagination:

 class PostQuery(BaseQuery):
     """GraphQL queries for Post model."""
 
-    recent_posts = graphene.List(PostNode, limit=graphene.Int(default_value=5))
+    recent_posts = graphene.Field(
+        graphene.List(PostNode),
+        limit=graphene.Int(default_value=5),
+        offset=graphene.Int(default_value=0),
+        description="Retrieve recent posts with pagination support"
+    )
 
-    def resolve_recent_posts(root, info, limit):
-        """Return the 5 most recent posts."""
-        return Post.recent_posts[:limit]
+    def resolve_recent_posts(root, info, limit, offset=0):
+        """Return the most recent posts with pagination support.
+        
+        Args:
+            limit: Maximum number of posts to return (default: 5)
+            offset: Number of posts to skip (default: 0)
+        """
+        try:
+            return Post.recent_posts[offset:offset+limit]
+        except Exception as e:
+            # Log the error
+            info.context.logger.error(f"Error fetching recent posts: {e}")
+            return []
backend/apps/owasp/admin.py (1)

74-79: Consider adding a list_filter for published_at

The PostAdmin configuration is well-structured, but adding date filtering capabilities would improve usability for administrators, especially as the number of posts grows over time.

class PostAdmin(admin.ModelAdmin):
    """Admin configuration for Post model."""

    list_display = ("author_name", "author_image_url", "published_at", "title", "url")
    search_fields = ("author_name", "author_image_url", "published_at", "title", "url")
+    list_filter = ("published_at",)
backend/apps/owasp/models/post.py (1)

26-28: Add type hinting for better code readability

Adding type annotations to the parameters and return value would improve code maintainability and help catch errors during development.

@staticmethod
-def bulk_save(posts, fields=None):
+def bulk_save(posts: list, fields: list = None) -> None:
    """Bulk save posts."""
    BulkSaveModel.bulk_save(Post, posts, fields=fields)
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7807c1a and 12c008a.

📒 Files selected for processing (6)
  • backend/Makefile (1 hunks)
  • backend/apps/owasp/admin.py (3 hunks)
  • backend/apps/owasp/graphql/queries/post.py (1 hunks)
  • backend/apps/owasp/management/commands/owasp_sync_posts.py (1 hunks)
  • backend/apps/owasp/migrations/0023_post.py (1 hunks)
  • backend/apps/owasp/models/post.py (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (5)
  • GitHub Check: CodeQL (python)
  • GitHub Check: Run frontend e2e tests
  • GitHub Check: Run backend tests
  • GitHub Check: CodeQL (javascript-typescript)
  • GitHub Check: Run frontend unit tests
🔇 Additional comments (6)
backend/apps/owasp/migrations/0023_post.py (1)

11-36: LGTM! The Post model migration is well-structured.

The migration file correctly defines the Post model with appropriate fields, constraints, and table configuration.

backend/apps/owasp/management/commands/owasp_sync_posts.py (1)

13-52: ⚠️ Potential issue

Add error handling and validation for API requests and data processing.

The command lacks robust error handling for API requests, YAML parsing, and required field validation. This could lead to silent failures or invalid data being stored.

Apply these changes to improve error handling and validation:

 def handle(self, *args, **options):
-    post_repository_content = get_repository_file_content(
-        "https://api.github.com/repos/OWASP/owasp.github.io/contents/_posts"
-    )
-    repository_files = json.loads(post_repository_content)
+    try:
+        post_repository_content = get_repository_file_content(
+            "https://api.github.com/repos/OWASP/owasp.github.io/contents/_posts"
+        )
+        repository_files = json.loads(post_repository_content)
+    except Exception as e:
+        self.stderr.write(self.style.ERROR(f"Failed to fetch or parse repository contents: {e}"))
+        return
+        
     posts = []
+    processed_count = 0
+    skipped_count = 0

     for repository_file in repository_files:
         if repository_file.get("name", "").endswith(".md"):
             download_url = repository_file.get("download_url")
-            post_content = get_repository_file_content(download_url)
-
-            if post_content.startswith("---"):
-                yaml_content = re.search(r"^---\s*(.*?)\s*---", post_content, re.DOTALL)
-                metadata = yaml.safe_load(yaml_content.group(1)) or {}
-
-                title = metadata.get("title")
-                published_at = metadata.get("date")
-                author_name = metadata.get("author")
-                author_image_url = metadata.get("author_image") or ""
-                url = download_url.replace(
-                    "https://raw.githubusercontent.com/OWASP/owasp.github.io/main/_posts/",
-                    "https://owasp.org/blog/",
-                )
-
-                post = Post(
-                    title=title,
-                    published_at=published_at,
-                    author_name=author_name,
-                    author_image_url=author_image_url,
-                    url=url,
-                )
-
-                posts.append(post)
+            try:
+                post_content = get_repository_file_content(download_url)
+                
+                if post_content.startswith("---"):
+                    yaml_content = re.search(r"^---\s*(.*?)\s*---", post_content, re.DOTALL)
+                    if not yaml_content:
+                        self.stderr.write(self.style.WARNING(f"Could not extract YAML from {repository_file.get('name')}"))
+                        skipped_count += 1
+                        continue
+                        
+                    try:
+                        metadata = yaml.safe_load(yaml_content.group(1)) or {}
+                    except yaml.YAMLError as e:
+                        self.stderr.write(self.style.ERROR(f"Failed to parse YAML for {repository_file.get('name')}: {e}"))
+                        skipped_count += 1
+                        continue
+                    
+                    title = metadata.get("title")
+                    published_at = metadata.get("date")
+                    author_name = metadata.get("author")
+                    author_image_url = metadata.get("author_image") or ""
+                    
+                    # Validate required fields
+                    if not all([title, published_at, author_name]):
+                        self.stderr.write(self.style.WARNING(
+                            f"Skipping {repository_file.get('name')}: Missing required fields (title, date, or author)"
+                        ))
+                        skipped_count += 1
+                        continue
+                        
+                    url = download_url.replace(
+                        "https://raw.githubusercontent.com/OWASP/owasp.github.io/main/_posts/",
+                        "https://owasp.org/blog/",
+                    )
+                    
+                    post = Post(
+                        title=title,
+                        published_at=published_at,
+                        author_name=author_name,
+                        author_image_url=author_image_url,
+                        url=url,
+                    )
+                    
+                    posts.append(post)
+                    processed_count += 1
+            except Exception as e:
+                self.stderr.write(self.style.ERROR(f"Error processing {repository_file.get('name')}: {e}"))
+                skipped_count += 1

-    Post.bulk_save(
-        posts, fields=["title", "published_at", "author_name", "author_image_url", "url"]
-    )
+    if posts:
+        try:
+            Post.bulk_save(
+                posts, fields=["title", "published_at", "author_name", "author_image_url", "url"]
+            )
+            self.stdout.write(self.style.SUCCESS(f"Successfully processed {processed_count} posts"))
+        except Exception as e:
+            self.stderr.write(self.style.ERROR(f"Failed to save posts: {e}"))
+    else:
+        self.stdout.write(self.style.WARNING("No posts found to process"))
+        
+    if skipped_count > 0:
+        self.stdout.write(self.style.WARNING(f"Skipped {skipped_count} posts due to errors or missing data"))
backend/apps/owasp/models/post.py (4)

11-13: LGTM! Meta configuration properly set.

The Meta class is well-configured with appropriate table name and verbose plural name.


15-19: Field lengths seem appropriate and naming follows project conventions.

The model fields are well-defined with appropriate data types, verbose names, and max_length constraints where needed.


16-16: Consider adding help_text to field definitions

Adding help_text to model fields would improve developer experience when working with this model in forms or the admin interface.

-    author_image_url = models.URLField(verbose_name="Author image URL", blank=True, default="")
+    author_image_url = models.URLField(
+        verbose_name="Author image URL", 
+        blank=True, 
+        default="",
+        help_text="URL to the author's profile image"
+    )

8-9: Model design follows project patterns

The Post model properly inherits from BulkSaveModel and TimestampedModel, which aligns with the project's patterns and provides necessary functionality for bulk operations and timestamps.

Copy link
Contributor

@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.

Actionable comments posted: 0

🔭 Outside diff range comments (1)
backend/apps/owasp/graphql/nodes/post.py (1)

7-19: ⚠️ Potential issue

Fix the typo in the field name.

There's a spelling error in the field name "publised_at" which should be "published_at". This typo will cause issues when trying to access this field through GraphQL queries.

        fields = (
            "author_name",
            "author_image_url",
-           "publised_at",
+           "published_at",
            "title",
            "url",
        )
🧹 Nitpick comments (1)
backend/apps/owasp/graphql/nodes/post.py (1)

5-6: Remove extra blank lines.

There are multiple consecutive blank lines between imports and the class definition. According to PEP 8, you should limit blank lines to two between top-level definitions and one between method definitions.

from apps.owasp.models.post import Post


-
class PostNode(BaseNode):
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 12c008a and d370dde.

📒 Files selected for processing (3)
  • backend/apps/owasp/graphql/nodes/post.py (1 hunks)
  • backend/apps/owasp/graphql/queries/post.py (1 hunks)
  • backend/apps/owasp/management/commands/owasp_sync_posts.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend/apps/owasp/management/commands/owasp_sync_posts.py
  • backend/apps/owasp/graphql/queries/post.py
⏰ Context from checks skipped due to timeout of 90000ms (5)
  • GitHub Check: CodeQL (python)
  • GitHub Check: Run backend tests
  • GitHub Check: CodeQL (javascript-typescript)
  • GitHub Check: Run frontend unit tests
  • GitHub Check: Run frontend e2e tests
🔇 Additional comments (1)
backend/apps/owasp/graphql/nodes/post.py (1)

1-5: Code follows best practices for imports and documentation.

The imports are correctly organized, and the module has a clear docstring that describes its purpose.

Copy link
Contributor

@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.

Actionable comments posted: 3

🔭 Outside diff range comments (1)
backend/tests/owasp/management/commands/owasp_sync_posts_test.py (1)

1-81: ⚠️ Potential issue

Duplicate test file needs to be refactored or removed

This file is an exact duplicate of backend/tests/owasp/models/post_test.py. Based on the filename, this should be testing the management command for syncing posts, not the Post model directly.

You have two options:

  1. Remove this file and create a proper test for the sync_posts command
  2. Refactor to use a shared test class that both files can import

For option 1, create a proper test for the management command:

from unittest.mock import patch, Mock
import pytest
from io import StringIO
from django.core.management import call_command

class TestOwaspSyncPostsCommand:
    @patch("apps.owasp.management.commands.owasp_sync_posts.Command._fetch_posts")
    @patch("apps.owasp.models.post.Post.update_data")
    def test_handle_syncs_posts(self, mock_update_data, mock_fetch_posts):
        # Setup mock post data
        mock_posts = [
            {"url": "https://example.com/1", "title": "Post 1"},
            {"url": "https://example.com/2", "title": "Post 2"}
        ]
        mock_fetch_posts.return_value = mock_posts
        mock_update_data.side_effect = lambda data: Mock(url=data["url"])
        
        # Call the command
        out = StringIO()
        call_command("owasp_sync_posts", stdout=out)
        
        # Assert
        assert mock_fetch_posts.called
        assert mock_update_data.call_count == len(mock_posts)
        assert "Synced 2 posts" in out.getvalue()
🧰 Tools
🪛 Ruff (0.8.2)

22-22: datetime.datetime() called without a tzinfo argument

(DTZ001)


37-37: Line too long (174 > 99)

(E501)


37-37: datetime.datetime() called without a tzinfo argument

(DTZ001)


38-38: Line too long (174 > 99)

(E501)


38-38: datetime.datetime() called without a tzinfo argument

(DTZ001)


58-58: Line too long (153 > 99)

(E501)


59-59: Blank line contains whitespace

Remove whitespace from blank line

(W293)


61-61: Blank line contains whitespace

Remove whitespace from blank line

(W293)


76-76: Blank line contains whitespace

Remove whitespace from blank line

(W293)


78-78: Blank line contains whitespace

Remove whitespace from blank line

(W293)

🪛 GitHub Actions: Run CI/CD

[error] 22-26: DTZ001 datetime.datetime() called without a tzinfo argument. Pass a datetime.timezone object to the tzinfo parameter.

♻️ Duplicate comments (2)
backend/apps/owasp/models/post.py (2)

19-19: 🛠️ Refactor suggestion

Add unique constraint to URL field

Posts should have unique URLs to prevent duplicates. This is important for the sync_posts command mentioned in the PR description.

-    url = models.URLField(verbose_name="URL")
+    url = models.URLField(verbose_name="URL", unique=True)

30-33: 🛠️ Refactor suggestion

Avoid hardcoding all() in model method

The recent_posts() method currently returns all posts. Consider adding a parameter to limit the number of results, especially since the PR mentions fetching only the five most recent posts.

@staticmethod
-def recent_posts():
+def recent_posts(limit=None):
    """Get recent posts."""
-    return Post.objects.all().order_by("-published_at")
+    queryset = Post.objects.all().order_by("-published_at")
+    if limit is not None:
+        queryset = queryset[:limit]
+    return queryset
🧹 Nitpick comments (3)
backend/tests/owasp/models/post_test.py (2)

58-58: Fix long line

This line exceeds the 99-character line length limit specified in your code style guidelines.

-        data = {"url": "https://existing.com", "title": "Updated Title", "author_name": "Updated Author", "author_image_url": "https://updatedimage.com"}
+        data = {
+            "url": "https://existing.com", 
+            "title": "Updated Title", 
+            "author_name": "Updated Author", 
+            "author_image_url": "https://updatedimage.com"
+        }
🧰 Tools
🪛 Ruff (0.8.2)

58-58: Line too long (153 > 99)

(E501)

🪛 GitHub Actions: Run CI/CD

[error] 58-100: E501 Line too long (153 > 99)


59-61: Remove whitespace from blank lines

The blank lines contain whitespace, which is flagged by the static analysis tools.

        data = {"url": "https://existing.com", "title": "Updated Title", "author_name": "Updated Author", "author_image_url": "https://updatedimage.com"}
-        
+
        result = Post.update_data(data)
-        
+
        mock_get.assert_called_once_with(url=data["url"])

And similarly for lines 76-78.

Also applies to: 76-78

🧰 Tools
🪛 Ruff (0.8.2)

59-59: Blank line contains whitespace

Remove whitespace from blank line

(W293)


61-61: Blank line contains whitespace

Remove whitespace from blank line

(W293)

backend/apps/owasp/models/post.py (1)

15-18: Add help_text to model fields for better documentation

Adding help_text would improve developer experience when using this model in forms or admin interfaces.

-    author_name = models.CharField(verbose_name="Author name", max_length=100)
-    author_image_url = models.URLField(verbose_name="Author image URL", blank=True, default="")
-    published_at = models.DateTimeField(verbose_name="Publication date")
-    title = models.CharField(verbose_name="Title", max_length=200)
+    author_name = models.CharField(
+        verbose_name="Author name", 
+        max_length=100, 
+        help_text="The name of the post author"
+    )
+    author_image_url = models.URLField(
+        verbose_name="Author image URL", 
+        blank=True, 
+        default="", 
+        help_text="URL to the author's profile image"
+    )
+    published_at = models.DateTimeField(
+        verbose_name="Publication date", 
+        help_text="When the post was published"
+    )
+    title = models.CharField(
+        verbose_name="Title", 
+        max_length=200, 
+        help_text="The title of the post"
+    )
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d370dde and 01a7920.

📒 Files selected for processing (6)
  • backend/apps/owasp/graphql/nodes/post.py (1 hunks)
  • backend/apps/owasp/management/commands/owasp_sync_posts.py (1 hunks)
  • backend/apps/owasp/migrations/0023_post.py (1 hunks)
  • backend/apps/owasp/models/post.py (1 hunks)
  • backend/tests/owasp/management/commands/owasp_sync_posts_test.py (1 hunks)
  • backend/tests/owasp/models/post_test.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • backend/apps/owasp/graphql/nodes/post.py
  • backend/apps/owasp/management/commands/owasp_sync_posts.py
  • backend/apps/owasp/migrations/0023_post.py
🧰 Additional context used
🪛 Ruff (0.8.2)
backend/tests/owasp/management/commands/owasp_sync_posts_test.py

22-22: datetime.datetime() called without a tzinfo argument

(DTZ001)


37-37: Line too long (174 > 99)

(E501)


37-37: datetime.datetime() called without a tzinfo argument

(DTZ001)


38-38: Line too long (174 > 99)

(E501)


38-38: datetime.datetime() called without a tzinfo argument

(DTZ001)


58-58: Line too long (153 > 99)

(E501)


59-59: Blank line contains whitespace

Remove whitespace from blank line

(W293)


61-61: Blank line contains whitespace

Remove whitespace from blank line

(W293)


76-76: Blank line contains whitespace

Remove whitespace from blank line

(W293)


78-78: Blank line contains whitespace

Remove whitespace from blank line

(W293)

backend/tests/owasp/models/post_test.py

22-22: datetime.datetime() called without a tzinfo argument

(DTZ001)


37-37: Line too long (174 > 99)

(E501)


37-37: datetime.datetime() called without a tzinfo argument

(DTZ001)


38-38: Line too long (174 > 99)

(E501)


38-38: datetime.datetime() called without a tzinfo argument

(DTZ001)


58-58: Line too long (153 > 99)

(E501)


59-59: Blank line contains whitespace

Remove whitespace from blank line

(W293)


61-61: Blank line contains whitespace

Remove whitespace from blank line

(W293)


76-76: Blank line contains whitespace

Remove whitespace from blank line

(W293)


78-78: Blank line contains whitespace

Remove whitespace from blank line

(W293)

🪛 GitHub Actions: Run CI/CD
backend/tests/owasp/management/commands/owasp_sync_posts_test.py

[error] 22-26: DTZ001 datetime.datetime() called without a tzinfo argument. Pass a datetime.timezone object to the tzinfo parameter.


[error] 37-100: E501 Line too long (174 > 99)


[error] 38-100: DTZ001 datetime.datetime() called without a tzinfo argument. Pass a datetime.timezone object to the tzinfo parameter.


[error] 58-100: E501 Line too long (153 > 99)

backend/tests/owasp/models/post_test.py

[error] 22-26: DTZ001 datetime.datetime() called without a tzinfo argument. Pass a datetime.timezone object to the tzinfo parameter.


[error] 37-100: E501 Line too long (174 > 99)


[error] 38-100: DTZ001 datetime.datetime() called without a tzinfo argument. Pass a datetime.timezone object to the tzinfo parameter.


[error] 58-100: E501 Line too long (153 > 99)

Copy link
Contributor

@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.

Actionable comments posted: 0

🧹 Nitpick comments (3)
backend/tests/owasp/management/commands/owasp_sync_posts_test.py (2)

14-16: Remove unnecessary parentheses from pytest fixture decorators

According to pytest best practices and your linting rules, you should use @pytest.fixture without empty parentheses.

-    @pytest.fixture()
+    @pytest.fixture
     def command(self):
         return Command()

-    @pytest.fixture()
+    @pytest.fixture
     def mock_repository_files(self):
         return [

-    @pytest.fixture()
+    @pytest.fixture
     def mock_post_content(self):
         return """---

Also applies to: 18-20, 31-33

🧰 Tools
🪛 Ruff (0.8.2)

14-14: Use @pytest.fixture over @pytest.fixture()

Remove parentheses

(PT001)


150-150: Add a newline at the end of file

Add a trailing newline to fix the linting error and pipeline failure.

             [], fields=["title", "published_at", "author_name", "author_image_url", "url"]
         )
+
🧰 Tools
🪛 Ruff (0.8.2)

150-150: No newline at end of file

Add trailing newline

(W292)

backend/tests/owasp/models/post_test.py (1)

88-98: LGTM! Clean test for recent_posts method

The test effectively verifies that recent_posts applies the correct ordering.

Add a newline at the end of the file to comply with style guidelines.

        mock_queryset.order_by.assert_called_once_with("-published_at")
        assert result == mock_queryset.order_by.return_value
+
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 01a7920 and 835ecf0.

📒 Files selected for processing (2)
  • backend/tests/owasp/management/commands/owasp_sync_posts_test.py (1 hunks)
  • backend/tests/owasp/models/post_test.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.8.2)
backend/tests/owasp/management/commands/owasp_sync_posts_test.py

14-14: Use @pytest.fixture over @pytest.fixture()

Remove parentheses

(PT001)


18-18: Use @pytest.fixture over @pytest.fixture()

Remove parentheses

(PT001)


31-31: Use @pytest.fixture over @pytest.fixture()

Remove parentheses

(PT001)


150-150: No newline at end of file

Add trailing newline

(W292)

🪛 GitHub Actions: Run CI/CD
backend/tests/owasp/management/commands/owasp_sync_posts_test.py

[error] 147-147: Process completed with exit code 1. Pre-commit hook(s) made changes.

🔇 Additional comments (10)
backend/tests/owasp/management/commands/owasp_sync_posts_test.py (4)

9-17: LGTM! Good test class structure

The test class is well-organized with appropriate constants and fixtures. The tests cover the main scenarios including successful processing and error handling cases.

🧰 Tools
🪛 Ruff (0.8.2)

14-14: Use @pytest.fixture over @pytest.fixture()

Remove parentheses

(PT001)


42-101: Well-structured test for the happy path

This test thoroughly verifies the behavior of the command when processing is successful. It includes good assertions to check:

  1. The correct API endpoints are called
  2. The expected number of calls are made
  3. The update_data method is called with the correct parameters
  4. The bulk_save method is called with the expected parameters

The mocking approach cleanly isolates the test from actual API calls.


102-123: Comprehensive error handling test

Good test coverage for the scenario when posts don't have front matter. The test verifies that:

  1. The API calls are still made
  2. No updates are attempted
  3. The bulk_save method is called with an empty list

This ensures the command handles malformed post data gracefully.


124-150: Good edge case coverage

This test appropriately handles the case when the update_data method returns None, ensuring that the bulk_save method is still called with an empty list. This verifies that the command properly handles this edge case without errors.

🧰 Tools
🪛 Ruff (0.8.2)

150-150: No newline at end of file

Add trailing newline

(W292)

🪛 GitHub Actions: Run CI/CD

[error] 147-147: Process completed with exit code 1. Pre-commit hook(s) made changes.

backend/tests/owasp/models/post_test.py (6)

1-8: LGTM! Good imports organization

The imports are well-organized, with standard library imports first, followed by third-party imports, and then local imports.


9-25: Well-designed parameterized test for str method

Good use of pytest's parameterize to test multiple cases with a single test function. The test covers both a normal title and an empty string.


26-32: Effective mocking of bulk_save method

This test properly mocks the parent class method and verifies it's called with the correct parameters.


33-64: Comprehensive parameterized test for from_dict method

Good test design with parameterized test cases covering:

  1. Full data set with all fields
  2. Partial data set with only a subset of fields

The assertions verify that each field is correctly updated.


65-83: Thorough test for update_data method

This test properly mocks the database query and verifies:

  1. The correct object is retrieved using the URL
  2. The from_dict method is called with the provided data
  3. The save method is called
  4. The result is the updated post

Good coverage of the expected behavior.


84-87: Good edge case testing for update_data

This test confirms that the update_data method returns None when URL is missing, which is important for error handling.

Copy link
Contributor

@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.

Actionable comments posted: 0

🧹 Nitpick comments (2)
backend/tests/owasp/management/commands/owasp_sync_posts_test.py (2)

14-14: Remove unnecessary parentheses from pytest.fixture decorators

The pytest fixture decorators should not have empty parentheses according to best practices. While functionally equivalent, the preferred style is to use @pytest.fixture without parentheses.

-    @pytest.fixture()
+    @pytest.fixture

Apply this change to all three fixture decorators in the file (lines 14, 18, and 31).

Also applies to: 18-18, 31-31

🧰 Tools
🪛 Ruff (0.8.2)

14-14: Use @pytest.fixture over @pytest.fixture()

Remove parentheses

(PT001)


1-151: Consider adding API failure test cases

The current tests cover successful API calls and content parsing scenarios, but there are no tests for API failures like network errors or invalid responses.

Consider adding a test case that simulates API failures by having mock_get_content raise exceptions, ensuring the command handles these gracefully. Example:

@mock.patch("apps.owasp.management.commands.owasp_sync_posts.get_repository_file_content")
@mock.patch("apps.owasp.models.post.Post.bulk_save")
def test_handle_api_failure(self, mock_bulk_save, mock_get_content, command):
    mock_get_content.side_effect = Exception("API connection failed")
    
    # Command should handle the exception gracefully
    command.handle()
    
    # No data should be saved when the API fails
    mock_bulk_save.assert_called_once_with(
        [], fields=["title", "published_at", "author_name", "author_image_url", "url"]
    )
🧰 Tools
🪛 Ruff (0.8.2)

14-14: Use @pytest.fixture over @pytest.fixture()

Remove parentheses

(PT001)


18-18: Use @pytest.fixture over @pytest.fixture()

Remove parentheses

(PT001)


31-31: Use @pytest.fixture over @pytest.fixture()

Remove parentheses

(PT001)

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 835ecf0 and 5f3c3b6.

📒 Files selected for processing (1)
  • backend/tests/owasp/management/commands/owasp_sync_posts_test.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.8.2)
backend/tests/owasp/management/commands/owasp_sync_posts_test.py

14-14: Use @pytest.fixture over @pytest.fixture()

Remove parentheses

(PT001)


18-18: Use @pytest.fixture over @pytest.fixture()

Remove parentheses

(PT001)


31-31: Use @pytest.fixture over @pytest.fixture()

Remove parentheses

(PT001)

⏰ Context from checks skipped due to timeout of 90000ms (5)
  • GitHub Check: CodeQL (python)
  • GitHub Check: Run frontend e2e tests
  • GitHub Check: CodeQL (javascript-typescript)
  • GitHub Check: Run backend tests
  • GitHub Check: Run frontend unit tests
🔇 Additional comments (3)
backend/tests/owasp/management/commands/owasp_sync_posts_test.py (3)

42-101: Well-structured test for successful processing path

The test thoroughly validates the command's behavior when successfully processing posts. It checks all essential aspects:

  • Correct number of API calls
  • Correct API endpoints being called
  • Expected data transformations
  • Proper bulk saving of processed data

102-123: Good error handling test for missing front matter

This test properly verifies that the command handles posts without front matter gracefully:

  • No update_data calls are made
  • The bulk_save is still called with an empty list
  • The command continues processing without errors

This ensures robustness when dealing with potentially malformed content.


124-150: Appropriate handling of None return values

The test verifies that the command properly handles None values returned from update_data, ensuring these posts are excluded from bulk saving. This defensive programming approach prevents potential issues with invalid data.

Copy link
Contributor

@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.

Actionable comments posted: 1

🧹 Nitpick comments (3)
backend/apps/common/utils.py (1)

20-36: Consider adding error logging

While the function handles non-matching paths gracefully by returning the original path, it might be helpful to log warnings when a path doesn't match the expected pattern. This would assist in diagnosing issues if unexpected URL formats are encountered.

def get_blog_url(path):
    """Return OWASP blog URL for a given path."""
    pattern = re.compile(
        r"(https://raw\.githubusercontent\.com/OWASP/owasp\.github\.io/main/_posts/)"
        r"(\d{4})-(\d{2})-(\d{2})-"
        r"(.+)"
        r"\.md$"
    )
    match = pattern.match(path)
    if match:
        year = match.group(2)
        month = match.group(3)
        day = match.group(4)
        slug = match.group(5)
        return f"{OWASP_BLOG_URL}{year}/{month}/{day}/{slug}.html"
+    # Consider adding logging here
+    # import logging
+    # logger = logging.getLogger(__name__)
+    # logger.warning(f"Path did not match expected blog post format: {path}")
    return path
backend/tests/owasp/management/commands/owasp_sync_posts_test.py (2)

14-16: Fix pytest fixture syntax

According to pytest best practices, you should remove the parentheses from the @pytest.fixture decorators.

-    @pytest.fixture()
+    @pytest.fixture
     def command(self):
         return Command()

-    @pytest.fixture()
+    @pytest.fixture
     def mock_repository_files(self):
         return [

-    @pytest.fixture()
+    @pytest.fixture
     def mock_post_content(self):
         return """---

Also applies to: 18-29, 31-40

🧰 Tools
🪛 Ruff (0.8.2)

14-14: Use @pytest.fixture over @pytest.fixture()

Remove parentheses

(PT001)


1-151: Consider parameterizing test cases to improve maintainability

The test cases follow similar patterns but with different input data and expected outcomes. Consider using pytest's parameterization to reduce code duplication and make it easier to add more test cases in the future.

Here's an example of how you could parameterize the tests:

@pytest.mark.parametrize(
    "post_content,update_return,expected_update_calls,expected_bulk_save_items",
    [
        (
            """---
title: Test Post
date: 2023-01-01
author: John Doe
author_image: https://example.com/john.jpg
---

This is the content of the test post.""",
            [mock.Mock(), mock.Mock()],
            2,
            2,
        ),
        (
            "This is a post without front matter",
            [],
            0,
            0,
        ),
        (
            """---
title: Test Post
date: 2023-01-01
author: John Doe
author_image: https://example.com/john.jpg
---

This is the content of the test post.""",
            [None, None],
            2,
            0,
        ),
    ],
)
@mock.patch("apps.owasp.management.commands.owasp_sync_posts.get_repository_file_content")
@mock.patch("apps.owasp.models.post.Post.update_data")
@mock.patch("apps.owasp.models.post.Post.bulk_save")
def test_handle_cases(
    self,
    mock_bulk_save,
    mock_update_data,
    mock_get_content,
    post_content,
    update_return,
    expected_update_calls,
    expected_bulk_save_items,
    command,
    mock_repository_files,
):
    # Test implementation
🧰 Tools
🪛 Ruff (0.8.2)

14-14: Use @pytest.fixture over @pytest.fixture()

Remove parentheses

(PT001)


18-18: Use @pytest.fixture over @pytest.fixture()

Remove parentheses

(PT001)


31-31: Use @pytest.fixture over @pytest.fixture()

Remove parentheses

(PT001)

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5f3c3b6 and 3a39be0.

📒 Files selected for processing (4)
  • backend/apps/common/constants.py (1 hunks)
  • backend/apps/common/utils.py (1 hunks)
  • backend/apps/owasp/management/commands/owasp_sync_posts.py (1 hunks)
  • backend/tests/owasp/management/commands/owasp_sync_posts_test.py (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • backend/apps/common/constants.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/apps/owasp/management/commands/owasp_sync_posts.py
🧰 Additional context used
🪛 Ruff (0.8.2)
backend/tests/owasp/management/commands/owasp_sync_posts_test.py

14-14: Use @pytest.fixture over @pytest.fixture()

Remove parentheses

(PT001)


18-18: Use @pytest.fixture over @pytest.fixture()

Remove parentheses

(PT001)


31-31: Use @pytest.fixture over @pytest.fixture()

Remove parentheses

(PT001)

⏰ Context from checks skipped due to timeout of 90000ms (5)
  • GitHub Check: Run frontend unit tests
  • GitHub Check: CodeQL (python)
  • GitHub Check: Run backend tests
  • GitHub Check: CodeQL (javascript-typescript)
  • GitHub Check: Run frontend e2e tests
🔇 Additional comments (4)
backend/apps/common/utils.py (1)

20-36: Good implementation of the blog URL formatter

The new get_blog_url function effectively extracts components from GitHub raw content URLs and constructs proper OWASP blog URLs. The regex pattern is well-designed to match the expected format of OWASP blog post paths.

backend/tests/owasp/management/commands/owasp_sync_posts_test.py (3)

42-101: Great test coverage for successful processing path

The test thoroughly validates the command's behavior when processing posts successfully. It properly mocks dependencies and verifies all important aspects of the implementation, including API calls, data transformation, and database operations.


102-123: Good error handling test for posts without front matter

This test effectively ensures that posts without proper front matter are skipped and don't cause any errors. The command correctly handles invalid input without attempting to create invalid post entries.


124-151: Well-handled edge case for update_data returning None

The test properly validates that the command continues processing even when the update_data method returns None, ensuring the bulk_save method is still called with the correct parameters. This verifies the robustness of the implementation.

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

Successfully merging this pull request may close these issues.

Implement owasp.org posts previews
2 participants