From 205fe1d5e874c77ec3f2386980b8ff50c263f9c3 Mon Sep 17 00:00:00 2001
From: Versun <2398708+versun@users.noreply.github.com>
Date: Tue, 9 Jul 2024 01:02:20 +0000
Subject: [PATCH] Next (#91)
---
.github/workflows/latest-docker-image.yml | 2 +-
core/actions.py | 257 ++++++++++++
core/admin.py | 487 ++--------------------
core/custom_admin_site.py | 133 ++++++
core/forms.py | 97 +++++
core/inlines.py | 69 +++
core/tasks.py | 35 +-
core/urls.py | 1 +
dev-diaries/versun-dev-diary.md | 24 ++
templates/admin/base_site.html | 110 +++--
translator/models/base.py | 2 +-
translator/models/deeplweb.py | 7 +-
translator/models/deeplx.py | 7 +-
translator/models/doubao.py | 3 +-
translator/models/google_translate_web.py | 7 +-
utils/modelAdmin_utils.py | 138 ------
utils/text_handler.py | 1 +
17 files changed, 718 insertions(+), 662 deletions(-)
create mode 100644 core/actions.py
create mode 100644 core/custom_admin_site.py
create mode 100644 core/forms.py
create mode 100644 core/inlines.py
create mode 100644 dev-diaries/versun-dev-diary.md
diff --git a/.github/workflows/latest-docker-image.yml b/.github/workflows/latest-docker-image.yml
index 27c16ff..b97b8f0 100644
--- a/.github/workflows/latest-docker-image.yml
+++ b/.github/workflows/latest-docker-image.yml
@@ -2,7 +2,7 @@ name: latest:Docker Image CI
on:
release:
- types: [published]
+ types: [released]
branches:
- main
diff --git a/core/actions.py b/core/actions.py
new file mode 100644
index 0000000..fcbefeb
--- /dev/null
+++ b/core/actions.py
@@ -0,0 +1,257 @@
+import logging
+from datetime import datetime
+from ast import literal_eval
+from opyml import OPML, Outline, Head
+from huey.contrib.djhuey import HUEY as huey
+
+from django.contrib import admin
+from django.shortcuts import render, redirect
+from django.urls import reverse
+from django.http import HttpResponse
+from django.db import transaction
+from django.utils.translation import gettext_lazy as _
+
+from utils.modelAdmin_utils import get_translator_and_summary_choices
+from .custom_admin_site import core_admin_site
+from .models import O_Feed
+from .tasks import update_original_feed, update_translated_feed
+
+
+@admin.display(description=_("Export selected feeds as OPML"))
+def o_feed_export_as_opml(modeladmin, request, queryset):
+ try:
+ opml_obj = OPML()
+ opml_obj.head = Head(
+ title="Original Feeds | RSS Translator",
+ date_created=datetime.now().strftime("%a, %d %b %Y %H:%M:%S %z"),
+ owner_name="RSS Translator",
+ )
+
+ categories = {}
+ for item in queryset:
+ category = item.category.name if item.category else "default"
+ # category_outline = Outline(text=category)
+ if category not in categories:
+ categories[category] = Outline(text=category)
+
+ item_outline = Outline(
+ title=item.name,
+ text=item.name,
+ type="rss",
+ xml_url=item.feed_url,
+ html_url=item.feed_url,
+ )
+ categories[category].outlines.append(item_outline)
+
+ # category_outline.outlines.append(item_outline)
+ # opml_obj.body.outlines.append(category_outline)
+ for category_outline in categories.values():
+ opml_obj.body.outlines.append(category_outline)
+
+ response = HttpResponse(opml_obj.to_xml(), content_type="application/xml")
+ response["Content-Disposition"] = (
+ 'attachment; filename="rsstranslator_original_feeds.opml"'
+ )
+ return response
+ except Exception as e:
+ logging.error("o_feed_export_as_opml: %s", str(e))
+ return HttpResponse("An error occurred", status=500)
+
+
+@admin.display(description=_("Export selected feeds as OPML"))
+def t_feed_export_as_opml(modeladmin, request, queryset):
+ try:
+ opml_obj = OPML()
+ opml_obj.head = Head(
+ title="Translated Feeds | RSS Translator",
+ date_created=datetime.now().strftime("%a, %d %b %Y %H:%M:%S %z"),
+ owner_name="RSS Translator",
+ )
+
+ categories = {}
+ for item in queryset:
+ category = item.o_feed.category.name if item.o_feed.category else "default"
+ text = item.o_feed.name or "No Name"
+ xml_url = request.build_absolute_uri(
+ reverse("core:rss", kwargs={"feed_sid": item.sid})
+ )
+
+ if category not in categories:
+ categories[category] = Outline(text=category)
+
+ item_outline = Outline(
+ title=text,
+ text=text,
+ type="rss",
+ xml_url=xml_url,
+ html_url=item.o_feed.feed_url,
+ )
+ categories[category].outlines.append(item_outline)
+ for category_outline in categories.values():
+ opml_obj.body.outlines.append(category_outline)
+
+ response = HttpResponse(opml_obj.to_xml(), content_type="application/xml")
+ response["Content-Disposition"] = (
+ 'attachment; filename="rsstranslator_translated_feeds.opml"'
+ )
+ return response
+ except Exception as e:
+ logging.error("t_feed_export_as_opml: %s", str(e))
+ return HttpResponse("An error occurred", status=500)
+
+
+@admin.display(description=_("Force update"))
+def o_feed_force_update(modeladmin, request, queryset):
+ logging.info("Call o_feed_force_update: %s", queryset)
+ with transaction.atomic():
+ for instance in queryset:
+ instance.etag = ""
+ instance.valid = None
+ instance.save()
+ #logging.info("Call revoke_tasks_by_arg in o_feed_force_update")
+ #revoke_tasks_by_arg(instance.sid)
+ update_original_feed.schedule(
+ args=(instance.sid,True), delay=1,
+ ) # 会执行一次save()
+
+
+@admin.display(description=_("Force update"))
+def t_feed_force_update(modeladmin, request, queryset):
+ logging.info("Call t_feed_force_update: %s", queryset)
+ with transaction.atomic():
+ for instance in queryset:
+ instance.modified = None
+ instance.status = None
+ instance.save()
+ #logging.info("Call revoke_tasks_by_arg in o_feed_force_update")
+ #revoke_tasks_by_arg(instance.sid) #will check in update_translated_feed task
+ update_translated_feed.schedule(
+ args=(instance.sid,True), delay=1
+ ) # 会执行一次save()
+
+
+@admin.display(description=_("Batch modification"))
+def o_feed_batch_modify(modeladmin, request, queryset):
+ if "apply" in request.POST:
+ logging.info("Apply o_feed_batch_modify")
+ post_data = request.POST
+ fields = {
+ "update_frequency": "update_frequency_value",
+ "max_posts": "max_posts_value",
+ "translator": "translator_value",
+ "translation_display": "translation_display_value",
+ "summary_engine": "summary_engine_value",
+ "summary_detail": "summary_detail_value",
+ "additional_prompt": "additional_prompt_value",
+ "fetch_article": "fetch_article",
+ "quality": "quality",
+ "category": "category_value",
+ }
+ field_types = {
+ "update_frequency": int,
+ "max_posts": int,
+ "translation_display": int,
+ "summary_detail": float,
+ "additional_prompt": str,
+ "fetch_article": literal_eval,
+ "quality": literal_eval,
+ }
+ update_fields = {}
+ # tags_value = None
+
+ for field, value_field in fields.items():
+ value = post_data.get(value_field)
+ if post_data.get(field, "Keep") != "Keep" and value:
+ match field:
+ case "translator":
+ content_type_id, object_id = map(int, value.split(":"))
+ update_fields["content_type_id"] = content_type_id
+ update_fields["object_id"] = object_id
+ case "summary_engine":
+ content_type_summary_id, object_id_summary = map(
+ int, value.split(":")
+ )
+ update_fields["content_type_summary_id"] = (
+ content_type_summary_id
+ )
+ update_fields["object_id_summary"] = object_id_summary
+ case "category":
+ tag_model = O_Feed.category.tag_model
+ category_o, _ = tag_model.objects.get_or_create(name=value)
+ update_fields["category"] = category_o
+
+ case _:
+ update_fields[field] = field_types.get(field, str)(value)
+
+ if update_fields:
+ queryset.update(**update_fields)
+ # for obj in queryset:
+ # obj.category.update_count()
+
+ # if tags_value is not None:
+ # for obj in queryset:
+ # obj.tags = [*tags_value]
+ # obj.save()
+ # O_Feed.objects.bulk_update(queryset, ['tags'])??
+
+ # self.message_user(request, f"Successfully modified {queryset.count()} items.")
+ # return HttpResponseRedirect(request.get_full_path())
+ return redirect(request.get_full_path())
+
+ translator_choices, summary_engine_choices = get_translator_and_summary_choices()
+ logging.info(
+ "translator_choices: %s, summary_engine_choices: %s",
+ translator_choices,
+ summary_engine_choices,
+ )
+ return render(
+ request,
+ "admin/o_feed_batch_modify.html",
+ context={
+ **core_admin_site.each_context(request),
+ "items": queryset,
+ "translator_choices": translator_choices,
+ "summary_engine_choices": summary_engine_choices,
+ },
+ )
+
+
+@admin.display(description=_("Batch modification"))
+def t_feed_batch_modify(modeladmin, request, queryset):
+ if "apply" in request.POST:
+ logging.info("Apply t_feed_batch_modify")
+ translate_title = request.POST.get("translate_title", "Keep")
+ translate_content = request.POST.get("translate_content", "Keep")
+ summary = request.POST.get("summary", "Keep")
+ match translate_title:
+ case "Keep":
+ pass
+ case "True":
+ queryset.update(translate_title=True)
+ case "False":
+ queryset.update(translate_title=False)
+
+ match translate_content:
+ case "Keep":
+ pass
+ case "True":
+ queryset.update(translate_content=True)
+ case "False":
+ queryset.update(translate_content=False)
+
+ match summary:
+ case "Keep":
+ pass
+ case "True":
+ queryset.update(summary=True)
+ case "False":
+ queryset.update(summary=False)
+
+ # self.message_user(request, f"Successfully modified {queryset.count()} items.")
+ # return HttpResponseRedirect(request.get_full_path())
+ return redirect(request.get_full_path())
+ return render(
+ request,
+ "admin/t_feed_batch_modify.html",
+ context={**core_admin_site.each_context(request), "items": queryset},
+ )
diff --git a/core/admin.py b/core/admin.py
index 9513fdb..37f9785 100644
--- a/core/admin.py
+++ b/core/admin.py
@@ -1,315 +1,27 @@
import logging
-from ast import literal_eval
-from django import forms
from django.contrib import admin
from django.conf import settings
-from django.shortcuts import render
-from django.urls import path
-from django.shortcuts import redirect
-from django.core.paginator import Paginator
from django.contrib.auth.models import User, Group
-from django.urls import reverse
from django.utils.html import format_html
-from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.translation import gettext_lazy as _
-from .models import O_Feed, T_Feed
-# from taggit.models import Tag
-from .tasks import update_original_feed, update_translated_feed
-from utils.modelAdmin_utils import (
- CustomModelActions,
- get_translator_and_summary_choices,
- get_all_app_models,
- valid_icon,
+from .models import O_Feed, T_Feed
+from .custom_admin_site import core_admin_site
+from .forms import O_FeedForm
+from .inlines import T_FeedInline
+from .actions import (
+ o_feed_export_as_opml,
+ t_feed_export_as_opml,
+ o_feed_force_update,
+ t_feed_force_update,
+ o_feed_batch_modify,
+ t_feed_batch_modify,
)
+from .tasks import update_original_feed, update_translated_feed
+from utils.modelAdmin_utils import valid_icon
-class CoreAdminSite(admin.AdminSite):
- site_header = _("RSS Translator Admin")
- site_title = _("RSS Translator")
- index_title = _("Dashboard")
-
- def get_urls(self):
- urls = super().get_urls()
- custom_urls = [
- path("translator/add", translator_add_view, name="translator_add"),
- path("translator/list", translator_list_view, name="translator_list"),
- ]
- return custom_urls + urls
-
- def get_app_list(self, request, app_label=None):
- app_list = super().get_app_list(request, app_label)
- app_list += [
- {
- "name": _("Engine"),
- "app_label": "engine",
- "models": [
- {
- "name": _("Translator"),
- "object_name": "Translator",
- "admin_url": "/translator/list",
- "add_url": "/translator/add",
- # "view_only": False,
- }
- ],
- }
- ]
-
- return app_list
-
-
-core_admin_site = CoreAdminSite()
-
-
-class TranslatorPaginator(Paginator):
- def __init__(self):
- super().__init__(self, 100)
-
- self.translator_count = len(get_all_app_models("translator"))
-
- @property
- def count(self):
- return self.translator_count
-
- def page(self, number):
- limit = self.per_page
- offset = (number - 1) * self.per_page
- return self._get_page(
- self.enqueued_items(limit, offset),
- number,
- self,
- )
-
- # Copied from Huey's SqliteStorage with some modifications to allow pagination
- def enqueued_items(self, limit, offset):
- translators = get_all_app_models("translator")
- translator_list = []
- for model in translators:
- objects = (
- model.objects.all()
- .order_by("name")
- .values_list("id", "name", "valid")[offset : offset + limit]
- )
- for obj_id, obj_name, obj_valid in objects:
- translator_list.append(
- {
- "id": obj_id,
- "table_name": model._meta.db_table.split("_")[1],
- "name": obj_name,
- "valid": valid_icon(obj_valid),
- "provider": model._meta.verbose_name,
- }
- )
-
- return translator_list
-
-
-def translator_list_view(request):
- page_number = int(request.GET.get("p", 1))
- paginator = TranslatorPaginator()
- page = paginator.get_page(page_number)
- page_range = paginator.get_elided_page_range(page_number, on_each_side=2, on_ends=2)
-
- context = {
- **core_admin_site.each_context(request),
- "title": "Translator",
- "page": page,
- "page_range": page_range,
- "translators": page.object_list,
- }
- return render(request, "admin/translator.html", context)
-
-
-def translator_add_view(request):
- if request.method == "POST":
- translator_name = request.POST.get("translator_name", "/")
- # redirect to example.com/translator/translator_name/add
- target = f"/translator/{translator_name}/add"
- return (
- redirect(target)
- if url_has_allowed_host_and_scheme(target, allowed_hosts=None)
- else redirect("/")
- )
- else:
- models = get_all_app_models("translator")
- translator_list = []
- for model in models:
- translator_list.append(
- {
- "table_name": model._meta.db_table.split("_")[1],
- "provider": model._meta.verbose_name,
- }
- )
- context = {
- **core_admin_site.each_context(request),
- "translator_choices": translator_list,
- }
- return render(request, "admin/translator_add.html", context)
-
-
-class T_FeedForm(forms.ModelForm):
- class Meta:
- model = T_Feed
- fields = ["language", "translate_title", "sid"]
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.fields["sid"].required = False
- if self.instance.pk:
- self.fields["language"].disabled = True
- self.fields["sid"].disabled = True
-
-
-class T_FeedInline(admin.TabularInline):
- model = T_Feed
- form = T_FeedForm
- fields = [
- "language",
- "obj_status",
- "feed_url",
- "translate_title",
- "translate_content",
- "summary",
- "total_tokens",
- "total_characters",
- "size_in_kb",
- "sid",
- ]
- readonly_fields = (
- "feed_url",
- "obj_status",
- "size_in_kb",
- "total_tokens",
- "total_characters",
- )
- extra = 1
-
- def feed_url(self, obj):
- if obj.sid:
- rss = reverse("core:rss", kwargs={"feed_sid": obj.sid})
- rss_url = self.request.build_absolute_uri(rss)
- json = reverse("core:json", kwargs={"feed_sid": obj.sid})
- json_url = self.request.build_absolute_uri(json)
- return format_html(
- "RSS "
- " | "
- "JSON "
- "",
- rss_url,
- json_url,
- )
- return ""
-
- feed_url.short_description = _("Translated Feed URL")
-
- class Media:
- js = ("js/admin/copytoclipboard.js",)
-
- def size_in_kb(self, obj):
- return int(obj.size / 1024)
-
- size_in_kb.short_description = _("Size(KB)")
-
- def obj_status(self, obj):
- if not obj.pk:
- return ""
- return valid_icon(obj.status)
-
- obj_status.short_description = _("Status")
-
- def get_formset(self, request, obj=None, **kwargs):
- # Store the request for use in feed_url
- self.request = request
- return super(T_FeedInline, self).get_formset(request, obj, **kwargs)
-
-
-class O_FeedForm(forms.ModelForm):
- # 自定义字段,使用ChoiceField生成下拉菜单
- translator = forms.ChoiceField(
- choices=(),
- required=False,
- help_text=_("Select a valid translator"),
- label=_("Translator"),
- )
- summary_engine = forms.ChoiceField(
- choices=(),
- required=False,
- help_text=_("Select a valid AI engine"),
- label=_("Summary Engine"),
- )
-
- def __init__(self, *args, **kwargs):
- super(O_FeedForm, self).__init__(*args, **kwargs)
-
- self.fields["translator"].choices, self.fields["summary_engine"].choices = (
- get_translator_and_summary_choices()
- )
-
- # 如果已经有关联的对象,设置默认值
- instance = getattr(self, "instance", None)
- if instance and instance.pk and instance.content_type and instance.object_id:
- self.fields[
- "translator"
- ].initial = f"{instance.content_type.id}:{instance.object_id}"
-
- if (
- instance
- and instance.pk
- and instance.content_type_summary
- and instance.object_id_summary
- ):
- self.fields[
- "summary_engine"
- ].initial = (
- f"{instance.content_type_summary.id}:{instance.object_id_summary}"
- )
-
- # self.fields['translator'].short_description = _("Translator")
-
- class Meta:
- model = O_Feed
- fields = [
- "feed_url",
- "update_frequency",
- "max_posts",
- "translator",
- "translation_display",
- "summary_engine",
- "summary_detail",
- "additional_prompt",
- "fetch_article",
- "quality",
- "name",
- "category",
- ]
-
- # 重写save方法,以处理自定义字段的数据
- def save(self, commit=True):
- # 获取选择的translator,并设置content_type和translator_object_id
- if self.cleaned_data["translator"]:
- content_type_id, object_id = map(
- int, self.cleaned_data["translator"].split(":")
- )
- self.instance.content_type_id = content_type_id
- self.instance.object_id = object_id
- else:
- self.instance.content_type = None
- self.instance.object_id = None
-
- if self.cleaned_data["summary_engine"]:
- content_type_summary_id, object_id_summary = map(
- int, self.cleaned_data["summary_engine"].split(":")
- )
- self.instance.content_type_summary_id = content_type_summary_id
- self.instance.object_id_summary = object_id_summary
- else:
- self.instance.content_type_summary_id = None
- self.instance.object_id_summary = None
-
- return super(O_FeedForm, self).save(commit=commit)
-
-
-class O_FeedAdmin(admin.ModelAdmin, CustomModelActions):
+class O_FeedAdmin(admin.ModelAdmin):
form = O_FeedForm
inlines = [T_FeedInline]
list_display = [
@@ -325,7 +37,8 @@ class O_FeedAdmin(admin.ModelAdmin, CustomModelActions):
]
search_fields = ["name", "feed_url", "category__name"]
list_filter = ["valid", "category"]
- actions = ["o_feed_force_update", "o_feed_export_as_opml", "o_feed_batch_modify"]
+ actions = [o_feed_force_update, o_feed_export_as_opml, o_feed_batch_modify]
+ list_per_page = 20
def save_formset(self, request, form, formset, change):
instances = formset.save(commit=False)
@@ -333,11 +46,11 @@ def save_formset(self, request, form, formset, change):
if instance.o_feed.pk: # 不保存o_feed为空的T_Feed实例
instance.status = None
instance.save()
- self.revoke_tasks_by_arg(instance.sid)
- update_translated_feed(instance.sid, force=True)
+ #revoke_tasks_by_arg(instance.sid)
+ update_translated_feed.schedule(args=(instance.sid,True), delay=1)
for instance in formset.deleted_objects:
- self.revoke_tasks_by_arg(instance.sid)
+ #revoke_tasks_by_arg(instance.sid)
instance.delete()
formset.save_m2m()
@@ -350,51 +63,43 @@ def save_model(self, request, obj, form, change):
# translator_changed = 'content_type' in form.changed_data or 'object_id' in form.changed_data
if feed_url_changed or translation_display_changed:
obj.valid = None
- obj.name = "Loading" if not obj.name else obj.name
+ obj.name = obj.name or "Loading"
obj.save()
update_original_feed.schedule(
- args=(obj.sid,), delay=1
+ args=(obj.sid,True), delay=1
) # 会执行一次save() # 不放在model的save里是为了排除translator的更新,省流量
elif frequency_changed:
obj.save()
- self.revoke_tasks_by_arg(obj.sid)
+ #revoke_tasks_by_arg(obj.sid)
update_original_feed.schedule(
args=(obj.sid,), delay=obj.update_frequency * 60
)
else:
- obj.name = "Empty" if not obj.name else obj.name
+ obj.name = obj.name or "Empty"
obj.save()
+ @admin.display(description=_("Translator"))
def translator(self, obj):
return obj.translator
- translator.short_description = _("Translator")
-
+ @admin.display(description=_("Translated Language"))
def translated_language(self, obj):
return ", ".join(t_feed.language for t_feed in obj.t_feed_set.all())
- translated_language.short_description = _("Translated Language")
-
# def get_queryset(self, request):
# return super().get_queryset(request).prefetch_related('tags')
-
# def tag_list(self, obj):
# return ", ".join(o.name for o in obj.tags.all())
+ @admin.display(description=_("Size(KB)"), ordering="size")
def size_in_kb(self, obj):
return int(obj.size / 1024)
- size_in_kb.short_description = _("Size(KB)")
-
+ @admin.display(description=_("Valid"), ordering="valid")
def is_valid(self, obj):
return valid_icon(obj.valid)
- is_valid.short_description = _("Valid")
-
- is_valid.admin_order_field = "valid"
- size_in_kb.admin_order_field = "size"
-
- @admin.display(description="feed_url")
+ @admin.display(description=_("Feed URL"))
def show_feed_url(self, obj):
if obj.feed_url:
url = obj.feed_url
@@ -406,8 +111,6 @@ def show_feed_url(self, obj):
)
return ""
- show_feed_url.short_description = _("Feed URL")
-
def proxy_feed_url(self, obj):
if obj.sid:
return format_html(
@@ -415,96 +118,8 @@ def proxy_feed_url(self, obj):
)
return ""
- def o_feed_batch_modify(self, request, queryset):
- if "apply" in request.POST:
- logging.info("Apply o_feed_batch_modify")
- post_data = request.POST
- fields = {
- "update_frequency": "update_frequency_value",
- "max_posts": "max_posts_value",
- "translator": "translator_value",
- "translation_display": "translation_display_value",
- "summary_engine": "summary_engine_value",
- "summary_detail": "summary_detail_value",
- "additional_prompt": "additional_prompt_value",
- "fetch_article": "fetch_article",
- "quality": "quality",
- "category": "category_value",
- }
- field_types = {
- "update_frequency": int,
- "max_posts": int,
- "translation_display": int,
- "summary_detail": float,
- "additional_prompt": str,
- "fetch_article": literal_eval,
- "quality": literal_eval,
- }
- update_fields = {}
- # tags_value = None
-
- for field, value_field in fields.items():
- value = post_data.get(value_field)
- if post_data.get(field, "Keep") != "Keep" and value:
- match field:
- case "translator":
- content_type_id, object_id = map(int, value.split(":"))
- update_fields["content_type_id"] = content_type_id
- update_fields["object_id"] = object_id
- case "summary_engine":
- content_type_summary_id, object_id_summary = map(
- int, value.split(":")
- )
- update_fields["content_type_summary_id"] = (
- content_type_summary_id
- )
- update_fields["object_id_summary"] = object_id_summary
- case "category":
- tag_model = O_Feed.category.tag_model
- category_o, _ = tag_model.objects.get_or_create(name=value)
- update_fields["category"] = category_o
- case _:
- update_fields[field] = field_types.get(field, str)(value)
-
- if update_fields:
- queryset.update(**update_fields)
- # for obj in queryset:
- # obj.category.update_count()
-
- # if tags_value is not None:
- # for obj in queryset:
- # obj.tags = [*tags_value]
- # obj.save()
- # O_Feed.objects.bulk_update(queryset, ['tags'])??
-
- # self.message_user(request, f"Successfully modified {queryset.count()} items.")
- # return HttpResponseRedirect(request.get_full_path())
- return redirect(request.get_full_path())
-
- translator_choices, summary_engine_choices = (
- get_translator_and_summary_choices()
- )
- logging.info(
- "translator_choices: %s, summary_engine_choices: %s",
- translator_choices,
- summary_engine_choices,
- )
- return render(
- request,
- "admin/o_feed_batch_modify.html",
- context={
- **core_admin_site.each_context(request),
- "items": queryset,
- "translator_choices": translator_choices,
- "summary_engine_choices": summary_engine_choices,
- },
- )
-
- o_feed_batch_modify.short_description = _("Batch modification")
-
-
-class T_FeedAdmin(admin.ModelAdmin, CustomModelActions):
+class T_FeedAdmin(admin.ModelAdmin):
list_display = [
"id",
"feed_url",
@@ -531,7 +146,8 @@ class T_FeedAdmin(admin.ModelAdmin, CustomModelActions):
"size",
"modified",
]
- actions = ["t_feed_force_update", "t_feed_export_as_opml", "t_feed_batch_modify"]
+ actions = [t_feed_force_update, t_feed_export_as_opml, t_feed_batch_modify]
+ list_per_page = 20
# def get_search_results(self, request, queryset, search_term):
# queryset, use_distinct = super().get_search_results(request, queryset, search_term)
# queryset |= self.model.objects.filter(o_feed__feed_url__icontains=search_term)
@@ -561,47 +177,6 @@ def status_icon(self, obj):
status_icon.short_description = _("Status")
status_icon.admin_order_field = "status"
- def t_feed_batch_modify(self, request, queryset):
- if "apply" in request.POST:
- logging.info("Apply t_feed_batch_modify")
- translate_title = request.POST.get("translate_title", "Keep")
- translate_content = request.POST.get("translate_content", "Keep")
- summary = request.POST.get("summary", "Keep")
- match translate_title:
- case "Keep":
- pass
- case "True":
- queryset.update(translate_title=True)
- case "False":
- queryset.update(translate_title=False)
-
- match translate_content:
- case "Keep":
- pass
- case "True":
- queryset.update(translate_content=True)
- case "False":
- queryset.update(translate_content=False)
-
- match summary:
- case "Keep":
- pass
- case "True":
- queryset.update(summary=True)
- case "False":
- queryset.update(summary=False)
-
- # self.message_user(request, f"Successfully modified {queryset.count()} items.")
- # return HttpResponseRedirect(request.get_full_path())
- return redirect(request.get_full_path())
- return render(
- request,
- "admin/t_feed_batch_modify.html",
- context={**core_admin_site.each_context(request), "items": queryset},
- )
-
- t_feed_batch_modify.short_description = _("Batch modification")
-
core_admin_site.register(O_Feed, O_FeedAdmin)
core_admin_site.register(T_Feed, T_FeedAdmin)
diff --git a/core/custom_admin_site.py b/core/custom_admin_site.py
new file mode 100644
index 0000000..a97ba4d
--- /dev/null
+++ b/core/custom_admin_site.py
@@ -0,0 +1,133 @@
+from django.contrib.admin import AdminSite
+from django.urls import path
+from django.utils.translation import gettext_lazy as _
+from django.utils.http import url_has_allowed_host_and_scheme
+from django.core.paginator import Paginator
+from django.shortcuts import render, redirect
+
+from utils.modelAdmin_utils import (
+ get_all_app_models,
+ valid_icon,
+)
+class CoreAdminSite(AdminSite):
+ site_header = _("RSS Translator Admin")
+ site_title = _("RSS Translator")
+ index_title = _("Dashboard")
+
+ def get_urls(self):
+ urls = super().get_urls()
+ custom_urls = [
+ path("translator/add", translator_add_view, name="translator_add"),
+ path("translator/list", translator_list_view, name="translator_list"),
+ ]
+ return custom_urls + urls
+
+ def get_app_list(self, request, app_label=None):
+ app_list = super().get_app_list(request, app_label)
+ app_list += [
+ {
+ "name": _("Engine"),
+ "app_label": "engine",
+ "models": [
+ {
+ "name": _("Translator"),
+ "object_name": "Translator",
+ "admin_url": "/translator/list",
+ "add_url": "/translator/add",
+ # "view_only": False,
+ }
+ ],
+ }
+ ]
+
+ return app_list
+
+
+class TranslatorPaginator(Paginator):
+ def __init__(self):
+ super().__init__(self, 100)
+
+ self.translator_count = len(get_all_app_models("translator"))
+
+ @property
+ def count(self):
+ return self.translator_count
+
+ def page(self, number):
+ limit = self.per_page
+ offset = (number - 1) * self.per_page
+ return self._get_page(
+ self.enqueued_items(limit, offset),
+ number,
+ self,
+ )
+
+ # Copied from Huey's SqliteStorage with some modifications to allow pagination
+ def enqueued_items(self, limit, offset):
+ translators = get_all_app_models("translator")
+ translator_list = []
+ for model in translators:
+ objects = (
+ model.objects.all()
+ .order_by("name")
+ .values_list("id", "name", "valid")[offset : offset + limit]
+ )
+ for obj_id, obj_name, obj_valid in objects:
+ translator_list.append(
+ {
+ "id": obj_id,
+ "table_name": model._meta.db_table.split("_")[1],
+ "name": obj_name,
+ "valid": valid_icon(obj_valid),
+ "provider": model._meta.verbose_name,
+ }
+ )
+
+ return translator_list
+
+
+def translator_list_view(request):
+ page_number = int(request.GET.get("p", 1))
+ paginator = TranslatorPaginator()
+ page = paginator.get_page(page_number)
+ page_range = paginator.get_elided_page_range(page_number, on_each_side=2, on_ends=2)
+
+ context = {
+ **core_admin_site.each_context(request),
+ "title": "Translator",
+ "page": page,
+ "page_range": page_range,
+ "translators": page.object_list,
+ }
+ return render(request, "admin/translator.html", context)
+
+
+def translator_add_view(request):
+ if request.method == "POST":
+ translator_name = request.POST.get("translator_name", "/")
+ # redirect to example.com/translator/translator_name/add
+ target = f"/translator/{translator_name}/add"
+ return (
+ redirect(target)
+ if url_has_allowed_host_and_scheme(target, allowed_hosts=None)
+ else redirect("/")
+ )
+ else:
+ models = get_all_app_models("translator")
+ translator_list = []
+ for model in models:
+ translator_list.append(
+ {
+ "table_name": model._meta.db_table.split("_")[1],
+ "provider": model._meta.verbose_name,
+ }
+ )
+ context = {
+ **core_admin_site.each_context(request),
+ "translator_choices": translator_list,
+ }
+ return render(request, "admin/translator_add.html", context)
+
+
+core_admin_site = CoreAdminSite()
+
diff --git a/core/forms.py b/core/forms.py
new file mode 100644
index 0000000..860f5da
--- /dev/null
+++ b/core/forms.py
@@ -0,0 +1,97 @@
+from django import forms
+from django.db import transaction
+from django.utils.translation import gettext_lazy as _
+from .models import O_Feed, T_Feed
+from utils.modelAdmin_utils import get_translator_and_summary_choices
+
+class O_FeedForm(forms.ModelForm):
+ # 自定义字段,使用ChoiceField生成下拉菜单
+ translator = forms.ChoiceField(
+ choices=(),
+ required=False,
+ help_text=_("Select a valid translator"),
+ label=_("Translator"),
+ )
+ summary_engine = forms.ChoiceField(
+ choices=(),
+ required=False,
+ help_text=_("Select a valid AI engine"),
+ label=_("Summary Engine"),
+ )
+
+ def __init__(self, *args, **kwargs):
+ super(O_FeedForm, self).__init__(*args, **kwargs)
+ self.fields["translator"].choices, self.fields["summary_engine"].choices = (
+ get_translator_and_summary_choices()
+ )
+
+ # 如果已经有关联的对象,设置默认值
+ instance = getattr(self, "instance", None)
+ if instance and instance.pk:
+ self._set_initial_values(instance)
+
+ def _set_initial_values(self, instance):
+ if instance.content_type and instance.object_id:
+ self.fields["translator"].initial = f"{instance.content_type.id}:{instance.object_id}"
+ if instance.content_type_summary and instance.object_id_summary:
+ self.fields["summary_engine"].initial = f"{instance.content_type_summary.id}:{instance.object_id_summary}"
+
+ class Meta:
+ model = O_Feed
+ fields = [
+ "feed_url",
+ "update_frequency",
+ "max_posts",
+ "translator",
+ "translation_display",
+ "summary_engine",
+ "summary_detail",
+ "additional_prompt",
+ "fetch_article",
+ "quality",
+ "name",
+ "category",
+ ]
+
+ def _process_translator(self, instance):
+ if self.cleaned_data["translator"]:
+ content_type_id, object_id = map(int, self.cleaned_data["translator"].split(":"))
+ instance.content_type_id = content_type_id
+ instance.object_id = object_id
+ else:
+ instance.content_type = None
+ instance.object_id = None
+
+ def _process_summary_engine(self, instance):
+ if self.cleaned_data["summary_engine"]:
+ content_type_summary_id, object_id_summary = map(int, self.cleaned_data["summary_engine"].split(":"))
+ instance.content_type_summary_id = content_type_summary_id
+ instance.object_id_summary = object_id_summary
+ else:
+ instance.content_type_summary_id = None
+ instance.object_id_summary = None
+
+ # 重写save方法,以处理自定义字段的数据
+ @transaction.atomic
+ def save(self, commit=True):
+ instance = super(O_FeedForm, self).save(commit=False)
+
+ self._process_translator(instance)
+ self._process_summary_engine(instance)
+
+ if commit:
+ instance.save()
+
+ return instance
+
+class T_FeedForm(forms.ModelForm):
+ class Meta:
+ model = T_Feed
+ fields = ["language", "translate_title", "sid"]
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fields["sid"].required = False
+ if self.instance.pk:
+ self.fields["language"].disabled = True
+ self.fields["sid"].disabled = True
\ No newline at end of file
diff --git a/core/inlines.py b/core/inlines.py
new file mode 100644
index 0000000..d0b94cd
--- /dev/null
+++ b/core/inlines.py
@@ -0,0 +1,69 @@
+
+from django.contrib import admin
+from django.urls import reverse
+from django.utils.html import format_html
+from django.utils.translation import gettext_lazy as _
+from .models import T_Feed
+from .forms import T_FeedForm
+from utils.modelAdmin_utils import (
+ valid_icon,
+)
+
+class T_FeedInline(admin.TabularInline):
+ model = T_Feed
+ form = T_FeedForm
+ fields = [
+ "language",
+ "obj_status",
+ "feed_url",
+ "translate_title",
+ "translate_content",
+ "summary",
+ "total_tokens",
+ "total_characters",
+ "size_in_kb",
+ "sid",
+ ]
+ readonly_fields = (
+ "feed_url",
+ "obj_status",
+ "size_in_kb",
+ "total_tokens",
+ "total_characters",
+ )
+ extra = 1
+
+ @admin.display(description=_("Translated Feed URL"))
+ def feed_url(self, obj):
+ if obj.sid:
+ rss = reverse("core:rss", kwargs={"feed_sid": obj.sid})
+ rss_url = self.request.build_absolute_uri(rss)
+ json = reverse("core:json", kwargs={"feed_sid": obj.sid})
+ json_url = self.request.build_absolute_uri(json)
+ return format_html(
+ "RSS "
+ " | "
+ "JSON "
+ "",
+ rss_url,
+ json_url,
+ )
+ return ""
+
+ class Media:
+ js = ("js/admin/copytoclipboard.js",)
+
+ @admin.display(description=_("Size(KB)"))
+ def size_in_kb(self, obj):
+ return int(obj.size / 1024)
+
+ @admin.display(description=_("Status"))
+ def obj_status(self, obj):
+ if not obj.pk:
+ return ""
+ return valid_icon(obj.status)
+
+ def get_formset(self, request, obj=None, **kwargs):
+ # Store the request for use in feed_url
+ self.request = request
+ return super(T_FeedInline, self).get_formset(request, obj, **kwargs)
diff --git a/core/tasks.py b/core/tasks.py
index 2a02d96..248a52f 100644
--- a/core/tasks.py
+++ b/core/tasks.py
@@ -11,7 +11,7 @@
from django.db import IntegrityError
from huey.contrib.djhuey import HUEY as huey
-from huey.contrib.djhuey import on_startup, db_task
+from huey.contrib.djhuey import on_startup, db_task, on_shutdown
from .models import O_Feed, T_Feed
from translator.models import TranslatorEngine, Translated_Content
@@ -28,6 +28,12 @@
# from huey_monitor.models import TaskModel
unique_tasks = set()
+def revoke_tasks_by_arg(arg_to_match):
+ for task in huey.scheduled() + huey.pending():
+ # Assuming the first argument is the one we're interested in (e.g., obj.pk)
+ if task.args and task.args[0] == arg_to_match:
+ logging.info("Revoke task: %s", task)
+ huey.revoke_by_id(task)
# @periodic_task(crontab( minute='*/1'))
@on_startup()
@@ -42,19 +48,15 @@ def schedule_update():
args=(feed.sid,), delay=feed.update_frequency * 60
)
-
-# @on_shutdown()
-# def flush_all():
-# huey.storage.flush_queue()
-# huey.storage.flush_schedule()
-# huey.storage.flush_results()
-# clean TaskModel all data
-# TaskModel.objects.all().delete()
+@on_shutdown()
+def cleanup_tasks():
+ huey.storage.flush_all()
@db_task(retries=3)
-def update_original_feed(sid: str):
- if sid in unique_tasks:
+def update_original_feed(sid: str, force:bool = False):
+ if sid in unique_tasks: # 如果判断force的话,是没法停止正在执行的task
+ logging.warning("(skip)This task update_original_feed is executing: %s",sid)
return
else:
unique_tasks.add(sid)
@@ -65,7 +67,9 @@ def update_original_feed(sid: str):
except O_Feed.DoesNotExist:
return False
+ revoke_tasks_by_arg(sid)
logging.info("Call task update_original_feed: %s", obj.feed_url)
+
feed_dir_path = Path(settings.DATA_FOLDER) / "feeds"
if not os.path.exists(feed_dir_path):
@@ -119,8 +123,9 @@ def update_original_feed(sid: str):
@db_task(retries=3)
-def update_translated_feed(sid: str, force=False):
- if sid in unique_tasks and not force:
+def update_translated_feed(sid: str, force:bool = False):
+ if sid in unique_tasks: # 如果判断force的话,是没法停止正在执行的task
+ logging.warning("(skip)The task update_translated_feed is executing: %s",sid)
return
else:
unique_tasks.add(sid)
@@ -133,6 +138,7 @@ def update_translated_feed(sid: str, force=False):
return False
try:
+ revoke_tasks_by_arg(sid)
logging.info("Call task update_translated_feed: %s", obj.o_feed.feed_url)
if obj.o_feed.pk is None:
@@ -157,9 +163,6 @@ def update_translated_feed(sid: str, force=False):
return False
translated_feed_file_path = f"{feed_dir_path}/{obj.sid}"
- # if not os.path.exists(translated_feed_file_path):
- # with open(translated_feed_file_path, "w", encoding="utf-8") as f:
- # f.write("Translation in progress...")
original_feed = feedparser.parse(original_feed_file_path)
diff --git a/core/urls.py b/core/urls.py
index 280944c..84caad9 100644
--- a/core/urls.py
+++ b/core/urls.py
@@ -10,6 +10,7 @@
path("all//", views.all, name="all"),
path("category/", views.category, name="category"),
path("category//", views.category, name="category"),
+ path("json/", views.rss_json, name="json"),
path("json//", views.rss_json, name="json"),
re_path(r"(?P[^/]+)/?$", views.rss, name="rss"),
]
diff --git a/dev-diaries/versun-dev-diary.md b/dev-diaries/versun-dev-diary.md
new file mode 100644
index 0000000..92cee17
--- /dev/null
+++ b/dev-diaries/versun-dev-diary.md
@@ -0,0 +1,24 @@
+# 2024-07-04
+想使用[django-tasks](https://github.com/RealOrangeOne/django-tasks)来代替huey做为任务管理,但因为还在开发中,所以担心稳定性。主要是因为它的issues中有几个我在使用huey时遇到的问题,且正在解决,但huey还遥遥无期。。。。
+
+添加了huey的shutdown动作,flush所有任务(包括revoke的任务,防止无用的任务堆积)
+
+huey在revoke一个task后,并不会删除这个task,如果之后继续revoke的话,还会重复revoke一遍,本来想搭配restore_by_id来恢复并重新预约任务,但需要存储task的result才能调用reschedule,所以暂时放弃了。
+
+先这样吧,又不是不能用,等django-tasks完善吧
+
+# 2024-07-03
+看来还是有必要添加个开发日记,因为有些代码现在看起来很傻逼,但就是不知道当初为啥会这么写,感觉是为了某种边缘情况,但就是记不起来。
+
+开发日记的灵感来源于:https://github.com/cozemble/breezbook/discussions/32
+
+---
+今天主要是重构了core/admin.py,原先的代码太乱了。
+整个文件直接丢给pplx的claude 3.5 sonnet,效果非常好,基本一次就成了
+
+然后顺便发现了revoke_tasks_by_arg函数的使用逻辑有点问题:
+t_feed_force_update不应该调用revoke_tasks_by_arg,因为它是通过update_original_feed task来调用的,只有original_feed才会预约,而update_translated_feed task是实时性的,所以应该在update_translated_feed里检查任务唯一性就行了。
+
+o_feed_force_update也不用revoke_tasks_by_arg,直接放在update_original_feed task更方便直观,否则task完全不知道可能会在哪里被revoke掉
+
+新发布的版本,还是先发布pre release,push到docker dev标签,自己先测试一段时间再push到latest
\ No newline at end of file
diff --git a/templates/admin/base_site.html b/templates/admin/base_site.html
index fec1a50..c6ee290 100644
--- a/templates/admin/base_site.html
+++ b/templates/admin/base_site.html
@@ -1,47 +1,77 @@
{% extends "admin/base_site.html" %}
+{% block extrahead %}
+{{ block.super }}
+
+
+{% endblock %}
+
{% block footer %}
-{% endblock %}
+{% endblock %}
\ No newline at end of file
diff --git a/translator/models/base.py b/translator/models/base.py
index 87af615..deacf7f 100644
--- a/translator/models/base.py
+++ b/translator/models/base.py
@@ -181,7 +181,7 @@ def translate(
# else:
# translated_text = ''
# logging.warning("Translator->%s: %s", res.choices[0].finish_reason, text)
- tokens = res.usage.total_tokens
+ tokens = res.usage.total_tokens if res.usage else 0
except Exception as e:
logging.error("OpenAIInterface->%s: %s", e, text)
diff --git a/translator/models/deeplweb.py b/translator/models/deeplweb.py
index 308a7b8..5f68de0 100644
--- a/translator/models/deeplweb.py
+++ b/translator/models/deeplweb.py
@@ -39,13 +39,13 @@ class Meta:
def validate(self) -> bool:
try:
- resp = self.translate("Hello World", "Chinese Simplified")
+ resp = self.translate("Hello World", "Chinese Simplified", validate=True)
return resp.get("text") != ""
except Exception as e:
logging.error("DeepLWebTranslator validate ->%s", e)
return False
- def translate(self, text: str, target_language: str, **kwargs) -> dict:
+ def translate(self, text: str, target_language: str, validate:bool=False, **kwargs) -> dict:
logging.info(">>> DeepL Web Translate [%s]: %s", target_language, text)
target_code = self.language_code_map.get(target_language, None)
translated_text = ""
@@ -62,5 +62,6 @@ def translate(self, text: str, target_language: str, **kwargs) -> dict:
except Exception as e:
logging.error("DeepLWebTranslator->%s: %s", e, text)
finally:
- sleep(self.interval)
+ if not validate:
+ sleep(self.interval)
return {"text": translated_text, "characters": len(text)}
diff --git a/translator/models/deeplx.py b/translator/models/deeplx.py
index ff43fc3..35387e4 100644
--- a/translator/models/deeplx.py
+++ b/translator/models/deeplx.py
@@ -42,12 +42,12 @@ class Meta:
def validate(self) -> bool:
try:
- resp = self.translate("Hello World", "Chinese Simplified")
+ resp = self.translate("Hello World", "Chinese Simplified", validate=True)
return resp.get("text") != ""
except Exception:
return False
- def translate(self, text: str, target_language: str, **kwargs) -> dict:
+ def translate(self, text: str, target_language: str, validate:bool=False, **kwargs) -> dict:
logging.info(">>> DeepLX Translate [%s]: %s", target_language, text)
target_code = self.language_code_map.get(target_language, None)
translated_text = ""
@@ -73,5 +73,6 @@ def translate(self, text: str, target_language: str, **kwargs) -> dict:
except Exception as e:
logging.error("DeepLXTranslator->%s: %s", e, text)
finally:
- sleep(self.interval)
+ if not validate:
+ sleep(self.interval)
return {"text": translated_text, "characters": len(text)}
diff --git a/translator/models/doubao.py b/translator/models/doubao.py
index bccafc5..dedc014 100644
--- a/translator/models/doubao.py
+++ b/translator/models/doubao.py
@@ -72,7 +72,8 @@ def translate(self, text: str, target_language: str, system_prompt: str = None,
if res.choices and res.choices[0].message.content:
translated_text = res.choices[0].message.content
logging.info("DoubaoTranslator->%s: %s", res.choices[0].finish_reason, translated_text)
- tokens = res.usage.total_tokens
+
+ tokens = res.usage.total_tokens if res.usage else 0
except Exception as e:
logging.error("DoubaoTranslator->%s: %s", e, text)
diff --git a/translator/models/google_translate_web.py b/translator/models/google_translate_web.py
index d6227bb..8595635 100644
--- a/translator/models/google_translate_web.py
+++ b/translator/models/google_translate_web.py
@@ -41,10 +41,10 @@ class Meta:
verbose_name_plural = "Google Translate(Web)"
def validate(self) -> bool:
- results = self.translate("hi", "Chinese Simplified")
+ results = self.translate("hi", "Chinese Simplified", validate=True)
return results.get("text") != ""
- def translate(self, text: str, target_language: str, **kwargs) -> dict:
+ def translate(self, text: str, target_language: str, validate:bool=False, **kwargs) -> dict:
logging.info(">>> Google Translate Web Translate [%s]:", target_language)
target_language = self.language_code_map.get(target_language)
translated_text = ""
@@ -71,5 +71,6 @@ def translate(self, text: str, target_language: str, **kwargs) -> dict:
except Exception as e:
logging.error("GoogleTranslateWebTranslator->%s: %s", e, text)
finally:
- sleep(self.interval)
+ if not validate:
+ sleep(self.interval)
return {"text": translated_text, "characters": len(text)}
diff --git a/utils/modelAdmin_utils.py b/utils/modelAdmin_utils.py
index 1cdd0c0..196f4f2 100644
--- a/utils/modelAdmin_utils.py
+++ b/utils/modelAdmin_utils.py
@@ -1,150 +1,12 @@
-import logging
-from datetime import datetime
from django.conf import settings
-from django.urls import reverse
-from django.http import HttpResponse
from django.utils.html import format_html
from django.apps import apps
-
-
-from opyml import OPML, Outline, Head
-from huey.contrib.djhuey import HUEY as huey
-
-# from django.conf import settings
from django.utils.translation import gettext_lazy as _
-from django.db import transaction
from django.contrib.contenttypes.models import ContentType
-
-from core.tasks import update_original_feed, update_translated_feed
-
# if settings.DEBUG:
# from huey_monitor.models import TaskModel
-
-class CustomModelActions:
- def o_feed_export_as_opml(self, request, queryset):
- try:
- opml_obj = OPML()
- opml_obj.head = Head(
- title="Original Feeds | RSS Translator",
- date_created=datetime.now().strftime("%a, %d %b %Y %H:%M:%S %z"),
- owner_name="RSS Translator",
- )
-
- categories = {}
- for item in queryset:
- category = item.category.name if item.category else 'default'
- #category_outline = Outline(text=category)
- if category not in categories:
- categories[category] = Outline(text=category)
-
- item_outline = Outline(
- title=item.name,
- text=item.name,
- type="rss",
- xml_url=item.feed_url,
- html_url=item.feed_url,
- )
- categories[category].outlines.append(item_outline)
-
- #category_outline.outlines.append(item_outline)
- #opml_obj.body.outlines.append(category_outline)
- for category_outline in categories.values():
- opml_obj.body.outlines.append(category_outline)
-
- response = HttpResponse(opml_obj.to_xml(), content_type="application/xml")
- response["Content-Disposition"] = (
- 'attachment; filename="rsstranslator_original_feeds.opml"'
- )
- return response
- except Exception as e:
- logging.error("o_feed_export_as_opml: %s", str(e))
- return HttpResponse("An error occurred", status=500)
-
- o_feed_export_as_opml.short_description = _("Export selected feeds as OPML")
-
- def t_feed_export_as_opml(self, request, queryset):
- try:
- opml_obj = OPML()
- opml_obj.head = Head(
- title="Translated Feeds | RSS Translator",
- date_created=datetime.now().strftime("%a, %d %b %Y %H:%M:%S %z"),
- owner_name="RSS Translator",
- )
-
- categories = {}
- for item in queryset:
- category = item.o_feed.category.name if item.o_feed.category else 'default'
- text = item.o_feed.name or "No Name"
- xml_url = request.build_absolute_uri(
- reverse("core:rss", kwargs={"feed_sid": item.sid})
- )
-
- if category not in categories:
- categories[category] = Outline(text=category)
-
- item_outline = Outline(
- title=text,
- text=text,
- type="rss",
- xml_url=xml_url,
- html_url=item.o_feed.feed_url,
- )
- categories[category].outlines.append(item_outline)
- for category_outline in categories.values():
- opml_obj.body.outlines.append(category_outline)
-
- response = HttpResponse(opml_obj.to_xml(), content_type="application/xml")
- response["Content-Disposition"] = (
- 'attachment; filename="rsstranslator_translated_feeds.opml"'
- )
- return response
- except Exception as e:
- logging.error("t_feed_export_as_opml: %s", str(e))
- return HttpResponse("An error occurred", status=500)
-
- t_feed_export_as_opml.short_description = _("Export selected feeds as OPML")
-
- def o_feed_force_update(self, request, queryset):
- logging.info("Call o_feed_force_update: %s", queryset)
- with transaction.atomic():
- for instance in queryset:
- instance.etag = ""
- instance.valid = None
- instance.save()
- self.revoke_tasks_by_arg(instance.sid)
- update_original_feed.schedule(
- args=(instance.sid,), delay=1
- ) # 会执行一次save()
-
- o_feed_force_update.short_description = _("Force update")
-
- def t_feed_force_update(self, request, queryset):
- logging.info("Call t_feed_force_update: %s", queryset)
- with transaction.atomic():
- for instance in queryset:
- instance.modified = None
- instance.status = None
- instance.save()
- self.revoke_tasks_by_arg(instance.sid)
- update_translated_feed.schedule(
- args=(instance.sid,), delay=1
- ) # 会执行一次save()
-
- t_feed_force_update.short_description = _("Force update")
-
- def revoke_tasks_by_arg(self, arg_to_match):
- for task in huey.scheduled():
- # Assuming the first argument is the one we're interested in (e.g., obj.pk)
- if task.args and task.args[0] == arg_to_match:
- logging.info("Revoke task: %s", task)
- huey.revoke_by_id(task)
- # delete TaskModel data
- # if settings.DEBUG:
- # TaskModel.objects.filter(task_id=task.id).delete()
-
-
# def get_all_subclasses(cls):
# subclasses = set()
# for subclass in cls.__subclasses__():
diff --git a/utils/text_handler.py b/utils/text_handler.py
index fec8d85..64f1fbf 100644
--- a/utils/text_handler.py
+++ b/utils/text_handler.py
@@ -137,6 +137,7 @@ def should_skip(element):
"bdo",
"cite",
"dfn",
+ "iframe",
]
if isinstance(element, Comment):
return True