From 92ec45d2301d9789f854337acdecd5b1c1a4ef31 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sat, 15 Nov 2025 18:56:12 +0000 Subject: [PATCH] 1.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 -> 946 bytes core/__pycache__/apps.cpython-311.pyc | Bin 524 -> 524 bytes core/__pycache__/forms.cpython-311.pyc | Bin 847 -> 2395 bytes core/__pycache__/models.cpython-311.pyc | Bin 1409 -> 6176 bytes core/__pycache__/urls.cpython-311.pyc | Bin 347 -> 1420 bytes core/__pycache__/views.cpython-311.pyc | Bin 2006 -> 6048 bytes core/admin.py | 14 +- core/forms.py | 23 +- core/migrations/0001_initial.py | 85 +++++- .../__pycache__/0001_initial.cpython-311.pyc | Bin 1660 -> 5364 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 168 -> 168 bytes core/models.py | 91 ++++-- core/templates/base.html | 56 ++++ core/templates/core/article_detail.html | 14 + core/templates/core/create_bid.html | 24 ++ core/templates/core/create_company.html | 24 ++ core/templates/core/create_tender.html | 24 ++ core/templates/core/dashboard.html | 29 ++ core/templates/core/index.html | 197 +++--------- core/templates/core/login.html | 27 ++ core/templates/core/signup.html | 27 ++ core/templates/core/tender_detail.html | 40 +++ core/templates/core/tender_list.html | 25 ++ core/urls.py | 17 +- core/views.py | 133 +++++++-- static/css/custom.css | 63 ++++ staticfiles/css/custom.css | 76 +++-- 36 files changed, 1034 insertions(+), 240 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/templates/base.html create mode 100644 core/templates/core/article_detail.html create mode 100644 core/templates/core/create_bid.html create mode 100644 core/templates/core/create_company.html create mode 100644 core/templates/core/create_tender.html create mode 100644 core/templates/core/dashboard.html create mode 100644 core/templates/core/login.html create mode 100644 core/templates/core/signup.html create mode 100644 core/templates/core/tender_detail.html create mode 100644 core/templates/core/tender_list.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..0bce5138bfc68a155c816604a8379035b1868e5c 100644 GIT binary patch delta 20 acmbQwIG>ScIWI340}$Apl*pXOGZ_Fc1_aIk 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..5c3cab0e08c4326e299e5be0e870b2c9122296d3 100644 GIT binary patch delta 21 bcmeyQ@JWGZIWI340}$Apl*ru3lO_NFMVtk- 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..7898252d9887538aaac5b756e44b5e176fb2b1d8 100644 GIT binary patch delta 21 bcmey)@tuQbIWI340}$Apl*ru3lg$DELed3@ 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..0dd72520fe94d227594fbce67b56e4f4f877fc8d 100644 GIT binary patch delta 21 bcmZ3^x}23~IWI340}$Apl*ru3Gn)wjHa-Oq delta 21 bcmZ3^x}23~IWI340}vQ{e#+R$Gn)wjH;n~g diff --git a/core/__pycache__/__init__.cpython-311.pyc b/core/__pycache__/__init__.cpython-311.pyc index 3b7774ea363dc0bd4dc92284fe89acea2051acd3..cd7f0f295db3c150684b3a691c1436ffd7dbb08f 100644 GIT binary patch delta 20 acmbQsIG2%UIWI340}$Apl*pXOGZ6qTxCF=m delta 20 acmbQsIG2%UIWI340}vQ{e#)50GZ6qUw*?pg diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 5e41572949c7873f8ad5478be4973869ac513d51..ea472c811165f4bf6d9c37a49673804d4c1fe17f 100644 GIT binary patch literal 946 zcmbVKzi-qq7=3Pj-a(PjEqA&*9qSD=D+5A;R)CPW1*UAd*lJVaIC7Fgcm4o2R>S~8 z{3lEaip0qih^Ye-Qnyang^Iug`SkhOe)fCTXZ!8;_7FP0d>AJ;2=LWRF2edXIDM^G zUV?&>6r6;FfNoJM-QregaXYlR6FS@tUG9Y*_d}l#!T|vzhv(kL<<+GN({9hKH`@8V zZa?FnG{T5T#TzG?Vw5kfY&R{xnBtm^;I&v2d( zKVkF{qZjuY%v#K9%r+hNL4!q$MU6$5z1v{gVp?O`Wp^8lTa0UryX^f2^A__O^DevB UVA5hzW71`R@73FN%B#BKAAg&`-v9sr 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&dbZi00cHCB{CQCFaZE8KLjlR 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..ed1e0610d287bd279f70c772aa7d14127149f763 100644 GIT binary patch literal 2395 zcmb_dJ8v6D5Z=8PDT*>hIksuVv0}q70ydcO#6?_z%Ql*(SjLw+ z!U_UzD|LARC1rXEB`j(Mn5g(?fGo^VqvkJkNaka|wsugvaj3LMr`V){FvbjVxEma! zI{+DRSy8+JYTr-jCAK~;gvD{W6)FMC5yz+jkU8YKZ7{k`r>rlZaG+?H2J!~v<`afl zmnM+ReengzB9}Ks&zGRUkBe6Uvb+tJFYTFwmBxOfz|1=%A4oaMIlqhn5wPjw`!mjm znr5yJNf8cl?r4r8B>~-i77{9Hs~0$!&^5wj%yO^|-S;|Up_X}=_Fm#8K9`G(#~tBy z@tl!m#$D!n;XHau5JP;0a4I?1L0f|-z6x-XoQptC{ky)p3j_(Y&XfH$&LNzGXAU3h z$VVQKk9FiDuSoeocb|oi5*)b&GCs8-s^i6JGgxjA!Ra)o9m=rV|H*d~OpuI1Gn~g} zSlwU!v|QiDNC?;%b8>wJbDamw^%cx@UXkV+-EB0Nq{VjKa61w@yvxh!ORE!wiSVAb z++MQNQpaz4h+C@pV}U1uQ1zygfc-8P9^%1(SyyeUYC~;0S2@S!`|1D0_o5B!kd?R$ zIMgYn1JdZv-UG7UpS=gMUxU1W0VF0fE9(@Fk0R{feAzz4`J+r( i(J2&f5%w>?`L1C<-g@op}@*yqcxEl8IlMM?^! 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 Q?Jd6v_U#DmJs2+j4SiGbApigX 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..5b5bea923ff486ac4555c5b96cc5edf95ee63ed6 100644 GIT binary patch literal 1420 zcmZ{jJ#5rS6vxNex4YS0HX$(|0YY(z2u0#{A|OIRC^STDfpSO*hZLl&ob_DRIe)Ci zHXm-FOhJ(X3CdIu1O+7(qNJc0qmeFhOGOunj*2(7cVSP<_+#Gtzu&wW&pf}Z*Q*3u z=hKwcED-WVIlVTP8{gh*gghfAF*Tbw+M1>*d&C}b3TuFN0qr8%1&xdlrkTJBTcutS zv-qa;0yDqHCu=EFX0TQ6DZ8UR>(|~KeN%aXGrz_sYi4z5t{To)=`)9hGIeCqFIAj5 zHk27hrq<7McqlW0%%MIrIh4_nsrQ*9Lz$z<^!AsS$A&V;ks0sjnHtKRK<03tIr)EP zv${56o?0QZ(_a-dt0mAif)4h@dltLze^q-Mm3?c|4ZAccWro6B!(ma$_BJh-M&oUd zGnzV$PT<%G)C1;pZ6jbUMJB~G7?rW}!T>X6MpQKozq8>P+@wjRZ(~Bv)Y!J?bPe|* zO~wX==#O6Hs^tb(a;IgPi!@-a$+%CGT4uFu%MYUCnL|@N4=7r#Q}Hd61%_ou=YE?v zUqGHGsSo(kncw8ksSOLiulmzTNCF+tVav0VNzL3c+)b|$a@*(XFG%!UX?t$Kt&N5e z1|1G6QdMRvJsLiN0$2Z0Qn{&);cq;!q-^+UIH_eDoH|@pEmvj9Ro!t_SX@;OSEa!x z@s-qYdOZ4oao2xEzM(|EpQmb#G~5W?Af)U8Ylnfy>3xs?>vxSdqv^4$bvL>XzmHyZ zJTtV}B0r6TRpI#8@qJLxG;N&RQdQM!vA)pyUQ$jNjoZn-zaqWX6U^RwS39Gq&W_L-R zz5LD-upGm(gymen@ODXFy73Y9^%&MAtmpcld!eM)KG*_QVpx%|lIq)&+qa*rJ^x3* WObjy;W)6 delta 258 zcmeC-zRgs>oR^o20SFda{>sP#(vLwL7+{4mKHC5p(-~42QW$d>av7r-85vTTQkZj? za+#x;85x)uQW;ZNQkhd&*RU;PW?)zi#1N3q7{!vp9?YQ0@e(AU$#{#UAh9IlB_ouR zVUVAjdW)e5WD!U*FEKaOPm}c)cS=@bUV6S>X;Dsb5y-S#tYw+0<;6v;lbdMF-&%3k+gWgAbtTAePCl?7&-6`ox#mp@DK*ZQ$!*-C7~rY%cOt5mfm%YTWKO0e8OX#*221X;V0D3hWx zyRu?|3Ru7g*$`^kFd!jF0bS}kuns!7K#n=~(8E$#z{COq3=}!!#=`I=r@lA4TyaSn zHc<4?0r|K)^XB)x?|X0fx2C2bfinNkw)8|3A^*acO5yE?SA-*EgUCeYG9=5TxGbOI zv!0ZP!#X}AWW6bGR!oUmU&_boJQ;sBkP5)FAbT^xtdx?np;Rc_lxoU`Q(;ywW+K^W zDw>U@V%c~qo=v0@Q181-QU{cP?7vH%!1T{5Yayv-IRI@fau8%IXh?M$ZBQGkt33#{ zO?9>HP#dnR?SR@yUF{(`Izf`LpKvWnk10}RURJ2-r;41W%Cu(2W)y8QcXJM&Cv$YN z?`$8ac{910^t>6GTGVEhc`ZFXr75NemAOSN$(do+JV})=7hw?jCpcn~6wKg6dS?E2 z3&S~@HKnV$?84Oi9rhCZjLpiPeOJ;lK7hW@)-i+Elt5XD!q$07w5IbUgaxgdQr>K-lZJ9r7lcOE4^%>+i7J`Afp6M9E?Q95HtXLRArE2t)}EqI}VZQ;-G09!x>yUaP;*>W~fX~W+#@fNombih&1S%w zFs-P|31*}HVVS|bG>U3I%fVGs{34yv6iPdwmiaj4Mm(p&=(y}S2Q@0h0CrxqNmXCOtkI>Utfu5t{$vGY&T654| zs6?o!1rP;N@`dkyz1gzsJEHrJ>_rZ4p7^%A7&&1?POJ(gDe~aL{R{Ulu3an>Pas)J zv~HYxbnfB65#bT&XbLo!Q7q`RG3ZQF1;YNMP3a6m3<61sc0&gm zLV@t1XHfK@K(?jFK$xP{-I|~=lyF)%l)~;g>90WJebib9Q6MkfkZRxT|8}q#IcY>r zHh@%1`$qav`r)m|w^m2?LM=w<@Qcv#-O%x;{o8{-wEkmB4;?RtJ~u+2>!Huz0I`mH zD{Ctb#6J99{6W|Ohz%Rkur3T+zUufItk8+?-@uC-Vp^46o`VPu@B8Tr&?g>|UG^h= zW9rTG+cgBb0&za?kv-q?D}tMdfhvUtLJk_#E@*| zP9nwaQKVuTLxEkO^gL>u0HI=kV9iP-m8zzogV2T^q)vc< zSQxqa! zr7W8I?P&Uoospt6Xh?&)F!=v$Ec$CW6G*#7>%hQZNa3b%GcBvjt<~0&6>Tb=aoLNR zDh~Q@uvfsg0+>uMU?wkF5@` zjh9042V?ig?p<5Ewt5X?%C()D=W}}Eaxrn)NLduOLtQBwG zyM=j4U2!9cj)Pv+g=Ak{_5zYz>F$aMSQRJ36`i~QZ~K{?*mxZGe1P1lD~#*|lFDaE9BDv4MfxZ-M&~R$!AE z+`o(OquuntO02+yhi{?1l1c<^yraq(w0;I()dOY(u-6BsScefi`XY8}H+E{r_dKM> zP8DMpjM#-2v8%hWtHs#IM(pFlXh~`=6W)9L$4Kj@RE!)mBFA+9v6taCBisdPhdUK* zSzmgbx0Gs9Q7lV=nxDz_sIxh$!5>^IJATVxh;cJvE8rg=G<~xN|NXPB!Wc-{RnI)!K&MAmz+;6^yq z8R49+tg3~z`nU^a0>Wv-in!ebWBa&^RZSP|rU*_8!fC>a_gSwtZm>*1I8BubR?7_Y EA2-alzW@LL 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..5246405 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,8 +1,10 @@ from django.contrib import admin -from .models import Ticket +from .models import Company, Membership, Tender, Bid, Document, Note, Approval -@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.site.register(Company) +admin.site.register(Membership) +admin.site.register(Tender) +admin.site.register(Bid) +admin.site.register(Document) +admin.site.register(Note) +admin.site.register(Approval) \ No newline at end of file diff --git a/core/forms.py b/core/forms.py index 7a6b83b..fe2e8d2 100644 --- a/core/forms.py +++ b/core/forms.py @@ -1,7 +1,22 @@ from django import forms -from .models import Ticket +from django.contrib.auth.forms import UserCreationForm +from .models import Company, Tender, Bid -class TicketForm(forms.ModelForm): +class SignUpForm(UserCreationForm): + class Meta(UserCreationForm.Meta): + fields = UserCreationForm.Meta.fields + ('email', 'first_name', 'last_name',) + +class CompanyForm(forms.ModelForm): class Meta: - model = Ticket - fields = ['subject', 'requester_email', 'priority', 'description'] + model = Company + fields = ['name'] + +class TenderForm(forms.ModelForm): + class Meta: + model = Tender + fields = ['title', 'description', 'deadline'] + +class BidForm(forms.ModelForm): + class Meta: + model = Bid + fields = ['amount'] diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py index 31c6f06..c563d3f 100644 --- a/core/migrations/0001_initial.py +++ b/core/migrations/0001_initial.py @@ -1,5 +1,7 @@ -# Generated by Django 5.2.7 on 2025-10-23 10:09 +# Generated by Django 5.2.7 on 2025-11-15 18:48 +import django.db.models.deletion +from django.conf import settings from django.db import migrations, models @@ -8,20 +10,89 @@ class Migration(migrations.Migration): initial = True dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name='Ticket', + name='Bid', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('subject', models.CharField(max_length=255)), - ('status', models.CharField(choices=[('open', 'Open'), ('in_progress', 'In Progress'), ('closed', 'Closed')], default='open', max_length=20)), - ('priority', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High')], default='medium', max_length=20)), - ('requester_email', models.EmailField(max_length=254)), - ('description', models.TextField()), + ('amount', models.DecimalField(decimal_places=2, max_digits=10)), ('created_at', models.DateTimeField(auto_now_add=True)), ('updated_at', models.DateTimeField(auto_now=True)), ], ), + migrations.CreateModel( + name='Company', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + ), + migrations.CreateModel( + name='Approval', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('pending', 'Pending'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='pending', max_length=20)), + ('comments', models.TextField(blank=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('approver', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('bid', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.bid')), + ], + ), + migrations.AddField( + model_name='bid', + name='company', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.company'), + ), + migrations.CreateModel( + name='Membership', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('role', models.CharField(choices=[('owner', 'Owner'), ('admin', 'Admin'), ('member', 'Member')], max_length=20)), + ('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.company')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Tender', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255)), + ('description', models.TextField()), + ('deadline', models.DateTimeField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.company')), + ], + ), + migrations.CreateModel( + name='Note', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('note', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('tender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.tender')), + ], + ), + migrations.CreateModel( + name='Document', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file', models.FileField(upload_to='documents/')), + ('uploaded_at', models.DateTimeField(auto_now_add=True)), + ('bid', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.bid')), + ('tender', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.tender')), + ], + ), + migrations.AddField( + model_name='bid', + name='tender', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.tender'), + ), ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc index 64d8a5533a01aeb9823459fa0e2a5787563687ab..9c758ad7c2a4bfc8391f3831236932d62041dfe2 100644 GIT binary patch literal 5364 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 literal 1660 zcmZuxy;B=U6yLiOr%z-FLlhffF&Pz+zeG$-G8vML!H76xu#+$@G)Zob+eJFK4|(s7 z4^Ik;6e-<>(vyNr2L1p_mnl~>SCFkdlZs9ZG;!hN?MWhWVDIkj`}n>0cHi#1Uk3+M z2*w|8{%TfY2>mI9{ut;nC)2?Ej4;BIg>0!V*>YW$kc{RLmVZQ8A=0YSciIJR?@X#I zIQ9*y3>*n_Mdm5nY*5`dUB~09?P6kiM&DD+*`9FXNFpUcbqS-ojHSBLK=qi6$iS-F zcfc}MKoeh0oM|L1;TRs+S9f7Ed+>F~ij3nCGTGPQ#C~!Ya_qr(hDp7RX&{{bMZy_8 zxSxG{TM+NkIcyHF`3tc_5jHBc$b~Ookb5}7qiuR3F4w;gCjoa-n}ZOI%$tjWyzP*~B$2Ke%6s@r)k$u4Yi8 z`vmiJi<-7hx3o2~#giVPrf!+f2+yn&y6k#Hb99^VxECz162m|G8?sd-xB&X5W)Y|1 zH@WKhx*vEI1ujO+Z4rmZ9sva@Z8}p2B8_jeG0v`>F z=n9V;P1iJt$K#l+=z-WWD$>@8NZJfTH3(=?vaG-!^%BEV-f?P;J}0t`~zbW9&A%`=!ltuS#6 zlXz4#98b?iN3$T>ii*3>Gc#u69voUta4D(&+vU}XiHQ%i-lA`} zwy5adhzQ1e1nswd_PojtZO?aN(y+Syu#-T^(od-{aqTdFxi_>9on&mzPXEu6cXDS@M!9M(<`*skT zmltH1>hiZ@Dt;#)NAxtJpviwPALd8d*sYx>;n*!U<#b=LY)SWo%cA^POxL~=ochtUT8)&xPeVma84+ud(ro7iu^@(H_4Oj^AOq$;12@D^2dK zho#Av!>m+mmug|D#&YxjF&r$h(oDNF6P9LJZnl$84re<9nPlo10XY?S9Pc1-p0$(9f__*_Y9^4EW&;a-0Q@2_r`rjKLJtv6}=}wI*KGo9TiCx chTcE_cGM9`?V$7Ic`D46kA>m%0&A4_ACx-AzW@LL diff --git a/core/migrations/__pycache__/__init__.cpython-311.pyc b/core/migrations/__pycache__/__init__.cpython-311.pyc index 58b1c14eb06fea9cfb9a0d59788218572a75b51c..893d602019b2a4466e3504db5b794110565d6e32 100644 GIT binary patch delta 20 acmZ3%xPp;qIWI340}$Apl*pXOGY0@LWd!K} delta 20 acmZ3%xPp;qIWI340}vQ{e#)50GY0@MWCb|@ diff --git a/core/models.py b/core/models.py index 78b60d1..f4cc691 100644 --- a/core/models.py +++ b/core/models.py @@ -1,25 +1,78 @@ 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'), - ] - - 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() +class Company(models.Model): + name = models.CharField(max_length=255) 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 + +class Membership(models.Model): + ROLE_CHOICES = [ + ('owner', 'Owner'), + ('admin', 'Admin'), + ('member', 'Member'), + ] + user = models.ForeignKey(User, on_delete=models.CASCADE) + company = models.ForeignKey(Company, on_delete=models.CASCADE) + role = models.CharField(max_length=20, choices=ROLE_CHOICES) + + def __str__(self): + return f"{self.user.username} - {self.company.name} ({self.role})" + +class Tender(models.Model): + company = models.ForeignKey(Company, on_delete=models.CASCADE) + title = models.CharField(max_length=255) + description = models.TextField() + deadline = models.DateTimeField() + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.title + +class Bid(models.Model): + tender = models.ForeignKey(Tender, on_delete=models.CASCADE) + company = models.ForeignKey(Company, on_delete=models.CASCADE) + amount = models.DecimalField(max_digits=10, decimal_places=2) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"Bid for {self.tender.title} by {self.company.name}" + +class Document(models.Model): + tender = models.ForeignKey(Tender, on_delete=models.CASCADE, null=True, blank=True) + bid = models.ForeignKey(Bid, on_delete=models.CASCADE, null=True, blank=True) + file = models.FileField(upload_to='documents/') + uploaded_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return self.file.name + +class Note(models.Model): + tender = models.ForeignKey(Tender, on_delete=models.CASCADE) + user = models.ForeignKey(User, on_delete=models.CASCADE) + note = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"Note on {self.tender.title} by {self.user.username}" + +class Approval(models.Model): + STATUS_CHOICES = [ + ('pending', 'Pending'), + ('approved', 'Approved'), + ('rejected', 'Rejected'), + ] + bid = models.ForeignKey(Bid, on_delete=models.CASCADE) + approver = models.ForeignKey(User, on_delete=models.CASCADE) + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending') + comments = models.TextField(blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"Approval for {self.bid} by {self.approver.username}" diff --git a/core/templates/base.html b/core/templates/base.html new file mode 100644 index 0000000..5c9e980 --- /dev/null +++ b/core/templates/base.html @@ -0,0 +1,56 @@ +{% load static %} + + + + + + {% block title %}BID Master{% endblock %} + + + + + + {% block head %}{% endblock %} + + + +
+ +
+ +
+ {% block content %}{% endblock %} +
+ + + + 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/create_bid.html b/core/templates/core/create_bid.html new file mode 100644 index 0000000..69f6785 --- /dev/null +++ b/core/templates/core/create_bid.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+
+
+

Create a Bid for {{ tender.title }}

+
+
+
+ {% csrf_token %} + {{ form.as_p }} +
+ +
+
+
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/create_company.html b/core/templates/core/create_company.html new file mode 100644 index 0000000..cab2461 --- /dev/null +++ b/core/templates/core/create_company.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+
+
+

Create a Company

+
+
+
+ {% csrf_token %} + {{ form.as_p }} +
+ +
+
+
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/create_tender.html b/core/templates/core/create_tender.html new file mode 100644 index 0000000..93ac6d8 --- /dev/null +++ b/core/templates/core/create_tender.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+
+
+

Create a New Tender for {{ company.name }}

+
+
+
+ {% csrf_token %} + {{ form.as_p }} +
+ +
+
+
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/dashboard.html b/core/templates/core/dashboard.html new file mode 100644 index 0000000..4b2c203 --- /dev/null +++ b/core/templates/core/dashboard.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+

Welcome, {{ user.username }}!

+ + {% if companies %} +

Your Companies:

+
+ {% for company in companies %} + + {{ company.name }} + + {% endfor %} +
+ {% else %} +
+

You don't have a company yet.

+

Get started by creating a new company.

+ Create a Company +
+ {% endif %} + +
+
+
+{% endblock %} diff --git a/core/templates/core/index.html b/core/templates/core/index.html index f4e4991..b05b8aa 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,157 +1,48 @@ - - +{% extends "base.html" %} - - - - {{ project_name }} - {% if project_description %} - - - - {% endif %} - {% if project_image_url %} - - - {% endif %} - - - - - - - -
-
-

Analyzing your requirements and generating your website…

-
- Loading… +
+

Key Features

+
+
+
+
+ +
+

Tender Discovery

+

Find relevant tenders with our powerful search and filtering capabilities.

+
+
+
+
+
+ +
+

Document Management

+

Organize and manage all your tender-related documents in one place.

+
+
+
+
+
+ +
+

Collaboration

+

Collaborate with your team and track progress with our workflow tools.

+
-

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 + + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/login.html b/core/templates/core/login.html new file mode 100644 index 0000000..80099a5 --- /dev/null +++ b/core/templates/core/login.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+
+
+

Login

+
+
+
+ {% csrf_token %} + {{ form.as_p }} +
+ +
+
+
+ +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/core/templates/core/signup.html b/core/templates/core/signup.html new file mode 100644 index 0000000..d0ef6e8 --- /dev/null +++ b/core/templates/core/signup.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+
+
+

Sign Up

+
+
+
+ {% csrf_token %} + {{ form.as_p }} +
+ +
+
+
+ +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/core/templates/core/tender_detail.html b/core/templates/core/tender_detail.html new file mode 100644 index 0000000..b0f1fa3 --- /dev/null +++ b/core/templates/core/tender_detail.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+

{{ tender.title }}

+
+
+

Company: {{ tender.company.name }}

+

Description:

+

{{ tender.description }}

+

Deadline: {{ tender.deadline }}

+
+ +
+ +
+
+

Bids

+ Create Bid +
+
+ {% if bids %} +
    + {% for bid in bids %} +
  • + {{ bid.company.name }} - ${{ bid.amount }} +
  • + {% endfor %} +
+ {% else %} +

No bids yet.

+ {% endif %} +
+
+
+{% endblock %} diff --git a/core/templates/core/tender_list.html b/core/templates/core/tender_list.html new file mode 100644 index 0000000..5d4c84a --- /dev/null +++ b/core/templates/core/tender_list.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

Tenders for {{ company.name }}

+ Create Tender +
+ + {% if tenders %} + + {% else %} +
+

No tenders yet.

+

Get started by creating a new tender.

+
+ {% endif %} +
+{% endblock %} diff --git a/core/urls.py b/core/urls.py index 6299e3d..4f0e941 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,16 @@ from django.urls import path - -from .views import home +from django.contrib.auth import views as auth_views +from . import views urlpatterns = [ - path("", home, name="home"), -] + path('signup/', views.signup, name='signup'), + path('login/', auth_views.LoginView.as_view(template_name='core/login.html'), name='login'), + path('logout/', views.logout_view, name='logout'), + path('dashboard/', views.dashboard, name='dashboard'), + path('create_company/', views.create_company, name='create_company'), + path('company//tenders/', views.tender_list, name='tender_list'), + path('tender//', views.tender_detail, name='tender_detail'), + path('company//create_tender/', views.create_tender, name='create_tender'), + path('tender//create_bid/', views.create_bid, name='create_bid'), + path('', views.home, name='home'), +] \ No newline at end of file diff --git a/core/views.py b/core/views.py index c1a6d45..1432241 100644 --- a/core/views.py +++ b/core/views.py @@ -1,37 +1,110 @@ -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 - +from django.shortcuts import render, redirect, get_object_or_404 +from django.contrib.auth import login, authenticate, logout +from django.contrib.auth.decorators import login_required +from .forms import SignUpForm, CompanyForm, TenderForm, BidForm +from .models import Company, Membership, Tender, Bid 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() + return render(request, "core/index.html") +@login_required +def dashboard(request): + memberships = Membership.objects.filter(user=request.user) + companies = [m.company for m in memberships] 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", ""), + 'companies': companies } - return render(request, "core/index.html", context) + return render(request, "core/dashboard.html", context) +def signup(request): + if request.method == 'POST': + form = SignUpForm(request.POST) + if form.is_valid(): + user = form.save() + login(request, user) + return redirect('dashboard') + else: + form = SignUpForm() + return render(request, 'core/signup.html', {'form': form}) + +def logout_view(request): + logout(request) + return redirect('home') + +@login_required +def create_company(request): + if request.method == 'POST': + form = CompanyForm(request.POST) + if form.is_valid(): + company = form.save() + Membership.objects.create(user=request.user, company=company, role='owner') + return redirect('dashboard') + else: + form = CompanyForm() + return render(request, 'core/create_company.html', {'form': form}) + +@login_required +def tender_list(request, company_id): + company = get_object_or_404(Company, pk=company_id) + tenders = Tender.objects.filter(company=company) + context = { + 'company': company, + 'tenders': tenders + } + return render(request, 'core/tender_list.html', context) + +@login_required +def tender_detail(request, tender_id): + tender = get_object_or_404(Tender, pk=tender_id) + bids = Bid.objects.filter(tender=tender) + context = { + 'tender': tender, + 'bids': bids + } + return render(request, 'core/tender_detail.html', context) + +@login_required +def create_tender(request, company_id): + company = get_object_or_404(Company, pk=company_id) + if request.method == 'POST': + form = TenderForm(request.POST) + if form.is_valid(): + tender = form.save(commit=False) + tender.company = company + tender.save() + return redirect('tender_list', company_id=company.id) + else: + form = TenderForm() + context = { + 'form': form, + 'company': company + } + return render(request, 'core/create_tender.html', context) + +@login_required +def create_bid(request, tender_id): + tender = get_object_or_404(Tender, pk=tender_id) + membership = Membership.objects.filter(user=request.user).first() + if not membership: + # Handle case where user has no company + return redirect('dashboard') # Or show an error + + company = membership.company + + if request.method == 'POST': + form = BidForm(request.POST) + if form.is_valid(): + bid = form.save(commit=False) + bid.tender = tender + bid.company = company + bid.save() + return redirect('tender_detail', tender_id=tender.id) + else: + form = BidForm() + + context = { + 'form': form, + 'tender': tender + } + return render(request, 'core/create_bid.html', context) -class TicketCreateView(CreateView): - model = Ticket - form_class = TicketForm - template_name = "core/ticket_create.html" - success_url = reverse_lazy("home") diff --git a/static/css/custom.css b/static/css/custom.css new file mode 100644 index 0000000..95890c0 --- /dev/null +++ b/static/css/custom.css @@ -0,0 +1,63 @@ + +body { + background-color: #f8f9fa; + color: #333; + font-family: 'Inter', sans-serif; +} + +.header { + border-bottom: 1px solid #e9ecef; + background-color: #fff; + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); +} + +.hero-section { + padding: 4rem 0; + text-align: center; + background-color: #fff; +} + +.hero-section h1 { + font-size: 3.5rem; + font-weight: 700; + color: #212529; +} + +.hero-section .lead { + font-size: 1.25rem; + color: #6c757d; +} + +.hero-section .btn-primary { + background-color: #0d6efd; + border-color: #0d6efd; + font-size: 1.25rem; + padding: 0.75rem 1.5rem; +} + +.features-section { + padding: 4rem 0; +} + +.features-section h2 { + text-align: center; + margin-bottom: 3rem; + font-size: 2.5rem; + font-weight: 700; +} + +.feature-card { + text-align: center; + padding: 2rem; + border: 1px solid #e9ecef; + border-radius: 0.5rem; + background-color: #fff; + margin-bottom: 2rem; + height: 100%; +} + +.feature-icon { + font-size: 3rem; + color: #0d6efd; + margin-bottom: 1rem; +} diff --git a/staticfiles/css/custom.css b/staticfiles/css/custom.css index 108056f..95890c0 100644 --- a/staticfiles/css/custom.css +++ b/staticfiles/css/custom.css @@ -1,21 +1,63 @@ -: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; + background-color: #f8f9fa; + color: #333; 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; +} + +.header { + border-bottom: 1px solid #e9ecef; + background-color: #fff; + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); +} + +.hero-section { + padding: 4rem 0; + text-align: center; + background-color: #fff; +} + +.hero-section h1 { + font-size: 3.5rem; + font-weight: 700; + color: #212529; +} + +.hero-section .lead { + font-size: 1.25rem; + color: #6c757d; +} + +.hero-section .btn-primary { + background-color: #0d6efd; + border-color: #0d6efd; + font-size: 1.25rem; + padding: 0.75rem 1.5rem; +} + +.features-section { + padding: 4rem 0; +} + +.features-section h2 { + text-align: center; + margin-bottom: 3rem; + font-size: 2.5rem; + font-weight: 700; +} + +.feature-card { + text-align: center; + padding: 2rem; + border: 1px solid #e9ecef; + border-radius: 0.5rem; + background-color: #fff; + margin-bottom: 2rem; + height: 100%; +} + +.feature-icon { + font-size: 3rem; + color: #0d6efd; + margin-bottom: 1rem; }