Skip to content

Commit 6565608

Browse files
Merge branch 'develop' into generate_qr
2 parents fb31953 + 640be5e commit 6565608

File tree

11 files changed

+600
-68
lines changed

11 files changed

+600
-68
lines changed

.github/ISSUE_TEMPLATE/bug.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: Bug report
2+
description: Report a problem or unexpected behavior
3+
title: "[Bug]: "
4+
labels: ["bug"]
5+
body:
6+
- type: textarea
7+
id: description
8+
attributes:
9+
label: Description
10+
description: What happened?
11+
placeholder: Tell us what you saw.
12+
validations:
13+
required: true
14+
15+
- type: textarea
16+
id: steps
17+
attributes:
18+
label: Steps to reproduce
19+
placeholder: |
20+
1. ...
21+
2. ...
22+
3. ...
23+
validations:
24+
required: true
25+
26+
- type: textarea
27+
id: expected
28+
attributes:
29+
label: Expected behavior
30+
placeholder: What should have happened?
31+
32+
- type: input
33+
id: environment
34+
attributes:
35+
label: Environment
36+
placeholder: OS, version, browser, etc.

.github/ISSUE_TEMPLATE/config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
blank_issues_enabled: false

.github/ISSUE_TEMPLATE/feature.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: Feature request
2+
description: Suggest an improvement or new feature
3+
title: "[Feature]: "
4+
labels: ["enhancement"]
5+
body:
6+
- type: textarea
7+
id: problem
8+
attributes:
9+
label: Problem
10+
description: What problem are you trying to solve?
11+
validations:
12+
required: true
13+
14+
- type: textarea
15+
id: proposal
16+
attributes:
17+
label: Proposed solution
18+
description: What do you want to happen?
19+
validations:
20+
required: true
21+
22+
- type: textarea
23+
id: context
24+
attributes:
25+
label: Additional context
26+
description: Links, screenshots, alternatives considered, etc.

app/modules/dataset/routes.py

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,13 @@ def list_dataset():
190190
@dataset_bp.route("/datasets/download/<int:dataset_id>", methods=["GET"])
191191
def download_dataset(dataset_id):
192192
dataset = dataset_service.get_or_404(dataset_id)
193+
selected_formats = request.args.getlist("formats")
194+
selected_formats = selected_formats if selected_formats else None
193195

194-
zip_path = dataset_service.zip_from_storage(dataset)
196+
try:
197+
zip_path = dataset_service.zip_from_storage(dataset, formats=selected_formats)
198+
except ValueError as exc:
199+
abort(400, description=str(exc))
195200

196201
if not zip_path or not os.path.exists(zip_path):
197202
abort(404, description="ZIP file not found.")
@@ -293,20 +298,25 @@ def dataset_qr_by_doi(doi):
293298

294299
@dataset_bp.route("/datasets/download/all", methods=["GET"])
295300
def download_all_dataset():
301+
selected_formats = request.args.getlist("formats")
302+
selected_formats = selected_formats if selected_formats else None
303+
296304
# Crear un directorio temporal
297305
temp_dir = tempfile.mkdtemp()
298306
zip_path = os.path.join(temp_dir, "all_datasets.zip")
299307

300308
try:
301309
# Generar el archivo ZIP
302-
dataset_service.zip_all_datasets(zip_path)
310+
dataset_service.zip_all_datasets_by_formats(zip_path, formats=selected_formats)
303311

304312
# Crear el nombre del archivo con la fecha
305313
current_date = datetime.now().strftime("%Y_%m_%d")
306314
zip_filename = f"uvlhub_bulk_{current_date}.zip"
307315

308316
# Enviar el archivo como respuesta
309317
return send_file(zip_path, as_attachment=True, download_name=zip_filename)
318+
except ValueError as exc:
319+
abort(400, description=str(exc))
310320
finally:
311321
# Asegurar que la carpeta temporal se elimine después de que Flask sirva el archivo
312322
if os.path.exists(temp_dir):
@@ -343,6 +353,56 @@ def subdomain_index(doi):
343353
return resp
344354

345355

356+
@dataset_bp.route("/doi/<path:doi>/files/raw/<path:filename>", methods=["GET"])
357+
@dataset_bp.route("/doi/<path:doi>/files/raw/<path:filename>/", methods=["GET"])
358+
def doi_file_raw(doi, filename):
359+
360+
new_doi = doi_mapping_service.get_new_doi(doi)
361+
if new_doi:
362+
return redirect(
363+
url_for("dataset.doi_file_raw", doi=new_doi, filename=filename),
364+
code=302,
365+
)
366+
367+
ds_meta_data = dsmetadata_service.filter_by_doi(doi)
368+
if not ds_meta_data:
369+
abort(404)
370+
371+
dataset = ds_meta_data.dataset
372+
373+
selected_file = None
374+
for fm in dataset.feature_models:
375+
for hf in fm.hubfiles:
376+
if hf.name == filename:
377+
selected_file = hf
378+
break
379+
if selected_file:
380+
break
381+
382+
if not selected_file:
383+
abort(404, description="File not found in this DOI dataset")
384+
385+
file_path = os.path.join(
386+
current_app.root_path,
387+
"..",
388+
"uploads",
389+
f"user_{dataset.user_id}",
390+
f"dataset_{dataset.id}",
391+
"uvl",
392+
selected_file.name,
393+
)
394+
395+
if not os.path.exists(file_path):
396+
abort(404, description="File missing on disk")
397+
398+
return send_file(
399+
file_path,
400+
mimetype="text/plain; charset=utf-8",
401+
as_attachment=False,
402+
download_name=selected_file.name,
403+
)
404+
405+
346406
@dataset_bp.route("/datasets/unsynchronized/<int:dataset_id>/", methods=["GET"])
347407
@login_required
348408
@is_dataset_owner

app/modules/dataset/services.py

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ def calculate_checksum_and_size(file_path):
5757

5858

5959
class DataSetService(BaseService):
60+
AVAILABLE_DOWNLOAD_FORMATS = ("uvl", "glencoe", "dimacs", "splot")
61+
6062
def __init__(self):
6163
super().__init__(DataSetRepository())
6264
self.feature_model_repository = FeatureModelRepository()
@@ -505,7 +507,26 @@ def zip_dataset(self, dataset: DataSet) -> str:
505507

506508
return zip_path
507509

510+
def resolve_download_formats(self, formats: Optional[List[str]]) -> List[str]:
511+
if formats is None:
512+
return list(self.AVAILABLE_DOWNLOAD_FORMATS)
513+
514+
normalized_formats = [fmt.strip().lower() for fmt in formats if fmt and fmt.strip()]
515+
invalid_formats = sorted(set(normalized_formats) - set(self.AVAILABLE_DOWNLOAD_FORMATS))
516+
if invalid_formats:
517+
raise ValueError(f"Invalid format(s): {', '.join(invalid_formats)}")
518+
519+
selected_formats = [fmt for fmt in self.AVAILABLE_DOWNLOAD_FORMATS if fmt in normalized_formats]
520+
if not selected_formats:
521+
raise ValueError("No download formats selected.")
522+
523+
return selected_formats
524+
508525
def zip_all_datasets(self, zip_path: str):
526+
self.zip_all_datasets_by_formats(zip_path, self.AVAILABLE_DOWNLOAD_FORMATS)
527+
528+
def zip_all_datasets_by_formats(self, zip_path: str, formats: Optional[List[str]] = None):
529+
selected_formats = self.resolve_download_formats(formats)
509530
with ZipFile(zip_path, "w") as zipf:
510531
for user_dir in os.listdir("uploads"):
511532
user_path = os.path.join("uploads", user_dir)
@@ -518,37 +539,46 @@ def zip_all_datasets(self, zip_path: str):
518539
dataset_id = int(dataset_dir.split("_")[1])
519540

520541
if self.is_synchronized(dataset_id):
521-
for subdir, dirs, files in os.walk(dataset_path):
522-
for file in files:
523-
full_path = os.path.join(subdir, file)
524-
525-
relative_path = os.path.relpath(full_path, dataset_path)
526-
zipf.write(
527-
full_path,
528-
arcname=os.path.join(dataset_dir, relative_path),
529-
)
530-
531-
def zip_from_storage(self, dataset):
532-
dataset_folder = os.path.join(
533-
os.getenv("WORKING_DIR", ""),
534-
"uploads",
535-
f"user_{dataset.user_id}",
536-
f"dataset_{dataset.id}",
537-
"uvl",
542+
for fmt in selected_formats:
543+
format_path = os.path.join(dataset_path, fmt)
544+
if not os.path.isdir(format_path):
545+
continue
546+
547+
for subdir, _, files in os.walk(format_path):
548+
for file in files:
549+
full_path = os.path.join(subdir, file)
550+
relative_path = os.path.relpath(full_path, format_path)
551+
zipf.write(
552+
full_path,
553+
arcname=os.path.join(dataset_dir, fmt, relative_path),
554+
)
555+
556+
def zip_from_storage(self, dataset, formats: Optional[List[str]] = None):
557+
dataset_base_path = os.path.join(
558+
os.getenv("WORKING_DIR", ""), "uploads", f"user_{dataset.user_id}", f"dataset_{dataset.id}"
538559
)
539-
540-
if not os.path.exists(dataset_folder):
541-
current_app.logger.warning(f"[ZIP] Dataset folder not found: {dataset_folder}")
542-
return None # Lo manejarás con abort(404) fuera
560+
selected_formats = self.resolve_download_formats(formats)
543561

544562
temp_dir = tempfile.mkdtemp()
545563
zip_path = os.path.join(temp_dir, f"dataset_{dataset.id}.zip")
564+
written_files = 0
546565

547566
with zipfile.ZipFile(zip_path, "w") as zipf:
548-
for filename in os.listdir(dataset_folder):
549-
file_path = os.path.join(dataset_folder, filename)
550-
arcname = filename
551-
zipf.write(file_path, arcname=arcname)
567+
for fmt in selected_formats:
568+
format_path = os.path.join(dataset_base_path, fmt)
569+
if not os.path.isdir(format_path):
570+
continue
571+
572+
for subdir, _, files in os.walk(format_path):
573+
for filename in files:
574+
file_path = os.path.join(subdir, filename)
575+
arcname = os.path.join(fmt, os.path.relpath(file_path, format_path))
576+
zipf.write(file_path, arcname=arcname)
577+
written_files += 1
578+
579+
if written_files == 0:
580+
current_app.logger.warning(f"[ZIP] No files found for selected formats in dataset {dataset.id}")
581+
return None
552582

553583
return zip_path
554584

app/modules/dataset/templates/dataset/list_datasets.html

Lines changed: 90 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,51 @@ <h3 class="card-title">Synchronized datasets</h3>
6363
<span class="path2"></span>
6464
</i>
6565
</a>
66-
<a href="{{ url_for('dataset.download_dataset', dataset_id=dataset.id) }}" class="btn btn-sm btn-icon btn-light-success" title="Download">
67-
<i class="ki-duotone ki-folder-down fs-2">
68-
<span class="path1"></span>
69-
<span class="path2"></span>
70-
</i>
71-
</a>
66+
<div class="position-relative d-inline-block">
67+
<button class="btn btn-sm btn-icon btn-light-success"
68+
title="Download"
69+
data-kt-menu-trigger="click"
70+
data-kt-menu-placement="bottom-end"
71+
data-kt-menu-attach="parent">
72+
<i class="ki-duotone ki-folder-down fs-2">
73+
<span class="path1"></span>
74+
<span class="path2"></span>
75+
</i>
76+
</button>
77+
<div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-title-gray-700 menu-icon-gray-500 fw-semibold py-4 px-4 fs-base w-200px" data-kt-menu="true" data-download-menu>
78+
<div class="menu-item px-0 py-1">
79+
<label class="form-check form-check-custom form-check-solid align-items-center m-0">
80+
<input class="form-check-input me-3 download-format-checkbox" type="checkbox" value="uvl" checked />
81+
<span class="menu-title">UVL</span>
82+
</label>
83+
</div>
84+
<div class="menu-item px-0 py-1">
85+
<label class="form-check form-check-custom form-check-solid align-items-center m-0">
86+
<input class="form-check-input me-3 download-format-checkbox" type="checkbox" value="glencoe" checked />
87+
<span class="menu-title">Glencoe</span>
88+
</label>
89+
</div>
90+
<div class="menu-item px-0 py-1">
91+
<label class="form-check form-check-custom form-check-solid align-items-center m-0">
92+
<input class="form-check-input me-3 download-format-checkbox" type="checkbox" value="dimacs" checked />
93+
<span class="menu-title">DIMACS</span>
94+
</label>
95+
</div>
96+
<div class="menu-item px-0 py-1">
97+
<label class="form-check form-check-custom form-check-solid align-items-center m-0">
98+
<input class="form-check-input me-3 download-format-checkbox" type="checkbox" value="splot" checked />
99+
<span class="menu-title">SPLOT</span>
100+
</label>
101+
</div>
102+
<div class="separator my-3"></div>
103+
<button type="button"
104+
class="btn btn-light-primary btn-sm w-100"
105+
data-download-formats-action
106+
data-download-base-url="{{ url_for('dataset.download_dataset', dataset_id=dataset.id) }}">
107+
Download
108+
</button>
109+
</div>
110+
</div>
72111
</td>
73112
</tr>
74113
{% endfor %}
@@ -119,12 +158,51 @@ <h3 class="card-title">Unsynchronized datasets</h3>
119158
<span class="path3"></span>
120159
</i></i>
121160
</a>
122-
<a href="{{ url_for('dataset.download_dataset', dataset_id=dataset.id) }}" class="btn btn-sm btn-icon btn-light-success" title="Download">
123-
<i class="ki-duotone ki-folder-down fs-2">
124-
<span class="path1"></span>
125-
<span class="path2"></span>
126-
</i>
127-
</a>
161+
<div class="position-relative d-inline-block">
162+
<button class="btn btn-sm btn-icon btn-light-success"
163+
title="Download"
164+
data-kt-menu-trigger="click"
165+
data-kt-menu-placement="bottom-end"
166+
data-kt-menu-attach="parent">
167+
<i class="ki-duotone ki-folder-down fs-2">
168+
<span class="path1"></span>
169+
<span class="path2"></span>
170+
</i>
171+
</button>
172+
<div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-title-gray-700 menu-icon-gray-500 fw-semibold py-4 px-4 fs-base w-200px" data-kt-menu="true" data-download-menu>
173+
<div class="menu-item px-0 py-1">
174+
<label class="form-check form-check-custom form-check-solid align-items-center m-0">
175+
<input class="form-check-input me-3 download-format-checkbox" type="checkbox" value="uvl" checked />
176+
<span class="menu-title">UVL</span>
177+
</label>
178+
</div>
179+
<div class="menu-item px-0 py-1">
180+
<label class="form-check form-check-custom form-check-solid align-items-center m-0">
181+
<input class="form-check-input me-3 download-format-checkbox" type="checkbox" value="glencoe" checked />
182+
<span class="menu-title">Glencoe</span>
183+
</label>
184+
</div>
185+
<div class="menu-item px-0 py-1">
186+
<label class="form-check form-check-custom form-check-solid align-items-center m-0">
187+
<input class="form-check-input me-3 download-format-checkbox" type="checkbox" value="dimacs" checked />
188+
<span class="menu-title">DIMACS</span>
189+
</label>
190+
</div>
191+
<div class="menu-item px-0 py-1">
192+
<label class="form-check form-check-custom form-check-solid align-items-center m-0">
193+
<input class="form-check-input me-3 download-format-checkbox" type="checkbox" value="splot" checked />
194+
<span class="menu-title">SPLOT</span>
195+
</label>
196+
</div>
197+
<div class="separator my-3"></div>
198+
<button type="button"
199+
class="btn btn-light-primary btn-sm w-100"
200+
data-download-formats-action
201+
data-download-base-url="{{ url_for('dataset.download_dataset', dataset_id=dataset.id) }}">
202+
Download
203+
</button>
204+
</div>
205+
</div>
128206
<a href="{{ url_for('dataset.edit_metadata', dataset_id=dataset.id) }}"
129207
class="btn btn-sm btn-icon btn-light-primary me-1"
130208
title="Edit Metadata">

0 commit comments

Comments
 (0)