diff --git a/frontend/js/src/error/ErrorBoundary.tsx b/frontend/js/src/error/ErrorBoundary.tsx index 65ab0c76c9..0375033968 100644 --- a/frontend/js/src/error/ErrorBoundary.tsx +++ b/frontend/js/src/error/ErrorBoundary.tsx @@ -43,7 +43,8 @@ export function ErrorBoundary() { Error -

Error Occured!

+

An error occured

+

{errorMessage}

); } diff --git a/frontend/js/src/explore/fresh-releases/components/ReleaseTimeline.tsx b/frontend/js/src/explore/fresh-releases/components/ReleaseTimeline.tsx index b23dd46d00..f3b585de0f 100644 --- a/frontend/js/src/explore/fresh-releases/components/ReleaseTimeline.tsx +++ b/frontend/js/src/explore/fresh-releases/components/ReleaseTimeline.tsx @@ -10,6 +10,95 @@ type ReleaseTimelineProps = { direction: SortDirection; }; +function createMarks( + releases: Array, + sortDirection: string, + order: string +) { + let dataArr: Array = []; + let percentArr: Array = []; + // We want to filter out the keys that have less than 1.5% of the total releases + const minReleasesThreshold = Math.floor(releases.length * 0.015); + if (order === "release_date") { + const releasesPerDate = countBy( + releases, + (item: FreshReleaseItem) => item.release_date + ); + const filteredDates = Object.keys(releasesPerDate).filter( + (date) => releasesPerDate[date] >= minReleasesThreshold + ); + + dataArr = filteredDates.map((item) => formatReleaseDate(item)); + percentArr = filteredDates + .map((item) => (releasesPerDate[item] / releases.length) * 100) + .map((_, index, arr) => + arr.slice(0, index + 1).reduce((prev, curr) => prev + curr) + ); + } else if (order === "artist_credit_name") { + const artistInitialsCount = countBy(releases, (item: FreshReleaseItem) => + item.artist_credit_name.charAt(0).toUpperCase() + ); + const filteredInitials = Object.keys(artistInitialsCount).filter( + (initial) => artistInitialsCount[initial] >= minReleasesThreshold + ); + + dataArr = filteredInitials.sort(); + percentArr = filteredInitials + .map((item) => (artistInitialsCount[item] / releases.length) * 100) + .map((_, index, arr) => + arr.slice(0, index + 1).reduce((prev, curr) => prev + curr) + ); + } else if (order === "release_name") { + const releaseInitialsCount = countBy(releases, (item: FreshReleaseItem) => + item.release_name.charAt(0).toUpperCase() + ); + const filteredInitials = Object.keys(releaseInitialsCount).filter( + (initial) => releaseInitialsCount[initial] >= minReleasesThreshold + ); + + dataArr = filteredInitials.sort(); + percentArr = filteredInitials + .map((item) => (releaseInitialsCount[item] / releases.length) * 100) + .map((_, index, arr) => + arr.slice(0, index + 1).reduce((prev, curr) => prev + curr) + ); + } else { + // conutBy gives us an asc-sorted Dict by confidence + const confidenceInitialsCount = countBy( + releases, + (item: FreshReleaseItem) => item?.confidence + ); + dataArr = Object.keys(confidenceInitialsCount); + percentArr = Object.values(confidenceInitialsCount) + .map((item) => (item / releases.length) * 100) + .map((_, index, arr) => + arr.slice(0, index + 1).reduce((prev, curr) => prev + curr) + ); + } + + if (sortDirection === "descend") { + dataArr.reverse(); + percentArr = percentArr.reverse().map((v) => (v <= 100 ? 100 - v : 0)); + } + + /** + * We want the timeline dates or marks to start where the grid starts. + * So the 0% should always have the first date. Therefore we use unshift(0) here. + * With the same logic, we don't want the last date to be at 100% because + * that will mean we're at the bottom of the grid. + * The last date should start before 100%. That explains the pop(). + * For descending sort, the reverse computation above possibly already ensures that + * the percentArr starts with 0 and ends with a non-100% value, which is desired. + * Hence, we add a check to skip the unshift(0) and pop() operations in that case. + */ + if (percentArr[0] !== 0) { + percentArr.unshift(0); + percentArr.pop(); + } + + return zipObject(percentArr, dataArr); +} + export default function ReleaseTimeline(props: ReleaseTimelineProps) { const { releases, order, direction } = props; @@ -29,108 +118,24 @@ export default function ReleaseTimeline(props: ReleaseTimelineProps) { return scrollTo; }, []); - function createMarks(data: Array, sortDirection: string) { - let dataArr: Array = []; - let percentArr: Array = []; - // We want to filter out the keys that have less than 1.5% of the total releases - const minReleasesThreshold = Math.floor(data.length * 0.015); - if (order === "release_date") { - const releasesPerDate = countBy( - releases, - (item: FreshReleaseItem) => item.release_date - ); - const filteredDates = Object.keys(releasesPerDate).filter( - (date) => releasesPerDate[date] >= minReleasesThreshold - ); - - dataArr = filteredDates.map((item) => formatReleaseDate(item)); - percentArr = filteredDates - .map((item) => (releasesPerDate[item] / data.length) * 100) - .map((_, index, arr) => - arr.slice(0, index + 1).reduce((prev, curr) => prev + curr) - ); - } else if (order === "artist_credit_name") { - const artistInitialsCount = countBy(data, (item: FreshReleaseItem) => - item.artist_credit_name.charAt(0).toUpperCase() - ); - const filteredInitials = Object.keys(artistInitialsCount).filter( - (initial) => artistInitialsCount[initial] >= minReleasesThreshold - ); - - dataArr = filteredInitials.sort(); - percentArr = filteredInitials - .map((item) => (artistInitialsCount[item] / data.length) * 100) - .map((_, index, arr) => - arr.slice(0, index + 1).reduce((prev, curr) => prev + curr) - ); - } else if (order === "release_name") { - const releaseInitialsCount = countBy(data, (item: FreshReleaseItem) => - item.release_name.charAt(0).toUpperCase() - ); - const filteredInitials = Object.keys(releaseInitialsCount).filter( - (initial) => releaseInitialsCount[initial] >= minReleasesThreshold - ); - - dataArr = filteredInitials.sort(); - percentArr = filteredInitials - .map((item) => (releaseInitialsCount[item] / data.length) * 100) - .map((_, index, arr) => - arr.slice(0, index + 1).reduce((prev, curr) => prev + curr) - ); - } else { - // conutBy gives us an asc-sorted Dict by confidence - const confidenceInitialsCount = countBy( - data, - (item: FreshReleaseItem) => item?.confidence - ); - dataArr = Object.keys(confidenceInitialsCount); - percentArr = Object.values(confidenceInitialsCount) - .map((item) => (item / data.length) * 100) - .map((_, index, arr) => - arr.slice(0, index + 1).reduce((prev, curr) => prev + curr) - ); - } - - if (sortDirection === "descend") { - dataArr.reverse(); - percentArr = percentArr.reverse().map((v) => (v <= 100 ? 100 - v : 0)); - } - - /** - * We want the timeline dates or marks to start where the grid starts. - * So the 0% should always have the first date. Therefore we use unshift(0) here. - * With the same logic, we don't want the last date to be at 100% because - * that will mean we're at the bottom of the grid. - * The last date should start before 100%. That explains the pop(). - * For descending sort, the reverse computation above possibly already ensures that - * the percentArr starts with 0 and ends with a non-100% value, which is desired. - * Hence, we add a check to skip the unshift(0) and pop() operations in that case. - */ - if (percentArr[0] !== 0) { - percentArr.unshift(0); - percentArr.pop(); - } - - return zipObject(percentArr, dataArr); - } + React.useEffect(() => { + setMarks(createMarks(releases, direction, order)); + }, [releases, direction, order]); - const handleScroll = React.useCallback( - debounce(() => { - // TODO change to relative position of #release-cards-grid instead of window + React.useEffect(() => { + const handleScroll = debounce(() => { + const container = document.getElementById("release-card-grids"); + if (!container) { + return; + } const scrollPos = - (window.scrollY / document.documentElement.scrollHeight) * 100; + ((window.scrollY - container.offsetTop) / container.scrollHeight) * 100; setCurrentValue(scrollPos); - }, 300), - [] - ); + }, 500); - React.useEffect(() => { - setMarks(createMarks(releases, direction)); - }, [releases, direction]); - - React.useEffect(() => { window.addEventListener("scroll", handleScroll); return () => { + handleScroll.cancel(); window.removeEventListener("scroll", handleScroll); }; }, []); diff --git a/frontend/js/src/user/stats/UserReports.tsx b/frontend/js/src/user/stats/UserReports.tsx index 1afff528d5..c1930c924f 100644 --- a/frontend/js/src/user/stats/UserReports.tsx +++ b/frontend/js/src/user/stats/UserReports.tsx @@ -10,7 +10,6 @@ import { useLoaderData, useNavigate } from "react-router-dom"; import type { NavigateFunction } from "react-router-dom"; import { Helmet } from "react-helmet"; -import ErrorBoundary from "../../utils/ErrorBoundary"; import Pill from "../../components/Pill"; import UserListeningActivity from "./components/UserListeningActivity"; import UserTopEntity from "./components/UserTopEntity"; @@ -170,58 +169,46 @@ export default class UserReports extends React.Component<
- - - +
- - - +
- - - +
- - - +
{user && (
- - - +
)}
- - - +
); diff --git a/frontend/js/src/user/stats/components/UserArtistMap.tsx b/frontend/js/src/user/stats/components/UserArtistMap.tsx index 5b783cb6b8..921a6c7951 100644 --- a/frontend/js/src/user/stats/components/UserArtistMap.tsx +++ b/frontend/js/src/user/stats/components/UserArtistMap.tsx @@ -98,16 +98,11 @@ export default class UserArtistMap extends React.Component< try { return await this.APIService.getUserArtistMap(user?.name, range); } catch (error) { - if (error.response && error.response.status === 204) { - this.setState({ - loading: false, - hasError: true, - errorMessage: - "There are no statistics available for this user for this period", - }); - } else { - throw error; - } + this.setState({ + loading: false, + hasError: true, + errorMessage: error.message, + }); } return {} as UserArtistMapResponse; }; diff --git a/frontend/js/src/user/stats/components/UserDailyActivity.tsx b/frontend/js/src/user/stats/components/UserDailyActivity.tsx index 7a6141576f..1b831f6f22 100644 --- a/frontend/js/src/user/stats/components/UserDailyActivity.tsx +++ b/frontend/js/src/user/stats/components/UserDailyActivity.tsx @@ -66,16 +66,11 @@ export default class UserDailyActivity extends React.Component< const data = await this.APIService.getUserDailyActivity(user.name, range); return data; } catch (error) { - if (error.response && error.response.status === 204) { - this.setState({ - loading: false, - hasError: true, - errorMessage: - "There are no statistics available for this user for this period", - }); - } else { - throw error; - } + this.setState({ + loading: false, + hasError: true, + errorMessage: error.message, + }); } return {} as UserDailyActivityResponse; }; diff --git a/frontend/js/src/user/stats/components/UserListeningActivity.tsx b/frontend/js/src/user/stats/components/UserListeningActivity.tsx index 2eb7c11abe..c7ab1f6471 100644 --- a/frontend/js/src/user/stats/components/UserListeningActivity.tsx +++ b/frontend/js/src/user/stats/components/UserListeningActivity.tsx @@ -133,16 +133,11 @@ export default class UserListeningActivity extends React.Component< try { return await this.APIService.getUserListeningActivity(user?.name, range); } catch (error) { - if (error.response && error.response.status === 204) { - this.setState({ - loading: false, - hasError: true, - errorMessage: - "There are no statistics available for this user for this period", - }); - } else { - throw error; - } + this.setState({ + loading: false, + hasError: true, + errorMessage: error.message, + }); } return {} as UserListeningActivityResponse; }; diff --git a/frontend/js/src/user/stats/components/UserTopEntity.tsx b/frontend/js/src/user/stats/components/UserTopEntity.tsx index a9d26229d9..8bc0775b24 100644 --- a/frontend/js/src/user/stats/components/UserTopEntity.tsx +++ b/frontend/js/src/user/stats/components/UserTopEntity.tsx @@ -86,16 +86,11 @@ export default class UserTopEntity extends React.Component< 10 ); } catch (error) { - if (error.response && error.response.status === 204) { - this.setState({ - loading: false, - hasError: true, - errorMessage: - "There are no statistics available for this user for this period", - }); - } else { - throw error; - } + this.setState({ + loading: false, + hasError: true, + errorMessage: error.message, + }); } return {} as UserEntityResponse; }; diff --git a/frontend/js/src/utils/APIService.ts b/frontend/js/src/utils/APIService.ts index 45c9af9dfd..7440faa252 100644 --- a/frontend/js/src/utils/APIService.ts +++ b/frontend/js/src/utils/APIService.ts @@ -429,7 +429,9 @@ export default class APIService { await this.checkStatus(response); // if response code is 204, then statistics havent been calculated, send empty object if (response.status === 204) { - const error = new APIError(`HTTP Error ${response.statusText}`); + const error = new APIError( + "There are no statistics available for this user for this period" + ); error.status = response.statusText; error.response = response; throw error; @@ -450,7 +452,9 @@ export default class APIService { const response = await fetch(`${url}?range=${range}`); await this.checkStatus(response); if (response.status === 204) { - const error = new APIError(`HTTP Error ${response.statusText}`); + const error = new APIError( + "There are no statistics available for this user for this period" + ); error.status = response.statusText; error.response = response; throw error; @@ -466,7 +470,9 @@ export default class APIService { const response = await fetch(url); await this.checkStatus(response); if (response.status === 204) { - const error = new APIError(`HTTP Error ${response.statusText}`); + const error = new APIError( + "There are no statistics available for this user for this period" + ); error.status = response.statusText; error.response = response; throw error; @@ -489,7 +495,9 @@ export default class APIService { const response = await fetch(url); await this.checkStatus(response); if (response.status === 204) { - const error = new APIError(`HTTP Error ${response.statusText}`); + const error = new APIError( + "There are no statistics available for this user for this period" + ); error.status = response.statusText; error.response = response; throw error; diff --git a/frontend/js/src/utils/Loader.ts b/frontend/js/src/utils/Loader.ts index 7428129b0f..4603e8e35d 100644 --- a/frontend/js/src/utils/Loader.ts +++ b/frontend/js/src/utils/Loader.ts @@ -8,7 +8,7 @@ const RouteLoader = async ({ request }: { request: Request }) => { const response = await fetch(request.url, { method: "POST", headers: { - "Content-Type": "application/json", + Accept: "application/json", }, }); const data = await response.json(); @@ -24,7 +24,7 @@ const RouteLoaderURL = async (url: string) => { const response = await fetch(url, { method: "POST", headers: { - "Content-Type": "application/json", + Accept: "application/json", }, }); const data = await response.json(); diff --git a/frontend/js/tests/user/stats/UserArtistMap.test.tsx b/frontend/js/tests/user/stats/UserArtistMap.test.tsx index b0828b1ed2..8f83e99171 100644 --- a/frontend/js/tests/user/stats/UserArtistMap.test.tsx +++ b/frontend/js/tests/user/stats/UserArtistMap.test.tsx @@ -55,7 +55,9 @@ describe.each([ }); await waitForComponentToPaint(wrapper); - expect(wrapper.getDOMNode()).toHaveTextContent("Invalid range: invalid_range"); + expect(wrapper.getDOMNode()).toHaveTextContent( + "Invalid range: invalid_range" + ); expect(wrapper.find(CustomChoropleth)).toHaveLength(0); }); }); @@ -123,23 +125,9 @@ describe.each([ expect(wrapper.state()).toMatchObject({ loading: false, hasError: true, - errorMessage: "There are no statistics available for this user for this period", + errorMessage: "NO CONTENT", }); }); - - it("throws error", async () => { - const wrapper = shallow(); - const instance = wrapper.instance(); - - const spy = jest.spyOn(instance.APIService, "getUserArtistMap"); - const notFoundError = new APIError("NOT FOUND"); - notFoundError.response = { - status: 404, - } as Response; - spy.mockImplementation(() => Promise.reject(notFoundError)); - - await expect(instance.getData()).rejects.toThrow("NOT FOUND"); - }); }); describe("processData", () => { diff --git a/frontend/js/tests/user/stats/UserDailyActivity.test.tsx b/frontend/js/tests/user/stats/UserDailyActivity.test.tsx index 8c37f51c1e..77cabde4df 100644 --- a/frontend/js/tests/user/stats/UserDailyActivity.test.tsx +++ b/frontend/js/tests/user/stats/UserDailyActivity.test.tsx @@ -47,7 +47,9 @@ describe("UserDailyActivity", () => { }); await waitForComponentToPaint(wrapper); - expect(wrapper.getDOMNode()).toHaveTextContent("Invalid range: invalid_range"); + expect(wrapper.getDOMNode()).toHaveTextContent( + "Invalid range: invalid_range" + ); expect(wrapper.find(Heatmap)).toHaveLength(0); }); @@ -121,25 +123,9 @@ describe("UserDailyActivity", () => { expect(wrapper.state()).toMatchObject({ loading: false, hasError: true, - errorMessage: "There are no statistics available for this user for this period", + errorMessage: "NO CONTENT", }); }); - - it("throws error", async () => { - const wrapper = mount( - - ); - const instance = wrapper.instance(); - - const spy = jest.spyOn(instance.APIService, "getUserDailyActivity"); - const notFoundError = new APIError("NOT FOUND"); - notFoundError.response = { - status: 404, - } as Response; - spy.mockImplementation(() => Promise.reject(notFoundError)); - - await expect(instance.getData()).rejects.toThrow("NOT FOUND"); - }); }); describe("processData", () => { diff --git a/frontend/js/tests/user/stats/UserListeningActivity.test.tsx b/frontend/js/tests/user/stats/UserListeningActivity.test.tsx index c2494c767d..b5c22dfd88 100644 --- a/frontend/js/tests/user/stats/UserListeningActivity.test.tsx +++ b/frontend/js/tests/user/stats/UserListeningActivity.test.tsx @@ -2,6 +2,8 @@ import * as React from "react"; import { mount, ReactWrapper, shallow, ShallowWrapper } from "enzyme"; import { act } from "react-dom/test-utils"; +import { ResponsiveBar } from "@nivo/bar"; +import { Context as ResponsiveContext } from "react-responsive"; import UserListeningActivity, { UserListeningActivityProps, UserListeningActivityState, @@ -16,8 +18,6 @@ import * as userListeningActivityProcessedDataMonth from "../../__mocks__/userLi import * as userListeningActivityProcessedDataYear from "../../__mocks__/userListeningActivityProcessDataYear.json"; import * as userListeningActivityProcessedDataAllTime from "../../__mocks__/userListeningActivityProcessDataAllTime.json"; import { waitForComponentToPaint } from "../../test-utils"; -import { ResponsiveBar } from "@nivo/bar"; -import { Context as ResponsiveContext } from 'react-responsive' const userProps: UserListeningActivityProps = { user: { @@ -124,13 +124,18 @@ describe.each([ it("renders corectly when range is invalid", async () => { const wrapper = mount( - + ); await waitForComponentToPaint(wrapper); expect(wrapper.state().hasError).toBeTruthy(); expect(wrapper.find(ResponsiveBar)).toHaveLength(0); - expect(wrapper.getDOMNode()).toHaveTextContent("Invalid range: invalid_range"); + expect(wrapper.getDOMNode()).toHaveTextContent( + "Invalid range: invalid_range" + ); }); }); @@ -205,25 +210,9 @@ describe.each([ expect(wrapper.state()).toMatchObject({ loading: false, hasError: true, - errorMessage: "There are no statistics available for this user for this period", + errorMessage: "NO CONTENT", }); }); - - it("throws error", async () => { - const wrapper = shallow( - - ); - const instance = wrapper.instance(); - - const spy = jest.spyOn(instance.APIService, "getUserListeningActivity"); - const notFoundError = new APIError("NOT FOUND"); - notFoundError.response = { - status: 404, - } as Response; - spy.mockImplementation(() => Promise.reject(notFoundError)); - - await expect(instance.getData()).rejects.toThrow("NOT FOUND"); - }); }); describe("getNumberOfDaysInMonth", () => { diff --git a/frontend/js/tests/user/stats/UserTopEntity.test.tsx b/frontend/js/tests/user/stats/UserTopEntity.test.tsx index e7c070f5bc..12283bd49e 100644 --- a/frontend/js/tests/user/stats/UserTopEntity.test.tsx +++ b/frontend/js/tests/user/stats/UserTopEntity.test.tsx @@ -265,26 +265,9 @@ describe.each([ expect(childElement.state()).toMatchObject({ loading: false, hasError: true, - errorMessage: - "There are no statistics available for this user for this period", + errorMessage: "NO CONTENT", }); wrapper.unmount(); }); - - it("throws error", async () => { - const wrapper = shallow(getComponent(props)); - const childElement = shallow(wrapper.find(UserTopEntity).get(0)); - const instance = childElement.instance() as UserTopEntity; - - const spy = jest.spyOn(instance.APIService, "getUserEntity"); - const notFoundError = new APIError("NOT FOUND"); - notFoundError.response = { - status: 404, - } as Response; - spy.mockImplementation(() => Promise.reject(notFoundError)); - - await expect(instance.getData()).rejects.toThrow("NOT FOUND"); - wrapper.unmount(); - }); }); }); diff --git a/frontend/js/tests/utils/APIService.test.ts b/frontend/js/tests/utils/APIService.test.ts index eb0799653b..2f6653465e 100644 --- a/frontend/js/tests/utils/APIService.test.ts +++ b/frontend/js/tests/utils/APIService.test.ts @@ -15,7 +15,7 @@ describe("submitListens", () => { status: 200, }); }); - jest.useFakeTimers({advanceTimers: true}); + jest.useFakeTimers({ advanceTimers: true }); }); it("calls fetch with correct parameters", async () => { @@ -73,7 +73,7 @@ describe("submitListens", () => { }, }, ]); - + await jest.advanceTimersByTimeAsync(10000); expect(spy).toHaveBeenCalledTimes(2); @@ -321,12 +321,12 @@ describe("getUserEntity", () => { return Promise.resolve({ ok: true, status: 204, - statusText: "NO CONTENT", + statusText: "Whatever error", }); }); await expect(apiService.getUserEntity("foobar", "artist")).rejects.toThrow( - Error("HTTP Error NO CONTENT") + Error("There are no statistics available for this user for this period") ); }); @@ -376,12 +376,12 @@ describe("getUserListeningActivity", () => { return Promise.resolve({ ok: true, status: 204, - statusText: "NO CONTENT", + statusText: "Whatever error", }); }); await expect(apiService.getUserListeningActivity("foobar")).rejects.toThrow( - Error("HTTP Error NO CONTENT") + Error("There are no statistics available for this user for this period") ); }); @@ -424,12 +424,12 @@ describe("getUserDailyActivity", () => { return Promise.resolve({ ok: true, status: 204, - statusText: "NO CONTENT", + statusText: "Whatever error", }); }); await expect(apiService.getUserDailyActivity("foobar")).rejects.toThrow( - Error("HTTP Error NO CONTENT") + Error("There are no statistics available for this user for this period") ); }); @@ -479,12 +479,12 @@ describe("getUserArtistMap", () => { return Promise.resolve({ ok: true, status: 204, - statusText: "NO CONTENT", + statusText: "Whatever error", }); }); await expect(apiService.getUserArtistMap("foobar")).rejects.toThrow( - Error("HTTP Error NO CONTENT") + Error("There are no statistics available for this user for this period") ); }); diff --git a/listenbrainz/webserver/errors.py b/listenbrainz/webserver/errors.py index f33d86ae48..c125bd2ed5 100644 --- a/listenbrainz/webserver/errors.py +++ b/listenbrainz/webserver/errors.py @@ -141,8 +141,14 @@ def handle_error(error, code): A Response which will be a json error if request was made to the LB api and an html page otherwise """ - if current_app.config.get('IS_API_COMPAT_APP') or request.path.startswith(API_PREFIX): - response = jsonify({'code': code, 'error': error.description}) + if current_app.config.get('IS_API_COMPAT_APP') or \ + request.path.startswith(API_PREFIX) or \ + request.accept_mimetypes.accept_json: + if hasattr(error, "description"): + description = error.description + else: + description = "An unknown error occured." + response = jsonify({"code": code, "error": description}) response.headers["Access-Control-Allow-Origin"] = "*" return response, code return error_wrapper('errors/{code}.html'.format(code=code), error, code) @@ -180,11 +186,7 @@ def internal_server_error(error): # We specifically return json in the case that the request was within our API path original = getattr(error, "original_exception", None) - if request.path.startswith(API_PREFIX): - error = APIError("An unknown error occured.", 500) - return jsonify(error.to_dict()), error.status_code - else: - return handle_error(original or error, 500) + return handle_error(original or error, 500) @app.errorhandler(502) def bad_gateway(error): @@ -212,6 +214,7 @@ def handle_api_compat_error(error): def handle_playlist_api_xml_error(error): return error.render_error() + class InvalidAPIUsage(Exception): """ General error class for the API_compat to render errors in multiple formats """ @@ -245,8 +248,6 @@ def to_xml(self): return '\n' + yattag.indent(doc.getvalue()) - - class PlaylistAPIXMLError(Exception): """ Custom error class for Playlist API to render errors in XML format. @@ -269,6 +270,7 @@ def to_xml(self): text(self.message) return '\n' + yattag.indent(doc.getvalue()) + class ListenValidationError(Exception): """ Error class for raising when the submitted payload does not pass validation. Only use for code paths common to LB API, API compat & API compat deprecated. diff --git a/listenbrainz/webserver/templates/art/index.html b/listenbrainz/webserver/templates/art/index.html index ac394d08b1..1b9d2b2c28 100644 --- a/listenbrainz/webserver/templates/art/index.html +++ b/listenbrainz/webserver/templates/art/index.html @@ -1,4 +1,4 @@ -{%- extends 'base.html' -%} +{%- extends 'index.html' -%} {%- block content -%}

ListenBrainz Cover Art Grid demo

Custom layouts

diff --git a/listenbrainz/webserver/templates/base.html b/listenbrainz/webserver/templates/base.html index a70187f600..939c79a40b 100644 --- a/listenbrainz/webserver/templates/base.html +++ b/listenbrainz/webserver/templates/base.html @@ -29,14 +29,11 @@ {% endfor %} {% endwith %} - {%- block wrapper -%} -
- +
{%- block content -%} - + {%- endblock -%}
- {%- endblock -%} {%- block footer -%}