From 87854aee028240526ab71313762058202618d63a Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Wed, 18 Feb 2026 22:51:16 +0000 Subject: [PATCH] v1 --- config/__pycache__/settings.cpython-311.pyc | Bin 5552 -> 5635 bytes config/settings.py | 4 + core/__pycache__/admin.cpython-311.pyc | Bin 212 -> 1516 bytes core/__pycache__/forms.cpython-311.pyc | Bin 0 -> 740 bytes core/__pycache__/models.cpython-311.pyc | Bin 209 -> 2674 bytes core/__pycache__/urls.cpython-311.pyc | Bin 347 -> 784 bytes core/__pycache__/views.cpython-311.pyc | Bin 1364 -> 11647 bytes core/admin.py | 22 +- core/forms.py | 14 + core/migrations/0001_initial.py | 45 +++ .../__pycache__/0001_initial.cpython-311.pyc | Bin 0 -> 2265 bytes core/models.py | 30 +- core/templates/base.html | 10 +- core/templates/core/bundle_detail.html | 107 +++++++ core/templates/core/bundle_list.html | 59 ++++ core/templates/core/index.html | 283 +++++++++--------- core/urls.py | 6 +- core/views.py | 206 ++++++++++++- requirements.txt | 1 + static/css/custom.css | 210 ++++++++++++- 20 files changed, 837 insertions(+), 160 deletions(-) create mode 100644 core/__pycache__/forms.cpython-311.pyc create mode 100644 core/forms.py create mode 100644 core/migrations/0001_initial.py create mode 100644 core/migrations/__pycache__/0001_initial.cpython-311.pyc create mode 100644 core/templates/core/bundle_detail.html create mode 100644 core/templates/core/bundle_list.html diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 96bce5584823cd4ebfccfc40c2b36df8beaee7db..1e94cd8fbfcf16219c98cad070a8cc93bb59c4eb 100644 GIT binary patch delta 172 zcmdm>-K@jAoR^o20SH{Ire$84$ScWsXQTQR7GCEl{S=odgB0dq22IyZtYuuRnG6gI z7$%qTD=->vp3eV=(NfyQF~l)GG{DE-(Iwv3F(TgA)z?2LGCtTd%Jr7Co2QQ}4#~-r s1ogNwfOawhaq+9orvxuE%Ri7)yTBlQ15AG4Vh}UEz#s%cMS?(O0I)tWWB>pF delta 117 zcmZqH*`Uq4oR^o20SKzTC9XXZT3)F@F1r<<7%;p#qk9m10~g?uC|w?mi) zRzunBz6v(*q-`HwhuMIbg*%M70gQQI9C|Qg@eZRlfUy9K#RoI$O#ecR(iui=P^sC> zbNn+@ZKzUTjy(5W>@%0kGWOpGjo9OUl&*Z(Y6T){^aMjEOV`B{80-;FOv;F_2cHI{=7H92y_{TBB$6sbi{1Uu;s!R?4M%Z>~x@OTaSqqJ}{yK3ZR!LoeYh6Gj$u!j@}q%Sy;?(b%xo9qzJF76QSzaQx;0xj+S13-yh_ zRs!KP9p847WXL?kj%TGM+8}w4VbcRDrr`xi6J?B{ zf-sCQf-njoOG&2I9XH}ajN^0??j-i6GC;?BLSLcKc@R2YZGXZm^+cz^LUCnKVL(yj z(xOPqDw9}*sCXhTks(SL4p!Hyp$!x?g9djr;PD6qR1{)5ZXebXXy^|gO;?SQ9A=xg z-wYNTfgcHHP2iU<^H6Tw4TS~+fu}Fz=T;dNTN zZ^Sd8z>g;!1MI4l(i1Xw)PJ9nrK5k}-8@l7+mCi?yZSMm>Cl<3qSCo8xevR%Gc|vX z3zrDzDp6*8_T`MFd`znyT1A!`vfPDVT4(w>veb~JhAg$7eUGIVi-H9A;W`#rxWv_) KSo%zpqWTM|(oTo~ literal 212 zcmZ3^%ge<81k-=yXW9el#~=<2FhLogg@BCd3@HpLj5!Rsj8Tk?3@J>(44TX@K?*b( zZ?Pt(n;80F)6nZ2$lO diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b67afc491343e00fe3c188d7ef7743c5af1de36b GIT binary patch literal 740 zcmZuuyKmD#82`?Wod-#uh=)Ls4S6V*Ix(P16g7x}AYKy6V!6Jfak%*0vrlca(1D>t z2L?nJ<|5SoF^twtNK9;wlBpASc7nv?JKxXW%ldxb&tF=t1}N)3_!Mp;fFC-Tn>BML z_mp!27%-wRL_LJSfJcCh_kc};2FCn@Qxo7!-O_p{uDk%p`l&+)N$U;c!z7qhi&uRX zOkOMJ1PJsHhMr*nF$a}-BSyH)%=xIPxPqV(JH^ z&nPBbFXg8xPaIQP)ZZpl);QS}AtzYcdm-K-Y1gUBn&(CSka(VKc;1jQ_JjK57{ zMa>NuCvKtj-S~iO(F)LdCeIZd{ytgr#?jXC(`?~pwtOX9ymcgw-+jM+C%e6#TjuIQ z4jN2rsWAoxQw@2w-m=G_V5;G8OCP>EAJTOy(#8Wn+F|8J6|HtjVnsITTpV!rG0P4ad2Wdc5Ht$JMYcR zd$Zp+<3DsgAwl_e@|V(IAxZiNANES5)!4oVjfWDEh-^t^IWNnSB7H6q<(@=BR9;l} z2CTviPvIv@KE%Dk;1zj;SD1T6!7KI#uLy}>k+SM0PMwv5XxSmuatp1ccol9vY<~}p zhZ2?YGLiC%BGJ&|o()nU%0_4vLVnr~J0Iab(bjlKY$LoXg9rRX$;Tjt_<};B_riIV z#2}P-kodwYTc1^IV$sd#;5V05f<)O|HY{ovy&FN~x@Foo*X2MnE1qN6&TYdaB#1KV zRhV69PYy#d6x_G%5rBu%kJ2L4qAaOIhUd2TW9f+k%PX@hF|qa#EF1Ako<##?36~4I z)LL+Hz|ij4_zkB_$1B$>wpSUa%d`NYu<_dtyXh{O1v*}E7#$azIkt2sh#7|KF~eBt z+fQz+)fa~u-J<|iso9(UX{Fwqs~^w(>iu>bD*VQC+Zgj~HW9=P!#2y*FoJ|(!1NUh z+nQngP%$lGVVH2BPTw%uOo>{A>G-T)Fg-d~DpTGUBnpf|2Qf@92tj1551iH|mWGO@ z5RjxC@D;n?0aV+crZ#YY#@7bcLeD1a+EhcE^0lez#b#=>*6*iAp))nrNKN^vsp^+o zVL5W@QF=>)>a`%&#U(qK*A~j(V0j($XoSeX#5T(|lml{!aTek(^Fl(0Sg3LjE?9IS zk&Wm9p7wALP0S!ktI&A5_aNnUF0~j)Eq*`&M~F&$f!_(>carn~!L&d_3S4jsl`^%x zKqak~yRHNpqZWTH_$mX{u?}!)G2;*3G z0y>dD?%M#buq1Qr(Om7*`*Hk;K_BAq9)J#&BEz^!KY&K3N*}|)(y3CSJPAV*q1hE> z=;W@P1(oU!D-}z&X>n}?Du@!!4x>a^FH9&DD@ct|u4Fp0Z;23-bq4`Jgnj*f-$=dp z41CwZ8%IP7F@A=pF`G>t*dd|0#0-eORJ6aMcerwb*!1};)8{W-45A_=h8x7j0D{an zj$86dx9BC?qea@1>Pa-Ib9C7gGSvzWm~GGSpo{U{!-ImW^}xGe$I*a*EOj>kUU?Pz z;QgzJ3U-G?Wczu=}|vDTKyW8J&ck*2rsd3XWtN&BWJ}4a2DOWg5U>+-@)hh z3Gg#m_W8`!h_vD5|2YBR8r49_aZs{ORG?r&+*B8rmmKB=G0JO~y$3PPF%^)#k1&jI zi1R37&iN!XIynzh%d;SGb)f_-#*?5skNH(1R^PiykiagUW8x~gTaO=aG4@C|#tUQ4=<(29TqTo0ft>D1>}U(DC_YYqLHuU`Ywb-O8=$)Q@* zPY$h3*OR$MGUq3A)k{dSK|!8DBu`fm(44TX@K?*b( zZ?Wa(r=;c-`)M-W;!Md(%uCPLOGzqX21>4E_zY6>OHV%|KQ~psG^sSNq*On(A~m_R zB)>?%JijQrxF9h(RX;huC{-U~j9x+IFAf_ZyEG@&u80Guoe_wOWr4&8W=2NF8w@fR Ku%RM0pb7xLi8T)Z diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 5a69659f6c6e0ae848e54157af197c543a09315f..1b538579413a526fdd79ec0755bc9b177a8aa5e7 100644 GIT binary patch literal 784 zcma)2ziSjh6n=C2_E*;PLMjpA5UYcbZM;T8sx;TwXs*~0!g@0#tGheL?5-CBLduj@ zYMII?S_;XZu?JU#sRXNZ2kBBdv$u;VC-`<|nfHBf-uKNrZ?{)K#rfm+kt6^=b<%`e zA8x*@!v~;1Nel@I2m#unM%+q_z(_E_#0<>D3arEqYyvG{gd(%4`OW%d0DM(@5fx)I zlR8Toml^TTWPKV)~t+SbMTEuKitf|X-xY>pJ%?(1x6+9^aTt8r^{J%A~z;sWxPuEVLf3!;MPO&?~ z?iD%;yZ>(Y>}`qtDfVaBudvd6bhdZdEpaf#!3+mAxc-@!+dCx=r#PJ9a5*~ntWN91 K)1O#X-hTlc)ZH)u delta 232 zcmbQhcAKewIWI340}xbw%g@XL(vLwL7+{4mKHC5p(-~42QW$d>av7r-85vTTQkZj? za+#x;85x)uQW;ZNQkhd&*RU;PW?)zi#1N3q7{!vp9?YQ0@e(AU$#{#UAh9IlB_ouR zA)23?dW)e5WD!U*FEKaOPm^`>2}ZfezZiYF_<-UdwZ&eO1DRyZ#4j+Qpbu;eteg!l g9YP%;GuSS$$X{fUzrrH_ftiV)seu~=i+F&V0aBhZB>(^b diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 2a36fd69370b38a98d8b01bf8eb9817c42f16ed6..99b0c21bd2620cfb55e341ad24c224271af60f91 100644 GIT binary patch literal 11647 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 O^qE&|V)u|)j^SSfOjf7> literal 0 HcmV?d00001 diff --git a/core/models.py b/core/models.py index 71a8362..fa06591 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,31 @@ from django.db import models -# Create your models here. + +class HtmlBundle(models.Model): + title = models.CharField(max_length=200, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self) -> str: + return self.title or f"Bundle {self.pk}" + + +class HtmlDocument(models.Model): + bundle = models.ForeignKey(HtmlBundle, related_name="documents", on_delete=models.CASCADE) + original_name = models.CharField(max_length=255) + order = models.PositiveIntegerField(default=1) + content_text = models.TextField() + + class Meta: + ordering = ["order", "id"] + + def __str__(self) -> str: + return f"{self.original_name} ({self.bundle_id})" + + +class HtmlExport(models.Model): + bundle = models.ForeignKey(HtmlBundle, related_name="exports", on_delete=models.CASCADE) + created_at = models.DateTimeField(auto_now_add=True) + file_name = models.CharField(max_length=255) + + def __str__(self) -> str: + return self.file_name diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..fd48d28 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -3,7 +3,9 @@ + {% block title %}Knowledge Base{% endblock %} + {% block meta %} {% if project_description %} @@ -13,13 +15,19 @@ {% endif %} + {% endblock %} {% load static %} + + + + {% block head %}{% endblock %} - + {% block content %}{% endblock %} + diff --git a/core/templates/core/bundle_detail.html b/core/templates/core/bundle_detail.html new file mode 100644 index 0000000..85572ae --- /dev/null +++ b/core/templates/core/bundle_detail.html @@ -0,0 +1,107 @@ +{% extends "base.html" %} + +{% block title %}{{ page_title }}{% endblock %} + +{% block meta %} + + + +{% endblock %} + +{% block content %} +
+
+ +
+ {% if messages %} +
+ {% for message in messages %} + + {% endfor %} +
+ {% endif %} + +
+
+

{{ bundle.title|default:"Untitled bundle" }}

+

Created {{ bundle.created_at|date:"M d, Y · H:i" }} · {{ documents|length }} HTML files

+
+ Generate PDF +
+ +
+
+
+
+

Order your files

+ Lower number = earlier +
+
+ {% csrf_token %} + +
+ {% for document in documents %} +
+
+
+
{{ document.original_name }}
+
{{ document.content_text|truncatechars:120 }}
+
+
+ + +
+
+
+ {% empty %} +
No documents in this bundle yet.
+ {% endfor %} +
+
+ + Changes affect the next PDF export. +
+
+
+
+
+
+

Export history

+ {% if exports %} +
+ {% for export in exports %} +
+
+
{{ export.file_name }}
+
{{ export.created_at|date:"M d, Y · H:i" }}
+
+ Download +
+ {% endfor %} +
+ {% else %} +

No exports yet. Generate your first PDF to start history.

+ {% endif %} +
+
+

Next steps

+

Need quick changes? Update the order and export again. The PDF is always a single, continuous document.

+ Download PDF +
+
+
+
+
+ +
+{% endblock %} diff --git a/core/templates/core/bundle_list.html b/core/templates/core/bundle_list.html new file mode 100644 index 0000000..4885cf1 --- /dev/null +++ b/core/templates/core/bundle_list.html @@ -0,0 +1,59 @@ +{% extends "base.html" %} + +{% block title %}{{ page_title }}{% endblock %} + +{% block meta %} + + + +{% endblock %} + +{% block content %} +
+
+ +
+
+
+

All bundles

+

Review and re-download any previously generated bundle.

+
+ Create new bundle +
+ + {% if bundles %} +
+ {% for bundle in bundles %} +
+
+
+
+

{{ bundle.title|default:"Untitled bundle" }}

+
{{ bundle.created_at|date:"M d, Y · H:i" }}
+
{{ bundle.documents.count }} HTML files
+
+ Open +
+
+
+ {% endfor %} +
+ {% else %} +
+

No bundles yet. Upload HTML files to create your first bundle.

+
+ {% endif %} +
+
+ +
+{% endblock %} diff --git a/core/templates/core/index.html b/core/templates/core/index.html index faec813..c7582b6 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,145 +1,152 @@ {% extends "base.html" %} -{% block title %}{{ project_name }}{% endblock %} +{% block title %}{{ page_title }}{% endblock %} -{% block head %} - - - - +{% block meta %} + + + {% endblock %} {% block content %} -
-
-

Analyzing your requirements and generating your app…

-
- Loading… -
-

AppWizzy AI is collecting your requirements and applying the first changes.

-

This page will refresh automatically as the plan is implemented.

-

- Runtime: Django {{ django_version }} · Python {{ python_version }} - — UTC {{ current_time|date:"Y-m-d H:i:s" }} -

+
+
+ + +
+
+
+ Internal doc bundler +

Combine multiple HTML files into a single PDF in minutes.

+

+ Upload multiple HTML files, set the order, and download one clean PDF. Built for staff who + need to bundle text-heavy documents without manual copy/paste. +

+ +
+
+
+
+ Average prep time + Under 3 mins +
+

Upload → reorder → export

+

One continuous PDF with clean spacing.

+
+
+ What gets bundled +
Policies · Updates · Release notes · Staff memos
+
+
+
+
+ +
+ {% if messages %} +
+ {% for message in messages %} + + {% endfor %} +
+ {% endif %} +
+
+
+

Create a new bundle

+

Drop in your HTML files. We will keep their order and let you refine it on the next screen.

+
+ Continuous PDF output +
+
+ {% csrf_token %} + {{ form.non_field_errors }} +
+
+ + {{ form.title }} + {% if form.title.errors %} +
{{ form.title.errors|join:", " }}
+ {% endif %} +
+
+ +
+ +
HTML only · We extract the text and combine it in order.
+
+ {% if file_errors %} +
{{ file_errors|join:", " }}
+ {% endif %} +
+
+
+ + You can reorder before exporting. +
+
+
+
+ +
+
+
+
+
+

Recent bundles

+ See all +
+ {% if bundles %} +
+ {% for bundle in bundles %} +
+
+
{{ bundle.title|default:"Untitled bundle" }}
+
{{ bundle.created_at|date:"M d, Y · H:i" }} · {{ bundle.documents.count }} files
+
+ Open +
+ {% endfor %} +
+ {% else %} +

No bundles yet. Upload HTML files to create your first bundle.

+ {% endif %} +
+
+
+
+

Latest exports

+ {% if exports %} +
+ {% for export in exports %} +
+
+
{{ export.bundle.title|default:"Untitled bundle" }}
+
{{ export.created_at|date:"M d, Y · H:i" }}
+
+ Download +
+ {% endfor %} +
+ {% else %} +

No exports yet. Generate your first PDF to see history here.

+ {% endif %} +
+
+
+
-
-
- Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC) -
-{% endblock %} \ No newline at end of file + + +{% endblock %} diff --git a/core/urls.py b/core/urls.py index 6299e3d..f71b517 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,11 @@ from django.urls import path -from .views import home +from .views import bundle_detail, bundle_download, bundle_list, export_download, home urlpatterns = [ path("", home, name="home"), + path("bundles/", bundle_list, name="bundle_list"), + path("bundles//", bundle_detail, name="bundle_detail"), + path("bundles//download/", bundle_download, name="bundle_download"), + path("exports//download/", export_download, name="export_download"), ] diff --git a/core/views.py b/core/views.py index c9aed12..a7c389c 100644 --- a/core/views.py +++ b/core/views.py @@ -1,25 +1,199 @@ -import os -import platform +from html.parser import HTMLParser +from io import BytesIO +import textwrap -from django import get_version as django_version -from django.shortcuts import render +from django.contrib import messages +from django.http import HttpResponse +from django.shortcuts import get_object_or_404, redirect, render from django.utils import timezone +from django.utils.text import slugify + +from reportlab.lib.pagesizes import letter +from reportlab.pdfgen import canvas + +from .forms import BundleUploadForm +from .models import HtmlBundle, HtmlDocument, HtmlExport + + +class _HtmlTextExtractor(HTMLParser): + block_tags = { + "p", + "div", + "br", + "li", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "section", + "article", + "header", + "footer", + } + + def __init__(self): + super().__init__() + self.parts = [] + + def handle_starttag(self, tag, attrs): + if tag in self.block_tags: + self.parts.append("\n") + + def handle_endtag(self, tag): + if tag in self.block_tags: + self.parts.append("\n") + + def handle_data(self, data): + if data.strip(): + self.parts.append(data) + + +def _extract_text_from_html(html_bytes: bytes) -> str: + html_text = html_bytes.decode("utf-8", errors="replace") + parser = _HtmlTextExtractor() + parser.feed(html_text) + raw_text = "".join(parser.parts) + lines = [line.strip() for line in raw_text.splitlines()] + cleaned = "\n".join([line for line in lines if line]) + return cleaned + + +def _build_pdf(bundle: HtmlBundle) -> BytesIO: + buffer = BytesIO() + pdf = canvas.Canvas(buffer, pagesize=letter) + width, height = letter + margin = 72 + line_height = 14 + y = height - margin + + documents = bundle.documents.all() + for document in documents: + text = document.content_text or "" + paragraphs = text.splitlines() or [""] + for paragraph in paragraphs: + wrapped = textwrap.wrap(paragraph, width=95) or [""] + for line in wrapped: + if y < margin: + pdf.showPage() + y = height - margin + pdf.setFont("Helvetica", 11) + pdf.drawString(margin, y, line) + y -= line_height + y -= 6 + y -= 12 + + pdf.save() + buffer.seek(0) + return buffer def home(request): - """Render the landing screen with loader and environment details.""" - host_name = request.get_host().lower() - agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic" - now = timezone.now() + file_errors = [] + form = BundleUploadForm() + if request.method == "POST": + form = BundleUploadForm(request.POST) + files = request.FILES.getlist("files") + if form.is_valid(): + if not files: + file_errors.append("No files were received. Please ensure you have selected files and try again.") + else: + invalid_files = [f.name for f in files if not f.name.lower().endswith((".html", ".htm"))] + if invalid_files: + file_errors.append( + f"Only .html or .htm files are supported. Invalid: {', '.join(invalid_files[:3])}" + ) + else: + title = form.cleaned_data.get("title", "").strip() + bundle = HtmlBundle.objects.create(title=title) + for index, uploaded in enumerate(files, start=1): + content_text = _extract_text_from_html(uploaded.read()) + HtmlDocument.objects.create( + bundle=bundle, + original_name=uploaded.name, + order=index, + content_text=content_text, + ) + messages.success(request, f"Bundle '{bundle.title or 'Untitled'}' created with {len(files)} files.") + return redirect("bundle_detail", bundle_id=bundle.id) + + bundles = HtmlBundle.objects.order_by("-created_at")[:5] + exports = HtmlExport.objects.select_related("bundle").order_by("-created_at")[:5] context = { - "project_name": "New Style", - "agent_brand": agent_brand, - "django_version": django_version(), - "python_version": platform.python_version(), - "current_time": now, - "host_name": host_name, - "project_description": os.getenv("PROJECT_DESCRIPTION", ""), - "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), + "page_title": "HTML Bundle to PDF", + "page_description": "Upload multiple HTML files, arrange the order, and export a single PDF instantly.", + "form": form, + "file_errors": file_errors, + "bundles": bundles, + "exports": exports, } return render(request, "core/index.html", context) + + +def bundle_list(request): + bundles = HtmlBundle.objects.order_by("-created_at") + context = { + "page_title": "All Bundles", + "page_description": "Browse recent HTML bundles and download combined PDFs.", + "bundles": bundles, + } + return render(request, "core/bundle_list.html", context) + + +def bundle_detail(request, bundle_id: int): + bundle = get_object_or_404(HtmlBundle, pk=bundle_id) + documents = bundle.documents.all() + exports = bundle.exports.order_by("-created_at") + + if request.method == "POST" and request.POST.get("action") == "update_order": + updates = [] + for document in documents: + field_name = f"order_{document.id}" + raw_value = request.POST.get(field_name) + if raw_value is None: + continue + try: + order_value = int(raw_value) + if order_value < 1: + raise ValueError + except ValueError: + messages.error(request, "Order values must be positive numbers.") + return redirect("bundle_detail", bundle_id=bundle.id) + document.order = order_value + updates.append(document) + if updates: + HtmlDocument.objects.bulk_update(updates, ["order"]) + messages.success(request, "Order updated.") + return redirect("bundle_detail", bundle_id=bundle.id) + + context = { + "page_title": bundle.title or "Untitled bundle", + "page_description": "Review your bundle and generate a combined PDF.", + "bundle": bundle, + "documents": documents, + "exports": exports, + } + return render(request, "core/bundle_detail.html", context) + + +def bundle_download(request, bundle_id: int): + bundle = get_object_or_404(HtmlBundle, pk=bundle_id) + slug = slugify(bundle.title) or "bundle" + timestamp = timezone.now().strftime("%Y%m%d-%H%M") + file_name = f"{slug}-{timestamp}.pdf" + HtmlExport.objects.create(bundle=bundle, file_name=file_name) + + pdf_buffer = _build_pdf(bundle) + response = HttpResponse(pdf_buffer.getvalue(), content_type="application/pdf") + response["Content-Disposition"] = f'attachment; filename="{file_name}"' + return response + + +def export_download(request, export_id: int): + export = get_object_or_404(HtmlExport, pk=export_id) + pdf_buffer = _build_pdf(export.bundle) + response = HttpResponse(pdf_buffer.getvalue(), content_type="application/pdf") + response["Content-Disposition"] = f'attachment; filename="{export.file_name}"' + return response diff --git a/requirements.txt b/requirements.txt index e22994c..0f80449 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ Django==5.2.7 mysqlclient==2.2.7 python-dotenv==1.1.1 +reportlab==4.2.0 diff --git a/static/css/custom.css b/static/css/custom.css index 925f6ed..f89242f 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -1,4 +1,210 @@ /* Custom styles for the application */ -body { - font-family: system-ui, -apple-system, sans-serif; +:root { + --ink-900: #0b1f24; + --ink-700: #1e3a40; + --ink-500: #456870; + --primary-600: #1f6f5b; + --primary-500: #2f8f76; + --accent-500: #f2c94c; + --accent-600: #e7b93a; + --coral-500: #e07a5f; + --sand-50: #f6f4ef; + --sand-100: #efeae2; + --surface-0: #ffffff; + --shadow-soft: 0 16px 40px rgba(11, 31, 36, 0.12); + --shadow-card: 0 12px 28px rgba(11, 31, 36, 0.1); + --radius-xl: 28px; + --radius-lg: 20px; + --radius-md: 14px; + --radius-sm: 10px; + --gradient-hero: linear-gradient(120deg, rgba(47, 143, 118, 0.15), rgba(242, 201, 76, 0.2), rgba(224, 122, 95, 0.12)); +} + +body { + font-family: "DM Sans", system-ui, -apple-system, sans-serif; + color: var(--ink-900); + background: radial-gradient(circle at top left, #f7f5f2 0%, #f4f0e8 45%, #f1e8dc 100%); + min-height: 100vh; +} + +h1, h2, h3, h4, h5 { + font-family: "Space Grotesk", "DM Sans", sans-serif; + color: var(--ink-900); + letter-spacing: -0.02em; +} + +a { + color: inherit; + text-decoration: none; +} + +.app-shell { + background: var(--surface-0); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-soft); + border: 1px solid rgba(30, 58, 64, 0.08); +} + +.app-nav { + padding: 1.2rem 1.8rem; + border-bottom: 1px solid rgba(30, 58, 64, 0.08); + background: rgba(255, 255, 255, 0.9); + backdrop-filter: blur(10px); +} + +.brand-mark { + display: inline-flex; + align-items: center; + gap: 0.65rem; + font-weight: 700; + font-size: 1.1rem; +} + +.brand-dot { + width: 14px; + height: 14px; + border-radius: 50%; + background: linear-gradient(135deg, var(--primary-500), var(--accent-500)); + box-shadow: 0 0 0 6px rgba(47, 143, 118, 0.15); +} + +.hero { + padding: 4.5rem 2.5rem 3.5rem; + border-radius: var(--radius-xl); + position: relative; + overflow: hidden; + background: var(--gradient-hero); +} + +.hero::after { + content: ""; + position: absolute; + inset: 0; + background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120' viewBox='0 0 120 120'%3E%3Ccircle cx='24' cy='24' r='2.2' fill='rgba(30,58,64,0.15)'/%3E%3Ccircle cx='96' cy='58' r='1.8' fill='rgba(30,58,64,0.12)'/%3E%3Ccircle cx='54' cy='94' r='2' fill='rgba(30,58,64,0.1)'/%3E%3C/svg%3E"); + opacity: 0.35; + pointer-events: none; +} + +.hero-content { + position: relative; + z-index: 1; +} + +.hero h1 { + font-size: clamp(2.6rem, 3vw + 1.2rem, 3.6rem); + line-height: 1.1; +} + +.hero p { + font-size: 1.1rem; + color: var(--ink-700); +} + +.primary-btn { + background: var(--primary-600); + border: none; + color: #ffffff; + font-weight: 600; + padding: 0.75rem 1.4rem; + border-radius: 999px; + box-shadow: 0 12px 18px rgba(31, 111, 91, 0.24); +} + +.primary-btn:hover { + background: var(--primary-500); + color: #ffffff; +} + +.ghost-btn { + border-radius: 999px; + border: 1px solid rgba(31, 111, 91, 0.25); + color: var(--primary-600); + padding: 0.7rem 1.2rem; + font-weight: 600; +} + +.pill { + display: inline-flex; + align-items: center; + gap: 0.4rem; + padding: 0.35rem 0.8rem; + border-radius: 999px; + background: rgba(31, 111, 91, 0.12); + color: var(--ink-700); + font-size: 0.85rem; + font-weight: 600; +} + +.workflow-card { + border-radius: var(--radius-lg); + border: 1px solid rgba(30, 58, 64, 0.1); + background: var(--surface-0); + box-shadow: var(--shadow-card); + padding: 1.8rem; +} + +.section-title { + font-size: 1.5rem; +} + +.file-preview { + background: var(--sand-50); + border: 1px dashed rgba(30, 58, 64, 0.15); + border-radius: var(--radius-md); + padding: 1rem; + font-size: 0.95rem; + color: var(--ink-500); +} + +.stat-card { + padding: 1rem 1.2rem; + border-radius: var(--radius-md); + border: 1px solid rgba(30, 58, 64, 0.1); + background: rgba(255, 255, 255, 0.8); +} + +.list-card { + border-radius: var(--radius-md); + border: 1px solid rgba(30, 58, 64, 0.08); + padding: 1.2rem; + background: var(--surface-0); +} + +.muted-text { + color: var(--ink-500); +} + +.order-input { + width: 72px; + border-radius: var(--radius-sm); + border: 1px solid rgba(30, 58, 64, 0.2); + padding: 0.4rem 0.5rem; +} + +.app-footer { + color: var(--ink-500); + font-size: 0.9rem; +} + +.badge-soft { + background: rgba(242, 201, 76, 0.2); + color: var(--ink-700); + border-radius: 999px; + padding: 0.35rem 0.75rem; + font-weight: 600; + font-size: 0.8rem; +} + +.upload-drop { + border-radius: var(--radius-md); + border: 2px dashed rgba(47, 143, 118, 0.4); + padding: 1.2rem; + background: rgba(47, 143, 118, 0.06); +} + +.alert-custom { + border-radius: var(--radius-md); + border: 1px solid rgba(31, 111, 91, 0.2); + background: rgba(31, 111, 91, 0.08); + color: var(--ink-700); }