various bugs

This commit is contained in:
Flatlogic Bot 2026-02-09 04:37:40 +00:00
parent 556b73ecb5
commit aade25cd3a
23 changed files with 746 additions and 86 deletions

154
celery.log Normal file
View File

@ -0,0 +1,154 @@
-------------- celery@pool-python-d07be985 v5.4.0 (opalescent)
--- ***** -----
-- ******* ---- Linux-6.1.0-42-cloud-amd64-x86_64-with-glibc2.36 2026-02-08 16:57:58
- *** --- * ---
- ** ---------- [config]
- ** ---------- .> app: config:0x7f0365fc7010
- ** ---------- .> transport: redis://localhost:6379/0
- ** ---------- .> results: redis://localhost:6379/0
- *** --- * --- .> concurrency: 2 (prefork)
-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)
--- ***** -----
-------------- [queues]
.> celery exchange=celery(direct) key=celery
[tasks]
. config.celery.debug_task
. core.tasks.generate_summary
. core.tasks.process_bookmark
[2026-02-08 16:57:58,648: WARNING/MainProcess] /home/ubuntu/.local/lib/python3.11/site-packages/celery/worker/consumer/consumer.py:508: CPendingDeprecationWarning: The broker_connection_retry configuration setting will no longer determine
whether broker connection retries are made during startup in Celery 6.0 and above.
If you wish to retain the existing behavior for retrying connections on startup,
you should set broker_connection_retry_on_startup to True.
warnings.warn(
[2026-02-08 16:57:58,669: INFO/MainProcess] Connected to redis://localhost:6379/0
[2026-02-08 16:57:58,672: WARNING/MainProcess] /home/ubuntu/.local/lib/python3.11/site-packages/celery/worker/consumer/consumer.py:508: CPendingDeprecationWarning: The broker_connection_retry configuration setting will no longer determine
whether broker connection retries are made during startup in Celery 6.0 and above.
If you wish to retain the existing behavior for retrying connections on startup,
you should set broker_connection_retry_on_startup to True.
warnings.warn(
[2026-02-08 16:57:58,676: INFO/MainProcess] mingle: searching for neighbors
[2026-02-08 16:57:59,699: INFO/MainProcess] mingle: all alone
[2026-02-08 16:57:59,722: INFO/MainProcess] celery@pool-python-d07be985 ready.
[2026-02-08 16:58:54,807: INFO/MainProcess] Task core.tasks.process_bookmark[6aeefd1e-df28-42d3-bb17-d6836c389361] received
[2026-02-08 16:58:55,106: INFO/ForkPoolWorker-2] HTTP Request: GET https://openai.com "HTTP/1.1 403 Forbidden"
[2026-02-08 16:58:55,121: WARNING/ForkPoolWorker-2] Error fetching bookmark 9 (https://openai.com): Client error '403 Forbidden' for url 'https://openai.com'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403. Trying base domain backup.
[2026-02-08 16:58:55,121: ERROR/ForkPoolWorker-2] Error fetching base domain for bookmark 9: Client error '403 Forbidden' for url 'https://openai.com'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403
[2026-02-08 16:58:55,222: INFO/MainProcess] Task core.tasks.generate_summary[b7f3ce50-3949-4d4b-8124-986fc6fed32d] received
[2026-02-08 16:58:55,230: INFO/ForkPoolWorker-2] Task core.tasks.process_bookmark[6aeefd1e-df28-42d3-bb17-d6836c389361] succeeded in 0.41664800100261346s: 'Processed bookmark 9'
[2026-02-08 16:59:06,405: INFO/ForkPoolWorker-1] Task core.tasks.generate_summary[b7f3ce50-3949-4d4b-8124-986fc6fed32d] succeeded in 11.157703020959161s: 'Generated summary for bookmark 9'
[2026-02-08 17:49:01,106: INFO/MainProcess] Task core.tasks.process_bookmark[480f8119-0f34-4c8d-af74-8223f6d86777] received
[2026-02-08 17:49:01,495: INFO/ForkPoolWorker-2] HTTP Request: GET https://aimlapi.com/ "HTTP/1.1 200 OK"
[2026-02-08 17:49:01,833: INFO/MainProcess] Task core.tasks.generate_summary[5f9a1a29-9b56-4219-b073-86aa584fa574] received
[2026-02-08 17:49:01,840: INFO/ForkPoolWorker-2] Task core.tasks.process_bookmark[480f8119-0f34-4c8d-af74-8223f6d86777] succeeded in 0.7128028109436855s: 'Processed bookmark 10'
[2026-02-08 17:49:12,749: INFO/ForkPoolWorker-1] Task core.tasks.generate_summary[5f9a1a29-9b56-4219-b073-86aa584fa574] succeeded in 10.906358937965706s: 'Generated summary for bookmark 10'
[2026-02-08 17:50:13,199: INFO/MainProcess] Task core.tasks.process_bookmark[38a7f2f4-6b5e-48ab-8ce5-b973aecf0829] received
[2026-02-08 17:50:13,401: INFO/ForkPoolWorker-2] HTTP Request: GET https://dropoverapp.com/ "HTTP/1.1 200 OK"
[2026-02-08 17:50:13,476: INFO/MainProcess] Task core.tasks.generate_summary[f6142f4c-c377-44cf-8921-4de37564f845] received
[2026-02-08 17:50:13,486: INFO/ForkPoolWorker-2] Task core.tasks.process_bookmark[38a7f2f4-6b5e-48ab-8ce5-b973aecf0829] succeeded in 0.28576223901472986s: 'Processed bookmark 11'
[2026-02-08 17:50:19,030: INFO/ForkPoolWorker-1] Task core.tasks.generate_summary[f6142f4c-c377-44cf-8921-4de37564f845] succeeded in 5.548579726018943s: 'Generated summary for bookmark 11'
[2026-02-08 17:54:37,900: INFO/MainProcess] Task core.tasks.process_bookmark[49edb552-63b2-44e3-b6bf-29ea3921cbc0] received
[2026-02-08 17:54:39,151: INFO/ForkPoolWorker-2] HTTP Request: GET https://www.wikipedia.org/ "HTTP/1.1 200 OK"
[2026-02-08 17:54:39,938: INFO/MainProcess] Task core.tasks.generate_summary[62fbb6ba-951b-469a-ac79-19663a33d9a4] received
[2026-02-08 17:54:39,982: INFO/ForkPoolWorker-2] Task core.tasks.process_bookmark[49edb552-63b2-44e3-b6bf-29ea3921cbc0] succeeded in 1.9269755689892918s: 'Processed bookmark 12'
[2026-02-08 17:54:46,341: INFO/ForkPoolWorker-1] Task core.tasks.generate_summary[62fbb6ba-951b-469a-ac79-19663a33d9a4] succeeded in 6.309643072017934s: 'Generated summary for bookmark 12'
[2026-02-08 17:55:04,551: INFO/MainProcess] Task core.tasks.process_bookmark[20ea2b59-ed0f-4970-ad43-cea2bc3597e0] received
[2026-02-08 17:55:04,768: INFO/ForkPoolWorker-2] HTTP Request: GET https://www.wikipedia.org/ "HTTP/1.1 200 OK"
[2026-02-08 17:55:05,030: INFO/MainProcess] Task core.tasks.generate_summary[e29ad0ec-2683-44f9-964a-6883351d3e53] received
[2026-02-08 17:55:05,034: INFO/ForkPoolWorker-2] Task core.tasks.process_bookmark[20ea2b59-ed0f-4970-ad43-cea2bc3597e0] succeeded in 0.4778669200022705s: 'Processed bookmark 12'
[2026-02-08 17:55:15,878: INFO/ForkPoolWorker-1] Task core.tasks.generate_summary[e29ad0ec-2683-44f9-964a-6883351d3e53] succeeded in 10.846816856996156s: 'Generated summary for bookmark 12'
[2026-02-08 17:55:38,808: INFO/MainProcess] Task core.tasks.process_bookmark[631857db-83e4-448e-91bc-5964ff822b82] received
[2026-02-08 17:55:38,955: INFO/ForkPoolWorker-2] HTTP Request: GET https://openai.com "HTTP/1.1 403 Forbidden"
[2026-02-08 17:55:38,975: WARNING/ForkPoolWorker-2] Error fetching bookmark 9 (https://openai.com): Client error '403 Forbidden' for url 'https://openai.com'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403. Trying base domain backup.
[2026-02-08 17:55:38,976: ERROR/ForkPoolWorker-2] Error fetching base domain for bookmark 9: Client error '403 Forbidden' for url 'https://openai.com'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403
[2026-02-08 17:55:38,992: INFO/MainProcess] Task core.tasks.generate_summary[ac5da805-5c8e-4641-ba7d-594b569f9472] received
[2026-02-08 17:55:38,996: INFO/ForkPoolWorker-2] Task core.tasks.process_bookmark[631857db-83e4-448e-91bc-5964ff822b82] succeeded in 0.18439359701005742s: 'Processed bookmark 9'
[2026-02-08 17:55:49,704: INFO/ForkPoolWorker-1] Task core.tasks.generate_summary[ac5da805-5c8e-4641-ba7d-594b569f9472] succeeded in 10.710596179007553s: 'Generated summary for bookmark 9'
[2026-02-08 17:56:04,496: INFO/MainProcess] Task core.tasks.process_bookmark[8e44cfbd-da5d-4f2c-a670-205d1f35397c] received
[2026-02-08 17:56:06,477: INFO/ForkPoolWorker-2] HTTP Request: GET https://www.wikipedia.org/ "HTTP/1.1 200 OK"
[2026-02-08 17:56:10,002: INFO/MainProcess] Task core.tasks.generate_summary[d89d853b-545c-47ef-8d88-b3645948b928] received
[2026-02-08 17:56:10,231: INFO/ForkPoolWorker-2] Task core.tasks.process_bookmark[8e44cfbd-da5d-4f2c-a670-205d1f35397c] succeeded in 5.731945501989685s: 'Processed bookmark 12'
[2026-02-08 17:56:23,118: INFO/ForkPoolWorker-1] Task core.tasks.generate_summary[d89d853b-545c-47ef-8d88-b3645948b928] succeeded in 13.114061183994636s: 'Generated summary for bookmark 12'
-------------- celery@pool-python-d07be985 v5.4.0 (opalescent)
--- ***** -----
-- ******* ---- Linux-6.1.0-42-cloud-amd64-x86_64-with-glibc2.36 2026-02-08 19:37:54
- *** --- * ---
- ** ---------- [config]
- ** ---------- .> app: config:0x7f7767509510
- ** ---------- .> transport: redis://localhost:6379/0
- ** ---------- .> results: redis://localhost:6379/0
- *** --- * --- .> concurrency: 2 (prefork)
-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)
--- ***** -----
-------------- [queues]
.> celery exchange=celery(direct) key=celery
[tasks]
. config.celery.debug_task
. core.tasks.generate_summary
. core.tasks.process_bookmark
[2026-02-08 19:37:55,136: WARNING/MainProcess] /home/ubuntu/.local/lib/python3.11/site-packages/celery/worker/consumer/consumer.py:508: CPendingDeprecationWarning: The broker_connection_retry configuration setting will no longer determine
whether broker connection retries are made during startup in Celery 6.0 and above.
If you wish to retain the existing behavior for retrying connections on startup,
you should set broker_connection_retry_on_startup to True.
warnings.warn(
[2026-02-08 19:37:55,230: INFO/MainProcess] Connected to redis://localhost:6379/0
[2026-02-08 19:37:55,233: WARNING/MainProcess] /home/ubuntu/.local/lib/python3.11/site-packages/celery/worker/consumer/consumer.py:508: CPendingDeprecationWarning: The broker_connection_retry configuration setting will no longer determine
whether broker connection retries are made during startup in Celery 6.0 and above.
If you wish to retain the existing behavior for retrying connections on startup,
you should set broker_connection_retry_on_startup to True.
warnings.warn(
[2026-02-08 19:37:55,266: INFO/MainProcess] mingle: searching for neighbors
[2026-02-08 19:37:56,302: INFO/MainProcess] mingle: all alone
[2026-02-08 19:37:56,334: INFO/MainProcess] celery@pool-python-d07be985 ready.
[2026-02-08 19:37:56,351: INFO/MainProcess] Task core.tasks.process_bookmark[49276cb8-da4c-4e3b-a10c-1eff3e4358fc] received
[2026-02-08 19:37:56,364: INFO/MainProcess] Task core.tasks.process_bookmark[4cc4cf11-e28f-4a72-9d09-4da9b24bce6e] received
[2026-02-08 19:37:56,819: INFO/ForkPoolWorker-2] HTTP Request: GET https://dropoverapp.com/ "HTTP/1.1 200 OK"
[2026-02-08 19:37:56,955: INFO/ForkPoolWorker-1] HTTP Request: GET https://www.strella.io/ "HTTP/1.1 200 OK"
[2026-02-08 19:37:57,117: INFO/MainProcess] Task core.tasks.generate_summary[7ca1dbb6-005a-4fcf-b5d0-5fd6a2ae8ac5] received
[2026-02-08 19:37:57,130: INFO/ForkPoolWorker-2] Task core.tasks.process_bookmark[49276cb8-da4c-4e3b-a10c-1eff3e4358fc] succeeded in 0.7769331529852934s: 'Processed bookmark 13'
[2026-02-08 19:37:57,193: INFO/ForkPoolWorker-2] Generating summary/tags for bookmark 13...
[2026-02-08 19:37:57,361: INFO/MainProcess] Task core.tasks.generate_summary[71b0f9cb-36dc-4eb4-91f7-f6e8bba9c54d] received
[2026-02-08 19:37:57,389: INFO/ForkPoolWorker-1] Task core.tasks.process_bookmark[4cc4cf11-e28f-4a72-9d09-4da9b24bce6e] succeeded in 1.0219957570079714s: 'Processed bookmark 14'
[2026-02-08 19:37:57,419: INFO/ForkPoolWorker-1] Generating summary/tags for bookmark 14...
[2026-02-08 19:37:58,896: INFO/MainProcess] Events of group {task} enabled by remote.
[2026-02-08 19:38:07,952: INFO/ForkPoolWorker-2] AI Raw Response for 13: {
"summary": "Dropover is a macOS utility that provides floating shelves to collect, organize, and batch-move dragged items (files, folders, images, URLs, text) for streamlined file management. It offers built-in file actions, cloud uploads and sharing, plus extensive customization and automation features (custom actions, scripts, keyboard shortcuts, Siri Shortcuts) for power users.",
"tags": ["filesharing", "sharing", "documents", "productivity"]
}
[2026-02-08 19:38:07,953: INFO/ForkPoolWorker-2] Decoded JSON for 13: summary=True, tags=['filesharing', 'sharing', 'documents', 'productivity']
[2026-02-08 19:38:08,022: INFO/ForkPoolWorker-2] Successfully added tags ['filesharing', 'sharing', 'documents', 'productivity'] to bookmark 13
[2026-02-08 19:38:08,024: INFO/ForkPoolWorker-2] Task core.tasks.generate_summary[7ca1dbb6-005a-4fcf-b5d0-5fd6a2ae8ac5] succeeded in 10.876905062003061s: 'Generated summary and tags for bookmark 13'
[2026-02-08 19:38:08,341: INFO/ForkPoolWorker-1] AI Raw Response for 14: {
"summary": "Strella is an AI-powered customer research platform that runs AI-moderated interviews, recruits participants, generates discussion guides, and analyzes responses to deliver actionable insights within hours. Its built for teams (UX, product, consumer insights, marketing) to accelerate market research, usability testing, and concept validation and to share findings with stakeholders.",
"tags": ["marketing", "company", "sharing", "development"]
}
[2026-02-08 19:38:08,342: INFO/ForkPoolWorker-1] Decoded JSON for 14: summary=True, tags=['marketing', 'company', 'sharing', 'development']
[2026-02-08 19:38:08,422: INFO/ForkPoolWorker-1] Successfully added tags ['marketing', 'company', 'sharing', 'development'] to bookmark 14
[2026-02-08 19:38:08,426: INFO/ForkPoolWorker-1] Task core.tasks.generate_summary[71b0f9cb-36dc-4eb4-91f7-f6e8bba9c54d] succeeded in 11.034683802979998s: 'Generated summary and tags for bookmark 14'
[2026-02-09 04:08:42,072: INFO/MainProcess] Task core.tasks.process_bookmark[cd622357-234e-4dc9-9fab-eefa651ea2a4] received
[2026-02-09 04:08:43,746: INFO/ForkPoolWorker-2] HTTP Request: GET https://www.strella.io/ "HTTP/1.1 200 OK"
[2026-02-09 04:08:45,048: INFO/MainProcess] Task core.tasks.generate_summary[ba68ccad-d413-41cc-8b1c-65384d2792ab] received
[2026-02-09 04:08:45,073: INFO/ForkPoolWorker-2] Task core.tasks.process_bookmark[cd622357-234e-4dc9-9fab-eefa651ea2a4] succeeded in 2.6813507160404697s: 'Processed bookmark 15'
[2026-02-09 04:08:45,647: INFO/ForkPoolWorker-1] Generating summary/tags for bookmark 15...
[2026-02-09 04:08:51,671: INFO/ForkPoolWorker-1] AI Raw Response for 15: {
"summary": "Strella is an AI-powered customer research platform that runs AI-moderated interviews, recruits targeted participants, and analyzes responses to generate actionable insights in hours. Its designed for teams (UX, product, consumer insights, marketing) to accelerate research workflows, produce unbiased discussion guides, and share findings with stakeholders.",
"tags": ["marketing", "company", "productivity", "sharing"]
}
[2026-02-09 04:08:51,673: INFO/ForkPoolWorker-1] Decoded JSON for 15: summary=True, tags=['marketing', 'company', 'productivity', 'sharing']
[2026-02-09 04:08:51,759: INFO/ForkPoolWorker-1] Successfully added tags ['marketing', 'company', 'productivity', 'sharing'] to bookmark 15
[2026-02-09 04:08:51,894: INFO/ForkPoolWorker-1] Task core.tasks.generate_summary[ba68ccad-d413-41cc-8b1c-65384d2792ab] succeeded in 6.7007439360022545s: 'Generated summary and tags for bookmark 15'

6
check_import.py Normal file
View File

@ -0,0 +1,6 @@
try:
from taggit.serializers import TagListSerializerField, TaggitSerializer
print("Import SUCCESS")
except ImportError as e:
print(f"Import FAILED: {e}")

View File

@ -207,7 +207,7 @@ CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = TIME_ZONE CELERY_TIMEZONE = TIME_ZONE
# Run tasks synchronously in development (no Redis required) # Run tasks synchronously in development (no Redis required)
CELERY_TASK_ALWAYS_EAGER = True CELERY_TASK_ALWAYS_EAGER = False
CELERY_TASK_EAGER_PROPAGATES = True CELERY_TASK_EAGER_PROPAGATES = True
# Login/Logout Redirects # Login/Logout Redirects

View File

@ -1,10 +1,13 @@
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include, re_path
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from revproxy.views import ProxyView
from django.contrib.admin.views.decorators import staff_member_required
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('accounts/login/', auth_views.LoginView.as_view(), name='login'), path('accounts/login/', auth_views.LoginView.as_view(), name='login'),
path('accounts/logout/', auth_views.LogoutView.as_view(), name='logout'), path('accounts/logout/', auth_views.LogoutView.as_view(), name='logout'),
re_path(r'^flower/(?P<path>.*)$', staff_member_required(ProxyView.as_view(upstream='http://127.0.0.1:5555/flower/'))),
path('', include('core.urls')), path('', include('core.urls')),
] ]

Binary file not shown.

View File

@ -2,6 +2,7 @@ from rest_framework import viewsets, permissions, filters
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from django.db.models import Q
from core.models import Bookmark, Team from core.models import Bookmark, Team
from core.serializers import BookmarkSerializer, BookmarkDetailSerializer, TeamSerializer from core.serializers import BookmarkSerializer, BookmarkDetailSerializer, TeamSerializer
from core.tasks import process_bookmark from core.tasks import process_bookmark
@ -24,7 +25,11 @@ class BookmarkViewSet(viewsets.ModelViewSet):
ordering = ['-created_at'] ordering = ['-created_at']
def get_queryset(self): def get_queryset(self):
return Bookmark.objects.filter(user=self.request.user).select_related('extraction') user_teams = self.request.user.teams.all()
return Bookmark.objects.filter(
Q(user=self.request.user) |
Q(shares__team__in=user_teams)
).distinct().select_related('extraction', 'summary')
def get_serializer_class(self): def get_serializer_class(self):
if self.action == 'retrieve': if self.action == 'retrieve':
@ -40,4 +45,4 @@ class TeamViewSet(viewsets.ModelViewSet):
serializer_class = TeamSerializer serializer_class = TeamSerializer
def get_queryset(self): def get_queryset(self):
return self.request.user.teams.all() return self.request.user.teams.all()

View File

@ -13,12 +13,23 @@ class TeamSerializer(serializers.ModelSerializer):
model = Team model = Team
fields = ['id', 'name', 'description', 'created_at'] fields = ['id', 'name', 'description', 'created_at']
class ExtractionSerializer(serializers.ModelSerializer):
class Meta:
model = Extraction
fields = ['content_text', 'extracted_at']
class SummarySerializer(serializers.ModelSerializer):
class Meta:
model = Summary
fields = ['content', 'generated_at']
class BookmarkSerializer(TaggitSerializer, serializers.ModelSerializer): class BookmarkSerializer(TaggitSerializer, serializers.ModelSerializer):
tags = TagListSerializerField(required=False) tags = TagListSerializerField(required=False)
summary = SummarySerializer(read_only=True)
class Meta: class Meta:
model = Bookmark model = Bookmark
fields = ['id', 'url', 'title', 'notes', 'is_favorite', 'tags', 'created_at', 'updated_at'] fields = ['id', 'url', 'title', 'notes', 'is_favorite', 'tags', 'summary', 'created_at', 'updated_at']
read_only_fields = ['id', 'created_at', 'updated_at'] read_only_fields = ['id', 'created_at', 'updated_at']
def create(self, validated_data): def create(self, validated_data):
@ -28,11 +39,6 @@ class BookmarkSerializer(TaggitSerializer, serializers.ModelSerializer):
bookmark.tags.set(tags) bookmark.tags.set(tags)
return bookmark return bookmark
class ExtractionSerializer(serializers.ModelSerializer):
class Meta:
model = Extraction
fields = ['content_text', 'extracted_at']
class BookmarkDetailSerializer(BookmarkSerializer): class BookmarkDetailSerializer(BookmarkSerializer):
extraction = ExtractionSerializer(read_only=True) extraction = ExtractionSerializer(read_only=True)

View File

@ -7,6 +7,8 @@ from bs4 import BeautifulSoup
import html2text import html2text
import logging import logging
from urllib.parse import urlparse from urllib.parse import urlparse
from taggit.models import Tag
import json
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -100,12 +102,16 @@ def process_bookmark(self, bookmark_id):
def generate_summary(bookmark_id): def generate_summary(bookmark_id):
try: try:
bookmark = Bookmark.objects.get(id=bookmark_id) bookmark = Bookmark.objects.get(id=bookmark_id)
extraction = bookmark.extraction
except Bookmark.DoesNotExist: except Bookmark.DoesNotExist:
return return
try:
extraction = bookmark.extraction
except Extraction.DoesNotExist: except Extraction.DoesNotExist:
# If extraction doesn't exist yet, we might want to wait or just return Summary.objects.update_or_create(
# But in EAGER mode it should be there. bookmark=bookmark,
defaults={'content': "Content extraction failed or is still in progress. AI summary cannot be generated."}
)
return return
content_to_summarize = extraction.content_text.strip() content_to_summarize = extraction.content_text.strip()
@ -118,29 +124,71 @@ def generate_summary(bookmark_id):
) )
return return
# Check if we should generate tags (only if bookmark has no tags)
should_generate_tags = bookmark.tags.count() == 0
existing_tags = list(Tag.objects.values_list('name', flat=True).distinct()[:50])
existing_tags_str = ", ".join(existing_tags)
# Prepare prompt for AI # Prepare prompt for AI
if used_backup: system_prompt = "You are a helpful assistant that summarizes web content and suggests tags for researchers. Be concise and professional. Always return response in JSON format."
prompt = f"The specific page '{bookmark.url}' could not be reached. Summarize the main domain front page content instead to describe what this website is about.\n\nContent:\n{content_to_summarize[:4000]}"
user_prompt = f"Analyze the following content from the webpage '{bookmark.title or bookmark.url}'.\n\n"
user_prompt += "1. Provide a summary in 2-3 concise sentences.\n"
if should_generate_tags:
user_prompt += "2. Suggest 3-5 short and concise tags for this content.\n"
if existing_tags:
user_prompt += f"Prioritize these existing tags if they match: {existing_tags_str}\n"
user_prompt += "\nReturn your response in valid JSON format:\n"
user_prompt += "{\n \"summary\": \"your summary here\""
if should_generate_tags:
user_prompt += ",\n \"tags\": [\"tag1\", \"tag2\", \"tag3\"]\n"
else: else:
prompt = f"Summarize the following content from the webpage '{bookmark.title or bookmark.url}' in 2-3 concise sentences. Focus on the main points for a researcher.\n\nContent:\n{content_to_summarize[:4000]}" user_prompt += "\n"
user_prompt += "}\n\n"
user_prompt += f"Content:\n{content_to_summarize[:4000]}"
try: try:
logger.info(f"Generating summary/tags for bookmark {bookmark_id}...")
response = LocalAIApi.create_response({ response = LocalAIApi.create_response({
"input": [ "input": [
{"role": "system", "content": "You are a helpful assistant that summarizes web content for researchers and knowledge workers. Be concise and professional."}, {"role": "system", "content": system_prompt},
{"role": "user", "content": prompt}, {"role": "user", "content": user_prompt},
], ],
# "response_format": {"type": "json_object"} # Some proxies might not like this
}) })
summary_text = None summary_text = None
suggested_tags = []
if response.get("success"): if response.get("success"):
summary_text = LocalAIApi.extract_text(response) raw_text = LocalAIApi.extract_text(response)
logger.info(f"AI Raw Response for {bookmark_id}: {raw_text}")
data = LocalAIApi.decode_json_from_response(response)
if data:
summary_text = data.get("summary")
suggested_tags = data.get("tags", [])
logger.info(f"Decoded JSON for {bookmark_id}: summary={bool(summary_text)}, tags={suggested_tags}")
else:
logger.warning(f"JSON decoding failed for {bookmark_id}. Fallback to text.")
summary_text = raw_text
if summary_text and len(summary_text.strip()) > 10: if summary_text and len(summary_text.strip()) > 10:
Summary.objects.update_or_create( Summary.objects.update_or_create(
bookmark=bookmark, bookmark=bookmark,
defaults={'content': summary_text.strip()} defaults={'content': summary_text.strip()}
) )
# Add tags if we should
if should_generate_tags and suggested_tags:
# Limit to 5 tags and ensure they are strings
valid_tags = [str(t)[:50] for t in suggested_tags if t][:5]
if valid_tags:
bookmark.tags.add(*valid_tags)
logger.info(f"Successfully added tags {valid_tags} to bookmark {bookmark_id}")
return f"Generated summary and tags for bookmark {bookmark_id}"
return f"Generated summary for bookmark {bookmark_id}" return f"Generated summary for bookmark {bookmark_id}"
else: else:
error_msg = response.get('error') or "Empty response from AI" error_msg = response.get('error') or "Empty response from AI"
@ -161,7 +209,7 @@ def generate_summary(bookmark_id):
) )
return f"Failed to generate summary for bookmark {bookmark_id}, created fallback." return f"Failed to generate summary for bookmark {bookmark_id}, created fallback."
except Exception as e: except Exception as e:
logger.exception(f"Unexpected error in generate_summary for bookmark {bookmark_id}: {e}") logger.error(f"Unexpected error in generate_summary for bookmark {bookmark_id}: {e}")
Summary.objects.update_or_create( Summary.objects.update_or_create(
bookmark=bookmark, bookmark=bookmark,
defaults={'content': "An unexpected error occurred while generating the AI summary."} defaults={'content': "An unexpected error occurred while generating the AI summary."}

View File

@ -136,7 +136,10 @@
{{ user.username }} {{ user.username }}
</a> </a>
<ul class="dropdown-menu dropdown-menu-end shadow border-0 mt-2"> <ul class="dropdown-menu dropdown-menu-end shadow border-0 mt-2">
{% if user.is_staff %}
<li><a class="dropdown-item" href="/admin/">Admin Panel</a></li> <li><a class="dropdown-item" href="/admin/">Admin Panel</a></li>
<li><a class="dropdown-item" href="/flower/">Task Monitor</a></li>
{% endif %}
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li> <li>
<form method="post" action="{% url 'logout' %}"> <form method="post" action="{% url 'logout' %}">
@ -166,4 +169,4 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
{% block extra_js %}{% endblock %} {% block extra_js %}{% endblock %}
</body> </body>
</html> </html>

View File

@ -1,3 +1,4 @@
{
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ bookmark.title|default:bookmark.url }} - Knowledge Base{% endblock %} {% block title %}{{ bookmark.title|default:bookmark.url }} - Knowledge Base{% endblock %}
@ -14,7 +15,7 @@
<div class="col-md-8"> <div class="col-md-8">
<div class="card p-4 mb-4"> <div class="card p-4 mb-4">
<div class="d-flex justify-content-between align-items-start mb-3"> <div class="d-flex justify-content-between align-items-start mb-3">
<h1 class="h2">{{ bookmark.title|default:bookmark.url }}</h1> <h1 class="h2" id="bookmark-title">{{ bookmark.title|default:bookmark.url }}</h1>
{% if bookmark.user == request.user %} {% if bookmark.user == request.user %}
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<form action="{% url 'bookmark-regenerate' bookmark.pk %}" method="post"> <form action="{% url 'bookmark-regenerate' bookmark.pk %}" method="post">
@ -58,33 +59,37 @@
<div class="mb-4"> <div class="mb-4">
<div class="d-flex justify-content-between align-items-center mb-2"> <div class="d-flex justify-content-between align-items-center mb-2">
<h5 class="text-uppercase small fw-bold text-muted mb-0">AI Summary</h5> <h5 class="text-uppercase small fw-bold text-muted mb-0">AI Summary</h5>
{% if bookmark.user == request.user and bookmark.summary %} <div id="summary-actions" class="{% if not bookmark.summary %}d-none{% endif %}">
<button class="btn btn-link btn-sm p-0 text-decoration-none" onclick="toggleEdit('summary')">Edit</button> {% if bookmark.user == request.user %}
{% endif %} <button class="btn btn-link btn-sm p-0 text-decoration-none" onclick="toggleEdit('summary')">Edit</button>
{% endif %}
</div>
</div> </div>
{% if bookmark.summary %} <div id="summary-container">
<div id="summary-display" class="p-3 border rounded shadow-sm bg-white"> {% if bookmark.summary %}
{{ bookmark.summary.content }} <div id="summary-display" class="p-3 border rounded shadow-sm bg-white">
</div> {{ bookmark.summary.content }}
{% if bookmark.user == request.user %} </div>
<div id="summary-edit" class="d-none"> {% if bookmark.user == request.user %}
<form action="{% url 'summary-update' bookmark.pk %}" method="post"> <div id="summary-edit" class="d-none">
{% csrf_token %} <form action="{% url 'summary-update' bookmark.pk %}" method="post">
<textarea name="content" class="form-control mb-2" rows="4">{{ bookmark.summary.content }}</textarea> {% csrf_token %}
<div class="d-flex gap-2"> <textarea name="content" class="form-control mb-2" rows="4">{{ bookmark.summary.content }}</textarea>
<button type="submit" class="btn btn-primary btn-sm">Save</button> <div class="d-flex gap-2">
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="toggleEdit('summary')">Cancel</button> <button type="submit" class="btn btn-primary btn-sm">Save</button>
</div> <button type="button" class="btn btn-outline-secondary btn-sm" onclick="toggleEdit('summary')">Cancel</button>
</form> </div>
</div> </form>
</div>
{% endif %}
{% else %}
<div id="summary-loading" class="alert alert-light border text-center small py-3">
<div class="spinner-border spinner-border-sm text-primary me-2" role="status"></div>
AI Summary is being generated...
</div>
{% endif %} {% endif %}
{% else %} </div>
<div class="alert alert-light border text-center small py-3">
<div class="spinner-border spinner-border-sm text-primary me-2" role="status"></div>
AI Summary is being generated...
</div>
{% endif %}
</div> </div>
<div class="mt-4"> <div class="mt-4">
@ -97,33 +102,37 @@
<div class="card p-4"> <div class="card p-4">
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="text-uppercase small fw-bold text-muted mb-0">Extracted Text Content</h5> <h5 class="text-uppercase small fw-bold text-muted mb-0">Extracted Text Content</h5>
{% if bookmark.user == request.user and bookmark.extraction %} <div id="extraction-actions" class="{% if not bookmark.extraction %}d-none{% endif %}">
<button class="btn btn-link btn-sm p-0 text-decoration-none" onclick="toggleEdit('extraction')">Edit</button> {% if bookmark.user == request.user %}
{% endif %} <button class="btn btn-link btn-sm p-0 text-decoration-none" onclick="toggleEdit('extraction')">Edit</button>
{% endif %}
</div>
</div> </div>
{% if bookmark.extraction %} <div id="extraction-container">
<div id="extraction-display" class="extraction-content text-muted small" style="max-height: 500px; overflow-y: auto;"> {% if bookmark.extraction %}
{{ bookmark.extraction.content_text|linebreaks }} <div id="extraction-display" class="extraction-content text-muted small" style="max-height: 500px; overflow-y: auto;">
</div> {{ bookmark.extraction.content_text|linebreaks }}
{% if bookmark.user == request.user %} </div>
<div id="extraction-edit" class="d-none"> {% if bookmark.user == request.user %}
<form action="{% url 'extraction-update' bookmark.pk %}" method="post"> <div id="extraction-edit" class="d-none">
{% csrf_token %} <form action="{% url 'extraction-update' bookmark.pk %}" method="post">
<textarea name="content_text" class="form-control mb-2" rows="15">{{ bookmark.extraction.content_text }}</textarea> {% csrf_token %}
<div class="d-flex gap-2"> <textarea name="content_text" class="form-control mb-2" rows="15">{{ bookmark.extraction.content_text }}</textarea>
<button type="submit" class="btn btn-primary btn-sm">Save</button> <div class="d-flex gap-2">
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="toggleEdit('extraction')">Cancel</button> <button type="submit" class="btn btn-primary btn-sm">Save</button>
</div> <button type="button" class="btn btn-outline-secondary btn-sm" onclick="toggleEdit('extraction')">Cancel</button>
</form> </div>
</div> </form>
</div>
{% endif %}
{% else %}
<div id="extraction-loading" class="alert alert-light border text-center small py-3">
<div class="spinner-border spinner-border-sm text-primary me-2" role="status"></div>
Content is being extracted...
</div>
{% endif %} {% endif %}
{% else %} </div>
<div class="alert alert-light border text-center small py-3">
<div class="spinner-border spinner-border-sm text-primary me-2" role="status"></div>
Content is being extracted...
</div>
{% endif %}
</div> </div>
</div> </div>
@ -173,6 +182,8 @@
function toggleEdit(type) { function toggleEdit(type) {
const display = document.getElementById(type + '-display'); const display = document.getElementById(type + '-display');
const edit = document.getElementById(type + '-edit'); const edit = document.getElementById(type + '-edit');
if (!display || !edit) return;
if (display.classList.contains('d-none')) { if (display.classList.contains('d-none')) {
display.classList.remove('d-none'); display.classList.remove('d-none');
edit.classList.add('d-none'); edit.classList.add('d-none');
@ -204,5 +215,95 @@ document.querySelectorAll('.share-toggle').forEach(button => {
} }
}); });
}); });
// Polling for summary and extraction
const bookmarkId = "{{ bookmark.id }}";
const apiUrl = `/api/bookmarks/${bookmarkId}/`;
let pollInterval;
async function checkStatus() {
try {
const response = await fetch(apiUrl);
if (!response.ok) return;
const data = await response.json();
let allDone = true;
// Check Summary
if (data.summary && data.summary.content) {
const container = document.getElementById('summary-container');
const actions = document.getElementById('summary-actions');
if (document.getElementById('summary-loading')) {
container.innerHTML = `
<div id="summary-display" class="p-3 border rounded shadow-sm bg-white">
${data.summary.content}
</div>
{% if bookmark.user == request.user %}
<div id="summary-edit" class="d-none">
<form action="{% url 'summary-update' bookmark.pk %}" method="post">
{% csrf_token %}
<textarea name="content" class="form-control mb-2" rows="4">${data.summary.content}</textarea>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary btn-sm">Save</button>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="toggleEdit('summary')">Cancel</button>
</div>
</form>
</div>
{% endif %}
`;
actions.classList.remove('d-none');
}
} else {
allDone = false;
}
// Check Extraction
if (data.extraction && data.extraction.content_text) {
const container = document.getElementById('extraction-container');
const actions = document.getElementById('extraction-actions');
if (document.getElementById('extraction-loading')) {
// Convert newlines to <br> for simple display, or use linebreaks style
const formattedContent = data.extraction.content_text.replace(/\n/g, '<br>');
container.innerHTML = `
<div id="extraction-display" class="extraction-content text-muted small" style="max-height: 500px; overflow-y: auto;">
${formattedContent}
</div>
{% if bookmark.user == request.user %}
<div id="extraction-edit" class="d-none">
<form action="{% url 'extraction-update' bookmark.pk %}" method="post">
{% csrf_token %}
<textarea name="content_text" class="form-control mb-2" rows="15">${data.extraction.content_text}</textarea>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary btn-sm">Save</button>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="toggleEdit('extraction')">Cancel</button>
</div>
</form>
</div>
{% endif %}
`;
actions.classList.remove('d-none');
}
} else {
allDone = false;
}
// Update Title if it was empty
const titleEl = document.getElementById('bookmark-title');
if (data.title && (titleEl.textContent === data.url || titleEl.textContent === '')) {
titleEl.textContent = data.title;
}
if (allDone) {
clearInterval(pollInterval);
}
} catch (error) {
console.error('Error polling status:', error);
}
}
// Start polling if anything is missing
if (document.getElementById('summary-loading') || document.getElementById('extraction-loading')) {
pollInterval = setInterval(checkStatus, 3000);
}
</script> </script>
{% endblock %} {% endblock %}

View File

@ -5,10 +5,10 @@
{% block content %} {% block content %}
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-8"> <div class="col-md-8">
<div class="card p-4"> <div class="card p-4 shadow-sm border-0">
<h2 class="mb-4">{% if object %}Edit{% else %}Add New{% endif %} Bookmark</h2> <h2 class="mb-4">{% if object %}Edit{% else %}Add New{% endif %} Bookmark</h2>
<form method="post"> <form id="bookmark-form" method="post">
{% csrf_token %} {% csrf_token %}
<div class="mb-3"> <div class="mb-3">
@ -43,12 +43,62 @@
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<a href="{% url 'home' %}" class="btn btn-link text-muted">Cancel</a> <a href="{% url 'home' %}" class="btn btn-link text-muted">Cancel</a>
<button type="submit" class="btn btn-primary px-5 py-2 rounded-pill"> <button type="submit" id="submit-btn" class="btn btn-primary px-5 py-2 rounded-pill">
{% if object %}Update{% else %}Save{% endif %} Bookmark <span class="submit-text">{% if object %}Update{% else %}Save{% endif %} Bookmark</span>
<span class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
</button> </button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
{% endblock %}
{% block extra_js %}
<script>
document.getElementById('bookmark-form').addEventListener('submit', async function(e) {
// We only use AJAX for ADDING bookmarks to ensure "immediate" return
{% if not object %}
e.preventDefault();
const form = this;
const submitBtn = document.getElementById('submit-btn');
const submitText = submitBtn.querySelector('.submit-text');
const spinner = submitBtn.querySelector('.spinner-border');
// Show loading state
submitBtn.disabled = true;
submitText.classList.add('d-none');
spinner.classList.remove('d-none');
try {
const formData = new FormData(form);
const response = await fetch(window.location.href, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
if (response.ok) {
const data = await response.json();
if (data.status === 'success' && data.redirect_url) {
window.location.href = data.redirect_url;
} else {
// If not success, re-submit normally to show Django errors
form.submit();
}
} else if (response.redirected) {
window.location.href = response.url;
} else {
form.submit();
}
} catch (error) {
console.error('Error submitting form:', error);
form.submit();
}
{% endif %}
});
</script>
{% endblock %} {% endblock %}

View File

@ -58,16 +58,16 @@
</div> </div>
</form> </form>
<div class="row"> <div class="row" id="bookmarks-container">
{% for bookmark in bookmarks %} {% for bookmark in bookmarks %}
<div class="col-md-12 mb-3"> <div class="col-md-12 mb-3 bookmark-item" data-id="{{ bookmark.id }}">
<div class="card border-0 shadow-sm hover-elevate"> <div class="card border-0 shadow-sm hover-elevate">
<div class="card-body p-4"> <div class="card-body p-4">
<div class="d-flex justify-content-between align-items-start"> <div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1"> <div class="flex-grow-1">
<div class="d-flex align-items-center mb-1"> <div class="d-flex align-items-center mb-1">
<h5 class="card-title mb-0 me-2"> <h5 class="card-title mb-0 me-2">
<a href="{% url 'bookmark-detail' bookmark.pk %}" class="text-decoration-none text-dark fw-bold">{{ bookmark.title|default:bookmark.url }}</a> <a href="{% url 'bookmark-detail' bookmark.pk %}" class="text-decoration-none text-dark fw-bold bookmark-title-link">{{ bookmark.title|default:bookmark.url }}</a>
</h5> </h5>
{% if bookmark.is_favorite %} {% if bookmark.is_favorite %}
<i class="bi bi-star-fill text-warning" title="Favorite"></i> <i class="bi bi-star-fill text-warning" title="Favorite"></i>
@ -76,9 +76,20 @@
<p class="text-muted small mb-3 text-break">{{ bookmark.url }}</p> <p class="text-muted small mb-3 text-break">{{ bookmark.url }}</p>
{% if bookmark.notes %} {% if bookmark.notes %}
<p class="card-text text-secondary mb-3">{{ bookmark.notes|truncatewords:40 }}</p> <p class="card-text text-secondary mb-2">{{ bookmark.notes|truncatewords:40 }}</p>
{% endif %} {% endif %}
<div class="summary-preview mb-3">
{% if bookmark.summary %}
<p class="card-text small text-dark"><i class="bi bi-robot me-1"></i> {{ bookmark.summary.content|truncatewords:50 }}</p>
{% else %}
<div class="text-muted small loading-indicator">
<div class="spinner-border spinner-border-sm text-primary me-1" role="status"></div>
Generating AI summary...
</div>
{% endif %}
</div>
<div class="d-flex flex-wrap gap-1 mt-2"> <div class="d-flex flex-wrap gap-1 mt-2">
{% for tag in bookmark.tags.all %} {% for tag in bookmark.tags.all %}
<span class="badge bg-light text-muted border-0 small">#{{ tag.name }}</span> <span class="badge bg-light text-muted border-0 small">#{{ tag.name }}</span>
@ -153,4 +164,69 @@
transition: all 0.3s ease; transition: all 0.3s ease;
} }
</style> </style>
{% endblock %}
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const bookmarksToPoll = Array.from(document.querySelectorAll('.bookmark-item')).filter(item => {
return item.querySelector('.loading-indicator') !== null;
}).map(item => ({
element: item,
id: item.getAttribute('data-id'),
retries: 0
}));
if (bookmarksToPoll.length === 0) return;
const MAX_RETRIES = 60; // 3 minutes at 3s interval
let pollInterval = setInterval(async function() {
let stillLoading = false;
for (let i = 0; i < bookmarksToPoll.length; i++) {
const item = bookmarksToPoll[i];
const loadingIndicator = item.element.querySelector('.loading-indicator');
if (!loadingIndicator) continue;
if (item.retries >= MAX_RETRIES) {
loadingIndicator.innerHTML = '<span class="text-danger"><i class="bi bi-exclamation-triangle me-1"></i> Summarization timed out.</span>';
continue;
}
item.retries++;
try {
const response = await fetch(`/api/bookmarks/${item.id}/`);
if (response.ok) {
const data = await response.json();
// Update title if it was changed from URL to actual title
const titleLink = item.element.querySelector('.bookmark-title-link');
if (data.title && (titleLink.textContent.includes('://') || titleLink.textContent === '')) {
titleLink.textContent = data.title;
}
if (data.summary && data.summary.content) {
const summaryPreview = item.element.querySelector('.summary-preview');
const truncatedSummary = data.summary.content.split(' ').slice(0, 50).join(' ') + (data.summary.content.split(' ').length > 50 ? '...' : '');
summaryPreview.innerHTML = `<p class="card-text small text-dark"><i class="bi bi-robot me-1"></i> ${truncatedSummary}</p>`;
} else {
stillLoading = true;
}
} else if (response.status === 404) {
loadingIndicator.innerHTML = '<span class="text-danger">Bookmark not found.</span>';
} else {
stillLoading = true;
}
} catch (error) {
console.error('Error polling bookmark:', item.id, error);
stillLoading = true;
}
}
if (!stillLoading) {
clearInterval(pollInterval);
}
}, 3000);
});
</script>
{% endblock %} {% endblock %}

View File

@ -1,3 +1,56 @@
from django.test import TestCase from django.test import TestCase, Client
from django.contrib.auth.models import User
from django.urls import reverse
from .models import Bookmark
# Create your tests here. class BookmarkTagUpdateTest(TestCase):
def setUp(self):
self.user = User.objects.create_user(username='testuser', password='password')
self.client = Client()
self.client.login(username='testuser', password='password')
self.bookmark = Bookmark.objects.create(
user=self.user,
url='https://example.com',
title='Test Bookmark',
notes='Test Notes'
)
self.bookmark.tags.add('initial')
def test_update_tags(self):
url = reverse('bookmark-edit', args=[self.bookmark.pk])
data = {
'url': 'https://example.com',
'title': 'Updated Title',
'notes': 'Updated Notes',
'tags_input': 'updated, new-tag',
# 'is_favorite': False # BooleanField handling in forms usually requires 'on' or missing.
# If missing, it's false.
}
response = self.client.post(url, data)
# Check for redirect (success)
self.assertEqual(response.status_code, 302)
self.bookmark.refresh_from_db()
self.assertEqual(self.bookmark.title, 'Updated Title')
tags = list(self.bookmark.tags.names())
tags.sort()
self.assertEqual(tags, ['new-tag', 'updated'])
def test_clear_tags(self):
url = reverse('bookmark-edit', args=[self.bookmark.pk])
data = {
'url': 'https://example.com',
'title': 'Updated Title',
'notes': 'Updated Notes',
'tags_input': '', # Empty tags
}
response = self.client.post(url, data)
self.assertEqual(response.status_code, 302)
self.bookmark.refresh_from_db()
self.assertEqual(self.bookmark.tags.count(), 0)

View File

@ -4,6 +4,7 @@ from django.views.generic import ListView, CreateView, DetailView, UpdateView, D
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy, reverse from django.urls import reverse_lazy, reverse
from django.db.models import Q from django.db.models import Q
from django.db import transaction
from django.http import JsonResponse, HttpResponseRedirect from django.http import JsonResponse, HttpResponseRedirect
from .models import Bookmark, Team, Extraction, BookmarkShare, Summary from .models import Bookmark, Team, Extraction, BookmarkShare, Summary
from .tasks import process_bookmark from .tasks import process_bookmark
@ -50,14 +51,21 @@ class BookmarkCreateView(LoginRequiredMixin, CreateView):
def form_valid(self, form): def form_valid(self, form):
form.instance.user = self.request.user form.instance.user = self.request.user
response = super().form_valid(form) # Save first to get the object
self.object = form.save()
# Handle tags if provided in a separate field or as a comma-separated string # Handle tags if provided in a separate field or as a comma-separated string
tags = self.request.POST.get('tags_input') tags = self.request.POST.get('tags_input')
if tags: if tags:
form.instance.tags.add(*[t.strip() for t in tags.split(',')]) self.object.tags.add(*[t.strip() for t in tags.split(',')])
# Trigger background task
process_bookmark.delay(self.object.id) process_bookmark.delay(self.object.id)
return response
if self.request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({'status': 'success', 'redirect_url': str(self.success_url)})
return HttpResponseRedirect(self.get_success_url())
class BookmarkUpdateView(LoginRequiredMixin, UpdateView): class BookmarkUpdateView(LoginRequiredMixin, UpdateView):
model = Bookmark model = Bookmark
@ -67,6 +75,22 @@ class BookmarkUpdateView(LoginRequiredMixin, UpdateView):
def get_success_url(self): def get_success_url(self):
return reverse('bookmark-detail', kwargs={'pk': self.object.pk}) return reverse('bookmark-detail', kwargs={'pk': self.object.pk})
def form_valid(self, form):
# Handle tags update
tags_input = self.request.POST.get('tags_input', '')
with transaction.atomic():
self.object = form.save()
# Clear existing tags and set new ones
self.object.tags.clear()
if tags_input:
tag_names = [t.strip() for t in tags_input.split(',') if t.strip()]
if tag_names:
self.object.tags.add(*tag_names)
return HttpResponseRedirect(self.get_success_url())
def get_queryset(self): def get_queryset(self):
return Bookmark.objects.filter(user=self.request.user) return Bookmark.objects.filter(user=self.request.user)
@ -163,4 +187,4 @@ class BookmarkShareToggleView(LoginRequiredMixin, View):
else: else:
shared = True shared = True
return JsonResponse({'shared': shared}) return JsonResponse({'shared': shared})

63
debug_tags.py Normal file
View File

@ -0,0 +1,63 @@
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()
from django.test import Client
from django.contrib.auth.models import User
from core.models import Bookmark
from django.urls import reverse
def run():
# Setup
username = 'debug_user_123'
password = 'password'
if User.objects.filter(username=username).exists():
User.objects.get(username=username).delete()
user = User.objects.create_user(username=username, password=password)
bookmark = Bookmark.objects.create(
user=user,
url='https://debug.com',
title='Debug Bookmark',
notes='Debug Notes'
)
bookmark.tags.add('initial')
print(f"Initial tags: {list(bookmark.tags.names())}")
client = Client()
client.login(username=username, password=password)
url = reverse('bookmark-edit', args=[bookmark.pk])
# Test Clearing Tags
data = {
'url': 'https://debug.com',
'title': 'Updated Title',
'notes': 'Updated Notes',
'tags_input': '', # Empty string
'is_favorite': False
}
print(f"Posting to {url} with data: {data}")
response = client.post(url, data, HTTP_HOST='127.0.0.1')
bookmark.refresh_from_db()
final_tags = list(bookmark.tags.names())
print(f"Final tags: {final_tags}")
if final_tags == []:
print("SUCCESS: Tags cleared correctly.")
else:
print("FAILURE: Tags NOT cleared correctly.")
# Cleanup
bookmark.delete()
user.delete()
if __name__ == '__main__':
run()

66
repro_api_tags.py Normal file
View File

@ -0,0 +1,66 @@
import os
import json
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()
from django.test import Client
from django.contrib.auth.models import User
from core.models import Bookmark
from django.urls import reverse
def run():
# Setup
username = 'api_debug_user'
password = 'password'
if User.objects.filter(username=username).exists():
User.objects.get(username=username).delete()
user = User.objects.create_user(username=username, password=password)
bookmark = Bookmark.objects.create(
user=user,
url='https://api-debug.com',
title='API Debug Bookmark',
notes='Debug Notes'
)
bookmark.tags.add('initial')
print(f"Initial tags: {list(bookmark.tags.names())}")
client = Client()
client.login(username=username, password=password)
url = f'/api/bookmarks/{bookmark.id}/'
data = {
'tags': []
}
print(f"PATCHing to {url} with data: {data}")
response = client.patch(
url,
json.dumps(data),
content_type='application/json',
HTTP_HOST='127.0.0.1'
)
print(f"Response status: {response.status_code}")
print(f"Response content: {response.content}")
bookmark.refresh_from_db()
final_tags = list(bookmark.tags.names())
print(f"Final tags: {final_tags}")
if final_tags == []:
print("SUCCESS: API Tags cleared correctly.")
else:
print("FAILURE: API Tags NOT cleared correctly.")
# Cleanup
bookmark.delete()
user.delete()
if __name__ == '__main__':
run()

View File

@ -8,4 +8,6 @@ httpx==0.27.2
django-taggit==6.1.0 django-taggit==6.1.0
celery==5.4.0 celery==5.4.0
redis==5.0.8 redis==5.0.8
django-filter==24.3 django-filter==24.3
flower==2.0.1
django-revproxy==0.11.0