Skip to content

Commit

Permalink
Merge pull request #207 from linea-it/205-update-product
Browse files Browse the repository at this point in the history
Update product metadata and Auxiliary files.
  • Loading branch information
gschwend authored Nov 29, 2023
2 parents 80fa8d3 + 454fd18 commit 3b1f5d1
Show file tree
Hide file tree
Showing 15 changed files with 611 additions and 35 deletions.
3 changes: 2 additions & 1 deletion backend/core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class ProductAdmin(admin.ModelAdmin):
"official_product",
"pz_code",
"created_at",
"updated_at",
"status",
)

Expand Down Expand Up @@ -69,7 +70,7 @@ def has_delete_permission(self, request, obj=None):
@admin.register(ProductFile)
class ProductFileAdmin(admin.ModelAdmin):
list_display = ("id", "product", "file", "role",
"type", "size", "extension")
"type", "size", "extension", "created", "updated")

def has_add_permission(self, request):
return False
Expand Down
18 changes: 18 additions & 0 deletions backend/core/migrations/0027_product_updated_at.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.0.2 on 2023-11-09 13:31

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('core', '0026_productcontent_alias'),
]

operations = [
migrations.AddField(
model_name='product',
name='updated_at',
field=models.DateTimeField(auto_now=True),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 4.0.2 on 2023-11-09 13:45

from django.db import migrations, models
import django.utils.timezone


class Migration(migrations.Migration):

dependencies = [
('core', '0027_product_updated_at'),
]

operations = [
migrations.AddField(
model_name='productfile',
name='created',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='productfile',
name='updated',
field=models.DateTimeField(auto_now=True),
),
]
6 changes: 6 additions & 0 deletions backend/core/models/product.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class Product(models.Model):
pz_code = models.CharField(max_length=55, null=True, blank=True)
description = models.TextField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
status = models.IntegerField(
verbose_name="Status",
default=ProductStatus.REGISTERING,
Expand Down Expand Up @@ -70,3 +71,8 @@ def can_delete(self, user) -> bool:
if self.user.id == user.id or user.profile.is_admin():
return True
return False

def can_update(self, user) -> bool:
if self.user.id == user.id or user.profile.is_admin():
return True
return False
5 changes: 4 additions & 1 deletion backend/core/models/product_file.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os

from core.models import Product
from django.db import models
import os


def upload_product_files(instance, filename):
Expand Down Expand Up @@ -34,6 +35,8 @@ class ProductFile(models.Model):
extension = models.CharField(
verbose_name="Extension", max_length=10, null=True, blank=True
)
created = models.DateTimeField(auto_now_add=True, blank=True)
updated = models.DateTimeField(auto_now=True)

def __str__(self):
return f"{self.product.display_name} - {os.path.basename(self.file.name)}"
Expand Down
8 changes: 7 additions & 1 deletion backend/core/serializers/product.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ class ProductSerializer(serializers.ModelSerializer):

can_delete = serializers.SerializerMethodField()

can_update = serializers.SerializerMethodField()

class Meta:
model = Product
read_only_fields = ("internal_name", "is_owner")
Expand All @@ -48,4 +50,8 @@ def get_is_owner(self, obj):

def get_can_delete(self, obj):
current_user = self.context["request"].user
return obj.can_delete(current_user)
return obj.can_delete(current_user)

def get_can_update(self, obj):
current_user = self.context["request"].user
return obj.can_update(current_user)
2 changes: 2 additions & 0 deletions backend/core/test/test_product.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,13 +289,15 @@ def test_product_serialized_format(self):
"uploaded_by": self.user.username,
"is_owner": True,
"can_delete": True,
"can_update": True,
"internal_name": self.product.internal_name,
"display_name": self.product.display_name,
"official_product": self.product.official_product,
"pz_code": self.product.pz_code,
"description": self.product.description,
"created_at": self.product.created_at.strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
"status": self.product.status,
"updated_at": self.product.updated_at.strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
}

response = self.client.get(self.url)
Expand Down
5 changes: 4 additions & 1 deletion backend/core/test/test_product_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
from django.contrib.auth.models import User
from django.urls import reverse
from rest_framework.authtoken.models import Token
from rest_framework.test import APIRequestFactory, APITestCase, force_authenticate
from rest_framework.test import (APIRequestFactory, APITestCase,
force_authenticate)


class ProductFileListCreateAPIViewTestCase(APITestCase):
Expand Down Expand Up @@ -218,6 +219,8 @@ def test_product_file_serialized_format(self):
],
"size": self.product_file.file.size,
"extension": os.path.splitext(self.product_file.file.name)[1],
"created": self.product_file.created.strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
"updated": self.product_file.updated.strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
}

response = self.client.get(self.url)
Expand Down
4 changes: 2 additions & 2 deletions frontend/components/FileUploader.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react'
import { Input, Button } from '@mui/material'
import { Button, Input } from '@mui/material'
import prettyBytes from 'pretty-bytes'
import PropTypes from 'prop-types'
import React from 'react'

export default function FileUploader(props) {
const {
Expand Down
21 changes: 16 additions & 5 deletions frontend/components/ProductDetail.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ import ShareIcon from '@mui/icons-material/Share'
import VerifiedIcon from '@mui/icons-material/Verified'
import LoadingButton from '@mui/lab/LoadingButton'
import {
Tabs,
Tab,
Box,
Card,
CardContent,
CardMedia,
Chip,
Divider,
Grid,
Expand All @@ -17,13 +16,16 @@ import {
Paper,
Snackbar,
Stack,
Typography,
CardMedia
Tab,
Tabs,
Typography
} from '@mui/material'
import Alert from '@mui/material/Alert'
import ProductShare from './ProductShare'

import EditIcon from '@mui/icons-material/Edit'
import moment from 'moment'
import { useRouter } from 'next/router'
import prettyBytes from 'pretty-bytes'
import PropTypes from 'prop-types'
import React from 'react'
Expand All @@ -37,8 +39,8 @@ import {
getProducts
} from '../services/product'
import useStyles from '../styles/pages/product'

export default function ProductDetail({ productId, internalName }) {
const router = useRouter()
const classes = useStyles()

const [product, setProduct] = React.useState(null)
Expand Down Expand Up @@ -170,6 +172,10 @@ export default function ProductDetail({ productId, internalName }) {
})
}

const handleEdit = row => {
router.push(`/product/edit/${product.internal_name}`)
}

const createFileFields = file => {
// Se o nome do arquivo for grande,
// exibe só os primeiros caracteres + extensao.
Expand Down Expand Up @@ -250,6 +256,11 @@ export default function ProductDetail({ productId, internalName }) {
<ShareIcon />
</IconButton>
)}
{product.can_update === true && (
<IconButton onClick={handleEdit}>
<EditIcon />
</IconButton>
)}
{product.official_product === true && (
<Chip
variant="outlined"
Expand Down
82 changes: 82 additions & 0 deletions frontend/components/ProductFileTextField.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import CloseIcon from '@mui/icons-material/Close'
import FormGroup from '@mui/material/FormGroup'
import IconButton from '@mui/material/IconButton'
import InputAdornment from '@mui/material/InputAdornment'
import TextField from '@mui/material/TextField'
import prettyBytes from 'pretty-bytes'
import PropTypes from 'prop-types'
import React from 'react'
import { deleteProductFile } from '../services/product'

export default function ProductFileTextField(props) {
const { id, role, name, size, readOnly, onDelete } = props

const handleRemoveFile = () => {
deleteProductFile(id)
.then(() => {
// Forcar um reload dos arquivos
onDelete(id)
})
.catch(res => {
if (res.response.status === 500) {
// TODO: Tratamento erro no backend
}
})
}

const getLabelByRole = role => {
let label = ''
switch (role) {
case 0:
label = 'Main File'
break
case 1:
label = 'Description File'
break
case 2:
label = 'Auxiliary File'
break
}
return label
}

return (
<FormGroup row key={`display_file_${id}`}>
<TextField
value={name}
label={getLabelByRole(role)}
readOnly={true}
fullWidth
InputProps={
readOnly === false
? {
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={handleRemoveFile}
// onMouseDown={handleMouseDownPassword}
edge="end"
>
<CloseIcon />
</IconButton>
</InputAdornment>
)
}
: {}
}
helperText={prettyBytes(Number(size))}
/>
</FormGroup>
)
}
ProductFileTextField.propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
size: PropTypes.number.isRequired,
role: PropTypes.number.isRequired,
onDelete: PropTypes.func.isRequired,
readOnly: PropTypes.bool
}
ProductFileTextField.defaultProps = {
readOnly: false
}
30 changes: 29 additions & 1 deletion frontend/components/ProductGrid.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
/* eslint-disable multiline-ternary */
import DeleteIcon from '@mui/icons-material/Delete'
import DownloadIcon from '@mui/icons-material/Download'
import EditIcon from '@mui/icons-material/Edit'
import ShareIcon from '@mui/icons-material/Share'
import Alert from '@mui/material/Alert'
import Tooltip from '@mui/material/Tooltip'
import Link from '@mui/material/Link'
import Snackbar from '@mui/material/Snackbar'
import Tooltip from '@mui/material/Tooltip'
import { DataGrid, GridActionsCellItem } from '@mui/x-data-grid'
import moment from 'moment'
import { useRouter } from 'next/router'
Expand Down Expand Up @@ -85,6 +86,10 @@ export default function ProductGrid(props) {
setSnackbarOpen(true)
}

const handleEdit = row => {
router.push(`/product/edit/${row.internal_name}`)
}

return [
// Hide Id Column ISSUE #123
// { field: 'id', headerName: 'ID', width: 90, sortable: true },
Expand Down Expand Up @@ -175,6 +180,29 @@ export default function ProductGrid(props) {
</div>
</Tooltip>
)
},
{
field: 'can_update',
headerName: 'Edit',
width: 120,
sortable: false,
renderCell: params => (
<Tooltip
title={
!params.row.can_update
? 'You cannot update this data product because it belongs to another user.'
: 'Edit this data product.'
}
>
<div>
<GridActionsCellItem
icon={<EditIcon />}
onClick={() => handleEdit(params.row)}
disabled={!params.row.can_update}
/>
</div>
</Tooltip>
)
}
]
}, [getProductUrl, router])
Expand Down
Loading

0 comments on commit 3b1f5d1

Please sign in to comment.