Add import and export tool

This commit is contained in:
2024-09-08 20:57:57 +10:00
parent 10956c32dc
commit e8f8a1300d
12 changed files with 168 additions and 8 deletions
+9
View File
@@ -124,3 +124,12 @@ To run the URL Manager locally:
9. Open your browser and go to `http://127.0.0.1:8000`
Remember to keep both the Tailwind CSS build process and the Django development server running while you're developing.
## Tools
The URL Manager includes a Tools page with the following features:
- Export all aliases as a JSON file
- Import aliases from a JSON file
To access the Tools page, click on the "Tools" link in the navigation bar.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
+10 -8
View File
@@ -19,10 +19,10 @@
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-10">
<input type="checkbox" id="select-all" class="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50">
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-1/6">
<a href="?sort=alias&order={% if current_sort == 'alias' and current_order == 'asc' %}desc{% else %}asc{% endif %}" class="flex items-center">
{% trans "Alias" %}
{% if current_sort == 'alias' %}
@@ -32,8 +32,8 @@
{% endif %}
</a>
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{% trans "Original URL" %}</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-1/3">{% trans "Original URL" %}</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-1/12">
<a href="?sort=clicks&order={% if current_sort == 'clicks' and current_order == 'asc' %}desc{% else %}asc{% endif %}" class="flex items-center">
{% trans "Clicks" %}
{% if current_sort == 'clicks' %}
@@ -43,7 +43,7 @@
{% endif %}
</a>
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-1/6">
<a href="?sort=created_at&order={% if current_sort == 'created_at' and current_order == 'asc' %}desc{% else %}asc{% endif %}" class="flex items-center">
{% trans "Created At" %}
{% if current_sort == 'created_at' %}
@@ -53,7 +53,7 @@
{% endif %}
</a>
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-1/6">
<a href="?sort=updated_at&order={% if current_sort == 'updated_at' and current_order == 'asc' %}desc{% else %}asc{% endif %}" class="flex items-center">
{% trans "Updated At" %}
{% if current_sort == 'updated_at' %}
@@ -63,7 +63,7 @@
{% endif %}
</a>
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{% trans "Actions" %}</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-1/6">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@@ -76,7 +76,9 @@
<a href="{% url 'redirect_to_original' link.alias %}" target="_blank" class="text-blue-600 hover:text-blue-900">{{ link.alias }}</a>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<a href="{{ link.original_url }}" target="_blank" class="text-blue-600 hover:text-blue-900">{{ link.original_url }}</a>
<div class="truncate max-w-xs" title="{{ link.original_url }}">
<a href="{{ link.original_url }}" target="_blank" class="text-blue-600 hover:text-blue-900">{{ link.original_url }}</a>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">{{ link.click_count }}</td>
<td class="px-6 py-4 whitespace-nowrap">{{ link.created_at|date:"Y-m-d H:i" }}</td>
+32
View File
@@ -0,0 +1,32 @@
{% extends 'base.html' %}
{% load i18n %}
{% block content %}
<div class="max-w-4xl mx-auto mt-8">
<div class="bg-white shadow-md rounded-lg overflow-hidden">
<div class="px-6 py-4 bg-gray-50 border-b border-gray-200">
<h1 class="text-2xl font-bold text-gray-800">{% trans "Tools" %}</h1>
</div>
<div class="p-6">
<h2 class="text-xl font-semibold mb-4">{% trans "Export Aliases" %}</h2>
<form method="post">
{% csrf_token %}
<button type="submit" name="export" class="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded">
{% trans "Export as JSON" %}
</button>
</form>
<h2 class="text-xl font-semibold mt-8 mb-4">{% trans "Import Aliases" %}</h2>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="mb-4">
<input type="file" name="json_file" accept=".json" required class="border rounded p-2">
</div>
<button type="submit" name="import" class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded">
{% trans "Import from JSON" %}
</button>
</form>
</div>
</div>
</div>
{% endblock %}
+1
View File
@@ -9,5 +9,6 @@ urlpatterns = [
path('delete-selected/', views.delete_selected, name='delete_selected'),
path('detail/<int:pk>/', views.LinkDetailView.as_view(), name='link_detail'),
path('search/', views.SearchView.as_view(), name='search'),
path('tools/', views.ToolsView.as_view(), name='tools'), # Add this line
path('<str:alias>/', views.redirect_to_original, name='redirect_to_original'),
]
+52
View File
@@ -1,5 +1,6 @@
from django.shortcuts import render, redirect, get_object_or_404
from django.views.generic import ListView, CreateView, UpdateView, DeleteView, DetailView, TemplateView
from django.views import View # Add this import
from django.urls import reverse_lazy, reverse # Add 'reverse' here
from django.db.models import F, Count, Q
from django.db.models.functions import TruncDate
@@ -13,6 +14,10 @@ from django.utils.translation import gettext as _
from django.core.exceptions import ValidationError
from django.http import JsonResponse
from django.http import JsonResponse, HttpResponse
from django.core.serializers.json import DjangoJSONEncoder
from django.utils.dateparse import parse_datetime
class LinkListView(ListView):
model = Link
template_name = 'links/link_list.html'
@@ -146,3 +151,50 @@ class SearchView(TemplateView):
context['links'] = links
context['query'] = query
return context
class ToolsView(View):
template_name = 'links/tools.html'
def get(self, request):
return render(request, self.template_name)
def post(self, request):
if 'export' in request.POST:
links = Link.objects.all().values('alias', 'original_url', 'created_at', 'updated_at')
response = HttpResponse(json.dumps(list(links), cls=DjangoJSONEncoder), content_type='application/json')
response['Content-Disposition'] = 'attachment; filename="links_export.json"'
return response
elif 'import' in request.POST:
try:
json_file = request.FILES['json_file']
data = json.load(json_file)
if not isinstance(data, list):
raise ValueError(_("Invalid JSON format. Expected a list of objects."))
imported_count = 0
skipped_count = 0
for item in data:
if not isinstance(item, dict) or 'alias' not in item:
raise ValueError(_("Invalid item in JSON. Each item must be an object with an 'alias' field."))
if not Link.objects.filter(alias=item['alias']).exists():
Link.objects.create(
alias=item['alias'],
original_url=item.get('original_url', ''),
created_at=parse_datetime(item.get('created_at')) if item.get('created_at') else None,
updated_at=parse_datetime(item.get('updated_at')) if item.get('updated_at') else None
)
imported_count += 1
else:
skipped_count += 1
messages.success(request, _(f"{imported_count} aliases have been imported successfully. {skipped_count} were skipped."))
except json.JSONDecodeError:
messages.error(request, _("Invalid JSON file. Please check the file format."))
except ValueError as e:
messages.error(request, str(e))
except Exception as e:
messages.error(request, _(f"An error occurred during import: {str(e)}"))
return render(request, self.template_name)
Binary file not shown.
+27
View File
@@ -114,6 +114,33 @@ msgstr "未找到结果。"
msgid "Advanced Search"
msgstr "高级搜索"
msgid "Tools"
msgstr "工具"
msgid "Export Aliases"
msgstr "导出别名"
msgid "Export as JSON"
msgstr "导为JSON"
msgid "Import Aliases"
msgstr "导入别名"
msgid "Import from JSON"
msgstr "从JSON导入"
msgid "{} aliases have been imported successfully."
msgstr "{}个别名已成功导入。"
msgid "An error occurred during import: {}"
msgstr "导入过程中发生错误:{}"
msgid "Invalid JSON format. Expected a list of objects."
msgstr "无效的JSON格式。预期是对象列表。"
msgid "Invalid item in JSON. Each item must be an object with an 'alias' field."
msgstr "JSON中存在无效项。每个项目必须是包含'alias'字段的对象。"
#: 3.12/lib/python3.12/site-packages/django/forms/fields.py:95
msgid "This field is required."
msgstr ""
+36
View File
@@ -944,6 +944,22 @@ select {
width: 16rem;
}
.w-10 {
width: 2.5rem;
}
.w-1\/12 {
width: 8.333333%;
}
.w-1\/3 {
width: 33.333333%;
}
.w-1\/6 {
width: 16.666667%;
}
.min-w-full {
min-width: 100%;
}
@@ -956,6 +972,10 @@ select {
max-width: 56rem;
}
.max-w-xs {
max-width: 20rem;
}
.flex-grow {
flex-grow: 1;
}
@@ -1033,6 +1053,12 @@ select {
overflow-x: auto;
}
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.whitespace-nowrap {
white-space: nowrap;
}
@@ -1115,6 +1141,11 @@ select {
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
.bg-green-500 {
--tw-bg-opacity: 1;
background-color: rgb(34 197 94 / var(--tw-bg-opacity));
}
.p-2 {
padding: 0.5rem;
}
@@ -1306,6 +1337,11 @@ select {
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
}
.hover\:bg-green-600:hover {
--tw-bg-opacity: 1;
background-color: rgb(22 163 74 / var(--tw-bg-opacity));
}
.hover\:text-blue-800:hover {
--tw-text-opacity: 1;
color: rgb(30 64 175 / var(--tw-text-opacity));
+1
View File
@@ -36,6 +36,7 @@
<a href="{% url 'link_list' %}" class="text-xl font-bold">{% trans "URL Manager" %}</a>
<div class="flex items-center space-x-4">
<a href="{% url 'search' %}" class="text-white hover:text-gray-200">{% trans "Advanced Search" %}</a>
<a href="{% url 'tools' %}" class="text-white hover:text-gray-200">{% trans "Tools" %}</a>
<div class="relative">
<input type="text" id="search-input" class="bg-white text-gray-800 rounded px-4 py-2 w-64" placeholder="{% trans 'Search aliases...' %}">
<div id="search-results" class="absolute top-full left-0 right-0 bg-white border border-gray-300 rounded mt-1 hidden"></div>