diff --git a/backend/core/views/__init__.py b/backend/core/views/__init__.py index a12f595..5533463 100644 --- a/backend/core/views/__init__.py +++ b/backend/core/views/__init__.py @@ -2,6 +2,7 @@ from core.views.pipeline import PipelineViewSet from core.views.process import ProcessViewSet from core.views.product import ProductViewSet +from core.views.product import ProductSpeczViewSet from core.views.product_content import ProductContentViewSet from core.views.product_file import ProductFileViewSet from core.views.product_type import ProductTypeViewSet diff --git a/backend/core/views/product.py b/backend/core/views/product.py index 65cf08c..d0260cf 100644 --- a/backend/core/views/product.py +++ b/backend/core/views/product.py @@ -57,15 +57,40 @@ def filter_name(self, queryset, name, value): return queryset.filter(query) def filter_type_name(self, queryset, name, value): - query = format_query_to_char(name, value, ["product_type__display_name", "product_type__name"]) + query = format_query_to_char( + name, value, ["product_type__display_name", "product_type__name"] + ) return queryset.filter(query) def filter_release(self, queryset, name, value): - query = format_query_to_char(name, value, ["release__display_name", "release__name"]) + query = format_query_to_char( + name, value, ["release__display_name", "release__name"] + ) return queryset.filter(query) +class ProductSpeczViewSet(viewsets.ReadOnlyModelViewSet): + """Esse endpoint retorna apenas os produtos cujo o product type é = a 'spec-z' e o status é = a 1""" + + queryset = Product.objects.filter(product_type__name="specz_catalog", status=1) + serializer_class = ProductSerializer + search_fields = [ + "display_name", + "user__username", + "user__first_name", + "user__last_name", + ] + filterset_class = ProductFilter + ordering_fields = [ + "id", + "display_name", + "product_type", + "created_at", + ] + ordering = ["-created_at"] + + class ProductViewSet(viewsets.ModelViewSet): queryset = Product.objects.all() serializer_class = ProductSerializer diff --git a/backend/pzserver/settings.py b/backend/pzserver/settings.py index 2d84942..3d692ee 100644 --- a/backend/pzserver/settings.py +++ b/backend/pzserver/settings.py @@ -159,6 +159,9 @@ UPLOAD_DIR = os.getenv("UPLOAD_DIR", MEDIA_URL) +# Criando VA APPEND +APPEND_SLASH=False + # Default primary key field type # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field diff --git a/backend/pzserver/urls.py b/backend/pzserver/urls.py index fe6c47b..e3665f0 100644 --- a/backend/pzserver/urls.py +++ b/backend/pzserver/urls.py @@ -18,7 +18,7 @@ OrchestrationInfoView, OrchestrationPipelinesView, PipelineViewSet, ProcessViewSet, ProductContentViewSet, ProductFileViewSet, ProductTypeViewSet, ProductViewSet, - ReleaseViewSet, UserViewSet) + ProductSpeczViewSet, ReleaseViewSet, UserViewSet) from django.conf import settings from django.contrib import admin @@ -33,6 +33,7 @@ route.register(r"releases", ReleaseViewSet, basename="releases") route.register(r"product-types", ProductTypeViewSet, basename="product_types") route.register(r"products", ProductViewSet, basename="products") +route.register(r"products-specz", ProductSpeczViewSet, basename="products_specz") route.register(r"product-contents", ProductContentViewSet, basename="product_contents") route.register(r"product-files", ProductFileViewSet, basename="product_files") diff --git a/frontend/components/EmailField.js b/frontend/components/EmailField.js new file mode 100644 index 0000000..5b2bf70 --- /dev/null +++ b/frontend/components/EmailField.js @@ -0,0 +1,53 @@ +// import React, { useState, useEffect } from 'react' +// import TextField from '@mui/material/TextField' +// import PropTypes from 'prop-types' + +// function EmailField({ initialValue = '', onEmailChange, onClearForm }) { +// const [email, setEmail] = useState(initialValue) +// const [isValidEmail, setIsValidEmail] = useState(true) + +// const handleEmailChange = event => { +// const newEmail = event.target.value +// setEmail(newEmail) +// onEmailChange(newEmail) +// } + +// const handleBlur = () => { +// if (email.trim() !== '') { +// validateEmail(email) +// } +// } + +// const validateEmail = value => { +// const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ +// const isValid = emailRegex.test(value) +// setIsValidEmail(isValid) +// } + +// useEffect(() => { +// setEmail(initialValue) +// setIsValidEmail(true) +// }, [onClearForm, initialValue]) + +// return ( +//
+// +//
+// ) +// } + +// EmailField.propTypes = { +// initialValue: PropTypes.string, +// onEmailChange: PropTypes.func, +// onClearForm: PropTypes.func +// } + +// export default EmailField diff --git a/frontend/components/NNeighbors.js b/frontend/components/NNeighbors.js new file mode 100644 index 0000000..4bf5b35 --- /dev/null +++ b/frontend/components/NNeighbors.js @@ -0,0 +1,36 @@ +import React, { useState, useEffect } from 'react' +import TextField from '@mui/material/TextField' +import PropTypes from 'prop-types' + +const NNeighbors = ({ nNeighbors, onChange }) => { + const [localNNeighbors, setLocalNNeighbors] = useState(nNeighbors) + + useEffect(() => { + setLocalNNeighbors(nNeighbors) + }, [nNeighbors]) + + const handleNeighborsChange = event => { + const inputValue = event.target.value.replace(/[^\d]/g, '') + const newValue = parseInt(inputValue, 10) || 1 + setLocalNNeighbors(newValue) + onChange(newValue) + } + + return ( + + ) +} + +NNeighbors.propTypes = { + nNeighbors: PropTypes.oneOfType([PropTypes.number, PropTypes.string]) + .isRequired, + onChange: PropTypes.func.isRequired +} + +export default NNeighbors diff --git a/frontend/components/ReleaseSelect.js b/frontend/components/ReleaseSelect.js index 147518f..43f97f9 100644 --- a/frontend/components/ReleaseSelect.js +++ b/frontend/components/ReleaseSelect.js @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react' import PropTypes from 'prop-types' import MenuItem from '@mui/material/MenuItem' -import { getReleases } from '../services/product' +import { getReleases } from '../services/release' import { TextField } from '@mui/material' // export default function ReleaseSelect({ value, onChange, disabled }) { diff --git a/frontend/components/SearchRadius.js b/frontend/components/SearchRadius.js new file mode 100644 index 0000000..d65a0f7 --- /dev/null +++ b/frontend/components/SearchRadius.js @@ -0,0 +1,30 @@ +import TextField from '@mui/material/TextField' +import PropTypes from 'prop-types' +import React from 'react' + +function SearchRadius({ searchRadius, onChange }) { + const formattedRadius = searchRadius.toLocaleString('en-US', { + minimumFractionDigits: 1, + maximumFractionDigits: 1 + }) + + const handleRadiusChange = event => { + onChange(parseFloat(event.target.value)) + } + + return ( + + ) +} + +SearchRadius.propTypes = { + searchRadius: PropTypes.number.isRequired, + onChange: PropTypes.func.isRequired +} + +export default SearchRadius diff --git a/frontend/components/SpeczData.js b/frontend/components/SpeczData.js index 9a23f24..905983d 100644 --- a/frontend/components/SpeczData.js +++ b/frontend/components/SpeczData.js @@ -1,3 +1,5 @@ +import * as React from 'react' +import { getProducts } from '../services/product' import { DataGrid } from '@mui/x-data-grid' import moment from 'moment' import { Box } from '@mui/material' @@ -30,13 +32,29 @@ const columns = [ } ] +async function fetchData(filters, query) { + try { + const response = await getProducts({ + filters, + page: 0, + page_size: 25, + sort: [{ field: 'created_at', sort: 'desc' }], + search: query + }) + + return response.results + } catch (error) { + console.error(error) + throw error + } +} + function DataTable({ rows }) { return ( - + row.id} pageSizeOptions={[5, 10]} checkboxSelection /> @@ -48,39 +66,33 @@ DataTable.propTypes = { rows: PropTypes.arrayOf(PropTypes.object).isRequired } -const rows = [ - { - id: 1, - display_name: 'Jandson 1', - uploaded_by: 'User1', - created_at: '2023-01-01' - }, - { - id: 2, - display_name: 'Jandson 2', - uploaded_by: 'User2', - created_at: '2023-02-15' - }, - { - id: 3, - display_name: 'Jandson 3', - uploaded_by: 'User3', - created_at: '2023-03-22' - }, - { - id: 4, - display_name: 'Jandson V', - uploaded_by: 'User1', - created_at: '2023-04-10' - }, - { - id: 5, - display_name: 'Jandson Try', - uploaded_by: 'User2', - created_at: '2023-05-05' - } -] +function DataTableWrapper({ filters, query }) { + const [rows, setRows] = React.useState([]) + + React.useEffect(() => { + const fetchAndSetData = async () => { + try { + const data = await fetchData(filters, query) + setRows(data) + } catch (error) { + console.error(error) + } + } + + fetchAndSetData() + }, [filters, query]) -export default function App() { return } + +DataTableWrapper.propTypes = { + filters: PropTypes.object, + query: PropTypes.string +} + +DataTableWrapper.defaultProps = { + filters: {}, + query: '' +} + +export default DataTableWrapper diff --git a/frontend/components/TsmData.js b/frontend/components/TsmData.js index eceffca..5e0cb88 100644 --- a/frontend/components/TsmData.js +++ b/frontend/components/TsmData.js @@ -1,85 +1,115 @@ +import Box from '@mui/material/Box' +import Radio from '@mui/material/Radio' import { DataGrid } from '@mui/x-data-grid' import moment from 'moment' -import { Box } from '@mui/material' import PropTypes from 'prop-types' +import * as React from 'react' +import { useQuery } from 'react-query' +import { getProductsSpecz } from '../services/product' -const columns = [ - { - field: 'display_name', - headerName: 'Name', - sortable: true, - flex: 1 - }, - { - field: 'uploaded_by', - headerName: 'Uploaded By', - flex: 1, - sortable: false - }, - { - field: 'created_at', - headerName: 'Created at', - sortable: true, - width: 200, - valueFormatter: params => { - if (!params.value) { - return '' - } - return moment(params.value).format('YYYY-MM-DD') +const DataTableWrapper = ({ filters, query, onProductSelect }) => { + const [page, setPage] = React.useState(0) + const [pageSize, setPageSize] = React.useState(10) + const [selectedRowId, setSelectedRowId] = React.useState(null) + + const { data, isLoading } = useQuery( + ['productData', { filters, query, page, pageSize }], + () => + getProductsSpecz({ + filters, + page, + page_size: pageSize, + sort: [{ field: 'created_at', sort: 'desc' }], + search: query + }), + { + staleTime: Infinity, + refetchInterval: false, + retry: false + } + ) + + const handleRowSelection = rowId => { + setSelectedRowId(rowId) + if (onProductSelect) { + onProductSelect(rowId) } } -] -function DataTable({ rows }) { + const columns = [ + { + field: 'select', + headerName: '', + renderCell: params => ( + handleRowSelection(params.row.id)} + /> + ), + width: 50 + }, + { + field: 'display_name', + headerName: 'Name', + sortable: true, + flex: 1 + }, + { + field: 'uploaded_by', + headerName: 'Uploaded By', + flex: 1, + sortable: false + }, + { + field: 'created_at', + headerName: 'Created at', + sortable: true, + width: 200, + valueFormatter: params => { + if (!params.value) { + return '' + } + return moment(params.value).format('YYYY-MM-DD') + } + } + ] + return ( - - row.id} - pageSizeOptions={[5, 10]} - /> - + <> + + row.id || row.unique_key} + rows={data?.results || []} + columns={columns} + paginationMode="server" + page={page} + pageSize={pageSize} + rowCount={data?.count || 0} + onPageChange={newPage => setPage(newPage)} + onPageSizeChange={newPageSize => setPageSize(newPageSize)} + rowsPerPageOptions={[10]} + disableColumnMenu + disableColumnSelector + loading={isLoading} + localeText={{ + noRowsLabel: isLoading ? 'Loading...' : 'No products found' + }} + onRowClick={params => handleRowSelection(params.row.id)} + /> + + ) } -DataTable.propTypes = { - rows: PropTypes.arrayOf(PropTypes.object).isRequired +DataTableWrapper.propTypes = { + filters: PropTypes.object, + query: PropTypes.string, + onProductSelect: PropTypes.func } -const rows = [ - { - id: 1, - display_name: 'Jandson 1', - uploaded_by: 'User1', - created_at: '2023-01-01' - }, - { - id: 2, - display_name: 'Jandson 2', - uploaded_by: 'User2', - created_at: '2023-02-15' - }, - { - id: 3, - display_name: 'Jandson 3', - uploaded_by: 'User3', - created_at: '2023-03-22' - }, - { - id: 4, - display_name: 'Jandson V', - uploaded_by: 'User1', - created_at: '2023-04-10' - }, - { - id: 5, - display_name: 'Jandson Try', - uploaded_by: 'User2', - created_at: '2023-05-05' - } -] - -export default function App() { - return +DataTableWrapper.defaultProps = { + filters: {}, + query: '' } + +export default DataTableWrapper diff --git a/frontend/pages/specz_catalogs.js b/frontend/pages/specz_catalogs.js index b24392f..b586136 100644 --- a/frontend/pages/specz_catalogs.js +++ b/frontend/pages/specz_catalogs.js @@ -1,11 +1,198 @@ -import React from 'react' -import { Typography, Box } from '@mui/material' +// import InfoIcon from '@mui/icons-material/Info' +// import Box from '@mui/material/Box' +// import Button from '@mui/material/Button' +// import Card from '@mui/material/Card' +// import CardContent from '@mui/material/CardContent' +// import Grid from '@mui/material/Grid' +// import IconButton from '@mui/material/IconButton' +// import Link from '@mui/material/Link' +// import Breadcrumbs from '@mui/material/Breadcrumbs' +// import MenuItem from '@mui/material/MenuItem' +// import Paper from '@mui/material/Paper' +// import Select from '@mui/material/Select' +// import Snackbar from '@mui/material/Snackbar' +// import SnackbarContent from '@mui/material/SnackbarContent' +// import TextField from '@mui/material/TextField' +// import Typography from '@mui/material/Typography' +// import React, { useState } from 'react' +// import SearchField from '../components/SearchField' +// import SearchRadius from '../components/SearchRadius' +// import SpeczData from '../components/SpeczData' +// import { useTheme } from '@mui/system' + +// function SpeczCatalogs() { +// const theme = useTheme() + +// const [combinedCatalogName, setCombinedCatalogName] = useState('') +// const [search, setSearch] = React.useState('') +// const filters = React.useState() +// const [searchRadius, setSearchRadius] = useState('1.0') +// const [selectedOption, setSelectedOption] = useState('keepAll') +// const [snackbarOpen, setSnackbarOpen] = useState(false) + +// const handleCatalogNameChange = event => { +// setCombinedCatalogName(event.target.value) +// } + +// const handleSearchRadiusChange = event => { +// const newValue = parseFloat(event.target.value) +// setSearchRadius(isNaN(newValue) ? '' : newValue.toString()) +// } + +// const handleClearForm = () => { +// setCombinedCatalogName('') +// setSearchRadius('1.0') +// setSelectedOption('keepAll') +// } + +// const handleRun = () => { +// setSnackbarOpen(true) +// } + +// const handleSnackbarClose = () => { +// setSnackbarOpen(false) +// } + +// const styles = { +// root: { +// transition: 'box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms', +// borderRadius: '4px', +// padding: theme.spacing(3), +// flex: '1 1 0%' +// } +// } + +// return ( +// +// +// +// +// +// Home +// +// +// Pipelines +// +// Combine Spec-z Catalogs +// +// +// +// Combine Spec-z Catalogs +// +// +// +// +// +// +// +// +// 1. Combined catalog name: +// +// +// +// +// +// +// +// +// +// +// 2. Select the Spec-z Catalogs to include in your sample: +// +// setSearch(query)} /> +// +// +// +// +// +// +// +// +// + +// +// +// 3. Select the cross-matching configuration choices: +// +// + +// +// Search Radius (arcsec):{' '} +// +// + +// +// +// In case of multiple spec-z measurements for the same object: +// {' '} +// +// + +// +// +// +// +// +// +// +// +// +// +// +// +// ) +// } + +// export default SpeczCatalogs + +import ArrowBackIos from '@mui/icons-material/ArrowBackIos' +import InfoIcon from '@mui/icons-material/Info' +import { Box, Typography } from '@mui/material' import Breadcrumbs from '@mui/material/Breadcrumbs' -import Link from '@mui/material/Link' import Grid from '@mui/material/Grid' import IconButton from '@mui/material/IconButton' -import InfoIcon from '@mui/icons-material/Info' -import ArrowBackIos from '@mui/icons-material/ArrowBackIos' +import Link from '@mui/material/Link' +import React from 'react' function SpeczCatalogs() { return ( diff --git a/frontend/pages/training_set_maker.js b/frontend/pages/training_set_maker.js index c261d08..cdc25bd 100644 --- a/frontend/pages/training_set_maker.js +++ b/frontend/pages/training_set_maker.js @@ -1,47 +1,379 @@ -import React from 'react' -import { Typography, Box } from '@mui/material' +import InfoIcon from '@mui/icons-material/Info' +import Backdrop from '@mui/material/Backdrop' +import Box from '@mui/material/Box' import Breadcrumbs from '@mui/material/Breadcrumbs' -import Link from '@mui/material/Link' +import Button from '@mui/material/Button' +import Card from '@mui/material/Card' +import CardContent from '@mui/material/CardContent' +import CircularProgress from '@mui/material/CircularProgress' import Grid from '@mui/material/Grid' import IconButton from '@mui/material/IconButton' -import InfoIcon from '@mui/icons-material/Info' -import ArrowBackIos from '@mui/icons-material/ArrowBackIos' +import Link from '@mui/material/Link' +import MenuItem from '@mui/material/MenuItem' +import Paper from '@mui/material/Paper' +import Select from '@mui/material/Select' +import Snackbar from '@mui/material/Snackbar' +import SnackbarContent from '@mui/material/SnackbarContent' +import TextField from '@mui/material/TextField' +import Typography from '@mui/material/Typography' +import { useTheme } from '@mui/system' +import React, { useEffect, useState } from 'react' +import NNeighbors from '../components/NNeighbors' +import SearchField from '../components/SearchField' +import SearchRadius from '../components/SearchRadius' +import TsmData from '../components/TsmData' +import { getPipelineByName } from '../services/pipeline' +import { submitProcess } from '../services/process' +import { getReleases } from '../services/release' function TrainingSetMaker() { + const theme = useTheme() + const [combinedCatalogName, setCombinedCatalogName] = useState('') + const [search, setSearch] = useState('') + const [selectedProductId, setSelectedProductId] = useState(null) + const [snackbarOpen, setSnackbarOpen] = useState(false) + const [isSubmitting, setIsSubmitting] = useState(false) + const [snackbarMessage, setSnackbarMessage] = useState('') + const [snackbarColor, setSnackbarColor] = useState(theme.palette.warning.main) + const [selectedLsstCatalog, setSelectedLsstCatalog] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [releases, setReleases] = useState([]) + const [initialData, setInitialData] = useState({ + param: { + crossmatch: { + n_neighbors: 1, + radius_arcsec: 1, + output_catalog_name: 'tsm_cross_001' + }, + duplicate_criteria: 'closest' + } + }) + const [data, setData] = useState(initialData) + + useEffect(() => { + const fetchPipelineData = async () => { + try { + const response = await getPipelineByName({ name: 'training_set_maker' }) + const pipelineData = response.data.results[0] + + setInitialData(pipelineData) + setData(pipelineData.system_config) + } catch (error) { + console.error('Error fetching pipeline data from API', error) + } + } + + const fetchReleases = async () => { + try { + const releasesData = await getReleases() + + if (Array.isArray(releasesData.results)) { + const fetchedReleases = releasesData.results + setReleases(fetchedReleases) + + if (fetchedReleases.length > 0) { + setSelectedLsstCatalog(fetchedReleases[0].name) + } + } else { + console.error('No results found in the API response') + } + } catch (error) { + console.error('Error fetching releases from API', error) + } + } + + fetchPipelineData() + fetchReleases() + }, []) + + const handleClearForm = () => { + setCombinedCatalogName('') + setData(initialData.system_config) + setSelectedLsstCatalog('') + setIsSubmitting(false) + } + + const handleRun = async () => { + setIsSubmitting(true) + + if (combinedCatalogName.trim() === '') { + setSnackbarMessage( + 'Your process has not been submitted. Please fill in the training set name.' + ) + setSnackbarColor(theme.palette.warning.main) + setSnackbarOpen(true) + return + } + + const sanitizedCatalogName = combinedCatalogName + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .trim() + .replace(/[\s*,\-*/]+/g, '_') + + try { + const pipelineId = initialData.id + + const selectedRelease = releases.find( + release => release.name === selectedLsstCatalog + ) + const releaseId = selectedRelease ? selectedRelease.id : null + + if (!releaseId) { + setSnackbarMessage('No valid release selected.') + setSnackbarColor(theme.palette.error.main) + setSnackbarOpen(true) + return + } + + // Create the JSON object + const processData = { + display_name: sanitizedCatalogName, + pipeline: pipelineId, + used_config: { + param: { + crossmatch: { + n_neighbors: data.param.crossmatch.n_neighbors, + radius_arcsec: data.param.crossmatch.radius_arcsec, + output_catalog_name: sanitizedCatalogName + }, + duplicate_criteria: data.param.duplicate_criteria + } + }, + release: releaseId, + inputs: [selectedProductId] + } + + // tentativa de envio do json via POST + setIsLoading(true) + await submitProcess(processData) + setSnackbarMessage('Your process has been submitted successfully.') + setSnackbarColor(theme.palette.success.main) + handleClearForm() + } catch (error) { + console.error('Error submitting the process:', error) + setSnackbarMessage('There was an error submitting your process.') + setSnackbarColor(theme.palette.error.main) + } finally { + setIsLoading(false) + setSnackbarOpen(true) + } + } + + const handleSnackbarClose = () => { + setSnackbarOpen(false) + setIsSubmitting(false) + } + + const styles = { + root: { + transition: 'box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms', + borderRadius: '4px', + padding: theme.spacing(3), + flex: '1 1 0%' + } + } + return ( - - - - Home - - - Pipelines - - Training Set Maker - - - - window.history.back()} - > - - - Training Set Maker - - - - - - - Coming soon... - - + + + + + + Home + + + Pipelines + + Training Set Maker + + + + + Training Set Maker + + + + + + + + + + 1. Training set name: + + * + + + setCombinedCatalogName(event.target.value)} + error={isSubmitting && combinedCatalogName.trim() === ''} + helperText={ + isSubmitting && combinedCatalogName.trim() === '' + ? 'This field is required.' + : '' + } + /> + + + + + + + + + + 2. Select the Spec-z Catalog for the cross-matching: + + setSearch(query)} /> + + + + + + + + + + + + + + 3. Select the Objects catalog (photometric data): + + + + + + + 4. Select the cross-matching configuration choices: + + + + + The threshold distance in arcseconds beyond which neighbors + are not added: + + { + setData({ + ...data, + param: { + ...data.param, + crossmatch: { + ...data.param.crossmatch, + radius_arcsec: value + } + } + }) + }} + /> + + + + + + + The number of neighbors to find within each point: + + { + setData({ + ...data, + param: { + ...data.param, + crossmatch: { + ...data.param.crossmatch, + n_neighbors: value + } + } + }) + }} + reset={false} + /> + + + + + + In case of multiple spec-z measurements for the same object: + + + + + + + + + + + + + + ({ color: '#fff', zIndex: theme.zIndex.drawer + 1 })} + open={isLoading} + > + + + + + + + + ) } diff --git a/frontend/services/pipeline.js b/frontend/services/pipeline.js new file mode 100644 index 0000000..6911579 --- /dev/null +++ b/frontend/services/pipeline.js @@ -0,0 +1,5 @@ +import { api } from './api' + +export const getPipelineByName = ({ name }) => { + return api.get(`/api/pipelines/?name=${name}`) +} \ No newline at end of file diff --git a/frontend/services/process.js b/frontend/services/process.js new file mode 100644 index 0000000..377c8f9 --- /dev/null +++ b/frontend/services/process.js @@ -0,0 +1,16 @@ +import { api } from './api' + +export const submitProcess = (processData) => { + return api.post('/api/processes/', processData) + .then(response => { + if (response.status === 201) { + return response + } else { + throw new Error('Failed to submit process') + } + }) + .catch(error => { + console.error('Error response:', error.response || error.message) + throw error + }) +} \ No newline at end of file diff --git a/frontend/services/product.js b/frontend/services/product.js index 9bd2cab..6a77f43 100644 --- a/frontend/services/product.js +++ b/frontend/services/product.js @@ -3,10 +3,6 @@ import forIn from 'lodash/forIn' import { api } from './api' // import isEmpty from 'lodash/isEmpty' -export const getReleases = ({ }) => { - return api.get('/api/releases/').then(res => res.data) -} - export const getProductTypes = ({ }) => { return api.get('/api/product-types/').then(res => res.data) } @@ -212,3 +208,54 @@ export const createProductFile = (product_id, file, role, onUploadProgress) => { onUploadProgress: onUploadProgress }) } + + +export const getProductsSpecz = ({ + filters = {}, + search = '', + page = 0, + page_size = 25, + sort = [] +}) => { + // Esse endpoint retorna apenas os catalogos cujo o product type é = 'spec-z' e status = 1 + let ordering = null + + // Ordenação no DRF + // https://www.django-rest-framework.org/api-guide/filtering/#orderingfilter + if (sort.length === 1) { + ordering = sort[0].field + + if (sort[0].sort === 'desc') { + ordering = '-' + ordering + } + } + // Paginação no DRF + // https://www.django-rest-framework.org/api-guide/pagination/#pagenumberpagination + // Django não aceita pagina 0 por isso é somado 1 ao numero da página. + page += 1 + + // Todos os Query Params + const params = { page, page_size, ordering, search } + + // Filtros no DRF + // https://django-filter.readthedocs.io/en/stable/guide/rest_framework.html + // cada filtro que tiver valor deve virar uma propriedade no objeto params + // Só aplica os filtros caso não tenha um search dessa forma a busca é feita em todos os registros. + // o filtro official_product deve ser enviado no search também. + if (search === '') { + forIn(filters, function (value, key) { + if (key === 'release' && value === '0') { + params.release__isnull = true + params.release = null + } else { + if (value != null) { + params[key] = value + } + } + }) + } + + params.official_product = filters.official_product + + return api.get('/api/products-specz/', { params }).then(res => res.data) +} \ No newline at end of file diff --git a/frontend/services/release.js b/frontend/services/release.js new file mode 100644 index 0000000..60cd6be --- /dev/null +++ b/frontend/services/release.js @@ -0,0 +1,6 @@ +/* eslint-disable camelcase */ +import { api } from './api' + +export const getReleases = () => { + return api.get('/api/releases/').then(res => res.data) +} \ No newline at end of file