various bugs
This commit is contained in:
parent
556b73ecb5
commit
aade25cd3a
154
celery.log
Normal file
154
celery.log
Normal 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. It’s 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. It’s 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
6
check_import.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
try:
|
||||||
|
from taggit.serializers import TagListSerializerField, TaggitSerializer
|
||||||
|
print("Import SUCCESS")
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"Import FAILED: {e}")
|
||||||
Binary file not shown.
Binary file not shown.
@ -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
|
||||||
|
|||||||
@ -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.
Binary file not shown.
Binary file not shown.
BIN
core/__pycache__/tests.cpython-311.pyc
Normal file
BIN
core/__pycache__/tests.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
@ -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':
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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."}
|
||||||
|
|||||||
@ -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' %}">
|
||||||
|
|||||||
@ -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,11 +59,14 @@
|
|||||||
<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 %}">
|
||||||
|
{% if bookmark.user == request.user %}
|
||||||
<button class="btn btn-link btn-sm p-0 text-decoration-none" onclick="toggleEdit('summary')">Edit</button>
|
<button class="btn btn-link btn-sm p-0 text-decoration-none" onclick="toggleEdit('summary')">Edit</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="summary-container">
|
||||||
{% if bookmark.summary %}
|
{% if bookmark.summary %}
|
||||||
<div id="summary-display" class="p-3 border rounded shadow-sm bg-white">
|
<div id="summary-display" class="p-3 border rounded shadow-sm bg-white">
|
||||||
{{ bookmark.summary.content }}
|
{{ bookmark.summary.content }}
|
||||||
@ -80,12 +84,13 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="alert alert-light border text-center small py-3">
|
<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>
|
<div class="spinner-border spinner-border-sm text-primary me-2" role="status"></div>
|
||||||
AI Summary is being generated...
|
AI Summary is being generated...
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
{% for tag in bookmark.tags.all %}
|
{% for tag in bookmark.tags.all %}
|
||||||
@ -97,11 +102,14 @@
|
|||||||
<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 %}">
|
||||||
|
{% if bookmark.user == request.user %}
|
||||||
<button class="btn btn-link btn-sm p-0 text-decoration-none" onclick="toggleEdit('extraction')">Edit</button>
|
<button class="btn btn-link btn-sm p-0 text-decoration-none" onclick="toggleEdit('extraction')">Edit</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="extraction-container">
|
||||||
{% if bookmark.extraction %}
|
{% if bookmark.extraction %}
|
||||||
<div id="extraction-display" class="extraction-content text-muted small" style="max-height: 500px; overflow-y: auto;">
|
<div id="extraction-display" class="extraction-content text-muted small" style="max-height: 500px; overflow-y: auto;">
|
||||||
{{ bookmark.extraction.content_text|linebreaks }}
|
{{ bookmark.extraction.content_text|linebreaks }}
|
||||||
@ -119,13 +127,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="alert alert-light border text-center small py-3">
|
<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>
|
<div class="spinner-border spinner-border-sm text-primary me-2" role="status"></div>
|
||||||
Content is being extracted...
|
Content is being extracted...
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
{% if bookmark.user == request.user %}
|
{% if bookmark.user == request.user %}
|
||||||
@ -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 %}
|
||||||
@ -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,8 +43,9 @@
|
|||||||
|
|
||||||
<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>
|
||||||
@ -52,3 +53,52 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% 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 %}
|
||||||
@ -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>
|
||||||
@ -154,3 +165,68 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% 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 %}
|
||||||
@ -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)
|
||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
63
debug_tags.py
Normal file
63
debug_tags.py
Normal 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
66
repro_api_tags.py
Normal 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()
|
||||||
@ -9,3 +9,5 @@ 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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user