From ceeec65d5646100803e2cd39c844e0b62bf6c94b Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sat, 8 Nov 2025 17:48:25 +0000 Subject: [PATCH] Gst1.0 --- .perm_test_apache | 0 .perm_test_exec | 0 ai/__init__.py | 3 + ai/local_ai_api.py | 282 ++++++++++++++++++ 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 -> 2123 bytes core/__pycache__/urls.cpython-311.pyc | Bin 347 -> 365 bytes core/__pycache__/views.cpython-311.pyc | Bin 2006 -> 1083 bytes core/admin.py | 20 +- ...category_delete_ticket_article_category.py | 47 +++ .../__pycache__/0001_initial.cpython-311.pyc | Bin 1660 -> 1660 bytes ...te_ticket_article_category.cpython-311.pyc | Bin 0 -> 2488 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 168 -> 168 bytes core/models.py | 35 +-- core/templates/base.html | 50 ++++ core/templates/core/article_detail.html | 14 + core/templates/core/index.html | 197 +++--------- core/urls.py | 7 +- core/views.py | 44 +-- static/css/custom.css | 29 ++ 27 files changed, 514 insertions(+), 214 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_article_category_delete_ticket_article_category.py create mode 100644 core/migrations/__pycache__/0002_article_category_delete_ticket_article_category.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..bc1af27 --- /dev/null +++ b/ai/local_ai_api.py @@ -0,0 +1,282 @@ +""" +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() + if not payload.get("model"): + payload["model"] = cfg["default_model"] + + 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..42361adb0dd8099922fea3c32e441e988e8235ea 100644 GIT binary patch delta 20 acmbQwIG>ScIWI340}$jk@@G!unG66ffCR1p 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..b0cc2bb8e913b4cb0d7228051b31d82ea8cbc9ff 100644 GIT binary patch delta 21 bcmeyQ@JWGZIWI340}$jk@@H=3NfQ77MD_)h 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..141e0b1770c30503729926c8a63d92e78f653ece 100644 GIT binary patch delta 21 bcmey)@tuQbIWI340}$jk@@H=3$z}lnLM#Pn delta 21 bcmey)@tuQbIWI340}vQ{e#+R$lg$DEL?H#( diff --git a/config/__pycache__/wsgi.cpython-311.pyc b/config/__pycache__/wsgi.cpython-311.pyc index 79ce690f602e05f6bfa8a8e253edcb377296b788..1d187b2e41cfacdda1e9cec656fdd34a2b28766f 100644 GIT binary patch delta 21 bcmZ3^x}23~IWI340}$jk@@H=3nau@HU8+i}F}*=E*EvpWC8{Va_|{bMZNCoRwCbw3uoo}aqb+7%Y8$pNdDECIAH zpWidf?YF%<_r5;qR98FN{Gf|l#gdz~xexA9Z9o^*(V50{(8yp0Xk^OrP~w3^n$*)E z4Y{=Zq6UVSrQ5NPC13FTq+V((;x=wS4*>QY4AS)DX3Cpsp(}peuuUGriJ&)0zBb=&4sb0pH3KsL2E_CTaU(sm2Pp-rMt{9 zA%dHr(4m}*Yc~tx;N)FILGZo1kN5H3_wK#B&0lN9yJFEq!d@0Xj#fLxsfCsxHwYsP zJ_=wD0AU5Iz7nW=Dj)?BI3DUWDv;|uRF*ZYZ6izX%MKRsoPz_e*@thY?Rf49VXjR6 z7^F)ulkO|VxzOc!;k%?JWrFHad~i%$# zN*qS8?J!HBdGP?1i&!c(WLyxy3z$SM^;*men+;3jhHW>UfY>%S(<@j^`${>zhn1=> z6%vNTjb;TTzy3P9n{4$)mXgihSS_hbr=_3zhNY(m>ijx4C^_`xblj|mnE2U7SxY)5 zi!;Tzh(3*K5aQS;YqTt={Ho|8eN<Mjs diff --git a/core/__pycache__/apps.cpython-311.pyc b/core/__pycache__/apps.cpython-311.pyc index 6435d92a257f85ac41d6fd22a9e528c3da4a1ec5..35954ea8f17355adc02b50ad9cae8de72c439d4a 100644 GIT binary patch delta 20 ZcmeBS>0#kn&dbZi00g;>{Fw`Rm;fue1PuTH 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..7d4858560aff8321337b8fae266809060d8d2727 100644 GIT binary patch delta 21 bcmX@lcAkxAIWI340}$jk@@H=3ab^YpJ5L1b delta 21 bcmX@lcAkxAIWI340}!OEe9PF#HuA5PuZ)L48|s>iVmKP#{2+7}8<{8Uh1`KT@QL7CR}R8X#!BXIl;>I*+7d z8$qE08HCnDH+RhB+OUQU{R28?lmHF_*G}GS$djkuk)jpLYLO@Mc=zt!y?giWclt9P zj}REY{*tUb6bbnU1#bj;Om`cYBSHxkR8kd6f`GhF^;N}^C=efcN~rjrP(Ks4{odkZ z@OB2TJ4yk^Dgai&|FH_v&)=eQ^<*uknsnW_Wlf2lKG?m2Tl-bUSB5RV8WC4rKX z&qtWp4}uER*A$O^5DL8Rz)-*@=*dHELe&4Bl%#&p13toTptiK27;IDH`{xw=t)BYq zNLAUBRaV3HdN(n*dU?zg1zF7AITyI#cId!XrOmkK43`rwMh>-de|7tB!XNKdRVy_ogek}TsP zWZ90$GDKyn$j4;)jj5zdA7%N^C^ih4>o?#MAYwjVw7`@qKF)yII;Z+@Nh z!-qgOX15ErcxevWiJ>(-(drFmXX{NKgnoLUE8>;@s5` zg&P$HbnC9XO72d>t@ZC}5D87+G3d{{nZL9Sdo|Vw8pf z$@gglsy1qe=DATRtL*#}c+3hM!LZ8ld|A;BYc(1A4Ko-(&{S1Bu&ye#*I@RoV{OM2 z0G(XZ-pUH409477zZ~Hl^8-r*M#WGWPk`hi+$njd3=fjMAU(uRpiv*O2Q6vBDTQM8 zt6{~?r@@|ohvXY1X&`oZmF*dB!H3FP&0sadjx0mZsBG4LVEaym+tSL?s$6{e{JAZa zdwq@L-Pxgq`IUwF#U(okC*IPy9bW|fYNg6tmyt4OFhOMnyqY_-$GfN+08|EJLV_6A z{|53IpkmXFWh*v)BDP{P?bwVJo2f7T2b?;IbYsIxq}z#%mB`eec9NOKu9eJu82|X{ zFZt7K>&Ks3$+dQJ%}TD-zwgA;jaODY{fBX~+x)o|pKHhGtoU4gxs!VE`SM z+Npw-D%78OlP5+iHQP?jTB+IkGsN#C0(OwNxXi#wirJ7IrrSzwQ_InHxAjc+WXw9R ztmiO;Ik!=}$ql@~e>u0&pp$_PbS*t`8|WN=3|hpm{u_`BQ4oXHt_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..30a17876bf570ad37930531d0d3f28996677e0c9 100644 GIT binary patch delta 243 zcmcc3^p=TtIWI340}xDV5R$ScX{Hc?%*K8pn;1_CLpYuJ`CGcc?MVhBiQjABh; z4`$HhcnK2FWW2>vkXVxOk`cmTEz3+TFTTZai#0PZCA9)1otK!K>Zi$ei#sJNF)uw| zue2zqxCmrk5gUkL1rh8(;ubeh2545v#NEE3K0tvF%#4hTHyC6tprRWL!WU4{2Q~&) l&IXqbp^ng==o#e~Sky1Fs9#}G|G><|&(y#Tf<-()0|6a~KE?n5 delta 190 zcmaFMbeoBHIWI340}w2<{FRY4kynz@W}>>PSPJVJwq?u=46A_{0@4|ySSIF38?j{M z=ce9bC}IT)Yck$q$xF;l_0wd%#hsFsn3tZfS6Y-)Tm;f}i?u8>wY<29b>jZWU@xG+ z2WCb_#v2SW7f{gy2Js81=mQ%AD`$gChfqhz47Lj_@)ud;udv8}U}oZHYTyRJA|9Y| E0L9%iK>z>% diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 9d0ddd892cc8db8e347c5f97af5002a4ce469452..136fdeead5002b03596435b1873dafa28a64f288 100644 GIT binary patch literal 1083 zcmb7D&1(}u6rb6zEKN#GS1VePi1cGYo0Ih-N^MF-p*`qnFD2a_(~Xw*D&TLFfse(Rc-@NyG^WOgEz1_F{{e6JR zvqx$BI|Se>cRC{cG7iR=u?Z|-K?jtf0U@viOLT;?XozLWkeDw$28L|OcY&_#^E4e& z1tZr&c%PL8x$jf2-xa9*F`P3&G4OW ztO^5&ZxX3{0F-#WShjl54hBE~KL|%_{Aad!G}cmlSN*XV?I@ zkxP0}fvSb}0DanLwkK#Ke?4t_80Bm>>DBQ?Qg(EeiZj#qs8~b{5QkKi%<=moIM!CvD#U+&- zT194scsRG>;RpUw!9=;3&_f%o_~T1!RF0|PL6*-y0{;zDHPB3HPwzaRdouTGDokag zRJJzVOr%;sl=VIB?8_Uwn%>a#k5}rN9%_@3Hu-tx)7);}XylE$>(}!}m?u%5gc{jU zH`H(Kl#zxu5^7_SHnySeWzIyI3%i-&MrJt7j7FK!+U+kV&ou|LEm=$}>`iH0?Ji37$E%<2j~VU-GQ(Hr*{Jxtgw{kUILDSH-j{J6R8HuB58 z2lzDG@F}Ja)obib#X;Bc5Oeu2;@@JbB|-?B;9~vgYJ%bVpH@ps!mBNCT*Ytt39c3J AjsO4v 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..06947eb 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,8 +1,16 @@ from django.contrib import admin -from .models import Ticket +from .models import Category, Article -@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(Article) +class ArticleAdmin(admin.ModelAdmin): + list_display = ('title', 'category', 'author', 'created_at') + list_filter = ('category', 'author') + search_fields = ('title', 'content') + prepopulated_fields = {'slug': ('title',)} \ No newline at end of file diff --git a/core/migrations/0002_article_category_delete_ticket_article_category.py b/core/migrations/0002_article_category_delete_ticket_article_category.py new file mode 100644 index 0000000..c7edbd0 --- /dev/null +++ b/core/migrations/0002_article_category_delete_ticket_article_category.py @@ -0,0 +1,47 @@ +# Generated by Django 5.2.7 on 2025-11-08 17:45 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Article', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200)), + ('slug', models.SlugField(max_length=200, unique=True)), + ('content', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, unique=True)), + ('slug', models.SlugField(max_length=100, unique=True)), + ], + options={ + 'verbose_name_plural': 'Categories', + }, + ), + migrations.DeleteModel( + name='Ticket', + ), + migrations.AddField( + model_name='article', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.category'), + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc index 64d8a5533a01aeb9823459fa0e2a5787563687ab..2ae7cd3116e9ffd015d1a07dff9f9cba1a5836be 100644 GIT binary patch delta 21 bcmeyv^M{9LIWI340}$jk@@H=3DPRKtLzo4D delta 21 bcmeyv^M{9LIWI340}$N&_cLQ7PXQYMPud4l diff --git a/core/migrations/__pycache__/0002_article_category_delete_ticket_article_category.cpython-311.pyc b/core/migrations/__pycache__/0002_article_category_delete_ticket_article_category.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..90d66578b8decd81e29519290613a1ddf5d94e79 GIT binary patch literal 2488 zcmb7GJ#5=X6s9PNqJC^kifvhz5;;wfM22Cd4N#+LQB?ks`{SfREf=kTl%RDNnKnf# zBxT2`hZN}0F|f<5NtEnvRq5Qls!SMk9&ukv+&#qXnQel<`JR)Te* zBKSC-TjWsSTMh+ra6RN1+VgYVKD_RzgphBU%L~ux^k&{iL=#(M@1?d@Yj6&{P6uz?vFC%+;dMuiuY{Pc=&c|g z`C%oz<lqQ2?GG5?Y}>u(;p1dWT0M(~S1_hFg*2_CY7gbYe$3DA0=S0p5Lh-2qwy zOl!bf*#lWOC16z|z*PU3`4lIaY2rg3~d;qmL?K9}rsXFTrp7`D#* z6@M+%8a}=T=lIu;qB`el&*Cimx?(yy_UbVb3>|X=RT` zipfX%^0Vk1!_f6?0D-D*U=q7} z_3E3FqA8Xlt0Yu3ETyVq!Yim;BK$}5`2dN@O-q-m29_<1NVH)nb=laKHt;qHo7jMT z%6%}jiH%j=#F8f0F$r3VrQ+w$04ChKNL{`ssaUI7Yb0Q*&03x}X!eQF)RenTOhQ#% zv#@57NbeX)wqT!ovZilIGD0NMY#<83m5B@$$cO;knr?tgVO^8pB4Z02uypDp10&o~ zu!>9~&cXJYZfqOp;PB^&A~^FpY^k9(4G2S|r>I~v?`O(Jjw*-rMz3L%mFflth)hUA zaxZLRwRgh(0UF(DZwgTiuBx)O0f?HaYGlEhaTZA455b|$hlS0x7G!X5=EsOAv1Lgj z5lI5Iriy`&Nz&b>tWwt`$)G)h44YeWqam+?9N`Am5Z0>O25p37sJK%8P+D1D_)NO- z>HNYHiS;$WJrbQ`dA&g~7_<+Q*sM}3LdHupXe2zhCL29=88X*n%lMw#|nU36EBZ$npZ+mW1Yt%X7u~1rnWSsbWVHv51gsL0n!aOE*`R zmPkZz;9iUViF*myxL38pnqJ3+=4w;3ngx6hR{^O}*wT#+vmsY;frhFRkV8snlCm@lnNDh=lgMH2*>AGqW@Z*vpwr{giNtz5CWQ$AhcvQOx8C&|Nz6TkR^3Jy<^GE{#8fG?N5 literal 0 HcmV?d00001 diff --git a/core/migrations/__pycache__/__init__.cpython-311.pyc b/core/migrations/__pycache__/__init__.cpython-311.pyc index 58b1c14eb06fea9cfb9a0d59788218572a75b51c..f911ed2b4cd2a18410374747a8af4e928690f74e 100644 GIT binary patch delta 20 acmZ3%xPp;qIWI340}$jk@@G!unF9bX-vr43 delta 20 acmZ3%xPp;qIWI340}vQ{e#)50GY0@MWCb|@ diff --git a/core/models.py b/core/models.py index 78b60d1..58ff0f3 100644 --- a/core/models.py +++ b/core/models.py @@ -1,25 +1,26 @@ from django.db import models +from django.contrib.auth.models import User -class Ticket(models.Model): - STATUS_CHOICES = [ - ('open', 'Open'), - ('in_progress', 'In Progress'), - ('closed', 'Closed'), - ] - PRIORITY_CHOICES = [ - ('low', 'Low'), - ('medium', 'Medium'), - ('high', 'High'), - ] +class Category(models.Model): + name = models.CharField(max_length=100, unique=True) + slug = models.SlugField(max_length=100, unique=True) - 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 Meta: + verbose_name_plural = "Categories" + + +class Article(models.Model): + title = models.CharField(max_length=200) + slug = models.SlugField(max_length=200, unique=True) + content = models.TextField() + category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True) + author = models.ForeignKey(User, on_delete=models.CASCADE) 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.title diff --git a/core/templates/base.html b/core/templates/base.html new file mode 100644 index 0000000..2b6feba --- /dev/null +++ b/core/templates/base.html @@ -0,0 +1,50 @@ + + + + + + {% block title %}Knowledge Base{% 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..9e8c2fe 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,157 +1,46 @@ - - +{% extends 'base.html' %} - - - - {{ project_name }} - {% if project_description %} - - - - {% endif %} - {% if project_image_url %} - - - {% endif %} - - - - - - - -
-
-

Analyzing your requirements and generating your website…

-
- Loading… +
+
+
+

Categories

+ +
+
+
+ {% for article in articles %} +
+
+
+
{{ article.title }}
+
{{ article.category.name|default:"Uncategorized" }}
+

{{ article.content|truncatewords:30 }}

+ Read more +
-

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

-
-
- Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC) -
-
- - - \ No newline at end of file + + {% empty %} +

No articles found.

+ {% endfor %} + + + + +{% endblock %} diff --git a/core/urls.py b/core/urls.py index 6299e3d..c486297 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,6 @@ from django.urls import path - -from .views import home +from . import views urlpatterns = [ - path("", home, name="home"), -] + path('', views.index, name='index'), +] \ No newline at end of file diff --git a/core/views.py b/core/views.py index c1a6d45..834cf54 100644 --- a/core/views.py +++ b/core/views.py @@ -1,37 +1,15 @@ -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 Article, Category +def index(request): + query = request.GET.get('q') + if query: + articles = Article.objects.filter(title__icontains=query) | Article.objects.filter(content__icontains=query) + else: + articles = Article.objects.all() + categories = Category.objects.all() 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", ""), + 'articles': articles, + 'categories': categories, } - 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..83c8ca2 --- /dev/null +++ b/static/css/custom.css @@ -0,0 +1,29 @@ +body { + font-family: 'Roboto', sans-serif; +} + +h1, h2, h3, h4, h5, h6, .navbar-brand, .btn { + font-family: 'Poppins', sans-serif; +} + +.hero-section { + background: linear-gradient(to right, #007BFF, #00C6FF); + color: white; +} + +.card { + transition: transform .2s; +} + +.card:hover { + transform: scale(1.05); +} + +.list-group-item a { + text-decoration: none; + color: #212529; +} + +.list-group-item a:hover { + color: #007BFF; +}