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)