diff --git a/data/db.sqlite3 b/data/db.sqlite3 index 60f3aa4..a6f7eda 100644 Binary files a/data/db.sqlite3 and b/data/db.sqlite3 differ diff --git a/links/forms.py b/links/forms.py index eb71ceb..80b42f1 100644 --- a/links/forms.py +++ b/links/forms.py @@ -2,6 +2,8 @@ from django import forms from .models import Link from simplemde.fields import SimpleMDEField from django.utils.translation import gettext_lazy as _ +from django.core.validators import URLValidator +import re class LinkForm(forms.ModelForm): text = SimpleMDEField() @@ -14,6 +16,28 @@ class LinkForm(forms.ModelForm): 'description': forms.Textarea(attrs={'rows': 3}), } + def clean_original_url(self): + url = self.cleaned_data.get('original_url') + if not url: + return url + + # Extract template parameters + template_params = re.findall(r'\{([^{}]*)\}', url) + + # Temporarily replace template parameters with placeholder + temp_url = url + for param in template_params: + param_full = '{' + param + '}' + temp_url = temp_url.replace(param_full, 'template-param') + + # Validate the URL with placeholders + try: + URLValidator()(temp_url) + except forms.ValidationError: + raise forms.ValidationError(_("Please enter a valid URL. Template parameters are allowed in the format {param_name,default=value}.")) + + return url + def clean(self): cleaned_data = super().clean() link_type = cleaned_data.get('link_type') diff --git a/links/migrations/0008_alter_link_original_url.py b/links/migrations/0008_alter_link_original_url.py new file mode 100644 index 0000000..3941461 --- /dev/null +++ b/links/migrations/0008_alter_link_original_url.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.8 on 2024-11-02 11:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('links', '0007_link_description'), + ] + + operations = [ + migrations.AlterField( + model_name='link', + name='original_url', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/links/models.py b/links/models.py index 54d9f20..b9b88ea 100644 --- a/links/models.py +++ b/links/models.py @@ -2,6 +2,7 @@ from django.db import models from django.utils.translation import gettext_lazy as _ from django.urls import reverse import logging +import re logger = logging.getLogger(__name__) @@ -11,8 +12,8 @@ class Link(models.Model): CUSTOM = 'CUSTOM', _('Custom') alias = models.SlugField(max_length=100, unique=True) - original_url = models.URLField(blank=True, null=True) - text = models.TextField(blank=True, null=True) # 改为 TextField + original_url = models.TextField(blank=True, null=True) + text = models.TextField(blank=True, null=True) link_type = models.CharField( max_length=10, choices=LinkType.choices, @@ -23,6 +24,53 @@ class Link(models.Model): updated_at = models.DateTimeField(auto_now=True) description = models.TextField(blank=True, null=True, verbose_name=_("Description")) + def get_template_parameters(self): + """Extract template parameters and their default values from original_url""" + pattern = r'\{([^{}]*)\}' + matches = re.finditer(pattern, self.original_url) + params = {} + + for match in matches: + param_str = match.group(1) + if ',' in param_str: + param_name, default_value = param_str.split(',', 1) + default_value = default_value.strip() + if default_value.startswith('default='): + default_value = default_value[8:].strip('"\'') + params[param_name.strip()] = default_value + else: + params[param_str.strip()] = None + + return params + + def get_processed_url(self, **kwargs): + """Process template URL with provided parameters""" + if not self.original_url: + return None + + processed_url = self.original_url + pattern = r'\{([^{}]*)\}' + matches = re.finditer(pattern, self.original_url) + + for match in matches: + param_str = match.group(1) + if ',' in param_str: + param_name, default_value = param_str.split(',', 1) + default_value = default_value.strip() + if default_value.startswith('default='): + default_value = default_value[8:].strip('"\'') + else: + param_name = param_str.strip() + default_value = None + + value = kwargs.get(param_name, default_value) + if value is None: + raise ValueError(f"No value provided for parameter {param_name}") + + processed_url = processed_url.replace(f"{{{param_str}}}", str(value)) + + return processed_url + def __str__(self): return self.alias diff --git a/links/templates/links/link_form.html b/links/templates/links/link_form.html index d9d204b..75421ab 100644 --- a/links/templates/links/link_form.html +++ b/links/templates/links/link_form.html @@ -45,6 +45,14 @@ {% trans "Custom type allows you to create a link with custom HTML content. Use this when you want to display a message or custom content instead of redirecting." %}

+
+

+ {% trans "Template type allows you to create dynamic URLs with parameters. Use {query, default=value} syntax in the URL." %} +

+

+ {% trans "Example: https://google.com/search?q={query, default=hello}" %} +

+
diff --git a/links/urls.py b/links/urls.py index 44bd7d4..e7d553f 100644 --- a/links/urls.py +++ b/links/urls.py @@ -13,6 +13,7 @@ urlpatterns = [ path('link//edit/', views.LinkUpdateView.as_view(), name='link_update'), path('tools/', views.ToolsView.as_view(), name='tools'), path('/', views.redirect_to_original, name='redirect_to_original'), + path('//', views.redirect_to_original, name='redirect_to_original_with_param'), path('export/', views.export_links, name='export_links'), # New route for link detail view by alias diff --git a/links/views.py b/links/views.py index 4376f45..b115ce2 100644 --- a/links/views.py +++ b/links/views.py @@ -18,6 +18,7 @@ import random from django.utils.text import slugify import markdown import logging +import re logger = logging.getLogger(__name__) @@ -178,16 +179,40 @@ def delete_selected(request): Link.objects.filter(id__in=selected_ids).delete() return redirect('link_list') -def redirect_to_original(request, alias): - # 将别名转换为小写并移除特殊字符 +def redirect_to_original(request, alias, param=None): processed_alias = ''.join(e for e in alias.lower() if e.isalnum()) try: link = Link.objects.get(alias=processed_alias) link.click_count = F('click_count') + 1 link.save() - ClickLog.objects.create(link=link) # 记录点击 - return redirect(link.original_url) + ClickLog.objects.create(link=link) + + try: + # 如果是模板 URL 并且提供了参数 + if param: + # 从 URL 中提取参数名 + pattern = r'\{([^{}]*)\}' + matches = re.finditer(pattern, link.original_url) + for match in matches: + param_str = match.group(1) + if ',' in param_str: + param_name = param_str.split(',')[0].strip() + else: + param_name = param_str.strip() + # 使用提供的参数值 + url = link.get_processed_url(**{param_name: param}) + return redirect(url) + + # 如果没有提供参数,使用默认值 + url = link.get_processed_url() + if url is None: + url = link.original_url + return redirect(url) + except ValueError as e: + messages.error(request, str(e)) + return redirect('link_list') + except Link.DoesNotExist: messages.warning(request, _("The alias '{}' doesn't exist. Do you want to create a new one?").format(processed_alias)) return redirect(reverse('link_create') + f'?alias={processed_alias}') diff --git a/locale/zh_Hans/LC_MESSAGES/django.mo b/locale/zh_Hans/LC_MESSAGES/django.mo index 19ebde9..5ef2d40 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 cd3e47e..cec6387 100644 --- a/locale/zh_Hans/LC_MESSAGES/django.po +++ b/locale/zh_Hans/LC_MESSAGES/django.po @@ -1856,7 +1856,7 @@ msgstr "无效的JSON格式。预期是对象列表。" #: links/views.py:282 msgid "" "Invalid item in JSON. Each item must be an object with an 'alias' field." -msgstr "JSON中存在无效项。每个项目必须是包含'alias'字段的对象。" +msgstr "JSON中存在无效项。每个项���必须是包含'alias'字段的对象。" #: links/views.py:295 #, fuzzy, python-brace-format @@ -1910,3 +1910,6 @@ msgstr "简体中文" msgid "Description" msgstr "描述" + +msgid "Please enter a valid URL. Template parameters are allowed in the format {param_name,default=value}." +msgstr "请输入有效的URL。允许使用模板参数,格式为 {参数名,default=默认值}。" diff --git a/new_theme/static/css/dist/styles.css b/new_theme/static/css/dist/styles.css index 41b8910..cfeedf9 100644 --- a/new_theme/static/css/dist/styles.css +++ b/new_theme/static/css/dist/styles.css @@ -1039,6 +1039,11 @@ video { border-color: rgb(74 222 128 / var(--tw-border-opacity)); } +.border-purple-400 { + --tw-border-opacity: 1; + border-color: rgb(192 132 252 / var(--tw-border-opacity)); +} + .border-transparent { border-color: transparent; } @@ -1098,6 +1103,11 @@ video { background-color: rgb(34 197 94 / var(--tw-bg-opacity)); } +.bg-purple-50 { + --tw-bg-opacity: 1; + background-color: rgb(250 245 255 / var(--tw-bg-opacity)); +} + .bg-red-500 { --tw-bg-opacity: 1; background-color: rgb(239 68 68 / var(--tw-bg-opacity)); @@ -1352,6 +1362,11 @@ video { color: rgb(79 70 229 / var(--tw-text-opacity)); } +.text-purple-700 { + --tw-text-opacity: 1; + color: rgb(126 34 206 / var(--tw-text-opacity)); +} + .text-red-600 { --tw-text-opacity: 1; color: rgb(220 38 38 / var(--tw-text-opacity));