Skip to content

Commit

Permalink
Enable hearing/seeing speaker consent
Browse files Browse the repository at this point in the history
  • Loading branch information
imnasnainaec committed Nov 20, 2023
1 parent 4d678cd commit e581134
Show file tree
Hide file tree
Showing 22 changed files with 425 additions and 148 deletions.
25 changes: 24 additions & 1 deletion Backend/Controllers/SpeakerController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ public async Task<IActionResult> UploadConsentImage(
return NotFound(speakerId);
}

// Ensure file is not empty.
// Ensure file is valid
var file = fileUpload.File;
if (file is null)
{
Expand All @@ -319,5 +319,28 @@ public async Task<IActionResult> UploadConsentImage(
_ => Ok(speaker),
};
}

/// <summary> Get speaker's consent </summary>
/// <returns> Stream of local image file </returns>
[HttpGet("downloadconsentimage/{speakerId}", Name = "DownloadConsentImage")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(FileContentResult))]
public IActionResult DownloadConsentImage(string speakerId)
{
// SECURITY: Omitting authentication so the frontend can use the API endpoint directly as a URL.
// if (!await _permissionService.HasProjectPermission(HttpContext, Permission.WordEntry))
// {
// return Forbid();
// }

// Ensure file exists
var path = FileStorage.GenerateAvatarFilePath(speakerId);
if (!IO.File.Exists(path))
{
return NotFound(speakerId);
}

// Return file as stream
return File(IO.File.OpenRead(path), "application/octet-stream");
}
}
}
2 changes: 1 addition & 1 deletion public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@
"consent": {
"play": "Listen to the audio consent for this speaker",
"record": "Record audio consent for this speaker",
"see": "Look at the image consent for this speaker",
"look": "Look at the image consent for this speaker",
"upload": "Upload image consent for this speaker",
"remove": "Remove this speaker's consent",
"warning": "Warning: the speaker's current consent will be deleted--this cannot be undone."
Expand Down
133 changes: 133 additions & 0 deletions src/api/api/speaker-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,55 @@ export const SpeakerApiAxiosParamCreator = function (
options: localVarRequestOptions,
};
},
/**
*
* @param {string} speakerId
* @param {string} projectId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
downloadConsentImage: async (
speakerId: string,
projectId: string,
options: any = {}
): Promise<RequestArgs> => {
// verify required parameter 'speakerId' is not null or undefined
assertParamExists("downloadConsentImage", "speakerId", speakerId);
// verify required parameter 'projectId' is not null or undefined
assertParamExists("downloadConsentImage", "projectId", projectId);
const localVarPath =
`/v1/projects/{projectId}/speakers/downloadconsentimage/{speakerId}`
.replace(`{${"speakerId"}}`, encodeURIComponent(String(speakerId)))
.replace(`{${"projectId"}}`, encodeURIComponent(String(projectId)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}

const localVarRequestOptions = {
method: "GET",
...baseOptions,
...options,
};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;

setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
let headersFromBaseOptions =
baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {
...localVarHeaderParameter,
...headersFromBaseOptions,
...options.headers,
};

return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} projectId
Expand Down Expand Up @@ -630,6 +679,33 @@ export const SpeakerApiFp = function (configuration?: Configuration) {
configuration
);
},
/**
*
* @param {string} speakerId
* @param {string} projectId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async downloadConsentImage(
speakerId: string,
projectId: string,
options?: any
): Promise<
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<any>
> {
const localVarAxiosArgs =
await localVarAxiosParamCreator.downloadConsentImage(
speakerId,
projectId,
options
);
return createRequestFunction(
localVarAxiosArgs,
globalAxios,
BASE_PATH,
configuration
);
},
/**
*
* @param {string} projectId
Expand Down Expand Up @@ -865,6 +941,22 @@ export const SpeakerApiFactory = function (
.deleteSpeaker(projectId, speakerId, options)
.then((request) => request(axios, basePath));
},
/**
*
* @param {string} speakerId
* @param {string} projectId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
downloadConsentImage(
speakerId: string,
projectId: string,
options?: any
): AxiosPromise<any> {
return localVarFp
.downloadConsentImage(speakerId, projectId, options)
.then((request) => request(axios, basePath));
},
/**
*
* @param {string} projectId
Expand Down Expand Up @@ -1032,6 +1124,27 @@ export interface SpeakerApiDeleteSpeakerRequest {
readonly speakerId: string;
}

/**
* Request parameters for downloadConsentImage operation in SpeakerApi.
* @export
* @interface SpeakerApiDownloadConsentImageRequest
*/
export interface SpeakerApiDownloadConsentImageRequest {
/**
*
* @type {string}
* @memberof SpeakerApiDownloadConsentImage
*/
readonly speakerId: string;

/**
*
* @type {string}
* @memberof SpeakerApiDownloadConsentImage
*/
readonly projectId: string;
}

/**
* Request parameters for getProjectSpeakers operation in SpeakerApi.
* @export
Expand Down Expand Up @@ -1263,6 +1376,26 @@ export class SpeakerApi extends BaseAPI {
.then((request) => request(this.axios, this.basePath));
}

/**
*
* @param {SpeakerApiDownloadConsentImageRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof SpeakerApi
*/
public downloadConsentImage(
requestParameters: SpeakerApiDownloadConsentImageRequest,
options?: any
) {
return SpeakerApiFp(this.configuration)
.downloadConsentImage(
requestParameters.speakerId,
requestParameters.projectId,
options
)
.then((request) => request(this.axios, this.basePath));
}

/**
*
* @param {SpeakerApiGetProjectSpeakersRequest} requestParameters Request parameters.
Expand Down
26 changes: 21 additions & 5 deletions src/backend/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,9 @@ export async function deleteAudio(
}

// Use of the returned url acts as an HttpGet.
export function getAudioUrl(wordId: string, fileName: string): string {
return `${apiBaseURL}/projects/${LocalStorage.getProjectId()}/words/${wordId}/audio/download/${fileName}`;
export function getAudioUrl(id: string, fileName: string): string {
const projId = LocalStorage.getProjectId();
return `${apiBaseURL}/projects/${projId}/words/${id}/audio/download/${fileName}`;
}

/* AvatarController.cs */
Expand Down Expand Up @@ -520,18 +521,33 @@ export async function updateSpeakerName(
* Returns updated speaker. */
export async function uploadConsent(
speaker: Speaker,
file: File
file: File,
fileType: ConsentType
): Promise<Speaker> {
const { consent, id, projectId } = speaker;
const { id, projectId } = speaker;
const params = { projectId, speakerId: id, ...fileUpload(file) };
const headers = { ...authHeader(), "content-type": "application/json" };
const response =
consent.fileType === ConsentType.Audio
fileType === ConsentType.Audio
? await speakerApi.uploadConsentAudio(params, { headers })
: await speakerApi.uploadConsentImage(params, { headers });
return response.data;
}

/** Returns the string to display the image inline in Base64 <img src= */
export async function getConsentImageSrc(speaker: Speaker): Promise<string> {
const params = { projectId: speaker.projectId, speakerId: speaker.id };
const options = { headers: authHeader(), responseType: "arraybuffer" };
const resp = await speakerApi.downloadConsentImage(params, options);
const image = Base64.btoa(
new Uint8Array(resp.data).reduce(
(data, byte) => data + String.fromCharCode(byte),
""
)
);
return `data:${resp.headers["content-type"].toLowerCase()};base64,${image}`;
}

/* StatisticsController.cs */

export async function getSemanticDomainCounts(
Expand Down
25 changes: 25 additions & 0 deletions src/components/Buttons/CloseButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Close } from "@mui/icons-material";
import { IconButton } from "@mui/material";
import { CSSProperties, ReactElement } from "react";

interface CloseButtonProps {
close: () => void;
}

export default function CloseButton(props: CloseButtonProps): ReactElement {
const closeButtonStyle: CSSProperties = {
position: "absolute",
top: 0,
...(document.body.dir === "rtl" ? { left: 0 } : { right: 0 }),
};

return (
<IconButton
aria-label="close"
onClick={props.close}
style={closeButtonStyle}
>
<Close />
</IconButton>
);
}
45 changes: 45 additions & 0 deletions src/components/Buttons/DeleteButtonWithDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Delete } from "@mui/icons-material";
import { ReactElement, useState } from "react";

import { IconButtonWithTooltip } from "components/Buttons";
import { CancelConfirmDialog } from "components/Dialogs";

interface DeleteButtonWithDialogProps {
buttonId: string;
buttonIdCancel?: string;
buttonIdConfirm?: string;
delete: () => void | Promise<void>;
disabled?: boolean;
textId: string;
tooltipTextId?: string;
}

export default function DeleteButtonWithDialog(
props: DeleteButtonWithDialogProps
): ReactElement {
const [open, setOpen] = useState(false);

const handleConfirm = async (): Promise<void> => {
await props.delete();
setOpen(false);
};

return (
<>
<IconButtonWithTooltip
buttonId={props.buttonId}
icon={<Delete />}
onClick={props.disabled ? undefined : () => setOpen(true)}
textId={props.tooltipTextId || props.textId}
/>
<CancelConfirmDialog
buttonIdCancel={props.buttonIdCancel}
buttonIdConfirm={props.buttonIdConfirm}
handleCancel={() => setOpen(false)}
handleConfirm={handleConfirm}
open={open}
textId={props.textId}
/>
</>
);
}
2 changes: 1 addition & 1 deletion src/components/Buttons/FlagButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export default function FlagButton(props: FlagButtonProps): ReactElement {
}
text={text}
textId={active ? "flags.edit" : "flags.add"}
small
size="small"
onClick={
props.updateFlag ? () => setOpen(true) : active ? () => {} : undefined
}
Expand Down
4 changes: 2 additions & 2 deletions src/components/Buttons/IconButtonWithTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ interface IconButtonWithTooltipProps {
icon: ReactElement;
text?: ReactNode;
textId?: string;
small?: boolean;
size?: "large" | "medium" | "small";
onClick?: () => void;
buttonId: string;
side?: "bottom" | "left" | "right" | "top";
Expand All @@ -25,7 +25,7 @@ export default function IconButtonWithTooltip(
<span>
<IconButton
onClick={props.onClick}
size={props.small ? "small" : "medium"}
size={props.size || "medium"}
id={props.buttonId}
disabled={!props.onClick}
>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Buttons/PartOfSpeechButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default function PartOfSpeech(props: PartOfSpeechProps): ReactElement {
icon={<Hexagon fontSize="small" sx={{ color }} />}
onClick={props.onClick}
side="top"
small
size="small"
text={hoverText}
/>
);
Expand Down
4 changes: 4 additions & 0 deletions src/components/Buttons/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import CloseButton from "components/Buttons/CloseButton";
import DeleteButtonWithDialog from "components/Buttons/DeleteButtonWithDialog";
import FileInputButton from "components/Buttons/FileInputButton";
import FlagButton from "components/Buttons/FlagButton";
import IconButtonWithTooltip from "components/Buttons/IconButtonWithTooltip";
Expand All @@ -6,6 +8,8 @@ import LoadingDoneButton from "components/Buttons/LoadingDoneButton";
import PartOfSpeechButton from "components/Buttons/PartOfSpeechButton";

export {
CloseButton,
DeleteButtonWithDialog,
FileInputButton,
FlagButton,
IconButtonWithTooltip,
Expand Down
Loading

0 comments on commit e581134

Please sign in to comment.