diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..995ca0a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +.git +.gitignore +.env +.venv +venv/ +__pycache__ +*.pyc +*.pyo +*.pyd +.DS_Store +db.sqlite3 +staticfiles/ +media/ +.idea +.vscode diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9af1520 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,42 @@ +FROM python:3.11-slim-bookworm + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +# Set work directory +WORKDIR /app + +# Install system dependencies +# WeasyPrint needs: libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz-subset0 libjpeg-dev libopenjp2-7-dev libxcb1 +# MySQLclient needs: default-libmysqlclient-dev gcc pkg-config +RUN apt-get update && apt-get install -y \ + gcc \ + pkg-config \ + default-libmysqlclient-dev \ + libpango-1.0-0 \ + libpangoft2-1.0-0 \ + libharfbuzz-subset0 \ + libjpeg-dev \ + libopenjp2-7-dev \ + libxcb1 \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Install python dependencies +COPY requirements.txt /app/ +RUN pip install --upgrade pip && pip install --no-cache-dir -r requirements.txt + +# Copy project +COPY . /app/ + +# Copy entrypoint script and make it executable +COPY entrypoint.sh /app/entrypoint.sh +RUN chmod +x /app/entrypoint.sh + +# Expose port +EXPOSE 8000 + +# Entrypoint +ENTRYPOINT ["/app/entrypoint.sh"] + diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 9ec34c7..b7ec571 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/settings.py b/config/settings.py index d858f0c..5e1cce8 100644 --- a/config/settings.py +++ b/config/settings.py @@ -59,6 +59,7 @@ INSTALLED_APPS = [ 'rest_framework', 'rest_framework.authtoken', 'drf_yasg', + 'rangefilter', 'core', ] @@ -242,6 +243,7 @@ JAZZMIN_SETTINGS = { "user_avatar": None, "topmenu_links": [ {"name": "Home", "url": "admin:index", "permissions": ["auth.view_user"]}, + {"name": "View Website", "url": "index", "new_window": True}, {"model": "auth.User"}, {"app": "core"}, ], @@ -306,4 +308,4 @@ REST_FRAMEWORK = { 'rest_framework.authentication.TokenAuthentication', 'rest_framework.authentication.SessionAuthentication', ], -} \ No newline at end of file +} diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 870921f..cc24b2e 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index e34444c..d0ce2d0 100644 --- a/core/admin.py +++ b/core/admin.py @@ -13,6 +13,9 @@ from .mail import send_html_email import logging import csv from django.http import HttpResponse, HttpResponseRedirect +from rangefilter.filters import DateRangeFilter +from django.template.loader import render_to_string +import weasyprint class ProfileInline(admin.StackedInline): model = Profile @@ -92,9 +95,13 @@ class CustomUserAdmin(UserAdmin): class ParcelAdmin(admin.ModelAdmin): list_display = ('tracking_number', 'shipper', 'carrier', 'price', 'status', 'payment_status', 'created_at') - list_filter = ('status', 'payment_status', 'created_at') + list_filter = ( + 'status', + 'payment_status', + ('created_at', DateRangeFilter), + ) search_fields = ('tracking_number', 'shipper__username', 'receiver_name', 'carrier__username') - actions = ['export_as_csv'] + actions = ['export_as_csv', 'print_parcels', 'export_pdf'] def export_as_csv(self, request, queryset): response = HttpResponse(content_type='text/csv') @@ -118,6 +125,21 @@ class ParcelAdmin(admin.ModelAdmin): return response export_as_csv.short_description = _("Export Selected to CSV") + def print_parcels(self, request, queryset): + return render(request, 'admin/core/parcel/parcel_list_print.html', {'parcels': queryset, 'is_pdf': False}) + print_parcels.short_description = _("Print Selected Parcels") + + def export_pdf(self, request, queryset): + html_string = render_to_string('admin/core/parcel/parcel_list_print.html', {'parcels': queryset, 'is_pdf': True}) + html = weasyprint.HTML(string=html_string, base_url=request.build_absolute_uri()) + result = html.write_pdf() + + response = HttpResponse(content_type='application/pdf') + response['Content-Disposition'] = 'attachment; filename="parcels_list.pdf"' + response.write(result) + return response + export_pdf.short_description = _("Download Selected as PDF") + class PlatformProfileAdmin(admin.ModelAdmin): fieldsets = ( (_('General Info'), { @@ -260,4 +282,4 @@ class NotificationTemplateAdmin(admin.ModelAdmin): def has_delete_permission(self, request, obj=None): return False -admin.site.register(NotificationTemplate, NotificationTemplateAdmin) +admin.site.register(NotificationTemplate, NotificationTemplateAdmin) \ No newline at end of file diff --git a/core/templates/admin/core/parcel/parcel_list_print.html b/core/templates/admin/core/parcel/parcel_list_print.html new file mode 100644 index 0000000..9cf7c5b --- /dev/null +++ b/core/templates/admin/core/parcel/parcel_list_print.html @@ -0,0 +1,63 @@ + + + + + Parcel List + + + +
+

Parcel List

+

Generated on: {% now "Y-m-d H:i" %}

+
+ + {% if not is_pdf %} +
+ + +
+ {% endif %} + + + + + + + + + + + + + + + + + {% for parcel in parcels %} + + + + + + + + + + + + {% endfor %} + +
Tracking #ShipperCarrierReceiverFromToStatusPriceCreated At
{{ parcel.tracking_number }}{{ parcel.shipper.username|default:"-" }}{{ parcel.carrier.username|default:"-" }}{{ parcel.receiver_name }}{{ parcel.pickup_city.name_en|default:"-" }}{{ parcel.delivery_city.name_en|default:"-" }}{{ parcel.get_status_display }}{{ parcel.price }}{{ parcel.created_at|date:"Y-m-d H:i" }}
+ + \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..b1b9940 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# Exit immediately if a command exits with a non-zero status +set -e + +# Apply database migrations +echo "Applying database migrations..." +python manage.py migrate + +# Collect static files +echo "Collecting static files..." +python manage.py collectstatic --noinput + +# Start Gunicorn +echo "Starting Gunicorn..." +exec gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers 3 diff --git a/requirements.txt b/requirements.txt index e59ab91..47ee07c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,6 @@ qrcode django-jazzmin==3.0.1 djangorestframework==3.15.1 drf-yasg +gunicorn==22.0.0 +django-cors-headers +django-admin-rangefilter \ No newline at end of file