From aaaf2c8ee7cfc31d3ad48bd26d8e99c7ef2178e3 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 18 Nov 2025 19:58:02 +0000 Subject: [PATCH] First iteration --- .perm_test_apache | 0 .perm_test_exec | 0 ai/__init__.py | 3 + ai/local_ai_api.py | 281 ++++++++++++++++++ config/__pycache__/__init__.cpython-311.pyc | Bin 159 -> 159 bytes config/__pycache__/settings.cpython-311.pyc | Bin 4210 -> 4210 bytes config/__pycache__/urls.cpython-311.pyc | Bin 1143 -> 1143 bytes config/__pycache__/wsgi.cpython-311.pyc | Bin 679 -> 679 bytes core/__pycache__/__init__.cpython-311.pyc | Bin 157 -> 157 bytes core/__pycache__/admin.cpython-311.pyc | Bin 777 -> 1178 bytes core/__pycache__/apps.cpython-311.pyc | Bin 524 -> 524 bytes core/__pycache__/forms.cpython-311.pyc | Bin 847 -> 847 bytes core/__pycache__/models.cpython-311.pyc | Bin 1409 -> 2240 bytes core/__pycache__/urls.cpython-311.pyc | Bin 347 -> 378 bytes core/__pycache__/views.cpython-311.pyc | Bin 2006 -> 634 bytes core/admin.py | 17 +- .../0002_category_product_delete_ticket.py | 42 +++ .../__pycache__/0001_initial.cpython-311.pyc | Bin 1660 -> 1660 bytes ...gory_product_delete_ticket.cpython-311.pyc | Bin 0 -> 2383 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 168 -> 168 bytes core/models.py | 33 +- core/templates/base.html | 21 ++ core/templates/core/article_detail.html | 14 + core/templates/core/index.html | 185 ++---------- core/urls.py | 6 +- core/views.py | 38 +-- static/css/custom.css | 33 ++ staticfiles/css/custom.css | 50 ++-- 28 files changed, 494 insertions(+), 229 deletions(-) create mode 100644 .perm_test_apache create mode 100644 .perm_test_exec create mode 100644 ai/__init__.py create mode 100644 ai/local_ai_api.py create mode 100644 core/migrations/0002_category_product_delete_ticket.py create mode 100644 core/migrations/__pycache__/0002_category_product_delete_ticket.cpython-311.pyc create mode 100644 core/templates/base.html create mode 100644 core/templates/core/article_detail.html create mode 100644 static/css/custom.css diff --git a/.perm_test_apache b/.perm_test_apache new file mode 100644 index 0000000..e69de29 diff --git a/.perm_test_exec b/.perm_test_exec new file mode 100644 index 0000000..e69de29 diff --git a/ai/__init__.py b/ai/__init__.py new file mode 100644 index 0000000..37a7b09 --- /dev/null +++ b/ai/__init__.py @@ -0,0 +1,3 @@ +"""Helpers for interacting with the Flatlogic AI proxy from Django code.""" + +from .local_ai_api import LocalAIApi, create_response, request, decode_json_from_response # noqa: F401 diff --git a/ai/local_ai_api.py b/ai/local_ai_api.py new file mode 100644 index 0000000..516d05a --- /dev/null +++ b/ai/local_ai_api.py @@ -0,0 +1,281 @@ +""" +LocalAIApi — lightweight Python client for the Flatlogic AI proxy. + +Usage (inside the Django workspace): + + from ai.local_ai_api import LocalAIApi + + response = LocalAIApi.create_response({ + "input": [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Summarise this text in two sentences."}, + ], + "text": {"format": {"type": "json_object"}}, + }) + + if response.get("success"): + data = LocalAIApi.decode_json_from_response(response) + # ... + +The helper automatically injects the project UUID header and falls back to +reading executor/.env if environment variables are missing. +""" + +from __future__ import annotations + +import json +import os +import ssl +from typing import Any, Dict, Iterable, Optional +from urllib import error as urlerror +from urllib import request as urlrequest + +__all__ = [ + "LocalAIApi", + "create_response", + "request", + "decode_json_from_response", +] + + +_CONFIG_CACHE: Optional[Dict[str, Any]] = None + + +class LocalAIApi: + """Static helpers mirroring the PHP implementation.""" + + @staticmethod + def create_response(params: Dict[str, Any], options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + return create_response(params, options or {}) + + @staticmethod + def request(path: Optional[str] = None, payload: Optional[Dict[str, Any]] = None, + options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + return request(path, payload or {}, options or {}) + + @staticmethod + def decode_json_from_response(response: Dict[str, Any]) -> Optional[Dict[str, Any]]: + return decode_json_from_response(response) + + +def create_response(params: Dict[str, Any], options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """Signature compatible with the OpenAI Responses API.""" + options = options or {} + payload = dict(params) + + if not isinstance(payload.get("input"), list) or not payload["input"]: + return { + "success": False, + "error": "input_missing", + "message": 'Parameter "input" is required and must be a non-empty list.', + } + + cfg = _config() + payload["model"] = "gpt-5-mini" + + return request(options.get("path"), payload, options) + + +def request(path: Optional[str], payload: Dict[str, Any], options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """Perform a raw request to the AI proxy.""" + cfg = _config() + options = options or {} + + resolved_path = path or options.get("path") or cfg["responses_path"] + if not resolved_path: + return { + "success": False, + "error": "project_id_missing", + "message": "PROJECT_ID is not defined; cannot resolve AI proxy endpoint.", + } + + project_uuid = cfg["project_uuid"] + if not project_uuid: + return { + "success": False, + "error": "project_uuid_missing", + "message": "PROJECT_UUID is not defined; aborting AI request.", + } + + if "project_uuid" not in payload and project_uuid: + payload["project_uuid"] = project_uuid + + url = _build_url(resolved_path, cfg["base_url"]) + timeout = int(options.get("timeout", cfg["timeout"])) + verify_tls = options.get("verify_tls", cfg["verify_tls"]) + + headers: Dict[str, str] = { + "Content-Type": "application/json", + "Accept": "application/json", + cfg["project_header"]: project_uuid, + } + extra_headers = options.get("headers") + if isinstance(extra_headers, Iterable): + for header in extra_headers: + if isinstance(header, str) and ":" in header: + name, value = header.split(":", 1) + headers[name.strip()] = value.strip() + + body = json.dumps(payload, ensure_ascii=False).encode("utf-8") + req = urlrequest.Request(url, data=body, method="POST") + for name, value in headers.items(): + req.add_header(name, value) + + context = None + if not verify_tls: + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + + try: + with urlrequest.urlopen(req, timeout=timeout, context=context) as resp: + status = resp.getcode() + response_body = resp.read().decode("utf-8", errors="replace") + except urlerror.HTTPError as exc: + status = exc.getcode() + response_body = exc.read().decode("utf-8", errors="replace") + except Exception as exc: # pylint: disable=broad-except + return { + "success": False, + "error": "request_failed", + "message": str(exc), + } + + decoded = None + if response_body: + try: + decoded = json.loads(response_body) + except json.JSONDecodeError: + decoded = None + + if 200 <= status < 300: + return { + "success": True, + "status": status, + "data": decoded if decoded is not None else response_body, + } + + error_message = "AI proxy request failed" + if isinstance(decoded, dict): + error_message = decoded.get("error") or decoded.get("message") or error_message + elif response_body: + error_message = response_body + + return { + "success": False, + "status": status, + "error": error_message, + "response": decoded if decoded is not None else response_body, + } + + +def decode_json_from_response(response: Dict[str, Any]) -> Optional[Dict[str, Any]]: + """Attempt to decode JSON emitted by the model (handles markdown fences).""" + text = _extract_text(response) + if text == "": + return None + + try: + decoded = json.loads(text) + if isinstance(decoded, dict): + return decoded + except json.JSONDecodeError: + pass + + stripped = text.strip() + if stripped.startswith("```json"): + stripped = stripped[7:] + if stripped.endswith("```"): + stripped = stripped[:-3] + stripped = stripped.strip() + if stripped and stripped != text: + try: + decoded = json.loads(stripped) + if isinstance(decoded, dict): + return decoded + except json.JSONDecodeError: + return None + return None + + +def _extract_text(response: Dict[str, Any]) -> str: + payload = response.get("data") if response.get("success") else response.get("response") + if isinstance(payload, dict): + output = payload.get("output") + if isinstance(output, list): + combined = "" + for item in output: + content = item.get("content") if isinstance(item, dict) else None + if isinstance(content, list): + for block in content: + if isinstance(block, dict) and block.get("type") == "output_text" and block.get("text"): + combined += str(block["text"]) + if combined: + return combined + choices = payload.get("choices") + if isinstance(choices, list) and choices: + message = choices[0].get("message") + if isinstance(message, dict) and message.get("content"): + return str(message["content"]) + if isinstance(payload, str): + return payload + return "" + + +def _config() -> Dict[str, Any]: + global _CONFIG_CACHE # noqa: PLW0603 + if _CONFIG_CACHE is not None: + return _CONFIG_CACHE + + _ensure_env_loaded() + + base_url = os.getenv("AI_PROXY_BASE_URL", "https://flatlogic.com") + project_id = os.getenv("PROJECT_ID") or None + responses_path = os.getenv("AI_RESPONSES_PATH") + if not responses_path and project_id: + responses_path = f"/projects/{project_id}/ai-request" + + _CONFIG_CACHE = { + "base_url": base_url, + "responses_path": responses_path, + "project_id": project_id, + "project_uuid": os.getenv("PROJECT_UUID"), + "project_header": os.getenv("AI_PROJECT_HEADER", "project-uuid"), + "default_model": os.getenv("AI_DEFAULT_MODEL", "gpt-5"), + "timeout": int(os.getenv("AI_TIMEOUT", "30")), + "verify_tls": os.getenv("AI_VERIFY_TLS", "true").lower() not in {"0", "false", "no"}, + } + return _CONFIG_CACHE + + +def _build_url(path: str, base_url: str) -> str: + trimmed = path.strip() + if trimmed.startswith("http://") or trimmed.startswith("https://"): + return trimmed + if trimmed.startswith("/"): + return f"{base_url}{trimmed}" + return f"{base_url}/{trimmed}" + + +def _ensure_env_loaded() -> None: + """Populate os.environ from executor/.env if variables are missing.""" + if os.getenv("PROJECT_UUID") and os.getenv("PROJECT_ID"): + return + + env_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".env")) + if not os.path.exists(env_path): + return + + try: + with open(env_path, "r", encoding="utf-8") as handle: + for line in handle: + stripped = line.strip() + if not stripped or stripped.startswith("#") or "=" not in stripped: + continue + key, value = stripped.split("=", 1) + key = key.strip() + value = value.strip().strip('\'"') + if key and not os.getenv(key): + os.environ[key] = value + except OSError: + pass diff --git a/config/__pycache__/__init__.cpython-311.pyc b/config/__pycache__/__init__.cpython-311.pyc index 3d6501c67fa5c80fdda8a8ee57862699aabd319d..bea1479d539dc929713b420ca97ee7712b55de31 100644 GIT binary patch delta 20 acmbQwIG>ScIWI340}wntCzCmmXEFdY*9Cw8 delta 20 acmbQwIG>ScIWI340}vQ{e#)50GZ_Fd1qB`e diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index dadfaa7db630a06a9bc0da9edbc375273aefa0ed..e24a61a6a1dc4fe94c451dbdba9b220e91e17393 100644 GIT binary patch delta 21 bcmeyQ@JWGZIWI340}wntCzH96CrtnVOCJVj delta 21 bcmeyQ@JWGZIWI340}wn|@;zfCPnrM#PVNU1 diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 139db1071801d00bd7e1a167305bffd074d952f6..c4b9a51c65a3944f54bb8d272aee0b0c8b038f1d 100644 GIT binary patch delta 21 bcmey)@tuQbIWI340}wntCzH96Cz}NTNL2

HV%6u9<52Z4D z)#E;kevlI^?QF63A!zVgvXV68q{hE+H;DsL`y7b%D0Ez2a|6L^*_Nx}hA?1F1{xUA zJV5LGIUJkbd6ib?T5mhI?|proPA{g~M6ZjRgLT)dvjEe7)v3dM`9>`jMg>NVsZ0a4 zbf$w!Ia%rporz@XnJ`ihh39fva6UPn@2vP-7G1%!4Lh+cCL!zFrYde>F%t-r2;&I& zZQ0C;A#_RzWrSe>Ih3in>iIDjvYa`bc`;@YBN&zKZWOJt0pMvxRe*o=sVt>g2Z2VP zq*-sh>5R|ptfylST1)u!sE}pZEXZND<}~X;)eV}l@Kyxgd6|PV=YAycrxEy1NJH>C zoHrC#T*A3bnBGxk^<1p+%y`0C0AkVQM)pC Qj1#8_$O2g}$7kyO0cMC5(f|Me literal 777 zcmZuu&2AGh5FYO)n~*eADlJ7tAcTbI9_T&*2&r5+Kzji$d$Bfaw{g3FwY`Geg474- zt#1&7`Z7i8Au^XroO(+Y#Gxm~n{K5_9oysi#`EWyAH7}&kbb}XH+W3|{#a0xcP-2c z6pn!bBMGD=Bm|hl+{8)U&?VpiCwq2pvX3mpNgBxnYwv zFl5to=iZw(BEps``+Gdr##1`dr84b8id+gkG>eiSlw4^pDNiGjm`*Htq&cIJ_8oZ- zGfCSz-<54EcafPc<0_V-&?3*aeb2Nh&7zc3YC4prIV%$@FHm|=M#=TdLL!u=OsFD> zhNfGg9g$#S(^EW>@qq3Ko-p+jW|#x6UIl|ZN~QjO9+|3 zv(wvq0&AzY_c>V{tsFi*@_v!kGqO5$T=I@g;ckBP;%w>pW%c;C;+HNskM4A0#kn&dbZi00d9Z$z(3%VFCa#5Cq=< delta 20 acmeBS>0#kn&dbZi00hRKpE4HmFaZE9J_LOL diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index f6e5c4e3024d0f534a8b93046d15a44a0c8eeba6..9eaddedb41f03feaa9fb39c93cb340291079c340 100644 GIT binary patch delta 21 bcmX@lcAkxAIWI340}wntCzH96$C()bL3jnd delta 21 bcmX@lcAkxAIWI340}!OEe9PF#d<0N$e)qn&SM&ThSoZ4}l7%>XilZFzs-m^@H5|u~N zf5@OAg9i`Zyk>G8SVKpT9WxXt1#l1ubn4{IhBA4|-J|3m*(mxX9`D`zc<pj?(@OltY0CL^OnoSQJGeB>XIp(0hS|skjm9K5zscPT&eg zG0agBKt=xp6(O-@As0WxhjOAHtC)luR;lBJ-y)q!+Xm%Oph8h3LNOE)Xn3Q$2EjE# zMnphslfdf7oT~oTp|^tXhSTl`#C7mQEQU!4T!$}HUDx^7)YklD zXR1?cDHri2Rjq0js;YicRUtW!fx4oqZyn9(tPE^YwrW~5K-LV0X~svPjR(KX7uL-R zEjX)A)piPWo0c5gWQ8r0y|HRqi55yGqlF;r`Pz;j!629nkH+D$hCtM>eywl5bET2s z&+tHO5JahaM$iS+u?E^15QoA$VFT{LScbeULFytHHr_32tXcTLI2!=z$5qv`nX2w) zu2RymV3oE})P+`hV1LJzhWY+Wxg<-0nf0MaqrlDm#N4{Z7IbP5Hq7VChEwKq*cDVB zHyc5G76m^G#=x-Zxj;YG7Kj_imq+$~@Z^yXmMc#-GdQB+`iX5GcVByh9)ae8hSXGDj$Z2SCIL?j-eef)q#LE8>GBNU|aK z&@Ig|STq49)KGf%%5nt0sV*YbgBZAj=<7iYk~EO|E0O``W&QZC3NYtiKw~k@>(N#mMzG0YKhA$&0vZrv{O>}(}a$!H=)C^N2Do||# zfrvQ6M4n;m$%TUULG-e2TYev*CB32~u3b?PaIyudwm-_7dJ-f`)?UUCKrM z1liE4WiwA!gN_+}jUV2i{qe-y?u(bwv*6?NBbx;eB3#xEi1V;0iY;N(z5PFrNFoGr gSL|)>zq>&D=j+8m8L@Z8-rBx#f%eb$Eb`NQ0m=poe*gdg literal 1409 zcmZuxy>Ht_6hDg8w@169EsumE? zlf`$Pzf%bPA&e=dC(6Y#C~pu!M6yv&(j^I{&?+LSSBS_|>Ox*Z$1*~vuoBW`qHLo^ z`dpX|iD!Bap|)#I;8pCw%enF6V#5KVV8(?lYlDaR0h3M(N! zdnF=GWs*6|UO7;1CQ2c>nW%gm6}Ls~ehlk0DFa+D#%O8kxPq!NTArdLe^$71(#lLk z@k+!TDZN5^m8j6XGS6t1`ILGMxsl?EGoXF0Yyk(WX!Y@cIc-K=_eF8Nf9v7I04!tL zj!Oy8ti&?TmThOB%ilTs0`E~``8}T5h^5%OW3@Y6Sp$Cl51bzcR_GZA*rxrq*Ezoi zE+!_>kXa1`&zc>_GO5e6gzg!>?J*VH8l;sB-*a%^*~bPUVCSZ`c{wkL6uNjS7jwYg zpqHo%G9kFPv*Wc|@(*sp;slDfloLldkt&Y>9Eq~YVdwz=^P!sC0*1p4@x&y*<-+HvJJ)U})xF652bPRUiqBdb4 z3OnO^hUdFHH(-{-Eblol-0K;Z9f$A=qtATmdX!-*ygaYb124gfgt{iP2A<{gx%!Ra zQO)Yn1S^<~f`eeg;{|^}COVsklD{Ih0D|2RO#I?vCJyKR4lKC*j8jTIGe%PuY>rFH zb>Wz>P`NfPp~}s}y{P=r$;V;2IV?A$ax+*RsSAfYk-BhFd;8?Kolw<=surnQaDN2R zKShZ-yTf`TsyBkIQSJ88CsFP8u(lZ07K3j` z)jLP^sCwt^QdnIcR+ppda`0g4Jn_Qn(y+P|RhNPXV?|2OkI^hk?k%po*-&_%bd7%7 zX_DP!GU*NRhm>^WQ_+^oF1^ovnGF3a_89=;-MJgU#n3W_bVr diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 4e4f113d309e34129cca5a1bb3778fb33b4fe2e4..107d3b6f6482b216c34daaa81668d903bc29ee49 100644 GIT binary patch delta 131 zcmcc3^oxmiIWI340}$w+m&xp&$SW!60pv_)NMT4}%wfo7jACS*s2#+{nwgi9S~0Py zjWK32C!>sq%moG%^ngL)0xJ5z#=y$i;L;(~5jsQs0*m5B7R4(piXWJn_?a5GL9mDi GXb}M14$JU{~gXp$WQ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 9d0ddd892cc8db8e347c5f97af5002a4ce469452..36f41552898d3036ced9892c1b976a0339b6f409 100644 GIT binary patch literal 634 zcmZWlzfT)66t?eI($dt3N?qs>sYKOPZEhkApblM{DMD(eY?rf3F8JbL`yj1?$iT?J z#@Y#}=+OU!qe3b=sZym*Y>@&B6Yq|O7V%_%e(&D*{C&QIQt2UrczOIRynyphR!qY9 zn~dim*+Udj%u$4!7$a&>lN*uQG$FUPQPZOKd*nLj*@%l%N3t$u@*Llkg=Hyd9Oxjc zCt*Wk!X9&gJ)|e!;q2VLc+=2@f7Ah(8yNeba+E3M z0XFT%2k`j7U-v`qw>V3Sy&He(GYN!bUI^y<)wYhfYst)KS`e)c3$#j|k0IAg%IrSZ zObe2&#Y|~g%vpixYSz~(>z2#qtJhvzM9hm@aaYG4>$4!%LV6oQuBx6NFfW%|57~yQ z_CBR{E-W9zv;|u&0u0eF@^okGC#n1(mG4$UDko(Al*|w7zl$@w^^s{1XZY^YL62s3 zUnH}y4p&a5YNu1R#Hx*995q}!ou(bXyCSNpEu;nuDz~3Hk)VvL{DrybS0o9H$P&O+ paS7TnXK&;aD6>DU-U5tFjPV(Ip4?q$Xg0aKMwX2iM(94|8rOzqm2Ln4 literal 2006 zcmZ`(&2Q936d!wcz21)`&W4tJ#pJA?V%j1zOmzNm$ozG=l3ywe)Hb% z%?~3Zc?9FF3q=E*-j z479_0?hBiOCEO9YnTVF?30CU9NR#Y?c5QI}H@-|+=}nR529D9No${FX$C{LI!+SPU zn*tq6Z168R;jN4%Z9`N$QDj{EKl`$8?z7~_^8GZONIW6PmX+H~57!dvm~Wk5i3+CD z8de>{vutlw4NQWur#5X`Qyt%cda7VivA1p$-*d4?RSQ$Yc7kfFXw>UXyVcsLn!d~A zi}m`KTw=LPjzJxN)iztXW!zM6(hUc*i5n}|uPiRy(m!3ixv;WyYhaWLKH(DYNaRP^~=u`nh_pqn_KE-I{$i*}l4) zFFl<5w)(KzUg%5c(6R8~%x?K)w>;Y`&+e9Idgb@K67l%h2GSK{&;Ro>LVcS%fBB$#nRS^&g9(F)o$TzuW+`L zKD!4&B9oJ3Bu%z=8Z?g(M*ND?L<|=k&S;V)Xju_q*%G&*sdomAt>vr~Ksn8n_|o|M zZfPBi(nO=1MF&vBofDNaO4Twisve%7U0!$``60Z5tPnIz69)lD(vOH71syrU$$JUmQZNUkJ(yv!)Lsn7 zC)-!T1E<>8!qVZbx$oAyrQ^NQ@%B|dV@KRaWr9p`SMLI0QrruKXD>#g5^JhK%_kIq z6GQ-Hg?%+d6~uuS6Agt{j`4hGUll_OS`HK2tYXWiB+LJn;b#xxJV;Saf+#^zM&kpX z;(-hf{aXGw&g*&d0T_7If)GeQB?v-@a_wk_XsjL0m*{k7c!lU_XLyC^SZ8>J=tyUH tg~Lw3=Swuz8D1eOcZOGAQiKotXh4pqg%f@BZ?e_;m1}?d4^wpVzX0UP4AcMs diff --git a/core/admin.py b/core/admin.py index 639ff3a..5cf33ef 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,8 +1,13 @@ from django.contrib import admin -from .models import Ticket +from .models import Category, Product -@admin.register(Ticket) -class TicketAdmin(admin.ModelAdmin): - list_display = ('subject', 'status', 'priority', 'requester_email', 'created_at') - list_filter = ('status', 'priority') - search_fields = ('subject', 'requester_email', 'description') +@admin.register(Category) +class CategoryAdmin(admin.ModelAdmin): + list_display = ['name', 'slug'] + prepopulated_fields = {'slug': ('name',)} + +@admin.register(Product) +class ProductAdmin(admin.ModelAdmin): + list_display = ['name', 'price', 'available', 'created_at', 'updated_at'] + list_filter = ['available', 'created_at', 'updated_at'] + list_editable = ['price', 'available'] \ No newline at end of file diff --git a/core/migrations/0002_category_product_delete_ticket.py b/core/migrations/0002_category_product_delete_ticket.py new file mode 100644 index 0000000..622193d --- /dev/null +++ b/core/migrations/0002_category_product_delete_ticket.py @@ -0,0 +1,42 @@ +# Generated by Django 5.2.7 on 2025-11-18 19:56 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200)), + ('slug', models.SlugField(max_length=200, unique=True)), + ], + options={ + 'verbose_name_plural': 'Categories', + }, + ), + migrations.CreateModel( + name='Product', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200)), + ('description', models.TextField(blank=True)), + ('image', models.ImageField(blank=True, null=True, upload_to='products/')), + ('price', models.DecimalField(decimal_places=2, max_digits=10)), + ('available', models.BooleanField(default=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='products', to='core.category')), + ], + ), + migrations.DeleteModel( + name='Ticket', + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc index 64d8a5533a01aeb9823459fa0e2a5787563687ab..8d322dd50e1b2da35a84eaedf74847abd5dfd693 100644 GIT binary patch delta 21 bcmeyv^M{9LIWI340}wntCzH96r+^IrNx=qF delta 21 bcmeyv^M{9LIWI340}$N&_cLQ7PXQYMPud4l diff --git a/core/migrations/__pycache__/0002_category_product_delete_ticket.cpython-311.pyc b/core/migrations/__pycache__/0002_category_product_delete_ticket.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ca45a3a8b5fc33ba7cbf2640ba3f86744a1347b6 GIT binary patch literal 2383 zcmb_eO>omj6qaS#u@ooRxi&5N@qxJ?-xMwEMkp-@fF} zp&^NbYyItn`fHTq{-K@Viu4-q7oqV7hd30{xM~RAyv8@f)o@d&3L%c??r|vm8;1lO zZUo}Rg zBN_A?kfHw7INNWH1TAy{ebK(S0~z?bpM`6DU2IREj0x5d8bN+>=*#w{lRWC->KOV8 zvh^|%9|l5Q#k@e`ienpc&BWkD zWUytbO~u@jH}Dn_Eo{Pv>T{TB6Pv4sg=Jl7Vj@s`xDUbPLvT8jXR?O%y1h;WOLOXZ z-lP$esH3Y-985+&{#I^jjtOL>9vKx|`LLN__%#)4$RYzq%Rg%aWz#@T%_f5gTQyT< z!?0pxRa5j05&@=l{9J7LD60q)ou?BCx}#}CbXuCBAlWuZ1fr_pLwX_(X*z;H!wA>l zdky#~HEj7&AhoXA77>+AMb(s54Z~ss2(KxQW)rEWk*wG-?8~MbPh|xmA~`KY_49o( zCMozxtQJTf3s^oMqFYl49Qg?2Y)(?*qM^%BU9b&;n^q4qtRt+~Ky{PWJ2JSyHvSrnjMI*WqB#fP1py?l$*|Pk9+C-?y{S{)k)v-(zo2iHzyo#M%{F&lP-DblAE~K9S}x` zyKydg>D4c==-6=gj3i3$I50=lbjObwWc~|-tUERHD(X$m?Aq?sY-eiLo0@eK-yS5- zxtXimm%Pl?-RZr`{?Be^rIT6lGAnMP%AD7z^V;4|`#0RoQYW+IWtQB;j|a&KH+y}1 z&C6cjn|8BzI@vp3_KutQ?jSko=5B6pdbyhrX|B}CmAo8{^drY!#m&uka`Rqp-c2lk zwC-eK`;j+U*sZ(~-0~x@yyBG~du7!tKk0JZ9RGlaSDBaTEBly#$LtZaFp^aG?uZ03 zMuw<#V2-HiP8>^@25qSi%n>!+uP4t&Uwlt*^KOs~`6x;I`C34$1^=IcLXfoPZGaK6 z?`LO>9Vm% literal 0 HcmV?d00001 diff --git a/core/migrations/__pycache__/__init__.cpython-311.pyc b/core/migrations/__pycache__/__init__.cpython-311.pyc index 58b1c14eb06fea9cfb9a0d59788218572a75b51c..3f6985a222e44c6985d9e921a6674f26ffa0687c 100644 GIT binary patch delta 20 acmZ3%xPp;qIWI340}wntCzCmmXAS^0HU*mi delta 20 acmZ3%xPp;qIWI340}vQ{e#)50GY0@MWCb|@ diff --git a/core/models.py b/core/models.py index 78b60d1..240e8e5 100644 --- a/core/models.py +++ b/core/models.py @@ -1,25 +1,24 @@ from django.db import models -class Ticket(models.Model): - STATUS_CHOICES = [ - ('open', 'Open'), - ('in_progress', 'In Progress'), - ('closed', 'Closed'), - ] +class Category(models.Model): + name = models.CharField(max_length=200) + slug = models.SlugField(max_length=200, unique=True) - PRIORITY_CHOICES = [ - ('low', 'Low'), - ('medium', 'Medium'), - ('high', 'High'), - ] + class Meta: + verbose_name_plural = "Categories" - subject = models.CharField(max_length=255) - status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='open') - priority = models.CharField(max_length=20, choices=PRIORITY_CHOICES, default='medium') - requester_email = models.EmailField() - description = models.TextField() + def __str__(self): + return self.name + +class Product(models.Model): + category = models.ForeignKey(Category, related_name='products', on_delete=models.CASCADE, null=True, blank=True) + name = models.CharField(max_length=200) + description = models.TextField(blank=True) + image = models.ImageField(upload_to='products/', null=True, blank=True) + price = models.DecimalField(max_digits=10, decimal_places=2) + available = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def __str__(self): - return self.subject \ No newline at end of file + return self.name diff --git a/core/templates/base.html b/core/templates/base.html new file mode 100644 index 0000000..696cc71 --- /dev/null +++ b/core/templates/base.html @@ -0,0 +1,21 @@ + + + + + + {% block title %}B2B Portal{% endblock %} + + + + + {% load static %} + + {% block head %}{% endblock %} + + +

+ {% block content %}{% endblock %} +
+ + + \ No newline at end of file diff --git a/core/templates/core/article_detail.html b/core/templates/core/article_detail.html new file mode 100644 index 0000000..8820990 --- /dev/null +++ b/core/templates/core/article_detail.html @@ -0,0 +1,14 @@ +{% extends 'base.html' %} + +{% block title %}{{ article.title }}{% endblock %} + +{% block content %} +
+

{{ article.title }}

+

Published on {{ article.created_at|date:"F d, Y" }}

+
+
+ {{ article.content|safe }} +
+
+{% endblock %} diff --git a/core/templates/core/index.html b/core/templates/core/index.html index f4e4991..28db75a 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,157 +1,38 @@ - - +{% extends 'base.html' %} +{% load static %} - - - - {{ project_name }} - {% if project_description %} - - - - {% endif %} - {% if project_image_url %} - - - {% endif %} - - - - - - - -
-
-

Analyzing your requirements and generating your website…

-
- Loading… +
+
+
+ {% for product in products %} +
+
+ {{ product.name }} +
+
{{ product.name }}
+

{{ product.description|truncatewords:15 }}

+
+
+ + +
+ ${{ product.price }} +
+
+
-

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" }} -

+ {% endfor %}
- -
- - - \ No newline at end of file + + +{% endblock %} diff --git a/core/urls.py b/core/urls.py index 6299e3d..332eff0 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,9 @@ from django.urls import path -from .views import home +from django.urls import path + +from .views import index urlpatterns = [ - path("", home, name="home"), + path("", index, name="index"), ] diff --git a/core/views.py b/core/views.py index c1a6d45..1f868c4 100644 --- a/core/views.py +++ b/core/views.py @@ -1,37 +1,9 @@ -import os -import platform - -from django import get_version as django_version from django.shortcuts import render -from django.urls import reverse_lazy -from django.utils import timezone -from django.views.generic.edit import CreateView - -from .forms import TicketForm -from .models import Ticket - - -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() +from .models import Product +def index(request): + products = Product.objects.filter(available=True) 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", ""), + 'products': products } - return render(request, "core/index.html", context) - - -class TicketCreateView(CreateView): - model = Ticket - form_class = TicketForm - template_name = "core/ticket_create.html" - success_url = reverse_lazy("home") + return render(request, 'core/index.html', context) \ No newline at end of file diff --git a/static/css/custom.css b/static/css/custom.css new file mode 100644 index 0000000..4e0bc46 --- /dev/null +++ b/static/css/custom.css @@ -0,0 +1,33 @@ +body { + font-family: 'Lato', sans-serif; + color: #34495e; +} + +h1, h2, h3, h4, h5, h6 { + font-family: 'Montserrat', sans-serif; +} + +.hero-section { + background: linear-gradient(45deg, #2c3e50, #3498db); + padding: 4rem 0; + margin-bottom: 2rem; +} + +.product-card { + transition: transform .2s ease-in-out, box-shadow .2s ease-in-out; +} + +.product-card:hover { + transform: translateY(-5px); + box-shadow: 0 .5rem 1rem rgba(0,0,0,.15)!important; +} + +.btn-primary { + background-color: #3498db; + border-color: #3498db; +} + +.btn-primary:hover { + background-color: #2980b9; + border-color: #2980b9; +} diff --git a/staticfiles/css/custom.css b/staticfiles/css/custom.css index 108056f..4e0bc46 100644 --- a/staticfiles/css/custom.css +++ b/staticfiles/css/custom.css @@ -1,21 +1,33 @@ - -:root { - --bg-color-start: #6a11cb; - --bg-color-end: #2575fc; - --text-color: #ffffff; - --card-bg-color: rgba(255, 255, 255, 0.01); - --card-border-color: rgba(255, 255, 255, 0.1); -} body { - margin: 0; - font-family: 'Inter', sans-serif; - background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end)); - color: var(--text-color); - display: flex; - justify-content: center; - align-items: center; - min-height: 100vh; - text-align: center; - overflow: hidden; - position: relative; + font-family: 'Lato', sans-serif; + color: #34495e; +} + +h1, h2, h3, h4, h5, h6 { + font-family: 'Montserrat', sans-serif; +} + +.hero-section { + background: linear-gradient(45deg, #2c3e50, #3498db); + padding: 4rem 0; + margin-bottom: 2rem; +} + +.product-card { + transition: transform .2s ease-in-out, box-shadow .2s ease-in-out; +} + +.product-card:hover { + transform: translateY(-5px); + box-shadow: 0 .5rem 1rem rgba(0,0,0,.15)!important; +} + +.btn-primary { + background-color: #3498db; + border-color: #3498db; +} + +.btn-primary:hover { + background-color: #2980b9; + border-color: #2980b9; }