New card style frontend and alt auth token for api
This commit is contained in:
parent
aade25cd3a
commit
42369b6e1a
BIN
assets/pasted-20260209-182712-203381fe.png
Normal file
BIN
assets/pasted-20260209-182712-203381fe.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
12
celery.log
12
celery.log
@ -152,3 +152,15 @@ you should set broker_connection_retry_on_startup to True.
|
|||||||
[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,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,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'
|
[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'
|
||||||
|
[2026-02-09 18:30:44,317: INFO/MainProcess] Task core.tasks.process_bookmark[db2f07fc-75d0-4a11-961e-0a34e1cde744] received
|
||||||
|
[2026-02-09 18:30:48,712: INFO/ForkPoolWorker-2] HTTP Request: GET https://github.com/pydantic/monty "HTTP/1.1 200 OK"
|
||||||
|
[2026-02-09 18:30:50,263: INFO/MainProcess] Task core.tasks.generate_summary[944ef664-f321-45b6-82f8-607291008ea6] received
|
||||||
|
[2026-02-09 18:30:50,374: INFO/ForkPoolWorker-2] Task core.tasks.process_bookmark[db2f07fc-75d0-4a11-961e-0a34e1cde744] succeeded in 5.521026417030953s: 'Processed bookmark 27'
|
||||||
|
[2026-02-09 18:30:51,857: INFO/ForkPoolWorker-1] Generating summary/tags for bookmark 27...
|
||||||
|
[2026-02-09 18:31:03,704: INFO/ForkPoolWorker-1] AI Raw Response for 27: {
|
||||||
|
"summary": "monty is a minimal, secure Python interpreter implemented in Rust, designed for safely executing Python code in AI contexts. The GitHub repository from pydantic provides the codebase and tooling to sandbox and integrate this interpreter for AI agents and applications.",
|
||||||
|
"tags": ["development", "research", "new", "company"]
|
||||||
|
}
|
||||||
|
[2026-02-09 18:31:03,713: INFO/ForkPoolWorker-1] Decoded JSON for 27: summary=True, tags=['development', 'research', 'new', 'company']
|
||||||
|
[2026-02-09 18:31:03,987: INFO/ForkPoolWorker-1] Successfully added tags ['development', 'research', 'new', 'company'] to bookmark 27
|
||||||
|
[2026-02-09 18:31:04,321: INFO/ForkPoolWorker-1] Task core.tasks.generate_summary[944ef664-f321-45b6-82f8-607291008ea6] succeeded in 13.666462198016234s: 'Generated summary and tags for bookmark 27'
|
||||||
|
|||||||
Binary file not shown.
@ -192,6 +192,7 @@ REST_FRAMEWORK = {
|
|||||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||||
'rest_framework.authentication.SessionAuthentication',
|
'rest_framework.authentication.SessionAuthentication',
|
||||||
'rest_framework.authentication.BasicAuthentication',
|
'rest_framework.authentication.BasicAuthentication',
|
||||||
|
'core.authentication.APITokenAuthentication',
|
||||||
],
|
],
|
||||||
'DEFAULT_FILTER_BACKENDS': (
|
'DEFAULT_FILTER_BACKENDS': (
|
||||||
'django_filters.rest_framework.DjangoFilterBackend',
|
'django_filters.rest_framework.DjangoFilterBackend',
|
||||||
@ -212,4 +213,4 @@ CELERY_TASK_EAGER_PROPAGATES = True
|
|||||||
|
|
||||||
# Login/Logout Redirects
|
# Login/Logout Redirects
|
||||||
LOGIN_REDIRECT_URL = 'home'
|
LOGIN_REDIRECT_URL = 'home'
|
||||||
LOGOUT_REDIRECT_URL = 'home'
|
LOGOUT_REDIRECT_URL = 'home'
|
||||||
|
|||||||
Binary file not shown.
BIN
core/__pycache__/authentication.cpython-311.pyc
Normal file
BIN
core/__pycache__/authentication.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,5 +1,5 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import Team, TeamMembership, Bookmark, BookmarkShare, Extraction, Summary
|
from .models import Team, TeamMembership, Bookmark, BookmarkShare, Extraction, Summary, APIToken
|
||||||
|
|
||||||
class TeamMembershipInline(admin.TabularInline):
|
class TeamMembershipInline(admin.TabularInline):
|
||||||
model = TeamMembership
|
model = TeamMembership
|
||||||
@ -27,4 +27,11 @@ class ExtractionAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(Summary)
|
@admin.register(Summary)
|
||||||
class SummaryAdmin(admin.ModelAdmin):
|
class SummaryAdmin(admin.ModelAdmin):
|
||||||
list_display = ('bookmark', 'generated_at')
|
list_display = ('bookmark', 'generated_at')
|
||||||
|
|
||||||
|
@admin.register(APIToken)
|
||||||
|
class APITokenAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'user', 'created_at', 'last_used_at', 'is_active')
|
||||||
|
list_filter = ('is_active', 'created_at', 'last_used_at')
|
||||||
|
search_fields = ('name', 'user__username', 'token')
|
||||||
|
readonly_fields = ('token', 'created_at', 'last_used_at')
|
||||||
|
|||||||
35
core/authentication.py
Normal file
35
core/authentication.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
from rest_framework.authentication import BaseAuthentication
|
||||||
|
from rest_framework.exceptions import AuthenticationFailed
|
||||||
|
from django.utils import timezone
|
||||||
|
from .models import APIToken
|
||||||
|
|
||||||
|
class APITokenAuthentication(BaseAuthentication):
|
||||||
|
def authenticate(self, request):
|
||||||
|
auth_header = request.headers.get('Authorization')
|
||||||
|
if not auth_header:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Support "Bearer <token>" and "Token <token>"
|
||||||
|
prefix, token = auth_header.split()
|
||||||
|
if prefix.lower() not in ['bearer', 'token']:
|
||||||
|
return None
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self._authenticate_credentials(token)
|
||||||
|
|
||||||
|
def _authenticate_credentials(self, key):
|
||||||
|
try:
|
||||||
|
token = APIToken.objects.select_related('user').get(token=key, is_active=True)
|
||||||
|
except APIToken.DoesNotExist:
|
||||||
|
raise AuthenticationFailed('Invalid or inactive API token')
|
||||||
|
|
||||||
|
if not token.user.is_active:
|
||||||
|
raise AuthenticationFailed('User is inactive or deleted')
|
||||||
|
|
||||||
|
# Update last used timestamp
|
||||||
|
token.last_used_at = timezone.now()
|
||||||
|
token.save(update_fields=['last_used_at'])
|
||||||
|
|
||||||
|
return (token.user, token)
|
||||||
28
core/migrations/0002_apitoken.py
Normal file
28
core/migrations/0002_apitoken.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-09 18:35
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0001_initial'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='APIToken',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('token', models.CharField(editable=False, max_length=64, unique=True)),
|
||||||
|
('name', models.CharField(default='Default Token', help_text="A recognizable name for this token (e.g. 'Mobile App')", max_length=255)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('last_used_at', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('is_active', models.BooleanField(default=True)),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='api_tokens', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
BIN
core/migrations/__pycache__/0002_apitoken.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0002_apitoken.cpython-311.pyc
Normal file
Binary file not shown.
@ -1,6 +1,7 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from taggit.managers import TaggableManager
|
from taggit.managers import TaggableManager
|
||||||
|
import secrets
|
||||||
|
|
||||||
class Team(models.Model):
|
class Team(models.Model):
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
@ -66,3 +67,19 @@ class Summary(models.Model):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Summary for {self.bookmark}"
|
return f"Summary for {self.bookmark}"
|
||||||
|
|
||||||
|
class APIToken(models.Model):
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='api_tokens')
|
||||||
|
token = models.CharField(max_length=64, unique=True, editable=False)
|
||||||
|
name = models.CharField(max_length=255, help_text="A recognizable name for this token (e.g. 'Mobile App')", default="Default Token")
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
last_used_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
is_active = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.token:
|
||||||
|
self.token = secrets.token_hex(20)
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name} - {self.user.username}"
|
||||||
@ -1,4 +1,5 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from django.db import transaction
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from core.models import Bookmark, Team, TeamMembership, BookmarkShare, Extraction, Summary
|
from core.models import Bookmark, Team, TeamMembership, BookmarkShare, Extraction, Summary
|
||||||
from taggit.serializers import TagListSerializerField, TaggitSerializer
|
from taggit.serializers import TagListSerializerField, TaggitSerializer
|
||||||
@ -35,10 +36,19 @@ class BookmarkSerializer(TaggitSerializer, serializers.ModelSerializer):
|
|||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
user = self.context['request'].user
|
user = self.context['request'].user
|
||||||
tags = validated_data.pop('tags', [])
|
tags = validated_data.pop('tags', [])
|
||||||
bookmark = Bookmark.objects.create(user=user, **validated_data)
|
with transaction.atomic():
|
||||||
bookmark.tags.set(tags)
|
bookmark = Bookmark.objects.create(user=user, **validated_data)
|
||||||
|
bookmark.tags.set(tags)
|
||||||
return bookmark
|
return bookmark
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
tags = validated_data.pop('tags', None)
|
||||||
|
with transaction.atomic():
|
||||||
|
instance = super().update(instance, validated_data)
|
||||||
|
if tags is not None:
|
||||||
|
instance.tags.set(tags)
|
||||||
|
return instance
|
||||||
|
|
||||||
class BookmarkDetailSerializer(BookmarkSerializer):
|
class BookmarkDetailSerializer(BookmarkSerializer):
|
||||||
extraction = ExtractionSerializer(read_only=True)
|
extraction = ExtractionSerializer(read_only=True)
|
||||||
|
|
||||||
|
|||||||
@ -58,50 +58,71 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="row" id="bookmarks-container">
|
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4" id="bookmarks-container">
|
||||||
{% for bookmark in bookmarks %}
|
{% for bookmark in bookmarks %}
|
||||||
<div class="col-md-12 mb-3 bookmark-item" data-id="{{ bookmark.id }}">
|
<div class="col bookmark-item" data-id="{{ bookmark.id }}">
|
||||||
<div class="card border-0 shadow-sm hover-elevate">
|
<div class="card h-100 border-0 shadow-sm hover-elevate overflow-hidden">
|
||||||
<div class="card-body p-4">
|
<div class="card-body d-flex flex-column">
|
||||||
<div class="d-flex justify-content-between align-items-start">
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||||
<div class="flex-grow-1">
|
<h5 class="card-title h6 fw-bold mb-0 text-truncate w-100" title="{{ bookmark.title|default:bookmark.url }}">
|
||||||
<div class="d-flex align-items-center mb-1">
|
<a href="{% url 'bookmark-detail' bookmark.pk %}" class="text-decoration-none text-dark bookmark-title-link stretched-link">
|
||||||
<h5 class="card-title mb-0 me-2">
|
{% if bookmark.title %}
|
||||||
<a href="{% url 'bookmark-detail' bookmark.pk %}" class="text-decoration-none text-dark fw-bold bookmark-title-link">{{ bookmark.title|default:bookmark.url }}</a>
|
{{ bookmark.title|truncatechars:50 }}
|
||||||
</h5>
|
|
||||||
{% if bookmark.is_favorite %}
|
|
||||||
<i class="bi bi-star-fill text-warning" title="Favorite"></i>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<p class="text-muted small mb-3 text-break">{{ bookmark.url }}</p>
|
|
||||||
|
|
||||||
{% if bookmark.notes %}
|
|
||||||
<p class="card-text text-secondary mb-2">{{ bookmark.notes|truncatewords:40 }}</p>
|
|
||||||
{% 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 %}
|
{% else %}
|
||||||
<div class="text-muted small loading-indicator">
|
{{ bookmark.url|truncatechars:50 }}
|
||||||
<div class="spinner-border spinner-border-sm text-primary me-1" role="status"></div>
|
|
||||||
Generating AI summary...
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</a>
|
||||||
|
</h5>
|
||||||
|
{% if bookmark.is_favorite %}
|
||||||
|
<i class="bi bi-star-fill text-warning ms-2 flex-shrink-0" title="Favorite"></i>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="d-flex flex-wrap gap-1 mt-2">
|
<div class="mb-3">
|
||||||
{% for tag in bookmark.tags.all %}
|
<small class="text-muted d-block text-truncate">
|
||||||
<span class="badge bg-light text-muted border-0 small">#{{ tag.name }}</span>
|
<i class="bi bi-link-45deg me-1"></i>{{ bookmark.url }}
|
||||||
{% endfor %}
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3 flex-grow-1">
|
||||||
|
{% if bookmark.notes %}
|
||||||
|
<p class="card-text small text-secondary mb-2">{{ bookmark.notes|truncatewords:20 }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="summary-preview">
|
||||||
|
{% if bookmark.summary %}
|
||||||
|
<p class="card-text small text-dark bg-light p-2 rounded border-start border-3 border-primary">
|
||||||
|
{{ bookmark.summary.content|truncatewords:20 }}
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-muted small loading-indicator">
|
||||||
|
<div class="spinner-border spinner-border-sm text-primary me-1" role="status"></div>
|
||||||
|
Generating...
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-end ms-3">
|
</div>
|
||||||
<div class="dropdown">
|
|
||||||
<button class="btn btn-link text-muted p-0" type="button" data-bs-toggle="dropdown">
|
<div class="mt-auto">
|
||||||
<i class="bi bi-three-dots-vertical"></i>
|
<div class="d-flex flex-wrap gap-1 mb-3" style="max-height: 50px; overflow: hidden;">
|
||||||
|
{% for tag in bookmark.tags.all|slice:":3" %}
|
||||||
|
<span class="badge bg-light text-muted border small">#{{ tag.name }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
{% if bookmark.tags.all.count > 3 %}
|
||||||
|
<span class="badge bg-light text-muted border small">+{{ bookmark.tags.all.count|add:"-3" }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center pt-2 border-top">
|
||||||
|
<small class="text-muted" style="font-size: 0.75rem;">
|
||||||
|
{{ bookmark.created_at|date:"M d, Y" }}
|
||||||
|
</small>
|
||||||
|
|
||||||
|
<div class="dropdown position-static">
|
||||||
|
<button class="btn btn-link text-muted p-0" type="button" data-bs-toggle="dropdown" style="z-index: 2; position: relative;">
|
||||||
|
<i class="bi bi-three-dots"></i>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<ul class="dropdown-menu dropdown-menu-end shadow">
|
||||||
<li><a class="dropdown-item" href="{% url 'bookmark-detail' bookmark.pk %}">View Details</a></li>
|
<li><a class="dropdown-item" href="{% url 'bookmark-detail' bookmark.pk %}">View Details</a></li>
|
||||||
<li><a class="dropdown-item" href="{% url 'bookmark-edit' bookmark.pk %}">Edit</a></li>
|
<li><a class="dropdown-item" href="{% url 'bookmark-edit' bookmark.pk %}">Edit</a></li>
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
@ -113,7 +134,6 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-muted small d-block mt-4">{{ bookmark.created_at|date:"M d" }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -134,7 +154,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if is_paginated %}
|
{% if is_paginated %}
|
||||||
<nav class="mt-4">
|
<nav class="mt-5">
|
||||||
<ul class="pagination justify-content-center">
|
<ul class="pagination justify-content-center">
|
||||||
{% if page_obj.has_previous %}
|
{% if page_obj.has_previous %}
|
||||||
<li class="page-item"><a class="page-link rounded-circle me-2 border-0 shadow-sm" href="?page={{ page_obj.previous_page_number }}{% if request.GET.q %}&q={{ request.GET.q }}{% endif %}{% if request.GET.tag %}&tag={{ request.GET.tag }}{% endif %}"><i class="bi bi-chevron-left"></i></a></li>
|
<li class="page-item"><a class="page-link rounded-circle me-2 border-0 shadow-sm" href="?page={{ page_obj.previous_page_number }}{% if request.GET.q %}&q={{ request.GET.q }}{% endif %}{% if request.GET.tag %}&tag={{ request.GET.tag }}{% endif %}"><i class="bi bi-chevron-left"></i></a></li>
|
||||||
@ -163,6 +183,9 @@
|
|||||||
box-shadow: 0 10px 20px rgba(0,0,0,0.08) !important;
|
box-shadow: 0 10px 20px rgba(0,0,0,0.08) !important;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
.bookmark-item .card {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@ -201,14 +224,14 @@
|
|||||||
|
|
||||||
// Update title if it was changed from URL to actual title
|
// Update title if it was changed from URL to actual title
|
||||||
const titleLink = item.element.querySelector('.bookmark-title-link');
|
const titleLink = item.element.querySelector('.bookmark-title-link');
|
||||||
if (data.title && (titleLink.textContent.includes('://') || titleLink.textContent === '')) {
|
if (data.title && (titleLink.textContent.includes('://') || titleLink.textContent.trim().length === 0)) {
|
||||||
titleLink.textContent = data.title;
|
titleLink.textContent = data.title.length > 50 ? data.title.substring(0, 50) + '...' : data.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.summary && data.summary.content) {
|
if (data.summary && data.summary.content) {
|
||||||
const summaryPreview = item.element.querySelector('.summary-preview');
|
const summaryPreview = item.element.querySelector('.summary-preview');
|
||||||
const truncatedSummary = data.summary.content.split(' ').slice(0, 50).join(' ') + (data.summary.content.split(' ').length > 50 ? '...' : '');
|
const truncatedSummary = data.summary.content.split(' ').slice(0, 20).join(' ') + (data.summary.content.split(' ').length > 20 ? '...' : '');
|
||||||
summaryPreview.innerHTML = `<p class="card-text small text-dark"><i class="bi bi-robot me-1"></i> ${truncatedSummary}</p>`;
|
summaryPreview.innerHTML = `<p class="card-text small text-dark bg-light p-2 rounded border-start border-3 border-primary">${truncatedSummary}</p>`;
|
||||||
} else {
|
} else {
|
||||||
stillLoading = true;
|
stillLoading = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,6 @@ django.setup()
|
|||||||
from django.test import Client
|
from django.test import Client
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from core.models import Bookmark
|
from core.models import Bookmark
|
||||||
from django.urls import reverse
|
|
||||||
|
|
||||||
def run():
|
def run():
|
||||||
# Setup
|
# Setup
|
||||||
@ -34,8 +33,10 @@ def run():
|
|||||||
|
|
||||||
url = f'/api/bookmarks/{bookmark.id}/'
|
url = f'/api/bookmarks/{bookmark.id}/'
|
||||||
|
|
||||||
|
# Test 1: Update tags and title
|
||||||
data = {
|
data = {
|
||||||
'tags': []
|
'tags': ['new', 'tags'],
|
||||||
|
'title': 'Updated Title'
|
||||||
}
|
}
|
||||||
|
|
||||||
print(f"PATCHing to {url} with data: {data}")
|
print(f"PATCHing to {url} with data: {data}")
|
||||||
@ -47,18 +48,32 @@ def run():
|
|||||||
)
|
)
|
||||||
|
|
||||||
print(f"Response status: {response.status_code}")
|
print(f"Response status: {response.status_code}")
|
||||||
print(f"Response content: {response.content}")
|
|
||||||
|
|
||||||
bookmark.refresh_from_db()
|
bookmark.refresh_from_db()
|
||||||
final_tags = list(bookmark.tags.names())
|
final_tags = sorted(list(bookmark.tags.names()))
|
||||||
|
final_title = bookmark.title
|
||||||
print(f"Final tags: {final_tags}")
|
print(f"Final tags: {final_tags}")
|
||||||
|
print(f"Final title: {final_title}")
|
||||||
|
|
||||||
if final_tags == []:
|
success = True
|
||||||
print("SUCCESS: API Tags cleared correctly.")
|
if final_tags != ['new', 'tags']:
|
||||||
|
print("FAILURE: API Tags NOT updated correctly.")
|
||||||
|
success = False
|
||||||
else:
|
else:
|
||||||
print("FAILURE: API Tags NOT cleared correctly.")
|
print("SUCCESS: API Tags updated correctly.")
|
||||||
|
|
||||||
|
if final_title != 'Updated Title':
|
||||||
|
print("FAILURE: API Title NOT updated correctly.")
|
||||||
|
success = False
|
||||||
|
else:
|
||||||
|
print("SUCCESS: API Title updated correctly.")
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
|
if success:
|
||||||
|
print("All tests passed.")
|
||||||
|
else:
|
||||||
|
print("Some tests failed.")
|
||||||
|
|
||||||
bookmark.delete()
|
bookmark.delete()
|
||||||
user.delete()
|
user.delete()
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user