mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-07-10 14:52:42 +02:00
feat(import): more UI and endpoints
This commit is contained in:
@@ -55,4 +55,11 @@ class ImportRunFileUploadForm(forms.Form):
|
|||||||
self.helper = FormHelper()
|
self.helper = FormHelper()
|
||||||
self.helper.form_tag = False
|
self.helper.form_tag = False
|
||||||
self.helper.form_method = "post"
|
self.helper.form_method = "post"
|
||||||
self.helper.layout = Layout("file")
|
self.helper.layout = Layout(
|
||||||
|
"file",
|
||||||
|
FormActions(
|
||||||
|
NoClassSubmit(
|
||||||
|
"submit", _("Import"), css_class="btn btn-outline-primary w-100"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|||||||
@@ -491,7 +491,6 @@ class ImportService:
|
|||||||
|
|
||||||
for row_number, row in enumerate(reader, start=1):
|
for row_number, row in enumerate(reader, start=1):
|
||||||
self._process_row(row, row_number)
|
self._process_row(row, row_number)
|
||||||
self._increment_totals("processed", value=1)
|
|
||||||
|
|
||||||
def _validate_file_path(self, file_path: str) -> str:
|
def _validate_file_path(self, file_path: str) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -518,15 +517,14 @@ class ImportService:
|
|||||||
if self.settings.file_type == "csv":
|
if self.settings.file_type == "csv":
|
||||||
self._process_csv(file_path)
|
self._process_csv(file_path)
|
||||||
|
|
||||||
if self.import_run.processed_rows == self.import_run.total_rows:
|
self._update_status("FINISHED")
|
||||||
self._update_status("FINISHED")
|
self._log(
|
||||||
self._log(
|
"info",
|
||||||
"info",
|
f"Import completed successfully. "
|
||||||
f"Import completed successfully. "
|
f"Successful: {self.import_run.successful_rows}, "
|
||||||
f"Successful: {self.import_run.successful_rows}, "
|
f"Failed: {self.import_run.failed_rows}, "
|
||||||
f"Failed: {self.import_run.failed_rows}, "
|
f"Skipped: {self.import_run.skipped_rows}",
|
||||||
f"Skipped: {self.import_run.skipped_rows}",
|
)
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._update_status("FAILED")
|
self._update_status("FAILED")
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ urlpatterns = [
|
|||||||
views.import_profile_list,
|
views.import_profile_list,
|
||||||
name="import_profiles_list",
|
name="import_profiles_list",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"import/profiles/<int:profile_id>/delete/",
|
||||||
|
views.import_profile_delete,
|
||||||
|
name="import_profile_delete",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"import/profiles/add/",
|
"import/profiles/add/",
|
||||||
views.import_profile_add,
|
views.import_profile_add,
|
||||||
@@ -24,14 +29,19 @@ urlpatterns = [
|
|||||||
name="import_profile_edit",
|
name="import_profile_edit",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"import/profiles/<int:profile_id>/runs/",
|
"import/profiles/<int:profile_id>/runs/list/",
|
||||||
views.import_run_add,
|
views.import_runs_list,
|
||||||
name="import_profile_runs_index",
|
name="import_profile_runs_list",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"import/profiles/<int:profile_id>/runs/list/",
|
"import/profiles/<int:profile_id>/runs/<int:run_id>/log/",
|
||||||
views.import_run_add,
|
views.import_run_log,
|
||||||
name="import_profile_runs_list",
|
name="import_run_log",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"import/profiles/<int:profile_id>/runs/<int:run_id>/delete/",
|
||||||
|
views.import_run_delete,
|
||||||
|
name="import_run_delete",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"import/profiles/<int:profile_id>/runs/add/",
|
"import/profiles/<int:profile_id>/runs/add/",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from django.contrib.auth.decorators import login_required
|
|||||||
from django.core.files.storage import FileSystemStorage
|
from django.core.files.storage import FileSystemStorage
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import render, get_object_or_404
|
from django.shortcuts import render, get_object_or_404
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
@@ -107,11 +108,30 @@ def import_profile_edit(request, profile_id):
|
|||||||
|
|
||||||
@only_htmx
|
@only_htmx
|
||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["GET", "POST"])
|
@csrf_exempt
|
||||||
def import_run_list(request, profile_id):
|
@require_http_methods(["DELETE"])
|
||||||
|
def import_profile_delete(request, profile_id):
|
||||||
profile = ImportProfile.objects.get(id=profile_id)
|
profile = ImportProfile.objects.get(id=profile_id)
|
||||||
|
|
||||||
runs = ImportRun.objects.filter(profile=profile).order_by("id")
|
profile.delete()
|
||||||
|
|
||||||
|
messages.success(request, _("Import Profile deleted successfully"))
|
||||||
|
|
||||||
|
return HttpResponse(
|
||||||
|
status=204,
|
||||||
|
headers={
|
||||||
|
"HX-Trigger": "updated",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@only_htmx
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["GET", "POST"])
|
||||||
|
def import_runs_list(request, profile_id):
|
||||||
|
profile = ImportProfile.objects.get(id=profile_id)
|
||||||
|
|
||||||
|
runs = ImportRun.objects.filter(profile=profile).order_by("-id")
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
@@ -120,6 +140,19 @@ def import_run_list(request, profile_id):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@only_htmx
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["GET", "POST"])
|
||||||
|
def import_run_log(request, profile_id, run_id):
|
||||||
|
run = ImportRun.objects.get(profile__id=profile_id, id=run_id)
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"import_app/fragments/runs/log.html",
|
||||||
|
{"run": run},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@only_htmx
|
@only_htmx
|
||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
@@ -140,6 +173,8 @@ def import_run_add(request, profile_id):
|
|||||||
# Defer the procrastinate task
|
# Defer the procrastinate task
|
||||||
process_import.defer(import_run_id=import_run.id, file_path=file_path)
|
process_import.defer(import_run_id=import_run.id, file_path=file_path)
|
||||||
|
|
||||||
|
messages.success(request, _("Import Run queued successfully"))
|
||||||
|
|
||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
status=204,
|
status=204,
|
||||||
headers={
|
headers={
|
||||||
@@ -154,3 +189,22 @@ def import_run_add(request, profile_id):
|
|||||||
"import_app/fragments/runs/add.html",
|
"import_app/fragments/runs/add.html",
|
||||||
{"form": form, "profile": profile},
|
{"form": form, "profile": profile},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@only_htmx
|
||||||
|
@login_required
|
||||||
|
@csrf_exempt
|
||||||
|
@require_http_methods(["DELETE"])
|
||||||
|
def import_run_delete(request, profile_id, run_id):
|
||||||
|
run = ImportRun.objects.get(profile__id=profile_id, id=run_id)
|
||||||
|
|
||||||
|
run.delete()
|
||||||
|
|
||||||
|
messages.success(request, _("Run deleted successfully"))
|
||||||
|
|
||||||
|
return HttpResponse(
|
||||||
|
status=204,
|
||||||
|
headers={
|
||||||
|
"HX-Trigger": "updated",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|||||||
@@ -38,18 +38,32 @@
|
|||||||
hx-get="{% url 'import_profile_edit' profile_id=profile.id %}"
|
hx-get="{% url 'import_profile_edit' profile_id=profile.id %}"
|
||||||
hx-target="#generic-offcanvas">
|
hx-target="#generic-offcanvas">
|
||||||
<i class="fa-solid fa-pencil fa-fw"></i></a>
|
<i class="fa-solid fa-pencil fa-fw"></i></a>
|
||||||
{# <a class="btn btn-secondary btn-sm text-danger"#}
|
<a class="btn btn-secondary btn-sm text-success"
|
||||||
{# role="button"#}
|
role="button"
|
||||||
{# data-bs-toggle="tooltip"#}
|
data-bs-toggle="tooltip"
|
||||||
{# data-bs-title="{% translate "Delete" %}"#}
|
data-bs-title="{% translate "Runs" %}"
|
||||||
{# hx-delete="{% url 'account_delete' pk=account.id %}"#}
|
hx-get="{% url 'import_profile_runs_list' profile_id=profile.id %}"
|
||||||
{# hx-trigger='confirmed'#}
|
hx-target="#persistent-generic-offcanvas-left">
|
||||||
{# data-bypass-on-ctrl="true"#}
|
<i class="fa-solid fa-person-running fa-fw"></i></a>
|
||||||
{# data-title="{% translate "Are you sure?" %}"#}
|
<a class="btn btn-secondary btn-sm text-primary"
|
||||||
{# data-text="{% translate "You won't be able to revert this!" %}"#}
|
role="button"
|
||||||
{# data-confirm-text="{% translate "Yes, delete it!" %}"#}
|
data-bs-toggle="tooltip"
|
||||||
{# _="install prompt_swal"><i class="fa-solid fa-trash fa-fw"></i></a>#}
|
data-bs-title="{% translate "Import" %}"
|
||||||
{# </div>#}
|
hx-get="{% url 'import_run_add' profile_id=profile.id %}"
|
||||||
|
hx-target="#generic-offcanvas">
|
||||||
|
<i class="fa-solid fa-file-import fa-fw"></i></a>
|
||||||
|
<a class="btn btn-secondary btn-sm text-danger"
|
||||||
|
role="button"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-title="{% translate "Delete" %}"
|
||||||
|
hx-delete="{% url 'import_profile_delete' profile_id=profile.id %}"
|
||||||
|
hx-trigger='confirmed'
|
||||||
|
data-bypass-on-ctrl="true"
|
||||||
|
data-title="{% translate "Are you sure?" %}"
|
||||||
|
data-text="{% translate "You won't be able to revert this!" %}"
|
||||||
|
data-confirm-text="{% translate "Yes, delete it!" %}"
|
||||||
|
_="install prompt_swal"><i class="fa-solid fa-trash fa-fw"></i></a>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="col">{{ profile.name }}</td>
|
<td class="col">{{ profile.name }}</td>
|
||||||
<td class="col">{{ profile.get_version_display }}</td>
|
<td class="col">{{ profile.get_version_display }}</td>
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load crispy_forms_tags %}
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
{% block title %}{% translate 'Import file' %}{% endblock %}
|
{% block title %}{% translate 'Import file with profile' %} {{ profile.name }}{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<form hx-post="{% url 'import_run_add' profile_id=profile.id %}" hx-target="#generic-offcanvas" novalidate>
|
<form hx-post="{% url 'import_run_add' profile_id=profile.id %}" hx-target="#generic-offcanvas" enctype="multipart/form-data" novalidate>
|
||||||
{% crispy form %}
|
{% crispy form %}
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -5,5 +5,116 @@
|
|||||||
{% block title %}{% translate 'Runs for ' %}{{ profile.name }}{% endblock %}
|
{% block title %}{% translate 'Runs for ' %}{{ profile.name }}{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
<div hx-get="{% url "import_profile_runs_list" profile_id=profile.id %}"
|
||||||
|
hx-trigger="updated from:window"
|
||||||
|
hx-target="closest .offcanvas"
|
||||||
|
class="show-loading"
|
||||||
|
hx-swap="show:none scroll:none">
|
||||||
|
{% if runs %}
|
||||||
|
<div class="row row-cols-1 g-4">
|
||||||
|
{% for run in runs %}
|
||||||
|
<div class="col">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header tw-text-sm {% if run.status == run.Status.QUEUED %}tw-text-white{% elif run.status == run.Status.PROCESSING %}text-warning{% elif run.status == run.Status.FINISHED %}text-success{% else %}text-danger{% endif %}">
|
||||||
|
<span><i class="fa-solid {% if run.status == run.Status.QUEUED %}fa-hourglass-half{% elif run.status == run.Status.PROCESSING %}fa-spinner{% elif run.status == run.Status.FINISHED %}fa-check{% else %}fa-xmark{% endif %} fa-fw me-2"></i>{{ run.get_status_display }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title"><i class="fa-solid fa-hashtag me-1 tw-text-xs tw-text-gray-400"></i>{{ run.id }}<span class="tw-text-xs tw-text-gray-400 ms-1">({{ run.file_name }})</span></h5>
|
||||||
|
<hr>
|
||||||
|
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 w-100 g-4">
|
||||||
|
<div class="col">
|
||||||
|
<div class="d-flex flex-row">
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<div class="text-body-secondary tw-text-xs tw-font-medium">
|
||||||
|
{% trans 'Total Items' %}
|
||||||
|
</div>
|
||||||
|
<div class="tw-text-sm">
|
||||||
|
{{ run.total_rows }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<div class="d-flex flex-row">
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<div class="text-body-secondary tw-text-xs tw-font-medium">
|
||||||
|
{% trans 'Processed Items' %}
|
||||||
|
</div>
|
||||||
|
<div class="tw-text-sm">
|
||||||
|
{{ run.processed_rows }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<div class="d-flex flex-row">
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<div class="text-body-secondary tw-text-xs tw-font-medium">
|
||||||
|
{% trans 'Skipped Items' %}
|
||||||
|
</div>
|
||||||
|
<div class="tw-text-sm">
|
||||||
|
{{ run.skipped_rows }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<div class="d-flex flex-row">
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<div class="text-body-secondary tw-text-xs tw-font-medium">
|
||||||
|
{% trans 'Failed Items' %}
|
||||||
|
</div>
|
||||||
|
<div class="tw-text-sm">
|
||||||
|
{{ run.failed_rows }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<div class="d-flex flex-row">
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<div class="text-body-secondary tw-text-xs tw-font-medium">
|
||||||
|
{% trans 'Successful Items' %}
|
||||||
|
</div>
|
||||||
|
<div class="tw-text-sm">
|
||||||
|
{{ run.successful_rows }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer text-body-secondary">
|
||||||
|
<a class="text-decoration-none text-info"
|
||||||
|
role="button"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-title="{% translate "Logs" %}"
|
||||||
|
hx-get="{% url 'import_run_log' profile_id=profile.id run_id=run.id %}"
|
||||||
|
hx-target="#generic-offcanvas"><i class="fa-solid fa-file-lines"></i></a>
|
||||||
|
<a class="text-decoration-none text-danger"
|
||||||
|
role="button"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-title="{% translate "Delete" %}"
|
||||||
|
hx-delete="{% url 'import_run_delete' profile_id=profile.id run_id=run.id %}"
|
||||||
|
hx-trigger='confirmed'
|
||||||
|
data-bypass-on-ctrl="true"
|
||||||
|
data-title="{% translate "Are you sure?" %}"
|
||||||
|
data-text="{% translate "You won't be able to revert this! All imported items will be kept." %}"
|
||||||
|
data-confirm-text="{% translate "Yes, delete it!" %}"
|
||||||
|
_="install prompt_swal"><i class="fa-solid fa-trash fa-fw"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<c-msg.empty title="{% translate "No runs yet" %}"></c-msg.empty>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user