Skip to content

Commit 1d5e807

Browse files
refactoring downloads
1 parent d2d927e commit 1d5e807

File tree

9 files changed

+245
-63
lines changed

9 files changed

+245
-63
lines changed

backend-app/archive/models.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
DO_NOTHING,
77
DateTimeField,
88
IntegerField,
9-
JSONField
9+
JSONField,
10+
TextField
1011
)
1112

1213

@@ -16,6 +17,7 @@ class Archive(Model):
1617
id = CharField(max_length=36, primary_key=True)
1718
result_id = CharField(max_length=24, null=True)
1819
result_size = IntegerField(null=True)
20+
result_message = TextField(null=True)
1921
status = CharField(max_length=1, choices=STATUSES, default="p")
2022
filters = JSONField()
2123
create_date = DateTimeField(auto_now_add=True)

backend-app/archive/serializers.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from rest_framework.serializers import ModelSerializer, ChoiceField
1+
from rest_framework.serializers import ModelSerializer, ChoiceField, SerializerMethodField
22
from .models import Archive
33

44

@@ -8,7 +8,10 @@ def to_representation(self, value: str) -> str: return self.choices[value]
88

99
class ArchiveSerializer(ModelSerializer):
1010
status = StatusChoiceField(choices=Archive.STATUSES)
11+
author = SerializerMethodField()
1112

1213
class Meta:
1314
model = Archive
14-
fields = ("id", "result_id", "result_size", "status",)
15+
exclude = ("file", "project")
16+
17+
def get_author(self, instance: Archive): return instance.author.username

backend-app/archive/services.py

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from django.db import transaction
1717

1818

19-
@with_model_assertion(Project, "id", filter={"visible": True},)
19+
@with_model_assertion(Project, "id", filter={"visible": True}, class_based=False)
2020
def _get_archives(project: Project) -> tuple[dict[str, Any], int]:
2121
query = project.archive_set \
2222
.annotate(file_count=Count("file")) \
@@ -26,34 +26,43 @@ def _get_archives(project: Project) -> tuple[dict[str, Any], int]:
2626

2727
@with_model_assertion(Project, "id", filter={"visible": True}, class_based=False)
2828
def _make_archive(project: Project, request: Request) -> tuple[dict[str, Any], int]:
29-
request_data: dict[str, Any] = request.data
29+
request_query = type(
30+
"query",
31+
(object,),
32+
{
33+
"get": lambda *a: request.data.get(a[0], a[1] if len(a) > 1 else None),
34+
"getlist": lambda *a: request.data.get(a[0], a[1] if len(a) > 1 else []),
35+
}
36+
)
3037

3138
attributes = project.attribute_set \
32-
.filter(id__in=request_data.get("attr", [])) \
39+
.filter(id__in=request_query.get("attr", [])) \
3340
.order_by("level__order", "id") \
3441
.values("name") \
3542
.values_list("name", flat=True)
3643

3744
filter_data = {
38-
"status": request_data.get("card", []),
39-
"only_new": request_data.get("downloaded", False),
40-
"type": request_data.get("type", []),
45+
"status": request_query.get("card", []),
46+
"only_new": request_query.get("downloaded", False),
47+
"type": request_query.get("type", []),
4148
"attributes": list(attributes)
4249
}
4350

44-
files, _ = FileService()._get_files(project.id, request.user, request_data, True)
51+
files, _ = FileService()._get_files(project.id, request.user, request_query, True)
4552

46-
response = post(
47-
"url",
48-
headers={
49-
"Authorization": "Bearer " + request.user._make_token(),
50-
"Content-Type": "application/json",
51-
},
52-
json=FileSerializer(files, many=True).data
53-
)
53+
# response = post(
54+
# "url",
55+
# headers={
56+
# "Authorization": "Bearer " + request.user._make_token(),
57+
# "Content-Type": "application/json",
58+
# },
59+
# json=FileSerializer(files, many=True).data
60+
# )
5461

55-
assert response.status_code == 201
56-
assert (task_id := response.json().get("task_id"))
62+
# assert response.status_code == 201
63+
# assert (task_id := response.json().get("task_id"))
64+
from secrets import token_hex
65+
task_id = token_hex(16)
5766

5867
with transaction.atomic():
5968
archive = project.archive_set.create(
@@ -63,6 +72,6 @@ def _make_archive(project: Project, request: Request) -> tuple[dict[str, Any], i
6372
)
6473

6574
files.update(is_downloaded=True)
66-
archive.add(*files)
75+
archive.file.add(*files)
6776

6877
return ArchiveSerializer(archive).data, 201

backend-app/proj_back/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@
136136
DATA_UPLOAD_MAX_NUMBER_FIELDS = 1000
137137

138138
REST_FRAMEWORK = {
139+
'DATETIME_FORMAT': '%Y-%m-%d %H:%M',
139140
'DEFAULT_PERMISSION_CLASSES': (
140141
'rest_framework.permissions.IsAuthenticated',
141142
),

frontend-app/src/adapters/index.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,51 @@
11
import { deepCopy, extractFileMeta, formUID } from "../utils";
22

3+
/**
4+
* @param {object} data
5+
* @returns {string}
6+
*/
7+
export const formatFilterJSON = (data) => {
8+
var res = "";
9+
10+
for (let key in data) {
11+
var val = data[key];
12+
var skipKey = false;
13+
var addValue;
14+
15+
switch (typeof val) {
16+
case "boolean": {
17+
skipKey = !val;
18+
break;
19+
}
20+
case "object": {
21+
if (Array.isArray(val)) {
22+
if (val.length) {
23+
addValue = "[";
24+
for (let v of val) { addValue += v; addValue += ", "; }
25+
addValue = addValue.slice(0, addValue.length - 2);
26+
addValue += "]";
27+
}
28+
else addValue = "all";
29+
}
30+
else addValue = formatFilterJSON(val);
31+
break;
32+
}
33+
default: addValue = val;
34+
}
35+
36+
if (!skipKey) {
37+
res += key;
38+
res += ": ";
39+
if (addValue) {
40+
res += addValue;
41+
res += ", ";
42+
}
43+
}
44+
}
45+
46+
return res.slice(0, res.length - 2);
47+
};
48+
349
/**
450
* @param {File[]} files
551
* @param {object} groups
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { ReactElement } from "react";
2+
import { formatFilterJSON } from "../../adapters";
3+
4+
/**
5+
* @param {object} props
6+
* @param {object[]} props.data
7+
* @param {Function?} props.onDownload
8+
* @returns {ReactElement}
9+
*/
10+
export default function DownloadTable({ data, onDownload }) {
11+
return <table className="downloads__table">
12+
<thead>
13+
<tr>
14+
<th>ID</th>
15+
<th>author</th>
16+
<th>status</th>
17+
<th>create date</th>
18+
<th>result size</th>
19+
<th>result message</th>
20+
<th>requested</th>
21+
<th />
22+
</tr>
23+
</thead>
24+
<tbody>
25+
{
26+
data.map((item) => (
27+
<tr key={item.id}>
28+
<td>{item.id}</td>
29+
<td>{item.author}</td>
30+
<td>{item.status.toLowerCase()}</td>
31+
<td>{item.create_date}</td>
32+
<td>{item.result_size}</td>
33+
<td>{item.result_message}</td>
34+
<td>{formatFilterJSON(item.filters)}</td>
35+
<td onClick={() => onDownload(item.id)} className="downloads__table__button">
36+
<button type="button">
37+
<svg width="24" height="24" viewBox="0 0 24 24">
38+
<path d="M14.14 13H13V3a1 1 0 0 0-2 0v10H9.86a1 1 0 0 0-.69 1.5l2.14 3.12a.82.82 0 0 0 1.38 0l2.14-3.12a1 1 0 0 0-.69-1.5" />
39+
<path d="M19 22H5a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h3a1 1 0 0 1 0 2H5v11h14V9h-3a1 1 0 0 1 0-2h3a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2" />
40+
</svg>
41+
</button>
42+
</td>
43+
</tr>
44+
))
45+
}
46+
</tbody>
47+
</table>;
48+
}

frontend-app/src/components/FilesDownload/index.jsx

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { useState, ReactElement, useMemo } from "react";
1+
import { useState, ReactElement, useMemo, useRef, useEffect } from "react";
22
import { useNavigate } from "react-router-dom";
33
import { api } from "../../config/api";
44
import { useDispatch } from "react-redux";
55
import { addAlert } from "../../slices/alerts";
66
import Load from "../ui/Load";
77
import ValidationFilterGroup from '../forms/ValidationFilterGroup';
8+
import DownloadTable from "./DownloadTable";
89
import "./styles.css";
910

1011
/** @type {{name: string, id: string}[]} */
@@ -27,6 +28,8 @@ const TYPE_FILTER = [
2728
export default function FilesDownload({ pathID, attributes }) {
2829
const [filterData, setFilterData] = useState({});
2930
const [loading, setLoading] = useState(false);
31+
const [downloads, setDownloads] = useState([]);
32+
const newCheckBox = useRef(null);
3033
const dispatch = useDispatch();
3134
const navigate = useNavigate();
3235
const filterFields = useMemo(() => [
@@ -51,15 +54,12 @@ export default function FilesDownload({ pathID, attributes }) {
5154
const handleChange = (type, data) =>
5255
setFilterData((prev) => ({ ...prev, [type]: data }));
5356

54-
const createTask = async (event) => {
55-
event.preventDefault();
56-
var isNew = event.target.newFiles.checked;
57-
57+
const createTask = async () => {
5858
setLoading(true);
5959

6060
try {
6161
var payload = { ...filterData };
62-
if (isNew) payload.downloaded = false;
62+
if (newCheckBox.current.checked) payload.downloaded = false;
6363

6464
await api.post(`/api/projects/archive/${pathID}/`, payload, {
6565
headers: { Authorization: "Bearer " + localStorage.getItem("dtcAccess") },
@@ -82,29 +82,60 @@ export default function FilesDownload({ pathID, attributes }) {
8282
setLoading(false);
8383
};
8484

85-
return (
86-
<div className="iss__filesDownload">
87-
<h2 className="iss__filesDownload__caption">
88-
Choose files to download or enter existing taskID
89-
</h2>
85+
const getDownloads = async () => {
86+
try {
87+
const { data } = await api.get(`/api/projects/archive/${pathID}/`, {
88+
headers: { Authorization: "Bearer " + localStorage.getItem("dtcAccess") },
89+
});
90+
91+
setDownloads(data);
92+
}
93+
catch ({ message, response }) {
94+
var authFailed = response && (
95+
response.status === 401 || response.status === 403
96+
);
97+
98+
dispatch(addAlert({
99+
message: "Getting files data error:" + message,
100+
type: "error",
101+
noSession: authFailed
102+
}));
103+
104+
if (authFailed) navigate("/login");
105+
}
106+
};
107+
108+
useEffect(() => {
109+
getDownloads();
110+
}, []);
111+
112+
return <>
113+
{/* todo: supposed to be a form element but val filter group has it inside */}
114+
<div className="downloads">
90115
<ValidationFilterGroup
91116
disabled={false}
92117
filterData={filterFields}
93118
onChange={handleChange}
94119
/>
95-
<form onSubmit={createTask} className="iss__filesDownload__form">
96-
<fieldset className="iss__filesDownload__mainSet">
97-
<label className="iss__filesDownload__inputBox__wrap">
98-
<input name="newFiles" type="checkbox" />
99-
<span>not downloaded before</span>
100-
</label>
101-
</fieldset>
102-
<button
103-
className={
104-
`iss__filesDownload__button${ loading ? " block--button" : "" }`
105-
}
106-
>{loading ? <Load isInline /> : <span>request</span>}</button>
107-
</form>
120+
<fieldset className="downloads__mainSet">
121+
<label className="downloads__inputBox__wrap">
122+
<input ref={newCheckBox} name="newFiles" type="checkbox" />
123+
<span>not downloaded before</span>
124+
</label>
125+
</fieldset>
126+
<button
127+
type="button"
128+
onClick={createTask}
129+
className={
130+
`downloads__button${ loading ? " block--button" : "" }`
131+
}
132+
>{loading ? <Load isInline /> : <span>request</span>}</button>
108133
</div>
109-
);
134+
{
135+
downloads.length
136+
? <DownloadTable data={downloads} onDownload={(id) => { console.log(id); }}/>
137+
: "No Data"
138+
}
139+
140+
</>;
110141
}

0 commit comments

Comments
 (0)