From c482f6758c16bb53e300c501092c12b585418380 Mon Sep 17 00:00:00 2001 From: Lucas Jensen Date: Sun, 12 Jan 2025 16:47:04 -0800 Subject: [PATCH 1/5] basic frontend and backend functionality. Need to test with actual livestream --- client/src/EditModals/EditModal.tsx | 1 + client/src/Forms/Bio/BioForm.tsx | 31 +++++++++++++++++-- client/src/Group/Group.tsx | 11 +++++-- .../src/LivestreamPlayer/LivestreamPlayer.tsx | 25 +++++++++++++++ client/src/api.tsx | 18 +++++++++-- server/.gitignore | 5 ++- server/app/controllers/controller.py | 18 ++++++----- server/app/controllers/group.py | 10 ++++++ server/app/db/group.py | 12 +++++++ server/app/models/group.py | 4 ++- server/app/routers/group.py | 3 +- .../tests/controllers/test_main_controller.py | 2 +- 12 files changed, 122 insertions(+), 18 deletions(-) create mode 100644 client/src/LivestreamPlayer/LivestreamPlayer.tsx diff --git a/client/src/EditModals/EditModal.tsx b/client/src/EditModals/EditModal.tsx index 40a9397..655b81e 100644 --- a/client/src/EditModals/EditModal.tsx +++ b/client/src/EditModals/EditModal.tsx @@ -10,6 +10,7 @@ interface EditModalProps { form: JSX.Element; entity?: MusicianProps | GroupObj | EventSeriesObj; error?: string; + livestream_id?: string; } function EditModal(props: EditModalProps) { diff --git a/client/src/Forms/Bio/BioForm.tsx b/client/src/Forms/Bio/BioForm.tsx index 240a4b9..0e4dba5 100644 --- a/client/src/Forms/Bio/BioForm.tsx +++ b/client/src/Forms/Bio/BioForm.tsx @@ -11,10 +11,12 @@ interface EditBioFormProps { setGroup?: React.Dispatch>; setMusician?: React.Dispatch>; token: string; + livestream_id?: string; } function EditBioForm(props: EditBioFormProps) { const [formBio, setFormBio] = useState(props.entity.bio); + const [formLivestreamId, setFormLivestreamId] = useState(props.livestream_id) const [canSubmit, setCanSubmit] = useState(false); const [error, setError] = useState(""); @@ -23,6 +25,11 @@ function EditBioForm(props: EditBioFormProps) { setCanSubmit(true); }; + const handleLivestreamChange = (event: React.ChangeEvent) => { + setFormLivestreamId(event.target.value); + setCanSubmit(true); + } + const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); if (props.entity instanceof MusicianObj) { @@ -53,7 +60,6 @@ function EditBioForm(props: EditBioFormProps) { } }) .catch((error) => { - console.error(error); setError("Failed to update bio: " + error.response.data.detail); }); }; @@ -62,7 +68,8 @@ function EditBioForm(props: EditBioFormProps) { accessToken: string, group: GroupObj, ): Promise => { - patchGroup(group.id, formBio, group.name, accessToken) + const livestream_id = formLivestreamId ? formLivestreamId : ""; + patchGroup(group.id, formBio, livestream_id, group.name, accessToken) .then((patchedGroup) => { if (props.setGroup) { props.setGroup(patchedGroup); @@ -86,6 +93,26 @@ function EditBioForm(props: EditBioFormProps) { return (
+ {/* need to account for empty string, which is falsy but the field should still show */} + {props.livestream_id != undefined && ( + +

+ A livestream id is the part of a youtube url following "v=". + For example, "ncyl7cTU9k8" but without the quotations. + Don't mess it up.

+ To remove an embedded livestream, just clear this field and submit the form. +

+ Livestream ID: + + +
+ )} Bio } /> @@ -67,6 +71,9 @@ function Group(props: GroupProps) {

{group.name}

+ {group.livestream_id && ( + + )} {props.token && EditIcon} {group.bio} diff --git a/client/src/LivestreamPlayer/LivestreamPlayer.tsx b/client/src/LivestreamPlayer/LivestreamPlayer.tsx new file mode 100644 index 0000000..35c3d6a --- /dev/null +++ b/client/src/LivestreamPlayer/LivestreamPlayer.tsx @@ -0,0 +1,25 @@ +import { Container } from "react-bootstrap"; + +interface LivestreamPlayerProps { + livestreamId: string; +} + +const LivestreamPlayer = (props: LivestreamPlayerProps) => { + const iframeSrc = `https://www.youtube.com/embed/${props.livestreamId}`; + + return ( + +
+ +
+
+ ); +}; + +export default LivestreamPlayer; diff --git a/client/src/api.tsx b/client/src/api.tsx index 0f0aadd..f9ffdcf 100644 --- a/client/src/api.tsx +++ b/client/src/api.tsx @@ -38,6 +38,7 @@ export const getRoot = async (): Promise => { response.data.group.id, response.data.group.name, response.data.group.bio, + response.data.group.livestream_id, ), response.data.musicians.map( (musician: MusicianObj) => @@ -104,7 +105,12 @@ export const postUser = async (token: string): Promise => { export const getGroup = async (): Promise => { const response = await api.get("/group/"); - return new GroupObj(response.data.id, response.data.name, response.data.bio); + return new GroupObj( + response.data.id, + response.data.name, + response.data.bio, + response.data.livestream_id, + ); }; export const getMusicians = async (): Promise => { @@ -143,15 +149,21 @@ export const patchMusician = async ( export const patchGroup = async ( id: number, bio: string, + livestream_id: string, name: string, user_token: string, ): Promise => { const response = await api.patch( `/group/`, - { id, bio, name }, + { id, bio, livestream_id, name }, { headers: { Authorization: `Bearer ${user_token}` } }, ); - return new GroupObj(response.data.id, response.data.name, response.data.bio); + return new GroupObj( + response.data.id, + response.data.name, + response.data.bio, + response.data.livestream_id, + ); }; export const postHeadshot = async ( diff --git a/server/.gitignore b/server/.gitignore index ffeaa36..665afac 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -176,4 +176,7 @@ pyrightconfig.json # End of https://www.toptal.com/developers/gitignore/api/python *.log -.vscode/ \ No newline at end of file +.vscode/ + +# mysql dumps +*.dump \ No newline at end of file diff --git a/server/app/controllers/controller.py b/server/app/controllers/controller.py index 9cbd481..c1148e1 100644 --- a/server/app/controllers/controller.py +++ b/server/app/controllers/controller.py @@ -197,16 +197,20 @@ class MainController: """ return self.group_controller.get_group() - async def update_group_bio( - self, bio: str, token: HTTPAuthorizationCredentials + async def update_group( + self, group: Group, token: HTTPAuthorizationCredentials ) -> Group: """ Updates the group's bio and returns the updated group object. - - :param str bio: The new bio for the group - :param HTTPAuthorizationCredentials token: The OAuth token - :return Group: The updated group object which is suitable for a response body """ _, sub = self.oauth_token.email_and_sub(token) self.user_controller.get_user_by_sub(sub) - return self.group_controller.update_group_bio(bio) + self.group_controller.update_livestream(group.livestream_id) + return self.group_controller.update_group_bio(group.bio) + + async def update_livestream( + self, livestream_id: str, token: HTTPAuthorizationCredentials + ) -> Group: + _, sub = self.oauth_token.email_and_sub(token) + self.user_controller.get_user_by_sub(sub) + return self.group_controller.update_livestream(livestream_id) diff --git a/server/app/controllers/group.py b/server/app/controllers/group.py index 4c68b1f..de8182e 100644 --- a/server/app/controllers/group.py +++ b/server/app/controllers/group.py @@ -57,3 +57,13 @@ class GroupController(BaseController): detail=f"Error updating group bio: {e}", ) return self.get_group() + + def update_livestream(self, livestream_id: str) -> Group: + try: + self.group_queries.update_livestream(livestream_id) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error updating livestram: {e}", + ) + return self.get_group() diff --git a/server/app/db/group.py b/server/app/db/group.py index 246a2fd..5cd6626 100644 --- a/server/app/db/group.py +++ b/server/app/db/group.py @@ -34,3 +34,15 @@ class GroupQueries(BaseQueries): cursor.execute(query, (bio,)) conn.commit() self.close_cursor_and_conn(cursor, conn) + + def update_livestream(self, livestream_id: str) -> None: + cursor, conn = self.get_cursor_and_conn() + query = f"""-- sql + UPDATE {self.table} SET livestream_id = %s WHERE id = 1 + """ + cursor.execute(query, (livestream_id,)) + conn.commit() + self.close_cursor_and_conn(cursor, conn) + + def delete_livestream(self) -> None: + self.update_livestream(livestream_id="") diff --git a/server/app/models/group.py b/server/app/models/group.py index 4a36fbf..9aab010 100644 --- a/server/app/models/group.py +++ b/server/app/models/group.py @@ -1,7 +1,9 @@ from pydantic import BaseModel +from typing import Optional class Group(BaseModel): name: str bio: str - id: int | None = None + livestream_id: str = "" + id: Optional[int] = None diff --git a/server/app/routers/group.py b/server/app/routers/group.py index f059b6e..ddff74d 100644 --- a/server/app/routers/group.py +++ b/server/app/routers/group.py @@ -24,4 +24,5 @@ async def update_group( ) -> Group: """Updates the group bio, but requires the entire group object to be sent in the request body. Requires authentication.""" - return await controller.update_group_bio(group.bio, token) + ic(group) + return await controller.update_group(group, token) diff --git a/server/tests/controllers/test_main_controller.py b/server/tests/controllers/test_main_controller.py index df2dd6f..186f868 100644 --- a/server/tests/controllers/test_main_controller.py +++ b/server/tests/controllers/test_main_controller.py @@ -158,7 +158,7 @@ async def test_get_group(): async def test_update_group_bio(): """Tests the update_group_bio method.""" bio = "A new bio" - await controller.update_group_bio(bio, mock_token) + await controller.update_group(bio, mock_token) MagicMock.assert_called_with(mock_oauth_token.email_and_sub, mock_token) MagicMock.assert_called(mock_user_controller.get_user_by_sub) MagicMock.assert_called(mock_group_controller.update_group_bio) From fb305e55094bfed37567ce5bdd8baff7b62d9ae1 Mon Sep 17 00:00:00 2001 From: Lucas Jensen Date: Wed, 15 Jan 2025 17:50:44 -0800 Subject: [PATCH 2/5] minor cleanup and tested with actual livestream --- client/src/Forms/Bio/BioForm.tsx | 23 +++++++++++-------- .../src/LivestreamPlayer/LivestreamPlayer.tsx | 2 +- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/client/src/Forms/Bio/BioForm.tsx b/client/src/Forms/Bio/BioForm.tsx index 0e4dba5..e84d579 100644 --- a/client/src/Forms/Bio/BioForm.tsx +++ b/client/src/Forms/Bio/BioForm.tsx @@ -16,7 +16,9 @@ interface EditBioFormProps { function EditBioForm(props: EditBioFormProps) { const [formBio, setFormBio] = useState(props.entity.bio); - const [formLivestreamId, setFormLivestreamId] = useState(props.livestream_id) + const [formLivestreamId, setFormLivestreamId] = useState( + props.livestream_id, + ); const [canSubmit, setCanSubmit] = useState(false); const [error, setError] = useState(""); @@ -25,10 +27,12 @@ function EditBioForm(props: EditBioFormProps) { setCanSubmit(true); }; - const handleLivestreamChange = (event: React.ChangeEvent) => { + const handleLivestreamChange = ( + event: React.ChangeEvent, + ) => { setFormLivestreamId(event.target.value); setCanSubmit(true); - } + }; const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); @@ -97,10 +101,12 @@ function EditBioForm(props: EditBioFormProps) { {props.livestream_id != undefined && (

- A livestream id is the part of a youtube url following "v=". - For example, "ncyl7cTU9k8" but without the quotations. - Don't mess it up.

- To remove an embedded livestream, just clear this field and submit the form. + A livestream id is part of a youtube url. Either + ".../v=[livestream_id]" or ".../live/[livestream_id]". For example, + "ncyl7cTU9k8" but without the quotations. Don't mess it up.{" "} +

+ To remove an embedded livestream, just clear this field and submit + the form.

Livestream ID: -
)} diff --git a/client/src/LivestreamPlayer/LivestreamPlayer.tsx b/client/src/LivestreamPlayer/LivestreamPlayer.tsx index 35c3d6a..c8e22c3 100644 --- a/client/src/LivestreamPlayer/LivestreamPlayer.tsx +++ b/client/src/LivestreamPlayer/LivestreamPlayer.tsx @@ -5,7 +5,7 @@ interface LivestreamPlayerProps { } const LivestreamPlayer = (props: LivestreamPlayerProps) => { - const iframeSrc = `https://www.youtube.com/embed/${props.livestreamId}`; + const iframeSrc = `https://www.youtube.com/embed/${props.livestreamId}?autoplay=1`; return ( From a961118f77da8e19dece89a4f0bc06c49e6db0c7 Mon Sep 17 00:00:00 2001 From: Lucas Jensen Date: Wed, 15 Jan 2025 17:52:14 -0800 Subject: [PATCH 3/5] let env vars be overwritten --- server/app/db/conn.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/app/db/conn.py b/server/app/db/conn.py index b597fe3..19c7540 100644 --- a/server/app/db/conn.py +++ b/server/app/db/conn.py @@ -15,12 +15,17 @@ def connect_db() -> mysql.connector.MySQLConnection: Credential values are validated and an exception is raised if any are missing. """ - load_dotenv() + load_dotenv(override=True) host = os.getenv("DB_HOST") user = os.getenv("DB_USER") password = os.getenv("DB_PASSWORD") database = os.getenv("DB_DATABASE") + print(host) + print(user) + print(password) + print(database) + if None in [host, user, password, database]: raise DBException("Missing database credentials") From 842709884e417cd7412253a5342726b0c10f9f18 Mon Sep 17 00:00:00 2001 From: Lucas Jensen Date: Wed, 15 Jan 2025 18:07:43 -0800 Subject: [PATCH 4/5] fix type issues and remove logging --- client/src/EditModals/EditModal.tsx | 4 ++-- server/app/db/conn.py | 5 ----- server/app/routers/group.py | 1 - 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/client/src/EditModals/EditModal.tsx b/client/src/EditModals/EditModal.tsx index 655b81e..6f4ced7 100644 --- a/client/src/EditModals/EditModal.tsx +++ b/client/src/EditModals/EditModal.tsx @@ -1,5 +1,5 @@ import Modal from "react-bootstrap/Modal"; -import { MusicianProps } from "../Musicians/Musician/Musician"; +import { MusicianObj } from "../Musicians/Musician/Musician"; import { GroupObj } from "../Group/Group"; import { EventSeriesObj } from "../Series/SeriesList"; @@ -8,7 +8,7 @@ interface EditModalProps { show: boolean; onHide: () => void; form: JSX.Element; - entity?: MusicianProps | GroupObj | EventSeriesObj; + entity?: MusicianObj | GroupObj | EventSeriesObj; error?: string; livestream_id?: string; } diff --git a/server/app/db/conn.py b/server/app/db/conn.py index 19c7540..a1d5893 100644 --- a/server/app/db/conn.py +++ b/server/app/db/conn.py @@ -21,11 +21,6 @@ def connect_db() -> mysql.connector.MySQLConnection: password = os.getenv("DB_PASSWORD") database = os.getenv("DB_DATABASE") - print(host) - print(user) - print(password) - print(database) - if None in [host, user, password, database]: raise DBException("Missing database credentials") diff --git a/server/app/routers/group.py b/server/app/routers/group.py index ddff74d..cda1da4 100644 --- a/server/app/routers/group.py +++ b/server/app/routers/group.py @@ -24,5 +24,4 @@ async def update_group( ) -> Group: """Updates the group bio, but requires the entire group object to be sent in the request body. Requires authentication.""" - ic(group) return await controller.update_group(group, token) From 4b41a76eb0974ecdd88c0e467be9d6bbc2ff3930 Mon Sep 17 00:00:00 2001 From: Lucas Jensen Date: Wed, 15 Jan 2025 18:13:34 -0800 Subject: [PATCH 5/5] fix test to reflect changed group passing --- server/tests/controllers/test_main_controller.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/tests/controllers/test_main_controller.py b/server/tests/controllers/test_main_controller.py index 186f868..c31fdda 100644 --- a/server/tests/controllers/test_main_controller.py +++ b/server/tests/controllers/test_main_controller.py @@ -4,6 +4,7 @@ import pytest from app.controllers.controller import MainController from app.models.event import EventSeries, NewEventSeries +from app.models.group import Group from app.models.musician import Musician mock_user_controller = MagicMock() @@ -158,7 +159,8 @@ async def test_get_group(): async def test_update_group_bio(): """Tests the update_group_bio method.""" bio = "A new bio" - await controller.update_group(bio, mock_token) + group = Group(name="The Grapefruits Duo", bio=bio, livestream_id="") + await controller.update_group(group, mock_token) MagicMock.assert_called_with(mock_oauth_token.email_and_sub, mock_token) MagicMock.assert_called(mock_user_controller.get_user_by_sub) MagicMock.assert_called(mock_group_controller.update_group_bio)