Skip to content
Snippets Groups Projects
Commit 90ad01ec authored by dominip89's avatar dominip89
Browse files

Merge branch '4-neuen-sport-hinzufugen-sport-bearbeiten' into 'master'

Implementing Sport List and Incomplete List

Closes #5 and #4

See merge request swp-unisport/team-warumkeinrust/admin-frontend!3
parents e55385a1 22e4d05e
Branches
No related tags found
No related merge requests found
import * as React from 'react';
import { Admin, Resource, ListGuesser } from 'react-admin';
import './App.css';
import dashboard from './dashboard';
import jsonServerProvider from 'ra-data-json-server';
import drfProvider from 'ra-data-django-rest-framework';
// const dataProvider = jsonServerProvider('http://localhost:8000/api/admin')
const dataProvider = drfProvider('http://localhost:8000/api/admin');
import { Admin, Resource } from 'react-admin';
import dataProviderMapper from './dataProviderMapper';
import { sportList, sportEdit, sportCreate } from './sportList';
import { incompleteEdit, incompleteList } from './incompleteList';
const App = () => (
<Admin dataProvider={dataProvider}>
<Resource name='sport-list' list={ListGuesser} />
<Resource name='criterion-list' list={ListGuesser} />
<Admin
dataProvider={dataProviderMapper} // A custom mapper is used because different resources need different dataProviders
disableTelemetry
>
<Resource
name='sport' // name of the API endpoint
options={{ label: 'Sport Management' }} // if we do not rename the resource, the API name will be used
list={sportList}
edit={sportEdit}
create={sportCreate}
/>
<Resource
name='sport-incomplete'
options={{ label: 'Incomplete Sports' }}
list={incompleteList}
edit={incompleteEdit}
/>
</Admin>
);
......
export const criterionEditValidator = val => {
if (val % 1 != 0 || !Number.isInteger(val)) {
return "Muss ganzzahlig sein!";
};
if (val > 9) {
return "Darf nicht größer als 9 sein.";
};
if (val < -1) {
return "Nur -1 ist als negativer Wert erlaubt!";
};
if (val === 0) {
return "Muss entweder -1 oder zwischen 1 und 9 liegen!";
};
return undefined;
};
// Used as a validator for our criteria.
// Takes an input Int and corrects it if needed, returns correct value
export const criterionEditFormatter = val => {
if (val === 0) {
val = 1;
} else if (val === null || val === undefined || val <= -2) {
val = -1;
} else if (val >= 10) {
val = 9;
};
return Math.trunc(val);
};
import * as React from "react";
import { Card, CardContent, CardHeader } from '@material-ui/core';
export default () => (
<Card>
<CardHeader title="Welcome to the Administration" />
<CardContent>Lorem ipsum sic dolor amet...</CardContent>
</Card>
);
\ No newline at end of file
import drfProvider from 'ra-data-django-rest-framework';
import sportIncompleteProvider from './sportIncompleteProvider.js';
import {
GET_LIST,
GET_ONE,
CREATE,
UPDATE,
UPDATE_MANY,
DELETE,
GET_MANY,
GET_MANY_REFERENCE,
} from 'react-admin';
// Mapping of data providers onto resource names
// For every call, it is checked which data provider should be used
// Depending on the need, further data providers can be defined and added
const dataProviders = [
{
dataProvider: drfProvider('http://localhost:8000/api/admin'),
resources: ['sport'],
},
{
dataProvider: sportIncompleteProvider('http://localhost:8000/api/admin'),
resources: ['sport-incomplete'],
}
];
export default (type, resource, params) => {
// Goes through the data provider list and takes the one fitting to the resource
const dataProviderMapping = dataProviders.find((dp) =>
dp.resources.includes(resource));
// Maps the type of request onto the function of the data provider
const mappingType = {
[GET_LIST]: 'getList',
[GET_ONE]: 'getOne',
[GET_MANY]: 'getMany',
[GET_MANY_REFERENCE]: 'getManyReference',
[CREATE]: 'create',
[UPDATE]: 'update',
[UPDATE_MANY]: 'updateMany',
[DELETE]: 'delete',
};
// Debugging, yaay
console.log(type, resource, params)
return dataProviderMapping.dataProvider[mappingType[type]](resource, params);
};
\ No newline at end of file
import * as React from 'react';
import {
List,
Datagrid,
TextField,
ArrayInput,
SimpleFormIterator,
NumberInput,
Edit,
SimpleForm,
SaveButton,
Toolbar,
FormDataConsumer,
ReferenceField,
ChipField
} from 'react-admin';
import { criterionEditValidator } from './criterionEditValidator.js';
// Eliminates the delete function while editing the incomplete list
const IncompleteEditToolbar = props => (
<Toolbar {...props} >
<SaveButton />
</Toolbar>
);
export const incompleteList = props => (
<List {...props}>
{/* rowClick expand means that no edit page is opened, but instead shown below the data row itself without reloading */}
<Datagrid rowClick="expand" expand={incompleteEdit} >
{/* Reference Field fetches every Sport object if a reference field to it exists.
This means that n GETs are made for one load of the incomplete-list.
Could probably be improved in the future.
*/}
<ReferenceField label="ID" source="id" reference="sport" >
<ChipField source="id" />
</ReferenceField>
<TextField source="name" />
</Datagrid>
</List>
);
export const incompleteEdit = props => (
<Edit {...props}>
<SimpleForm toolbar={<IncompleteEditToolbar />}>
<TextField source="id" />
<TextField source="name" />
<ArrayInput source="criteria">
<SimpleFormIterator disableAdd disableRemove>
{/* Documentation is wrong, look here! https://github.com/marmelab/react-admin/issues/2500
getSource() needs to get called without arguments before the return,
instead of as the source parameter of the TextField with "name" as argument */}
<FormDataConsumer>
{({ getSource, scopedFormData }) => {
getSource();
return (
<TextField
source={"name"} record={scopedFormData}
/>
);
}}
</FormDataConsumer>
<NumberInput
source="value"
label="Wertung"
validate={[criterionEditValidator]}
/>
</SimpleFormIterator>
</ArrayInput>
</SimpleForm>
</Edit>
);
import { Pagination } from 'react-admin';
const PostPagination = props => <Pagination rowsPerPageOptions={[100, 500, 1000]} {...props} />;
\ No newline at end of file
import { stringify } from 'query-string';
import {
fetchUtils,
} from 'ra-core';
/*
DATA PROVIDER FOR INCOMPLETE LIST
Based on the default Django REST data provider https://github.com/bmihelac/ra-data-django-rest-framework
Changed here:
- getList:
Saves the returned data from the backend in the cached_incomplete variable for further use
- getOne:
Checks cached_incomplete before asking the server for the complete list of all incomplete Sports
- update:
Sends filled out criteria to sport/id instead of sport-incomplete/id, since that doesn't exist
Also, the function returns only the other incomplete criteria instead of all
It might be worth thinking about changing the API in the backend so that sport-incomplete is a whole Viewset
This would bloat the API but make handling in the frontend easier.
*/
// export {
// default as jwtTokenAuthProvider,
// fetchJsonWithAuthJWTToken,
// } from './jwtTokenAuthProvider';
const getPaginationQuery = (pagination) => {
return {
page: pagination.page,
page_size: pagination.perPage,
};
};
const getFilterQuery = (filter) => {
const { q: search, ...otherSearchParams } = filter;
return {
...otherSearchParams,
search,
};
};
export const getOrderingQuery = (sort) => {
const { field, order } = sort;
return {
ordering: `${order === 'ASC' ? '' : '-'}${field}`,
};
};
// Cache of the Incomplete List because they can only be called all at once, making getOne very unoptimized
let cached_incomplete = {};
export default (
apiUrl,
httpClient = fetchUtils.fetchJson
) => {
const getOneJson = (resource, id) =>
httpClient(`${apiUrl}/${resource}/${id}/`).then(
(response) => response.json
);
return {
getList: async (resource, params) => {
const query = {
...getFilterQuery(params.filter),
...getPaginationQuery(params.pagination),
...getOrderingQuery(params.sort),
};
const url = `${apiUrl}/${resource}/?${stringify(query)}`;
const { json } = await httpClient(url);
// Caches all results
cached_incomplete = json.results;
return {
data: json.results,
total: json.count,
};
},
getOne: async (resource, params) => {
let data = {}
// Checks if the data is already present in the variable cache, so it doesn't need to call everything
if (cached_incomplete == {}) {
const url = `${apiUrl}/${resource}/`;
const { json } = await httpClient(url);
data = json.results.find((data) => data.id == params.id);
} else {
data = cached_incomplete.find((data) => data.id == params.id);
}
let formatted_criteria = [];
data.criteria.forEach(criterion => {
formatted_criteria.push(
{
"id": criterion.id,
"name": criterion.name,
"value": -1
}
);
});
data.criteria = formatted_criteria;
return {
data
};
},
getMany: (resource, params) => {
return Promise.all(
params.ids.map(id => getOneJson(resource, id))
).then(data => ({ data }));
},
getManyReference: async (resource, params) => {
const query = {
...getFilterQuery(params.filter),
...getPaginationQuery(params.pagination),
...getOrderingQuery(params.sort),
[params.target]: params.id,
};
const url = `${apiUrl}/${resource}/?${stringify(query)}`;
const { json } = await httpClient(url);
return {
data: json.results,
total: json.count,
};
},
update: async (resource, params) => {
const verbose_criteria_list = [];
// Filter the criteria that were filled out
params.data.criteria.forEach(criterion => {
if (criterion.value !== -1) {
verbose_criteria_list.push({
"id": criterion.id,
"value": criterion.value
});
};
});
// Send criteria that were filled out
const data = {
"criteria": verbose_criteria_list
};
const { json } = await httpClient(`${apiUrl}/sport/${params.id}/`, {
method: 'PATCH',
body: JSON.stringify(data),
});
// The whole Sport object has been returned
// Just return the unreturned criteria, or None if everything is filled out
let new_criteria = []
json.criteria.forEach(element => {
if (element.value <= -1) {
new_criteria.push(element)
}
});
// At the moment, even sports with everything filled in are still shown
// At a future iteration, it may be useful to remove it from the shown list
json.criteria = new_criteria;
return { data: json };
},
updateMany: (resource, params) =>
Promise.all(
params.ids.map(id =>
httpClient(`${apiUrl}/${resource}/${id}/`, {
method: 'PATCH',
body: JSON.stringify(params.data),
})
)
).then(responses => ({ data: responses.map(({ json }) => json.id) })),
create: async (resource, params) => {
const { json } = await httpClient(`${apiUrl}/${resource}/`, {
method: 'POST',
body: JSON.stringify(params.data),
});
return {
data: { ...json },
};
},
delete: (resource, params) =>
httpClient(`${apiUrl}/${resource}/${params.id}/`, {
method: 'DELETE',
}).then(() => ({ data: params.previousData })),
deleteMany: (resource, params) =>
Promise.all(
params.ids.map(id =>
httpClient(`${apiUrl}/${resource}/${id}/`, {
method: 'DELETE',
})
)
).then(responses => ({ data: responses.map(({ json }) => json.id) })),
};
};
import {
List,
Datagrid,
UrlField,
BooleanField,
Edit,
SimpleForm,
TextInput,
ArrayInput,
BooleanInput,
DateInput,
TextField,
SimpleFormIterator,
NumberInput,
Create,
} from 'react-admin';
import { criterionEditValidator } from './criterionEditValidator.js';
export const sportList = props => (
<List {...props}>
<Datagrid rowClick="edit">
<TextField source="id" />
<TextField source="name" />
<UrlField source="url" />
<BooleanField source="is_filled" />
</Datagrid>
</List>
);
export const sportEdit = props => (
<Edit {...props}>
<SimpleForm>
<TextInput disabled source="id" />
<TextInput source="name" fullWidth />
<TextInput source="url" fullWidth />
<BooleanInput source="currently_active" />
<DateInput disabled source="last_used" />
<ArrayInput source="criteria">
<SimpleFormIterator disableAdd disableRemove> {/* Used because you cannot know how many objects will be sent */}
<TextInput disabled source="name" label="Kriterienname" />
{/* criterionEditValidator checks input if it is within 1-9 or -1 and sets it accordingly */}
<NumberInput
source="value"
validate={[criterionEditValidator]}
label="Kriterienwertung"
/>
</SimpleFormIterator>
</ArrayInput>
</SimpleForm>
</Edit>
);
// sportCreate cannot know at this point how many and which criteria are used, so they are editable after creating the sport
export const sportCreate = props => (
<Create {...props}>
<SimpleForm>
<TextInput source="name" fullWidth />
<TextInput source="url" fullWidth />
</SimpleForm>
</Create>
);
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment