Add pages

This commit is contained in:
2024-11-04 13:43:30 +11:00
parent f3a3eba0a3
commit 827b54d719
18 changed files with 740 additions and 256 deletions
+8 -30
View File
@@ -4,44 +4,22 @@ FROM python:3.12-slim
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV DJANGO_SETTINGS_MODULE url_manager.settings
# Set work directory
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
nodejs \
npm \
gettext \
RUN apt-get update && apt-get install -y \
build-essential \
python3-dev \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt /app/
RUN pip install --upgrade pip && pip install -r requirements.txt
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy project
COPY . /app/
COPY . .
# Install Tailwind CSS dependencies
RUN python manage.py tailwind install
# Build Tailwind CSS
RUN python manage.py tailwind build
# Collect static files
RUN python manage.py collectstatic --noinput
# Compile translation messages
RUN python manage.py compilemessages --locale=zh_Hans
# Create data directory for SQLite database
RUN mkdir -p /app/data && chmod 777 /app/data
ENV PYTHONPATH=/app
# Expose port 8000
EXPOSE 8000
# Run the application
CMD ["gunicorn", "--chdir", "/app", "url_manager.wsgi:application", "--bind", "0.0.0.0:8000"]
# Remove the automatic migration
# RUN python manage.py migrate
+134 -87
View File
@@ -1,135 +1,182 @@
# URL Manager
URL Manager is a Django-based web application for managing and tracking short URLs.
A URL management tool that helps you organize and access your links efficiently.
## Features
- Create, edit, and delete short URL links
- Track click statistics for each link
- Search functionality for quick access to links
- Internationalization support (English and Simplified Chinese)
- Responsive design using Tailwind CSS
- Create and manage short links
- Template links with dynamic parameters
- Bookmark pages with automatic title and summary extraction
- Advanced search capabilities
- API access
- Asynchronous page processing
## Prerequisites
## Installation
- Docker and Docker Compose
### Using Docker (Recommended)
## Installation and Setup with Docker
1. Prerequisites:
- Docker
- Docker Compose
1. Clone the repository:
```
2. Clone the repository:
```bash
git clone https://github.com/yourusername/url-manager.git
cd url-manager
```
2. Build the Docker image:
```
docker build -t url-manager .
3. Build and start services:
```bash
./manage-docker.sh build
./manage-docker.sh start
```
3. Create a Docker volume for persistent database storage:
```
docker volume create url-manager-data
```
The application will be available at `http://localhost:8000`
4. Run the Docker container:
```
docker run -d --name url-manager-container -p 8000:8000 -v url-manager-data:/app/data url-manager
```
### Docker Management Commands
5. Initialize the database and create a superuser:
```
docker exec -it url-manager-container python manage.py migrate
docker exec -it url-manager-container python manage.py createsuperuser
```
- Start all services:
```bash
./manage-docker.sh start
```
6. Open your web browser and navigate to `http://localhost:8000` to access the application.
- Stop all services:
```bash
./manage-docker.sh stop
```
## Usage
- Restart services:
```bash
./manage-docker.sh restart
```
- To create a new short URL, click on the "New Link" button on the home page.
- To edit or delete a link, use the corresponding buttons in the link list.
- To view detailed statistics for a link, click on the "Details" button.
- Use the search bar in the navigation to quickly find links by alias or original URL.
- View logs:
```bash
./manage-docker.sh logs
```
## Admin Interface
- Run database migrations:
```bash
./manage-docker.sh migrate
```
To access the admin interface:
- Create new migrations:
```bash
./manage-docker.sh makemigrations
```
1. Go to `http://localhost:8000/admin`
2. Log in with the superuser account you created earlier
- Access Django shell:
```bash
./manage-docker.sh shell
```
## Changing the Language
### Docker Services
To switch between English and Simplified Chinese, use the language selector in the navigation bar.
The application runs the following services:
## Persistent Data
- `web`: Django web server (port 8000)
- `celery_worker`: Processes background tasks
- `celery_beat`: Schedules periodic tasks
- `redis`: Message broker and result backend
The SQLite database is stored in the Docker volume `url-manager-data`. This ensures that your data persists even if the container is stopped or removed. To backup your data, you can copy the contents of this volume.
### Manual Installation
## Contributing
If you prefer not to use Docker:
Contributions are welcome! Please feel free to submit a Pull Request.
1. Install Python 3.12 and Redis
## License
This project is licensed under the MIT License.
## Local Development
To run the URL Manager locally:
1. Clone the repository:
```
git clone https://github.com/yourusername/url-manager.git
cd url-manager
```
2. Create and activate a virtual environment:
```
python -m venv venv
source venv/bin/activate # On Windows, use `venv\Scripts\activate`
```
3. Install dependencies:
```
2. Install dependencies:
```bash
pip install -r requirements.txt
```
4. Apply database migrations:
```
3. Run migrations:
```bash
python manage.py migrate
```
5. Create a superuser:
```
python manage.py createsuperuser
4. Start the development server:
```bash
./run_server.sh
```
6. Compile translation messages:
```
python manage.py compilemessages
5. Start Celery worker:
```bash
./worker.sh
```
7. In one terminal, start the Tailwind CSS build process:
```
python manage.py tailwind start
6. Start Celery beat:
```bash
./celery_beat.sh
```
8. In another terminal, run the Django development server:
```
python manage.py runserver
## Usage
### Managing Links
1. Create a new link:
- Visit `/create/`
- Enter the original URL and desired alias
- For template links, use `{param}` syntax
2. Access a link:
- Use `http://localhost:8000/your-alias`
- For template links: `http://localhost:8000/your-alias/parameter`
### Managing Pages
1. Create a new page:
- Visit `/ui/pages/new/`
- Enter the URL
- Title and summary will be automatically extracted
2. View all pages:
- Visit `/ui/pages/`
### API Access
The application provides a REST API:
- List pages: `GET /api/pages/`
- Create page: `POST /api/pages/`
Example:
## Monitoring Celery Tasks
The application includes Flower for monitoring Celery tasks. You can access it in two ways:
1. Direct access:
- Visit `http://localhost:5555` in your browser
2. Using management script:
```bash
./manage-docker.sh flower
```
9. Open your browser and go to `http://127.0.0.1:8000`
Flower provides:
- Real-time monitoring of Celery tasks
- Task progress and history
- Worker status and statistics
- Error tracking
- Task graphs and charts
Remember to keep both the Tailwind CSS build process and the Django development server running while you're developing.
### Monitoring Features
## Tools
1. View all tasks:
- Active tasks
- Scheduled tasks
- Failed tasks
- Success rate
The URL Manager includes a Tools page with the following features:
2. Worker information:
- Status
- Resource usage
- Queue length
- Export all aliases as a JSON file
- Import aliases from a JSON file
To access the Tools page, click on the "Tools" link in the navigation bar.
3. Task details:
- Arguments
- Start time
- Runtime
- Result
- Stack traces for failed tasks
Executable
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
set -eu
source 3.12/bin/activate
echo "Starting Django development server..."
celery -A url_manager beat -l info
Binary file not shown.
BIN
View File
Binary file not shown.
+89
View File
@@ -0,0 +1,89 @@
version: '3.8'
services:
web:
build: .
command: >
sh -c "python manage.py migrate &&
python manage.py runserver 0.0.0.0:8000"
volumes:
- .:/app
ports:
- "8000:8000"
environment:
- DJANGO_SETTINGS_MODULE=url_manager.settings
- CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/0
depends_on:
- redis
- tailwind
networks:
- app-network
celery_worker:
build: .
command: celery -A url_manager worker -l info
volumes:
- .:/app
environment:
- DJANGO_SETTINGS_MODULE=url_manager.settings
- CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/0
depends_on:
- redis
- web
networks:
- app-network
celery_beat:
build: .
command: celery -A url_manager beat -l info
volumes:
- .:/app
environment:
- DJANGO_SETTINGS_MODULE=url_manager.settings
- CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/0
depends_on:
- redis
- web
networks:
- app-network
redis:
image: redis:alpine
ports:
- "6379:6379"
networks:
- app-network
tailwind:
build: .
command: python manage.py tailwind start
volumes:
- .:/app
ports:
- "3000:3000"
networks:
- app-network
flower:
build: .
command: celery -A url_manager flower --port=5555
volumes:
- .:/app
ports:
- "5555:5555"
environment:
- DJANGO_SETTINGS_MODULE=url_manager.settings
- CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/0
depends_on:
- redis
- celery_worker
networks:
- app-network
networks:
app-network:
driver: bridge
@@ -0,0 +1,33 @@
# Generated by Django 5.0.9 on 2024-11-04 01:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('links', '0010_alter_page_title'),
]
operations = [
migrations.AddField(
model_name='page',
name='error_message',
field=models.TextField(blank=True),
),
migrations.AddField(
model_name='page',
name='last_retry_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='page',
name='process_status',
field=models.CharField(choices=[('pending', 'Pending'), ('processing', 'Processing'), ('completed', 'Completed'), ('failed', 'Failed')], default='pending', max_length=20),
),
migrations.AddField(
model_name='page',
name='retry_count',
field=models.IntegerField(default=0),
),
]
+22 -1
View File
@@ -128,6 +128,12 @@ class LinkChangeLog(models.Model):
return f"URL changed from {self.old_url} to {self.new_url}"
class Page(models.Model):
class ProcessStatus(models.TextChoices):
PENDING = 'pending', _('Pending')
PROCESSING = 'processing', _('Processing')
COMPLETED = 'completed', _('Completed')
FAILED = 'failed', _('Failed')
url = models.URLField(_('URL'), max_length=2000)
title = models.CharField(_('Title'), max_length=200, blank=True)
summary = models.TextField(_('Summary'), blank=True)
@@ -135,13 +141,28 @@ class Page(models.Model):
created_at = models.DateTimeField(_('Created at'), default=timezone.now)
updated_at = models.DateTimeField(_('Updated at'), auto_now=True)
# New fields for processing status
process_status = models.CharField(
max_length=20,
choices=ProcessStatus.choices,
default=ProcessStatus.PENDING
)
retry_count = models.IntegerField(default=0)
last_retry_at = models.DateTimeField(null=True, blank=True)
error_message = models.TextField(blank=True)
class Meta:
ordering = ['-updated_at']
verbose_name = _('Page')
verbose_name_plural = _('Pages')
def __str__(self):
return self.title
return self.title or self.url
def get_absolute_url(self):
return reverse('page-detail', kwargs={'pk': self.pk})
def needs_processing(self):
return (not self.title or not self.summary) and \
self.process_status != self.ProcessStatus.FAILED and \
self.retry_count < 3
+140
View File
@@ -0,0 +1,140 @@
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
from django.http import JsonResponse
import requests
from bs4 import BeautifulSoup
from urllib.parse import urlparse
import re
from rest_framework import viewsets, status
from rest_framework.response import Response
from rest_framework.pagination import PageNumberPagination
from .models import Page
from .forms import PageForm
from .serializers import PageSerializer
class PageListView(ListView):
model = Page
template_name = 'links/page_list.html'
context_object_name = 'pages'
paginate_by = 10
class PageDetailView(DetailView):
model = Page
template_name = 'links/page_detail.html'
class PageCreateView(CreateView):
model = Page
form_class = PageForm
template_name = 'links/page_form.html'
success_url = reverse_lazy('page-list')
def form_valid(self, form):
# 如果标题和摘要都已经自动填充,则将状态设置为已完成
if form.instance.title and form.instance.summary:
form.instance.process_status = Page.ProcessStatus.COMPLETED
else:
form.instance.process_status = Page.ProcessStatus.PENDING
return super().form_valid(form)
class PageUpdateView(UpdateView):
model = Page
form_class = PageForm
template_name = 'links/page_form.html'
success_url = reverse_lazy('page-list')
class PageDeleteView(DeleteView):
model = Page
template_name = 'links/page_confirm_delete.html'
success_url = reverse_lazy('page-list')
def fetch_page_info(request):
url = request.GET.get('url')
if not url:
return JsonResponse({'error': 'URL is required'}, status=400)
# 移除URL开头可能的@符号
url = url.lstrip('@')
try:
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
}
response = requests.get(url, headers=headers, timeout=10, verify=False)
if response.encoding == 'ISO-8859-1':
response.encoding = response.apparent_encoding or 'utf-8'
soup = BeautifulSoup(response.text, 'html.parser')
# 获取标题
title = None
if soup.title:
title = soup.title.string
elif soup.find('h1'):
title = soup.find('h1').get_text(strip=True)
elif soup.find('meta', property='og:title'):
title = soup.find('meta', property='og:title').get('content')
if title:
title = re.sub(r'\s+', ' ', title.strip())
title = title.replace(' | ', ' - ').replace(' :: ', ' - ')
else:
title = urlparse(url).netloc
# 获取描述
description = None
meta_desc = soup.find('meta', {'name': 'description'}) or soup.find('meta', {'property': 'og:description'})
if meta_desc:
description = meta_desc.get('content')
if not description:
for tag in soup(['script', 'style', 'nav', 'header', 'footer']):
tag.decompose()
paragraphs = soup.find_all(['p', 'div'])
for p in paragraphs:
text = p.get_text(strip=True)
if len(text) > 100:
description = text
break
if not description:
description = title
description = re.sub(r'\s+', ' ', description.strip())
description = description[:500] + '...' if len(description) > 500 else description
return JsonResponse({
'title': title,
'summary': description
})
except requests.exceptions.RequestException as e:
return JsonResponse({
'error': f'Failed to fetch page: {str(e)}'
}, status=400)
except Exception as e:
return JsonResponse({
'error': f'Error processing page: {str(e)}'
}, status=400)
class StandardResultsSetPagination(PageNumberPagination):
page_size = 10
page_size_query_param = 'page_size'
max_page_size = 100
class PageViewSet(viewsets.ModelViewSet):
queryset = Page.objects.all().order_by('-created_at')
serializer_class = PageSerializer
pagination_class = StandardResultsSetPagination
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
+113
View File
@@ -0,0 +1,113 @@
from celery import shared_task
from django.utils import timezone
from datetime import timedelta
import requests
from bs4 import BeautifulSoup
import logging
from .models import Page
logger = logging.getLogger(__name__)
def fibonacci(n):
if n <= 0:
return 0
elif n == 1:
return 1
else:
a, b = 0, 1
for _ in range(2, n + 1):
a, b = b, a + b
return b
@shared_task(bind=True, max_retries=3)
def process_page(self, page_id):
try:
page = Page.objects.get(id=page_id)
# Check if page still needs processing
if not page.needs_processing():
return
# Update status to processing
page.process_status = Page.ProcessStatus.PROCESSING
page.save()
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
}
response = requests.get(page.url, headers=headers, timeout=10, verify=False)
response.raise_for_status()
if response.encoding == 'ISO-8859-1':
response.encoding = response.apparent_encoding or 'utf-8'
soup = BeautifulSoup(response.text, 'html.parser')
# Extract title if not present
if not page.title:
title = None
if soup.title:
title = soup.title.string
elif soup.find('h1'):
title = soup.find('h1').get_text(strip=True)
elif soup.find('meta', property='og:title'):
title = soup.find('meta', property='og:title').get('content')
if title:
page.title = title.strip()
# Extract summary if not present
if not page.summary:
description = None
meta_desc = soup.find('meta', {'name': 'description'}) or soup.find('meta', {'property': 'og:description'})
if meta_desc:
description = meta_desc.get('content')
if not description:
for tag in soup(['script', 'style', 'nav', 'header', 'footer']):
tag.decompose()
paragraphs = soup.find_all(['p', 'div'])
for p in paragraphs:
text = p.get_text(strip=True)
if len(text) > 100:
description = text
break
if description:
page.summary = description[:500]
page.process_status = Page.ProcessStatus.COMPLETED
page.save()
except Exception as exc:
page.retry_count += 1
page.last_retry_at = timezone.now()
if page.retry_count >= 3:
page.process_status = Page.ProcessStatus.FAILED
page.error_message = str(exc)
else:
page.process_status = Page.ProcessStatus.PENDING
# Schedule retry with fibonacci backoff
retry_delay = fibonacci(page.retry_count)
self.retry(exc=exc, countdown=retry_delay)
page.save()
raise exc
@shared_task
def schedule_pending_pages():
"""Periodic task to schedule processing of pending pages"""
pending_pages = Page.objects.filter(
process_status=Page.ProcessStatus.PENDING,
retry_count__lt=3
).exclude(
last_retry_at__gte=timezone.now() - timedelta(seconds=fibonacci(3))
)
for page in pending_pages:
process_page.delay(page.id)
+90 -7
View File
@@ -6,25 +6,108 @@
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="bg-white shadow-sm rounded-lg overflow-hidden">
<div class="px-4 py-5 sm:p-6">
<!-- Header with actions -->
<div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-bold text-gray-900">{{ page.title }}</h1>
<h1 class="text-3xl font-bold text-gray-900">{{ page.title|default:"Untitled" }}</h1>
<div class="flex space-x-2">
<a href="{% url 'page-update' page.pk %}" class="text-blue-600 hover:text-blue-800">
<a href="{% url 'page-update' page.pk %}" class="inline-flex items-center text-blue-600 hover:text-blue-800">
<svg class="w-5 h-5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
{% trans "Edit" %}
</a>
<a href="{{ page.url }}" target="_blank" rel="noopener noreferrer" class="text-green-600 hover:text-green-800">
<a href="{{ page.url }}" target="_blank" rel="noopener noreferrer" class="inline-flex items-center text-green-600 hover:text-green-800">
<svg class="w-5 h-5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
</svg>
{% trans "Visit URL" %}
</a>
</div>
</div>
<div class="prose prose-sm sm:prose lg:prose-lg xl:prose-xl w-full">
{{ page.content|markdown|safe }}
<!-- Page Status -->
<div class="mb-6 flex items-center">
<span class="text-sm font-medium text-gray-500 mr-2">{% trans "Status" %}:</span>
{% if page.process_status == 'completed' %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
{% trans "Completed" %}
</span>
{% elif page.process_status == 'processing' %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
{% trans "Processing" %}
</span>
{% elif page.process_status == 'failed' %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
{% trans "Failed" %}
</span>
{% else %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
{% trans "Pending" %}
</span>
{% endif %}
{% if page.retry_count > 0 %}
<span class="ml-2 text-sm text-gray-500">
({% trans "Retries" %}: {{ page.retry_count }}/3)
</span>
{% endif %}
</div>
<div class="mt-6 text-sm text-gray-500">
{% trans "Last updated" %}: {{ page.updated_at|date:"Y-m-d H:i" }}
<!-- URL -->
<div class="mb-6">
<h2 class="text-sm font-medium text-gray-500 mb-1">{% trans "URL" %}</h2>
<a href="{{ page.url }}" target="_blank" rel="noopener noreferrer"
class="text-blue-600 hover:text-blue-800 break-all">
{{ page.url }}
</a>
</div>
<!-- Timestamps -->
<div class="mb-6 space-y-2">
<div class="flex items-center text-sm">
<span class="font-medium text-gray-500 w-24">{% trans "Created" %}:</span>
<span class="text-gray-900">{{ page.created_at|date:"Y-m-d H:i:s" }}</span>
</div>
<div class="flex items-center text-sm">
<span class="font-medium text-gray-500 w-24">{% trans "Updated" %}:</span>
<span class="text-gray-900">{{ page.updated_at|date:"Y-m-d H:i:s" }}</span>
</div>
{% if page.last_retry_at %}
<div class="flex items-center text-sm">
<span class="font-medium text-gray-500 w-24">{% trans "Last Retry" %}:</span>
<span class="text-gray-900">{{ page.last_retry_at|date:"Y-m-d H:i:s" }}</span>
</div>
{% endif %}
</div>
<!-- Summary -->
{% if page.summary %}
<div class="mb-6">
<h2 class="text-sm font-medium text-gray-500 mb-1">{% trans "Summary" %}</h2>
<div class="bg-gray-50 rounded-lg p-4 text-gray-700">
{{ page.summary }}
</div>
</div>
{% endif %}
<!-- Content -->
{% if page.content %}
<div class="mb-6">
<h2 class="text-sm font-medium text-gray-500 mb-1">{% trans "Content" %}</h2>
<div class="prose prose-sm sm:prose lg:prose-lg xl:prose-xl w-full">
{{ page.content|markdown|safe }}
</div>
</div>
{% endif %}
<!-- Error Message -->
{% if page.error_message %}
<div class="mb-6">
<h2 class="text-sm font-medium text-red-500 mb-1">{% trans "Error Message" %}</h2>
<div class="bg-red-50 text-red-700 p-4 rounded-lg">
{{ page.error_message }}
</div>
</div>
{% endif %}
</div>
</div>
</div>
+5 -131
View File
@@ -35,6 +35,11 @@ from rest_framework import viewsets, status
from rest_framework.response import Response
from rest_framework.pagination import PageNumberPagination
from .serializers import PageSerializer
from .page_views import (
PageListView, PageDetailView, PageCreateView,
PageUpdateView, PageDeleteView, fetch_page_info,
PageViewSet
)
logger = logging.getLogger(__name__)
@@ -440,134 +445,3 @@ def export_database(request):
class HelpView(TemplateView):
template_name = 'links/help.html'
class PageListView(ListView):
model = Page
template_name = 'links/page_list.html'
context_object_name = 'pages'
paginate_by = 10
class PageDetailView(DetailView):
model = Page
template_name = 'links/page_detail.html'
class PageCreateView(CreateView):
model = Page
form_class = PageForm
template_name = 'links/page_form.html'
success_url = reverse_lazy('page-list')
class PageUpdateView(UpdateView):
model = Page
form_class = PageForm
template_name = 'links/page_form.html'
success_url = reverse_lazy('page-list')
class PageDeleteView(DeleteView):
model = Page
template_name = 'links/page_confirm_delete.html'
success_url = reverse_lazy('page-list')
def fetch_page_info(request):
url = request.GET.get('url')
if not url:
return JsonResponse({'error': 'URL is required'}, status=400)
# 移除URL开头可能的@符号
url = url.lstrip('@')
try:
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
}
response = requests.get(url, headers=headers, timeout=10, verify=False)
# 确保使用正确的编码
if response.encoding == 'ISO-8859-1':
response.encoding = response.apparent_encoding or 'utf-8'
soup = BeautifulSoup(response.text, 'html.parser')
# 获取标题 - 尝试多种方式
title = None
# 1. 尝试获取title标签
if soup.title:
title = soup.title.string
# 2. 尝试获取第一个h1
if not title and soup.find('h1'):
title = soup.find('h1').get_text(strip=True)
# 3. 尝试获取og:title
if not title:
og_title = soup.find('meta', property='og:title')
if og_title:
title = og_title.get('content')
# 清理标题
if title:
title = re.sub(r'\s+', ' ', title.strip())
title = title.replace(' | ', ' - ').replace(' :: ', ' - ')
else:
title = urlparse(url).netloc
# 获取描述 - 尝试多种方式
description = None
# 1. 尝试meta description
meta_desc = soup.find('meta', {'name': 'description'}) or soup.find('meta', {'property': 'og:description'})
if meta_desc:
description = meta_desc.get('content')
# 2. 如果没有meta描述,尝试获取正文内容
if not description:
# 移除script, style等标签
for tag in soup(['script', 'style', 'nav', 'header', 'footer']):
tag.decompose()
# 获取所有段落
paragraphs = soup.find_all(['p', 'div'])
for p in paragraphs:
text = p.get_text(strip=True)
if len(text) > 100: # 确保段落有足够的内容
description = text
break
# 如果还是没有描述,使用标题
if not description:
description = title
# 清理描述
description = re.sub(r'\s+', ' ', description.strip())
description = description[:500] + '...' if len(description) > 500 else description
return JsonResponse({
'title': title,
'summary': description
})
except requests.exceptions.RequestException as e:
return JsonResponse({
'error': f'Failed to fetch page: {str(e)}'
}, status=400)
except Exception as e:
return JsonResponse({
'error': f'Error processing page: {str(e)}'
}, status=400)
class StandardResultsSetPagination(PageNumberPagination):
page_size = 10
page_size_query_param = 'page_size'
max_page_size = 100
class PageViewSet(viewsets.ModelViewSet):
queryset = Page.objects.all().order_by('-created_at')
serializer_class = PageSerializer
pagination_class = StandardResultsSetPagination
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
+53
View File
@@ -0,0 +1,53 @@
#!/bin/bash
case "$1" in
"start")
docker-compose up -d
echo "Services started. Access:"
echo "- Web: http://localhost:8000"
echo "- Flower (Celery Monitor): http://localhost:5555"
;;
"stop")
docker-compose down
;;
"restart")
docker-compose restart
;;
"logs")
if [ "$2" ]; then
docker-compose logs -f "$2"
else
docker-compose logs -f
fi
;;
"build")
docker-compose build
;;
"shell")
docker-compose exec web python manage.py shell
;;
"migrate")
docker-compose exec web python manage.py migrate
;;
"makemigrations")
docker-compose exec web python manage.py makemigrations
;;
"tailwind-logs")
docker-compose logs -f tailwind
;;
"tailwind-build")
docker-compose exec tailwind npm run build
;;
"flower")
echo "Opening Flower dashboard in default browser..."
docker-compose exec flower open http://localhost:5555
;;
*)
echo "Usage: $0 {start|stop|restart|logs|build|shell|migrate|makemigrations|tailwind-logs|tailwind-build|flower}"
echo ""
echo "Additional commands:"
echo " logs [service] View logs of all services or specific service"
echo " flower Open Flower dashboard in browser"
exit 1
;;
esac
+12
View File
@@ -0,0 +1,12 @@
{
"name": "url-manager",
"version": "1.0.0",
"scripts": {
"dev": "tailwindcss -i ./new_theme/static/css/src/styles.css -o ./new_theme/static/css/dist/styles.css --watch",
"build": "tailwindcss -i ./new_theme/static/css/src/styles.css -o ./new_theme/static/css/dist/styles.css --minify"
},
"dependencies": {
"tailwindcss": "^3.3.0",
"@tailwindcss/typography": "^0.5.9"
}
}
+3
View File
@@ -14,3 +14,6 @@ markdown==3.7
requests==2.32.3
beautifulsoup4==4.12.3
djangorestframework==3.15.2
celery>=5.3.0
redis>=4.5.0
flower>=2.0.0
+8
View File
@@ -0,0 +1,8 @@
import os
from celery import Celery
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'url_manager.settings')
app = Celery('url_manager')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()
+20
View File
@@ -192,3 +192,23 @@ REST_FRAMEWORK = {
'PAGE_SIZE': 10,
'UNAUTHENTICATED_USER': None,
}
# Time zone setting
TIME_ZONE = 'UTC'
USE_TZ = True
# Celery Configuration
CELERY_BROKER_URL = os.environ.get('CELERY_BROKER_URL', 'redis://localhost:6379/0')
CELERY_RESULT_BACKEND = os.environ.get('CELERY_RESULT_BACKEND', 'redis://localhost:6379/0')
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = TIME_ZONE # Now TIME_ZONE is defined
# Celery Beat Schedule
CELERY_BEAT_SCHEDULE = {
'check-pending-pages': {
'task': 'links.tasks.schedule_pending_pages',
'schedule': 300.0, # Run every 5 minutes
},
}
Executable
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
set -eu
source 3.12/bin/activate
echo "Starting Django development server..."
celery -A url_manager worker -l info