mirror of
https://github.com/wahyd4/links.git
synced 2026-08-09 05:06:16 +10:00
Add import and export tool
This commit is contained in:
@@ -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.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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'),
|
||||
]
|
||||
|
||||
@@ -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.
@@ -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 ""
|
||||
|
||||
Vendored
+36
@@ -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));
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user