Skip to content

Commit

Permalink
Move download-csv to NextJS action (#1735)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Nikita <nikita.develops.work@gmail.com>
Co-authored-by: Nikita <93587872+ncarazon@users.noreply.github.com>
  • Loading branch information
3 people authored Dec 18, 2024
1 parent 68bfdbd commit 1fb71f9
Show file tree
Hide file tree
Showing 12 changed files with 111 additions and 15 deletions.
1 change: 1 addition & 0 deletions front_end/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,7 @@
"feelFreeToJustSayHello": "Feel free to just say hello, too - we love hearing from the forecasting community!",
"contentBoosted": "Content boosted! value {score} activity. Total boost score for the week: {score_total}",
"contentBuried": "Content buried! value {score} activity. Total boost score for the week: {score_total}",
"downloadCSVError": "Error downloading CSV",
"cpRevealTime": "CP Reveal Time",
"openTime": "Open Time",
"Question": "Question",
Expand Down
14 changes: 14 additions & 0 deletions front_end/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions front_end/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"d3": "^7.9.0",
"date-fns": "^3.6.0",
"date-fns-tz": "^3.1.3",
"file-saver": "^2.0.5",
"html-react-parser": "^5.1.18",
"lexical-beautiful-mentions": "^0.1.44",
"lodash": "^4.17.21",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"use client";
import Link from "next/link";
import { useTranslations } from "next-intl";
import React, { FC, ReactNode, useState } from "react";

Expand Down Expand Up @@ -131,15 +130,6 @@ const ForecastMakerGroupControls: FC<Props> = ({
).then();
},
},
{
id: "downloadCSV",
name: t("downloadCSV"),
onClick: () => {
window.open(
`/api/posts/${post!.id}/download-csv/?sub-question=${question.id}`
);
},
},
]}
textAlign="left"
>
Expand Down
8 changes: 8 additions & 0 deletions front_end/src/app/(main)/questions/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,3 +338,11 @@ export async function changePostSubscriptions(
}
return response;
}

export async function getPostCSVData(postId: number) {
const blob = await PostsApi.getPostCSVData(postId);
const arrayBuffer = await blob.arrayBuffer();
const base64String = Buffer.from(arrayBuffer).toString("base64");

return `data:application/octet-stream;base64,${base64String}`;
}
22 changes: 18 additions & 4 deletions front_end/src/components/post_actions/post_dropdown_menu.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
"use client";
import { faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { saveAs } from "file-saver";
import { useTranslations } from "next-intl";
import React, { FC, useCallback, useMemo } from "react";
import toast from "react-hot-toast";

import { changePostActivityBoost } from "@/app/(main)/questions/actions";
import {
changePostActivityBoost,
getPostCSVData,
} from "@/app/(main)/questions/actions";
import Button from "@/components/ui/button";
import DropdownMenu from "@/components/ui/dropdown_menu";
import { useAuth } from "@/contexts/auth_context";
import { Post } from "@/types/post";
import { base64ToBlob } from "@/utils/files";

type Props = {
post: Post;
Expand Down Expand Up @@ -45,6 +50,17 @@ export const PostDropdownMenu: FC<Props> = ({ post }) => {
return `/questions/create/question?mode=create&post_id=${post.id}`;
};

const handleDownloadCSV = async () => {
try {
const base64 = await getPostCSVData(post.id);
const blob = base64ToBlob(base64);
const filename = `${post.url_title.replaceAll(" ", "_")}.csv`;
saveAs(blob, filename);
} catch (error) {
toast.error(t("downloadCSVError") + error);
}
};

const items = useMemo(
() => [
...(user?.is_superuser
Expand Down Expand Up @@ -73,9 +89,7 @@ export const PostDropdownMenu: FC<Props> = ({ post }) => {
{
id: "downloadCSV",
name: t("downloadCSV"),
onClick: () => {
window.open(`/api/posts/${post!.id}/download-csv/`);
},
onClick: handleDownloadCSV,
},
],
[changePostActivity, t, user?.is_superuser]
Expand Down
43 changes: 43 additions & 0 deletions front_end/src/declarations/file-saver.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
declare module "file-saver" {
export = FileSaver;

export as namespace saveAs;

/**
* FileSaver.js implements the saveAs() FileSaver interface in browsers that do not natively support it.
* @param data - The actual file data blob or URL.
* @param filename - The optional name of the file to be downloaded. If omitted, the name used in the file data will be used. If none is provided "download" will be used.
* @param options - Optional FileSaver.js config
*/
declare function FileSaver(
data: Blob | string,
filename?: string,
options?: FileSaver.FileSaverOptions
): void;

/**
* FileSaver.js implements the saveAs() FileSaver interface in browsers that do not natively support it.
* @param data - The actual file data blob or URL.
* @param filename - The optional name of the file to be downloaded. If omitted, the name used in the file data will be used. If none is provided "download" will be used.
* @param disableAutoBOM - Optional & defaults to `true`. Set to `false` if you want FileSaver.js to automatically provide Unicode text encoding hints
* @deprecated use `{ autoBom: false }` as the third argument
*/
// tslint:disable-next-line:unified-signatures
declare function FileSaver(
data: Blob | string,
filename?: string,
disableAutoBOM?: boolean
): void;

declare namespace FileSaver {
interface FileSaverOptions {
/**
* Automatically provide Unicode text encoding hints
* @default false
*/
autoBom: boolean;
}

const saveAs: typeof FileSaver;
}
}
4 changes: 4 additions & 0 deletions front_end/src/services/posts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,10 @@ class PostsApi {
static async getRandomPostId(): Promise<{ id: number; post_slug: string }> {
return await get<{ id: number; post_slug: string }>("/posts/random/");
}

static async getPostCSVData(postId: number): Promise<Blob> {
return await get<Blob>(`/posts/${postId}/download-csv/`);
}
}

export default PostsApi;
8 changes: 8 additions & 0 deletions front_end/src/utils/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ const handleResponse = async <T>(response: Response): Promise<T> => {
throw error;
}

// Check the content type to determine how to process the response
const contentType = response.headers.get("content-type");

if (contentType && contentType.includes("text/csv")) {
// If the response is a CSV, return it as a Blob
return response.blob() as unknown as T;
}

// Some endpoints might still have successful null response
// So need to handle such cases
const text = await response.text();
Expand Down
10 changes: 10 additions & 0 deletions front_end/src/utils/files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function base64ToBlob(base64: string): Blob {
const byteString = atob(base64.split(",")[1]);
const mimeString = base64.split(",")[0].split(":")[1].split(";")[0];
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
return new Blob([ab], { type: mimeString });
}
3 changes: 2 additions & 1 deletion front_end/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"types/**/*.d.ts"
"types/**/*.d.ts",
"declarations/**/*.d.ts"
],
"exclude": ["node_modules"]
}
2 changes: 2 additions & 0 deletions utils/csv_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ def _get_row_headers(question: Question) -> list[str]:
"forecaster",
"prediction_start_time",
"prediction_end_time",
"number_of_forecasters",
]
match question.type:
case "binary":
Expand Down Expand Up @@ -253,6 +254,7 @@ def build_csv(
method,
forecast.start_time,
forecast.end_time,
getattr(forecast, "forecaster_count", None),
]
match question.type:
case "binary":
Expand Down

0 comments on commit 1fb71f9

Please sign in to comment.