To be able to delete a screenshot

This commit is contained in:
2024-11-13 22:50:44 +11:00
parent 2637102631
commit 94589acedb
10 changed files with 216 additions and 48 deletions
+2 -2
View File
@@ -27,8 +27,8 @@ A URL management tool that helps you organize and access your links efficiently.
3. Build and start services:
```bash
./manage-docker.sh build
./manage-docker.sh start
./docker.sh build
./docker.sh start
```
The application will be available at `http://localhost:8000`
BIN
View File
Binary file not shown.
@@ -0,0 +1,31 @@
from django.db import migrations
def migrate_screenshots(apps, schema_editor):
Page = apps.get_model('links', 'Page')
Screenshot = apps.get_model('links', 'Screenshot')
for page in Page.objects.all():
if page.screenshot_path:
Screenshot.objects.create(
page=page,
path=page.screenshot_path,
status='completed'
)
def reverse_migrate(apps, schema_editor):
Page = apps.get_model('links', 'Page')
Screenshot = apps.get_model('links', 'Screenshot')
for screenshot in Screenshot.objects.filter(status='completed'):
screenshot.page.screenshot_path = screenshot.path
screenshot.page.save()
class Migration(migrations.Migration):
dependencies = [
('links', '0015_create_screenshot_model'),
]
operations = [
migrations.RunPython(migrate_screenshots, reverse_migrate),
]
@@ -0,0 +1,14 @@
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('links', '0016_migrate_existing_screenshots'),
]
operations = [
migrations.RemoveField(
model_name='page',
name='screenshot_path',
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.0.9 on 2024-11-13 11:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('links', '0017_remove_old_screenshot_fields'),
]
operations = [
migrations.AddField(
model_name='page',
name='screenshot_path',
field=models.CharField(blank=True, max_length=255),
),
]
+6 -2
View File
@@ -177,10 +177,14 @@ class Page(models.Model):
self.retry_count < 3
def get_screenshot_url(self):
if self.screenshot_path:
return f'/media/{self.screenshot_path}'
latest_screenshot = self.screenshots.first()
if latest_screenshot and latest_screenshot.path:
return os.path.join(settings.MEDIA_URL, latest_screenshot.path)
return None
def get_latest_screenshot(self):
return self.screenshots.first()
class Screenshot(models.Model):
class Status(models.TextChoices):
PENDING = 'pending', _('Pending')
+34
View File
@@ -222,6 +222,40 @@ class PageViewSet(viewsets.ModelViewSet):
'error': str(e)
}, status=400)
@action(detail=False, methods=['delete'], url_path='screenshots/(?P<screenshot_id>[^/.]+)')
def delete_screenshot(self, request, screenshot_id=None):
"""Delete a specific screenshot"""
try:
screenshot = Screenshot.objects.get(id=screenshot_id)
# Optional: Add permission check here
# if screenshot.page.user != request.user:
# return Response(status=status.HTTP_403_FORBIDDEN)
# Delete the actual file
if screenshot.path:
file_path = os.path.join(settings.MEDIA_ROOT, screenshot.path)
try:
os.remove(file_path)
except OSError:
pass # File doesn't exist or can't be deleted
# Delete the database record
screenshot.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except Screenshot.DoesNotExist:
return Response(
{"error": "Screenshot not found"},
status=status.HTTP_404_NOT_FOUND
)
except Exception as e:
return Response(
{"error": str(e)},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
class ScreenshotGalleryView(TemplateView):
template_name = 'links/screenshot_gallery.html'
+10 -4
View File
@@ -213,7 +213,7 @@ def capture_screenshot(self, page_id, screenshot_id):
@shared_task(bind=True, max_retries=3)
def process_page(self, page_id):
"""Task to process a page and extract its metadata"""
from .models import Page
from .models import Page, Screenshot
try:
page = Page.objects.get(id=page_id)
@@ -250,12 +250,18 @@ def process_page(self, page_id):
if description:
page.summary = description[:500]
# Trigger screenshot capture if needed
if not page.screenshot_path:
capture_screenshot.delay(page.id)
# Create a new screenshot record and trigger capture
screenshot = Screenshot.objects.create(
page=page,
status=Screenshot.Status.PENDING
)
# Trigger screenshot capture with the new screenshot ID
capture_screenshot.delay(page.id, screenshot.id)
page.process_status = Page.ProcessStatus.COMPLETED
page.save()
logger.info(f"Successfully processed page {page_id}")
except Exception as exc:
+87 -28
View File
@@ -5,6 +5,19 @@
{% block content %}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Back Button -->
<div class="mb-6">
<a href="{% url 'page-list' %}"
class="inline-flex items-center p-2 text-gray-600 hover:text-gray-800 hover:bg-gray-50 rounded-md"
title="{% trans 'Back to List' %}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
</svg>
<span class="ml-1 text-sm">{% trans "Back to List" %}</span>
</a>
</div>
<div class="bg-white shadow-sm rounded-lg overflow-hidden">
<div class="px-4 py-5 sm:p-6">
<!-- Header with actions -->
@@ -113,9 +126,11 @@
<div class="mt-8">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-medium text-gray-900">{% trans "Screenshots" %}</h3>
<button id="takeScreenshotBtn"
onclick="takeNewScreenshot({{ page.id }})"
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<button
id="takeScreenshotBtn"
type="button"
onclick="takeNewScreenshot({{ page.id }})"
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
{% trans "Take Another Screenshot" %}
</button>
</div>
@@ -167,31 +182,54 @@
<h4 class="text-sm font-medium text-gray-700 mb-2">{% trans "Previous Screenshots" %}</h4>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
{% for screenshot in previous_screenshots %}
<div class="relative">
<a href="{{ screenshot.get_url }}" target="_blank" class="block">
{% if screenshot.path %}
<img src="{{ screenshot.get_url }}"
alt="Screenshot {{ forloop.counter }}"
class="w-48 h-48 object-cover rounded-lg shadow-sm">
{% else %}
<img src="{% static 'images/default-screenshot.png' %}"
alt="Pending screenshot {{ forloop.counter }}"
class="w-48 h-48 object-cover rounded-lg shadow-sm">
{% endif %}
</a>
<div class=""">
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium
{% if screenshot.status == 'completed' %}bg-green-100 text-green-800
{% elif screenshot.status == 'processing' %}bg-yellow-100 text-yellow-800
{% elif screenshot.status == 'failed' %}bg-red-100 text-red-800
{% else %}bg-gray-100 text-gray-800{% endif %}">
{{ screenshot.get_status_display }}
</span>
</div>
<div class="mt-1 text-xs text-gray-500">
<span class="local-datetime" data-timestamp="{{ screenshot.created_at|date:'c' }}">
{{ screenshot.created_at|date:"Y-m-d H:i" }}
</span>
<div class="relative flex flex-col">
<!-- Image Container with Overlay -->
<div class="relative w-48 h-48 group rounded-lg overflow-hidden">
<!-- Image -->
<a href="{{ screenshot.get_url }}" target="_blank" class="block h-full">
{% if screenshot.path %}
<img src="{{ screenshot.get_url }}"
alt="Screenshot {{ forloop.counter }}"
class="w-full h-full object-cover">
{% else %}
<img src="{% static 'images/default-screenshot.png' %}"
alt="Pending screenshot {{ forloop.counter }}"
class="w-full h-full object-cover">
{% endif %}
</a>
<!-- Top Overlay for Delete Button -->
<div class="absolute top-0 left-0 right-0 p-2 bg-gradient-to-b from-black/40 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<div class="flex justify-end">
<button type="button"
onclick="deleteScreenshot({{ screenshot.id }})"
class="p-1.5 bg-red-100 text-red-600 rounded-full hover:bg-red-200 transition-colors duration-200"
title="{% trans 'Delete Screenshot' %}">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
</div>
<!-- Bottom Overlay for Status and Time -->
<div class="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent">
<div class="flex flex-col space-y-1.5">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium
{% if screenshot.status == 'completed' %}bg-green-100 text-green-800
{% elif screenshot.status == 'processing' %}bg-yellow-100 text-yellow-800
{% elif screenshot.status == 'failed' %}bg-red-100 text-red-800
{% else %}bg-gray-100 text-gray-800{% endif %}">
{{ screenshot.get_status_display }}
</span>
<span class="text-xs text-white font-medium">
<span class="local-datetime" data-timestamp="{{ screenshot.created_at|date:'c' }}">
{{ screenshot.created_at|date:"Y-m-d H:i" }}
</span>
</span>
</div>
</div>
</div>
</div>
{% endfor %}
@@ -255,5 +293,26 @@ function getCookie(name) {
}
return cookieValue;
}
function deleteScreenshot(screenshotId) {
if (confirm('{% trans "Are you sure you want to delete this screenshot?" %}')) {
fetch(`/api/pages/screenshots/${screenshotId}`, {
method: 'DELETE',
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
location.reload();
})
.catch(error => {
console.error('Error:', error);
alert('Failed to delete screenshot');
});
}
}
</script>
{% endblock %}
+14 -12
View File
@@ -16,20 +16,22 @@
{% for page in pages %}
<li class="p-4">
<div class="flex space-x-4">
<!-- Thumbnail Container - hidden on small screens, visible on sm and up -->
<!-- Thumbnail Container -->
<div class="hidden sm:block flex-shrink-0 w-48 h-48 rounded-lg overflow-hidden bg-gray-100 border border-gray-200">
<div class="w-full h-full flex items-center justify-center">
{% if page.screenshot_path %}
<img src="{{ page.get_screenshot_url }}"
alt="Page thumbnail"
class="w-full h-full object-cover"
loading="lazy"
onerror="this.onerror=null; this.src='{% static 'images/default-screenshot.png' %}'; this.classList.add('object-contain', 'p-4')">
{% else %}
<img src="{% static 'images/default-screenshot.png' %}"
alt="Default thumbnail"
class="w-3/4 h-3/4 object-contain">
{% endif %}
{% with latest_screenshot=page.screenshots.first %}
{% if latest_screenshot and latest_screenshot.path %}
<img src="{{ latest_screenshot.get_url }}"
alt="Page thumbnail"
class="w-full h-full object-cover"
loading="lazy"
onerror="this.onerror=null; this.src='{% static 'images/default-screenshot.png' %}'; this.classList.add('object-contain', 'p-4');">
{% else %}
<img src="{% static 'images/default-screenshot.png' %}"
alt="Default thumbnail"
class="w-3/4 h-3/4 object-contain">
{% endif %}
{% endwith %}
</div>
</div>