-
-
Notifications
You must be signed in to change notification settings - Fork 21
ZA | 25-SDC-JULY | Luke Manyamazi | Sprint 1 | New Feature Rebloom #58
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
base: main
Are you sure you want to change the base?
Changes from all commits
57b64bd
deb04d3
c5f8697
8f5e4e4
30b4999
a975288
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,37 +4,203 @@ | |
| from typing import Any, Dict, List, Optional | ||
|
|
||
| from data.connection import db_cursor | ||
| from data.users import User | ||
|
|
||
| from data.users import User, get_user_by_id | ||
|
|
||
| @dataclass | ||
| class Bloom: | ||
| id: int | ||
| sender: User | ||
| content: str | ||
| sent_timestamp: datetime.datetime | ||
| rebloom_count: int = 0 | ||
| original_bloom_id: Optional[int] = None | ||
| is_rebloom: bool = False | ||
| original_sender: Optional[User] = None | ||
|
|
||
|
|
||
| def add_bloom(*, sender: User, content: str) -> Bloom: | ||
| def add_bloom( | ||
| *, | ||
| sender: User, | ||
| content: str, | ||
| is_rebloom: bool = False, | ||
| original_bloom_id: Optional[int] = None | ||
| ) -> int: | ||
| """Create a new bloom and associate any hashtags.""" | ||
|
|
||
| hashtags = [word[1:] for word in content.split(" ") if word.startswith("#")] | ||
|
|
||
| now = datetime.datetime.now(tz=datetime.UTC) | ||
| bloom_id = int(now.timestamp() * 1000000) | ||
|
|
||
| with db_cursor() as cur: | ||
| cur.execute( | ||
| "INSERT INTO blooms (id, sender_id, content, send_timestamp) VALUES (%(bloom_id)s, %(sender_id)s, %(content)s, %(timestamp)s)", | ||
| dict( | ||
| bloom_id=bloom_id, | ||
| sender_id=sender.id, | ||
| try: | ||
| cur.execute( | ||
| """ | ||
| INSERT INTO blooms | ||
| (id, sender_id, content, send_timestamp, original_bloom_id) | ||
| VALUES | ||
| (%(bloom_id)s, %(sender_id)s, %(content)s, %(timestamp)s, %(original_bloom_id)s) | ||
| """, | ||
| dict( | ||
| bloom_id=bloom_id, | ||
| sender_id=sender.id, | ||
| content=content, | ||
| timestamp=now, # Pass Python datetime object to resolve SQL type error | ||
| original_bloom_id=original_bloom_id, | ||
| ), | ||
| ) | ||
|
|
||
| for hashtag in hashtags: | ||
| cur.execute( | ||
| "INSERT INTO hashtags (hashtag, bloom_id) VALUES (%(hashtag)s, %(bloom_id)s)", | ||
| dict(hashtag=hashtag, bloom_id=bloom_id), | ||
| ) | ||
|
|
||
| return bloom_id | ||
|
|
||
| except Exception as e: | ||
| # Keep error logging for debugging purposes | ||
| print(f"Error adding bloom: {e}") | ||
| # Returning 0 or raising an error are typical ways to indicate failure | ||
| return 0 | ||
|
|
||
| def add_rebloom(user_id: int, original_bloom_id: int, content: str) -> Optional[int]: | ||
| """Create a new rebloom""" | ||
|
|
||
| # 💥 FIX: Fetch the complete User object using the user_id | ||
| # This prevents the "missing required positional arguments" error | ||
| sender_user = get_user_by_id(user_id) | ||
|
|
||
| if sender_user is None: | ||
| print(f"Error adding rebloom: User with ID {user_id} not found.") | ||
| return None | ||
|
Comment on lines
+74
to
+75
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't bubble the error back up to the caller - the caller will need to know this failed. This applies throughout your change. |
||
|
|
||
| with db_cursor() as cur: | ||
| try: | ||
| # First create the new bloom | ||
| rebloom_bloom_id = add_bloom( | ||
| sender=sender_user, # <<< Use the fully hydrated User object | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AFAICT |
||
| content=content, | ||
| timestamp=datetime.datetime.now(datetime.UTC), | ||
| ), | ||
| is_rebloom=True, | ||
| original_bloom_id=original_bloom_id | ||
| ) | ||
|
|
||
| if rebloom_bloom_id: | ||
| # Create the rebloom relationship | ||
| cur.execute( | ||
| "INSERT INTO reblooms (user_id, original_bloom_id, rebloom_bloom_id) VALUES (%s, %s, %s) RETURNING id", | ||
| (user_id, original_bloom_id, rebloom_bloom_id) | ||
| ) | ||
| result = cur.fetchone() | ||
| if result is None: | ||
| return None | ||
| rebloom_id = result[0] | ||
|
|
||
| # Update rebloom count on original bloom | ||
| # This step will now execute successfully since the code no longer crashes above | ||
| cur.execute( | ||
| "UPDATE blooms SET rebloom_count = rebloom_count + 1 WHERE id = %s", | ||
| (original_bloom_id,) | ||
| ) | ||
|
|
||
| return rebloom_id | ||
| return None | ||
| except Exception as e: | ||
| # The count should now increase before any exception related to DB occurs | ||
| print(f"Error adding rebloom: {e}") | ||
| return None | ||
|
|
||
| def get_rebloom_by_user_and_bloom(user_id: int, original_bloom_id: int) -> Optional[Dict]: | ||
| """Check if user has already rebloomed a specific bloom""" | ||
| with db_cursor() as cur: | ||
| cur.execute( | ||
| "SELECT * FROM reblooms WHERE user_id = %s AND original_bloom_id = %s", | ||
| (user_id, original_bloom_id) | ||
| ) | ||
| for hashtag in hashtags: | ||
| row = cur.fetchone() | ||
| if row: | ||
| return { | ||
| 'id': row[0], | ||
| 'user_id': row[1], | ||
| 'original_bloom_id': row[2], | ||
| 'rebloom_bloom_id': row[3], | ||
| 'created_at': row[4] | ||
| } | ||
| return None | ||
|
|
||
|
|
||
| def delete_rebloom(rebloom_id: int) -> bool: | ||
| """Delete a rebloom and its associated bloom""" | ||
| with db_cursor() as cur: | ||
| try: | ||
| # Get the rebloom details first | ||
| cur.execute("SELECT rebloom_bloom_id, original_bloom_id FROM reblooms WHERE id = %s", (rebloom_id,)) | ||
| rebloom_data = cur.fetchone() | ||
|
|
||
| if not rebloom_data: | ||
| return False | ||
|
|
||
| rebloom_bloom_id, original_bloom_id = rebloom_data | ||
|
|
||
| # Delete the rebloom bloom | ||
| cur.execute("DELETE FROM blooms WHERE id = %s", (rebloom_bloom_id,)) | ||
|
|
||
| # Delete the rebloom relationship | ||
| cur.execute("DELETE FROM reblooms WHERE id = %s", (rebloom_id,)) | ||
|
|
||
| # Decrement rebloom count on original bloom | ||
| cur.execute( | ||
| "INSERT INTO hashtags (hashtag, bloom_id) VALUES (%(hashtag)s, %(bloom_id)s)", | ||
| dict(hashtag=hashtag, bloom_id=bloom_id), | ||
| "UPDATE blooms SET rebloom_count = rebloom_count - 1 WHERE id = %s", | ||
| (original_bloom_id,) | ||
| ) | ||
|
|
||
| return True | ||
| except Exception as e: | ||
| print(f"Error deleting rebloom: {e}") | ||
| return False | ||
|
|
||
|
|
||
| def has_user_rebloomed(user_id: int, bloom_id: int) -> bool: | ||
| """Check if user has rebloomed a specific bloom""" | ||
| return get_rebloom_by_user_and_bloom(user_id, bloom_id) is not None | ||
|
|
||
|
|
||
| def get_rebloom_count(bloom_id: int) -> int: | ||
| """Get the number of times a bloom has been rebloomed""" | ||
| with db_cursor() as cur: | ||
| cur.execute("SELECT rebloom_count FROM blooms WHERE id = %s", (bloom_id,)) | ||
| result = cur.fetchone() | ||
| return result[0] if result else 0 | ||
|
|
||
|
|
||
| def get_user_reblooms(user_id: int) -> List[Dict]: | ||
| """Get all reblooms by a user""" | ||
| with db_cursor() as cur: | ||
| cur.execute(""" | ||
| SELECT r.*, b.content, b.send_timestamp, ob.sender_id as original_sender_id, | ||
| ou.username as original_username, ob.id as original_bloom_id | ||
| FROM reblooms r | ||
| JOIN blooms b ON r.rebloom_bloom_id = b.id | ||
| JOIN blooms ob ON r.original_bloom_id = ob.id | ||
| JOIN users ou ON ob.sender_id = ou.id | ||
| WHERE r.user_id = %s | ||
| ORDER BY r.created_at DESC | ||
| """, (user_id,)) | ||
|
|
||
| reblooms = [] | ||
| for row in cur.fetchall(): | ||
| reblooms.append({ | ||
| 'id': row[0], | ||
| 'user_id': row[1], | ||
| 'original_bloom_id': row[2], | ||
| 'rebloom_bloom_id': row[3], | ||
| 'created_at': row[4], | ||
| 'content': row[5], | ||
| 'rebloom_timestamp': row[6], | ||
| 'original_sender_id': row[7], | ||
| 'original_username': row[8], | ||
| 'original_bloom_id': row[9] | ||
| }) | ||
| return reblooms | ||
|
|
||
|
|
||
| def get_blooms_for_user( | ||
|
|
@@ -54,7 +220,7 @@ def get_blooms_for_user( | |
|
|
||
| cur.execute( | ||
| f"""SELECT | ||
| blooms.id, users.username, content, send_timestamp | ||
| blooms.id, users.username, content, send_timestamp, rebloom_count, original_bloom_id | ||
| FROM | ||
| blooms INNER JOIN users ON users.id = blooms.sender_id | ||
| WHERE | ||
|
|
@@ -66,36 +232,57 @@ def get_blooms_for_user( | |
| kwargs, | ||
| ) | ||
| rows = cur.fetchall() | ||
| blooms = [] | ||
| blooms_list = [] | ||
| for row in rows: | ||
| bloom_id, sender_username, content, timestamp = row | ||
| blooms.append( | ||
| Bloom( | ||
| id=bloom_id, | ||
| sender=sender_username, | ||
| content=content, | ||
| sent_timestamp=timestamp, | ||
| ) | ||
| bloom_id, sender_username, content, timestamp, rebloom_count, original_bloom_id = row | ||
| bloom = Bloom( | ||
| id=bloom_id, | ||
| sender=sender_username, | ||
| content=content, | ||
| sent_timestamp=timestamp, | ||
| rebloom_count=rebloom_count, | ||
| original_bloom_id=original_bloom_id, | ||
| is_rebloom=original_bloom_id is not None | ||
| ) | ||
| return blooms | ||
|
|
||
| # If this is a rebloom, get original sender info | ||
| if original_bloom_id: | ||
| original_bloom = get_bloom(original_bloom_id) | ||
| if original_bloom: | ||
| bloom.original_sender = original_bloom.sender | ||
|
|
||
| blooms_list.append(bloom) | ||
| return blooms_list | ||
|
|
||
|
|
||
| def get_bloom(bloom_id: int) -> Optional[Bloom]: | ||
| with db_cursor() as cur: | ||
| cur.execute( | ||
| "SELECT blooms.id, users.username, content, send_timestamp FROM blooms INNER JOIN users ON users.id = blooms.sender_id WHERE blooms.id = %s", | ||
| "SELECT blooms.id, users.username, content, send_timestamp, rebloom_count, original_bloom_id FROM blooms INNER JOIN users ON users.id = blooms.sender_id WHERE blooms.id = %s", | ||
| (bloom_id,), | ||
| ) | ||
| row = cur.fetchone() | ||
| if row is None: | ||
| return None | ||
| bloom_id, sender_username, content, timestamp = row | ||
| return Bloom( | ||
| bloom_id, sender_username, content, timestamp, rebloom_count, original_bloom_id = row | ||
|
|
||
| bloom = Bloom( | ||
| id=bloom_id, | ||
| sender=sender_username, | ||
| content=content, | ||
| sent_timestamp=timestamp, | ||
| rebloom_count=rebloom_count, | ||
| original_bloom_id=original_bloom_id, | ||
| is_rebloom=original_bloom_id is not None | ||
| ) | ||
|
|
||
| # If this is a rebloom, get original sender info | ||
| if original_bloom_id: | ||
| original_bloom = get_bloom(original_bloom_id) | ||
| if original_bloom: | ||
| bloom.original_sender = original_bloom.sender | ||
|
|
||
| return bloom | ||
|
|
||
|
|
||
| def get_blooms_with_hashtag( | ||
|
|
@@ -108,7 +295,7 @@ def get_blooms_with_hashtag( | |
| with db_cursor() as cur: | ||
| cur.execute( | ||
| f"""SELECT | ||
| blooms.id, users.username, content, send_timestamp | ||
| blooms.id, users.username, content, send_timestamp, rebloom_count, original_bloom_id | ||
| FROM | ||
| blooms INNER JOIN hashtags ON blooms.id = hashtags.bloom_id INNER JOIN users ON blooms.sender_id = users.id | ||
| WHERE | ||
|
|
@@ -119,18 +306,27 @@ def get_blooms_with_hashtag( | |
| kwargs, | ||
| ) | ||
| rows = cur.fetchall() | ||
| blooms = [] | ||
| blooms_list = [] | ||
| for row in rows: | ||
| bloom_id, sender_username, content, timestamp = row | ||
| blooms.append( | ||
| Bloom( | ||
| id=bloom_id, | ||
| sender=sender_username, | ||
| content=content, | ||
| sent_timestamp=timestamp, | ||
| ) | ||
| bloom_id, sender_username, content, timestamp, rebloom_count, original_bloom_id = row | ||
| bloom = Bloom( | ||
| id=bloom_id, | ||
| sender=sender_username, | ||
| content=content, | ||
| sent_timestamp=timestamp, | ||
| rebloom_count=rebloom_count, | ||
| original_bloom_id=original_bloom_id, | ||
| is_rebloom=original_bloom_id is not None | ||
| ) | ||
| return blooms | ||
|
|
||
| # If this is a rebloom, get original sender info | ||
| if original_bloom_id: | ||
| original_bloom = get_bloom(original_bloom_id) | ||
| if original_bloom: | ||
| bloom.original_sender = original_bloom.sender | ||
|
|
||
| blooms_list.append(bloom) | ||
| return blooms_list | ||
|
|
||
|
|
||
| def make_limit_clause(limit: Optional[int], kwargs: Dict[Any, Any]) -> str: | ||
|
|
@@ -139,4 +335,4 @@ def make_limit_clause(limit: Optional[int], kwargs: Dict[Any, Any]) -> str: | |
| kwargs["limit"] = limit | ||
| else: | ||
| limit_clause = "" | ||
| return limit_clause | ||
| return limit_clause | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -106,3 +106,27 @@ def generate_salt() -> bytes: | |
| def lookup_user(header_info, payload_info): | ||
| """lookup_user is a hook for the jwt middleware to look-up authenticated users.""" | ||
| return get_user(payload_info["sub"]) | ||
|
|
||
| def get_user_by_id(user_id: int) -> Optional[User]: | ||
| """ | ||
| Retrieves a complete User object from the database by their ID. | ||
| This is necessary for operations like reblooming where only the ID is known. | ||
| """ | ||
| with db_cursor() as cur: | ||
| # NOTE: Adjust column names if they are different in your 'users' table | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does this comment mean? |
||
| cur.execute( | ||
| "SELECT id, username, password_salt, password_scrypt FROM users WHERE id = %s", | ||
| (user_id,) | ||
| ) | ||
| row = cur.fetchone() | ||
|
|
||
| if row: | ||
| user_id, username, password_salt, password_scrypt = row | ||
| # Create and return the full User object | ||
| return User( | ||
| id=user_id, | ||
| username=username, | ||
| password_salt=password_salt, | ||
| password_scrypt=password_scrypt | ||
| ) | ||
| return None | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think any other code in this codebases uses magic return values to indicate errors, and the code that calls this doesn't pass this error back to the client. You need to handle errors differently here.