diff --git a/README.md b/README.md index 7680786..9e704f3 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/data/db.sqlite3 b/data/db.sqlite3 index 91e3d72..ab521e1 100644 Binary files a/data/db.sqlite3 and b/data/db.sqlite3 differ diff --git a/links/__pycache__/urls.cpython-312.pyc b/links/__pycache__/urls.cpython-312.pyc index 2334921..dfbf32f 100644 Binary files a/links/__pycache__/urls.cpython-312.pyc and b/links/__pycache__/urls.cpython-312.pyc differ diff --git a/links/__pycache__/views.cpython-312.pyc b/links/__pycache__/views.cpython-312.pyc index 5ed4140..4c90855 100644 Binary files a/links/__pycache__/views.cpython-312.pyc and b/links/__pycache__/views.cpython-312.pyc differ diff --git a/links/templates/links/link_list.html b/links/templates/links/link_list.html index c0bd92a..aa18f2d 100644 --- a/links/templates/links/link_list.html +++ b/links/templates/links/link_list.html @@ -19,10 +19,10 @@ - - - - + - - - + @@ -76,7 +76,9 @@ {{ link.alias }} diff --git a/links/templates/links/tools.html b/links/templates/links/tools.html new file mode 100644 index 0000000..c0252ed --- /dev/null +++ b/links/templates/links/tools.html @@ -0,0 +1,32 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block content %} +
+
+
+

{% trans "Tools" %}

+
+
+

{% trans "Export Aliases" %}

+
+ {% csrf_token %} + + + +

{% trans "Import Aliases" %}

+
+ {% csrf_token %} +
+ +
+ + +
+
+
+{% endblock %} diff --git a/links/urls.py b/links/urls.py index 43b081a..3e3964b 100644 --- a/links/urls.py +++ b/links/urls.py @@ -9,5 +9,6 @@ urlpatterns = [ path('delete-selected/', views.delete_selected, name='delete_selected'), path('detail//', 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('/', views.redirect_to_original, name='redirect_to_original'), ] diff --git a/links/views.py b/links/views.py index 786d838..7601980 100644 --- a/links/views.py +++ b/links/views.py @@ -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) diff --git a/locale/zh_Hans/LC_MESSAGES/django.mo b/locale/zh_Hans/LC_MESSAGES/django.mo index 326b3c8..079e0c2 100644 Binary files a/locale/zh_Hans/LC_MESSAGES/django.mo and b/locale/zh_Hans/LC_MESSAGES/django.mo differ diff --git a/locale/zh_Hans/LC_MESSAGES/django.po b/locale/zh_Hans/LC_MESSAGES/django.po index b2ab5dc..a224fc7 100644 --- a/locale/zh_Hans/LC_MESSAGES/django.po +++ b/locale/zh_Hans/LC_MESSAGES/django.po @@ -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 "" diff --git a/new_theme/static/css/dist/styles.css b/new_theme/static/css/dist/styles.css index e5ef0ae..2373b97 100644 --- a/new_theme/static/css/dist/styles.css +++ b/new_theme/static/css/dist/styles.css @@ -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)); diff --git a/templates/base.html b/templates/base.html index 3719aea..6175cd4 100644 --- a/templates/base.html +++ b/templates/base.html @@ -36,6 +36,7 @@ {% trans "URL Manager" %}
+ + {% trans "Alias" %} {% if current_sort == 'alias' %} @@ -32,8 +32,8 @@ {% endif %} {% trans "Original URL" %} + {% trans "Original URL" %} {% trans "Clicks" %} {% if current_sort == 'clicks' %} @@ -43,7 +43,7 @@ {% endif %} + {% trans "Created At" %} {% if current_sort == 'created_at' %} @@ -53,7 +53,7 @@ {% endif %} + {% trans "Updated At" %} {% if current_sort == 'updated_at' %} @@ -63,7 +63,7 @@ {% endif %} {% trans "Actions" %}{% trans "Actions" %}
- {{ link.original_url }} + {{ link.click_count }} {{ link.created_at|date:"Y-m-d H:i" }}