Skip to content

Commit

Permalink
Merge pull request #792 from Health-Informatics-UoN/feat/749/scanrepo…
Browse files Browse the repository at this point in the history
…rt-details-form

Implementation of NextJS to Scan Report Details Page
  • Loading branch information
AndrewThien committed Jul 12, 2024
2 parents db27917 + 1aaff4d commit 1f7a3a4
Show file tree
Hide file tree
Showing 15 changed files with 390 additions and 13 deletions.
4 changes: 4 additions & 0 deletions app/api/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ class Meta:
"status",
"created_at",
"hidden",
"author",
"viewers",
"editors",
"visibility",
)

def get_parent_dataset(self, obj):
Expand Down
39 changes: 39 additions & 0 deletions app/api/mapping/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,33 @@ def is_az_function_user(user: User) -> bool:
return user.username == os.getenv("AZ_FUNCTION_USER")


def is_scan_report_author(obj: Any, request: Request) -> bool:
"""Check if the user is the author of a scan report.
Args:
obj (Any): The object to check (should be a ScanReport or related object).
request (Request): The request with the User instance.
Returns:
bool: `True` if the request's user is the author of the scan report, else `False`.
"""
try:
# If `obj` is a scan report table|field|value, get the scan report
# it belongs to and check the user has permission to view it.
if sub_scan_report_query := SCAN_REPORT_QUERIES.get(type(obj)):
sub_scan_report = sub_scan_report_query(obj)
else:
sub_scan_report = obj

# Check if the user is the author
if isinstance(sub_scan_report, ScanReport):
return sub_scan_report.author.id == request.user.id
else:
return False
except Exception:
return False


def has_viewership(obj: Any, request: Request) -> bool:
"""Check the viewership permission on an object.
Expand Down Expand Up @@ -250,6 +277,16 @@ def is_admin(obj: Any, request: Request) -> bool:
return False


class IsAuthor(permissions.BasePermission):
message = "You are not the author of this scan report."

def has_object_permission(self, request, view, obj):
"""
Return `True` if the User is the author of the scan report.
"""
return is_scan_report_author(obj, request)


class CanViewProject(permissions.BasePermission):
message = "You must be a member of this project to view its contents."

Expand Down Expand Up @@ -357,6 +394,8 @@ def get_user_permissions_on_scan_report(request, scan_report_id):
permissions.append("CanEdit")
if CanAdmin().has_object_permission(request, None, scan_report):
permissions.append("CanAdmin")
if IsAuthor().has_object_permission(request, None, scan_report):
permissions.append("IsAuthor")

return permissions
except ScanReport.DoesNotExist:
Expand Down
5 changes: 5 additions & 0 deletions app/api/proxy/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
ProxyView.as_view(upstream=f"{settings.NEXTJS_URL}/scanreports/"),
name="scan-report-tables",
),
re_path(
r"^scanreports/(?P<path>\d+/details)/$",
ProxyView.as_view(upstream=f"{settings.NEXTJS_URL}/scanreports/"),
name="scan-report-details",
),
re_path(
r"^scanreports/(?P<path>\d+/tables/\d+)/$",
ProxyView.as_view(upstream=f"{settings.NEXTJS_URL}/scanreports/"),
Expand Down
10 changes: 7 additions & 3 deletions app/next-client-app/api/datasets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ const fetchKeys = {
list: (filter?: string) =>
filter ? `datasets_data_partners/?${filter}` : "datasets_data_partners/",
dataset: (id: string) => `datasets/${id}/`,
datasetList: (dataPartnerId: string) =>
`datasets/?data_partner=${dataPartnerId}&hidden=false`,
datasetList: (dataPartnerId?: string) =>
dataPartnerId
? `datasets/?data_partner=${dataPartnerId}&hidden=false`
: "datasets/",
dataPartners: () => "datapartners/",
users: () => "usersfilter/?is_active=true",
projects: (dataset?: string) =>
Expand Down Expand Up @@ -50,7 +52,9 @@ export async function getDataSet(id: string): Promise<DataSetSRList> {
}
}

export async function getDatasetList(filter: string): Promise<DataSetSRList[]> {
export async function getDatasetList(
filter?: string
): Promise<DataSetSRList[]> {
try {
return await request<DataSetSRList>(fetchKeys.datasetList(filter));
} catch (error) {
Expand Down
15 changes: 13 additions & 2 deletions app/next-client-app/api/scanreports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ export async function getScanReport(id: string): Promise<ScanReportList> {
status: "",
created_at: new Date(),
hidden: true,
visibility: "",
author: 0,
editors: [],
viewers: [],
};
}
}
Expand Down Expand Up @@ -148,20 +152,27 @@ export async function getScanReportField(
}
}

export async function updateScanReport(id: number, field: string, value: any) {
export async function updateScanReport(
id: number,
data: {},
needRedirect?: boolean
) {
try {
await request(fetchKeys.update(id), {
method: "PATCH",
headers: {
"Content-type": "application/json",
},
body: JSON.stringify({ [field]: value }),
body: JSON.stringify(data),
});
revalidatePath("/scanreports/");
} catch (error: any) {
// Only return a response when there is an error
return { errorMessage: error.message };
}
if (needRedirect) {
redirect(`/scanreports/`);
}
}

export async function deleteScanReport(id: number) {
Expand Down
80 changes: 80 additions & 0 deletions app/next-client-app/app/scanreports/[id]/details/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import {
getDataUsers,
getDatasetList,
getDatasetPermissions,
} from "@/api/datasets";
import { ScanReportDetailsForm } from "@/components/scanreports/ScanReportDetailsForm";
import { getScanReport, getScanReportPermissions } from "@/api/scanreports";

interface ScanReportDetailsProps {
params: {
id: string;
};
}

export default async function ScanreportDetails({
params: { id },
}: ScanReportDetailsProps) {
const scanreport = await getScanReport(id);
const datasetList = await getDatasetList();
const users = await getDataUsers();
const parent_dataset = datasetList.find(
(dataset) => dataset.name === scanreport.parent_dataset
);
const permissionsDS = await getDatasetPermissions(
parent_dataset?.id.toString() || ""
);
const permissionsSR = await getScanReportPermissions(id);
const isAuthor = permissionsSR.permissions.includes("IsAuthor");

return (
<div className="pt-10 px-16">
<div>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator>/</BreadcrumbSeparator>
<BreadcrumbItem>
<BreadcrumbLink href="/scanreports">Scan Reports</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator>/</BreadcrumbSeparator>
<BreadcrumbItem>
<BreadcrumbLink href={`/scanreports/${id}/`}>
{scanreport.dataset}
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator>/</BreadcrumbSeparator>
<BreadcrumbItem>
<BreadcrumbLink href={`/scanreports/${id}/details`}>
Details
</BreadcrumbLink>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<div className="flex justify-between mt-3">
<h1 className="text-4xl font-semibold">
Details Page - Scan Report #{id}
</h1>
</div>
<div className="mt-4">
<ScanReportDetailsForm
scanreport={scanreport}
datasetList={datasetList}
users={users}
permissions={permissionsDS.permissions}
isAuthor={isAuthor}
/>
</div>
</div>
);
}
2 changes: 1 addition & 1 deletion app/next-client-app/components/HandleArchive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const HandleArchive = async ({
response = await archiveDataSets(id, !hidden);
break;
case "scanreports":
response = await updateScanReport(id, "hidden", !hidden);
response = await updateScanReport(id, { hidden: !hidden });
break;
}
// Because the response only returned when there is an error
Expand Down
1 change: 0 additions & 1 deletion app/next-client-app/components/data-table/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ export function DataTable<TData, TValue>({
clickableRow = true,
defaultPageSize,
}: DataTableProps<TData, TValue>) {
const router = useRouter();
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export function CreateDatasetForm({
{error.split(" * ").map((err, index) => (
<li key={index}>* {err}</li>
))}
<li>* Notice: The name of dataset should be unique *</li>
</ul>
</AlertDescription>
</div>
Expand Down
6 changes: 5 additions & 1 deletion app/next-client-app/components/datasets/DatasetForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -204,14 +204,18 @@ export function DatasetForm({
isDisabled={!canUpdate}
/>
</div>
<div>
<div className="flex">
<Button
type="submit"
className="px-4 py-2 bg-carrot text-white rounded text-lg"
disabled={!canUpdate}
>
Save <Save className="ml-2" />
</Button>
<Tooltips
content="You must be an admin of the this dataset
to update its details."
/>
</div>
</div>
</Form>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ export function CreateScanReportForm({
/>
</div>
</div>
<div className="mb-5">
<div className="mb-5 flex">
<Button
type="submit"
className="px-4 py-2 bg-carrot text-white rounded text-lg"
Expand All @@ -258,6 +258,7 @@ export function CreateScanReportForm({
>
Upload Scan Report <FileUp className="ml-2" />
</Button>
<Tooltips content="You must be either an admin or an editor of the parent dataset to add a new scan report to it." />
</div>
</div>
</Form>
Expand Down
Loading

0 comments on commit 1f7a3a4

Please sign in to comment.