mirror of
https://github.com/wahyd4/links.git
synced 2026-08-08 21:04:53 +10:00
To be able to delete a screenshot
This commit is contained in:
@@ -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`
|
||||
|
||||
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
@@ -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')
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user