Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 59 additions & 4 deletions backend/data/blooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ class Bloom:
content: str
sent_timestamp: datetime.datetime

@dataclass
class Rebloom:
id: int
resender: User
sender: User
content: str
sent_timestamp: datetime.datetime
rebloomed: int


def add_bloom(*, sender: User, content: str) -> Bloom:
hashtags = [word[1:] for word in content.split(" ") if word.startswith("#")]
Expand All @@ -36,6 +45,26 @@ def add_bloom(*, sender: User, content: str) -> Bloom:
dict(hashtag=hashtag, bloom_id=bloom_id),
)

def rebloom(*, rebloom_id: int, resender: User, sender: User, content: str, sent_timestamp: datetime.datetime) -> Rebloom:
with db_cursor() as cur:
cur.execute(
"INSERT INTO reblooms (id, resender_name, original_sender_name, content, send_timestamp, times_rebloomed) VALUES (%(rebloom_id)s, %(resender_name)s, %(sender_name)s, %(content)s, %(timestamp)s, 1) ON CONFLICT (id) DO UPDATE SET times_rebloomed = reblooms.times_rebloomed + 1",
dict(
rebloom_id=rebloom_id,
resender_name=resender.username,
sender_name=sender,
content=content,
timestamp=sent_timestamp
),
)
return Rebloom(
id=rebloom_id,
resender=resender,
sender=sender,
content=content,
sent_timestamp=sent_timestamp,
rebloomed=1
)

def get_blooms_for_user(
username: str, *, before: Optional[int] = None, limit: Optional[int] = None
Expand All @@ -50,8 +79,6 @@ def get_blooms_for_user(
else:
before_clause = ""

limit_clause = make_limit_clause(limit, kwargs)

cur.execute(
f"""SELECT
blooms.id, users.username, content, send_timestamp
Expand All @@ -60,8 +87,6 @@ def get_blooms_for_user(
WHERE
username = %(sender_username)s
{before_clause}
ORDER BY send_timestamp DESC
{limit_clause}
""",
kwargs,
)
Expand All @@ -80,6 +105,35 @@ def get_blooms_for_user(
return blooms


def get_reblooms_for_user(username: str) -> List[Rebloom]:
with db_cursor() as cur:
cur.execute(
"""SELECT
reblooms.id, users.username, original_sender_name, content, send_timestamp, times_rebloomed
FROM
reblooms INNER JOIN users ON users.username = reblooms.resender_name
WHERE
users.username = %(username)s
""",
{"username": username},
)
rows = cur.fetchall()
reblooms = []
for row in rows:
rebloom_id, resender_name, sender_name, content, timestamp, times_rebloomed = row
reblooms.append(
Rebloom(
id=rebloom_id,
resender=resender_name,
sender=sender_name,
content=content,
sent_timestamp=timestamp,
rebloomed=times_rebloomed
)
)
return reblooms


def get_bloom(bloom_id: int) -> Optional[Bloom]:
with db_cursor() as cur:
cur.execute(
Expand Down Expand Up @@ -140,3 +194,4 @@ def make_limit_clause(limit: Optional[int], kwargs: Dict[Any, Any]) -> str:
else:
limit_clause = ""
return limit_clause

34 changes: 32 additions & 2 deletions backend/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,15 @@ def other_profile(profile_username):

followers = get_inverse_followed_usernames(profile_user)
all_blooms = blooms.get_blooms_for_user(profile_username)
all_blooms.reverse()
all_reblooms = blooms.get_reblooms_for_user(profile_username)

bloom_data = all_blooms + all_reblooms
bloom_data.reverse()

return jsonify(
{
"username": profile_username,
"recent_blooms": all_blooms[:10],
"recent_blooms": bloom_data[:10],
"follows": get_followed_usernames(profile_user),
"followers": list(followers),
"is_following": current_user is not None
Expand Down Expand Up @@ -167,6 +171,26 @@ def send_bloom():
)


@jwt_required()
def send_rebloom():
type_check_error = verify_request_fields({"content": str})
if type_check_error is not None:
return type_check_error

user = get_current_user()

if user.username == request.json["sender"]:
return make_response((f"Cannot rebloom own bloom", 422))

blooms.rebloom(rebloom_id=request.json["id"], resender=user, sender=request.json["sender"], content=request.json["content"], sent_timestamp=request.json["sent_timestamp"])

return jsonify(
{
"success": True,
}
)


def get_bloom(id_str):
try:
id_int = int(id_str)
Expand Down Expand Up @@ -212,6 +236,12 @@ def user_blooms(profile_username):
return jsonify(user_blooms)


def user_reblooms(profile_username):
user_reblooms = blooms.get_reblooms_for_user(profile_username)
user_reblooms.reverse()
return jsonify(user_reblooms)


@jwt_required()
def suggested_follows(limit_str):
try:
Expand Down
4 changes: 4 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
register,
self_profile,
send_bloom,
send_rebloom,
suggested_follows,
user_blooms,
user_reblooms
)

from dotenv import load_dotenv
Expand Down Expand Up @@ -59,6 +61,8 @@ def main():
app.add_url_rule("/bloom", methods=["POST"], view_func=send_bloom)
app.add_url_rule("/bloom/<id_str>", methods=["GET"], view_func=get_bloom)
app.add_url_rule("/blooms/<profile_username>", view_func=user_blooms)
app.add_url_rule("/rebloom", methods=["POST"], view_func=send_rebloom)
app.add_url_rule("/reblooms/<profile_username>", view_func=user_reblooms)
app.add_url_rule("/hashtag/<hashtag>", view_func=hashtag)

app.run(host="0.0.0.0", port="3000", debug=True)
Expand Down
9 changes: 9 additions & 0 deletions db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ CREATE TABLE blooms (
send_timestamp TIMESTAMP NOT NULL
);

CREATE TABLE reblooms (
id BIGSERIAL NOT NULL PRIMARY KEY,
resender_name TEXT NOT NULL,
original_sender_name TEXT NOT NULL,
content TEXT NOT NULL,
send_timestamp TIMESTAMP NOT NULL,
times_rebloomed INT NOT NULL
);

CREATE TABLE follows (
id SERIAL PRIMARY KEY,
follower INT NOT NULL REFERENCES users(id),
Expand Down
15 changes: 15 additions & 0 deletions front-end/components/bloom.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { apiService } from "../lib/api.mjs"
import { state } from "../lib/state.mjs"

/**
* Create a bloom component
* @param {string} template - The ID of the template to clone
Expand All @@ -20,6 +23,8 @@ const createBloom = (template, bloom) => {
const bloomTime = bloomFrag.querySelector("[data-time]");
const bloomTimeLink = bloomFrag.querySelector("a:has(> [data-time])");
const bloomContent = bloomFrag.querySelector("[data-content]");
const rebloomButton = bloomFrag.querySelector("[data-rebloom]")
const timesRebloomedCounter = bloomFrag.querySelector("[data-times-rebloomed]")

bloomArticle.setAttribute("data-bloom-id", bloom.id);
bloomUsername.setAttribute("href", `/profile/${bloom.sender}`);
Expand All @@ -30,6 +35,16 @@ const createBloom = (template, bloom) => {
...bloomParser.parseFromString(_formatHashtags(bloom.content), "text/html")
.body.childNodes
);
rebloomButton.hidden = state.currentUser === bloom.sender
rebloomButton.addEventListener("click", async () => {
try {
await apiService.postRebloom(bloom.id, bloom.sender, bloom.content, bloom.sent_timestamp);
} catch (error) {
throw error;
}
});
timesRebloomedCounter.hidden = !bloom.rebloomed;
timesRebloomedCounter.textContent = `Times rebloomed: ${bloom.rebloomed}`

return bloomFrag;
};
Expand Down
1 change: 1 addition & 0 deletions front-end/components/error.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const _STATUS_MESSAGES = {
404: "Not Found - The requested resource does not exist.",
405: "Not Allowed - The server knows the request method, but the target resource doesn't support this method.",
418: "I'm a teapot - Server refuses to brew coffee with a teapot.",
422: "Invalid data - The request was well-formed but was unable to be followed due to semantic errors.",
500: "Internal Server Error - Something went wrong on the server.",
};

Expand Down
2 changes: 2 additions & 0 deletions front-end/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,8 @@ <h2 id="bloom-form-title" class="bloom-form__title">Share a Bloom</h2>
<a href="#" class="bloom__time"><time class="bloom__time" data-time>2m</time></a>
</div>
<div class="bloom__content" data-content></div>
<p data-times-rebloomed></p>
<button class="bloom__rebloom" data-rebloom>Rebloom</button>
</article>
</template>

Expand Down
20 changes: 20 additions & 0 deletions front-end/lib/api.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,25 @@ async function postBloom(content) {
}
}

async function postRebloom(id, sender, content, sent_timestamp) {
try {
const data = await _apiRequest("/rebloom", {
method: "POST",
body: JSON.stringify({id, sender, content, sent_timestamp}),
});

if (data.success) {
await getBlooms();
await getProfile(state.currentUser);
}

return data;
} catch (error) {
// Error already handled by _apiRequest
return {success: false};
}
}

// ======= USER methods
async function getProfile(username) {
const endpoint = username ? `/profile/${username}` : "/profile";
Expand Down Expand Up @@ -291,6 +310,7 @@ const apiService = {
getBloom,
getBlooms,
postBloom,
postRebloom,
getBloomsByHashtag,

// User methods
Expand Down