1.0
This commit is contained in:
parent
cd996ba56a
commit
92ec45d230
0
.perm_test_apache
Normal file
0
.perm_test_apache
Normal file
0
.perm_test_exec
Normal file
0
.perm_test_exec
Normal file
3
ai/__init__.py
Normal file
3
ai/__init__.py
Normal file
@ -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
|
||||
282
ai/local_ai_api.py
Normal file
282
ai/local_ai_api.py
Normal file
@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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)
|
||||
@ -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']
|
||||
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -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
|
||||
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}"
|
||||
|
||||
56
core/templates/base.html
Normal file
56
core/templates/base.html
Normal file
@ -0,0 +1,56 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}BID Master{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{% static 'bootstrap/dist/css/bootstrap.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="header">
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="d-inline-block align-text-top"><path d="M9 17.25V12m0 0V6.75M9 12h6.75M16.5 12v5.25M16.5 12V6.75M3 12h.75M21 12h-.75M12 3v.75M12 21v-.75M4.5 19.5l.53-.53M18.97 5.53l.53-.53M4.5 4.5l.53.53M18.97 18.97l.53.53"/></svg>
|
||||
BID Master
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
{% if user.is_authenticated %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'dashboard' %}">Dashboard</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'logout' %}">Logout</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'login' %}">Login</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="btn btn-primary" href="{% url 'signup' %}">Get Started</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<script src="{% static 'bootstrap/dist/js/bootstrap.bundle.min.js' %}"></script>
|
||||
</body>
|
||||
</html>
|
||||
14
core/templates/core/article_detail.html
Normal file
14
core/templates/core/article_detail.html
Normal file
@ -0,0 +1,14 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}{{ article.title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<h1>{{ article.title }}</h1>
|
||||
<p class="text-muted">Published on {{ article.created_at|date:"F d, Y" }}</p>
|
||||
<hr>
|
||||
<div>
|
||||
{{ article.content|safe }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
24
core/templates/core/create_bid.html
Normal file
24
core/templates/core/create_bid.html
Normal file
@ -0,0 +1,24 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title text-center">Create a Bid for {{ tender.title }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary">Submit Bid</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
24
core/templates/core/create_company.html
Normal file
24
core/templates/core/create_company.html
Normal file
@ -0,0 +1,24 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title text-center">Create a Company</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary">Create Company</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
24
core/templates/core/create_tender.html
Normal file
24
core/templates/core/create_tender.html
Normal file
@ -0,0 +1,24 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title text-center">Create a New Tender for {{ company.name }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary">Create Tender</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
29
core/templates/core/dashboard.html
Normal file
29
core/templates/core/dashboard.html
Normal file
@ -0,0 +1,29 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1 class="display-4">Welcome, {{ user.username }}!</h1>
|
||||
|
||||
{% if companies %}
|
||||
<h2 class="mt-5">Your Companies:</h2>
|
||||
<div class="list-group">
|
||||
{% for company in companies %}
|
||||
<a href="{% url 'tender_list' company.id %}" class="list-group-item list-group-item-action">
|
||||
{{ company.name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="mt-5 p-5 bg-light rounded">
|
||||
<h2>You don't have a company yet.</h2>
|
||||
<p class="lead">Get started by creating a new company.</p>
|
||||
<a href="{% url 'create_company' %}" class="btn btn-primary btn-lg">Create a Company</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -1,157 +1,48 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
{% extends "base.html" %}
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ project_name }}</title>
|
||||
{% if project_description %}
|
||||
<meta name="description" content="{{ project_description }}">
|
||||
<meta property="og:description" content="{{ project_description }}">
|
||||
<meta property="twitter:description" content="{{ project_description }}">
|
||||
{% endif %}
|
||||
{% if project_image_url %}
|
||||
<meta property="og:image" content="{{ project_image_url }}">
|
||||
<meta property="twitter:image" content="{{ project_image_url }}">
|
||||
{% endif %}
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.08);
|
||||
--card-border-color: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<section class="hero-section">
|
||||
<h1 class="display-4">Win More Tenders with <span class="text-primary">AI-Powered</span> Management</h1>
|
||||
<p class="lead">
|
||||
Complete tender lifecycle management from opportunity discovery to project delivery.
|
||||
</p>
|
||||
<p>
|
||||
<a href="#" class="btn btn-primary btn-lg">Get Started</a>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(130deg, 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;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='140' height='140' viewBox='0 0 140 140'><path d='M-20 20L160 20M20 -20L20 160' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
|
||||
animation: bg-pan 24s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
@keyframes bg-pan {
|
||||
0% {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate3d(-140px, -140px, 0);
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
padding: clamp(2rem, 4vw, 3rem);
|
||||
width: min(640px, 92vw);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 20px;
|
||||
padding: clamp(2rem, 4vw, 3rem);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
box-shadow: 0 20px 60px rgba(15, 23, 42, 0.35);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 1.2rem;
|
||||
font-weight: 700;
|
||||
font-size: clamp(2.2rem, 3vw + 1.3rem, 3rem);
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.6rem 0;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.7;
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.loader {
|
||||
margin: 1.5rem auto;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border: 4px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
code {
|
||||
background: rgba(15, 23, 42, 0.35);
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 2.4rem;
|
||||
font-size: 0.86rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your website…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
<section class="features-section">
|
||||
<h2 class="text-center mb-5">Key Features</h2>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
||||
</div>
|
||||
<h3>Tender Discovery</h3>
|
||||
<p>Find relevant tenders with our powerful search and filtering capabilities.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>
|
||||
</div>
|
||||
<h3>Document Management</h3>
|
||||
<p>Organize and manage all your tender-related documents in one place.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
|
||||
</div>
|
||||
<h3>Collaboration</h3>
|
||||
<p>Collaborate with your team and track progress with our workflow tools.</p>
|
||||
</div>
|
||||
</div>
|
||||
<p>Appwizzy AI is collecting your requirements and applying the first changes.</p>
|
||||
<p>This page will refresh automatically as the plan is implemented.</p>
|
||||
<p>
|
||||
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code> —
|
||||
UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
|
||||
</p>
|
||||
</div>
|
||||
<footer>
|
||||
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
|
||||
</footer>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
27
core/templates/core/login.html
Normal file
27
core/templates/core/login.html
Normal file
@ -0,0 +1,27 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title text-center">Login</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary">Login</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-footer text-center">
|
||||
<p>Don't have an account? <a href="{% url 'signup' %}">Sign Up</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
27
core/templates/core/signup.html
Normal file
27
core/templates/core/signup.html
Normal file
@ -0,0 +1,27 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title text-center">Sign Up</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary">Sign Up</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-footer text-center">
|
||||
<p>Already have an account? <a href="{% url 'login' %}">Login</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
40
core/templates/core/tender_detail.html
Normal file
40
core/templates/core/tender_detail.html
Normal file
@ -0,0 +1,40 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h1 class="card-title">{{ tender.title }}</h1>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><strong>Company:</strong> {{ tender.company.name }}</p>
|
||||
<p><strong>Description:</strong></p>
|
||||
<p>{{ tender.description }}</p>
|
||||
<p><strong>Deadline:</strong> {{ tender.deadline }}</p>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<a href="{% url 'tender_list' tender.company.id %}" class="btn btn-secondary">Back to Tenders</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h2 class="h4">Bids</h2>
|
||||
<a href="{% url 'create_bid' tender.id %}" class="btn btn-primary">Create Bid</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if bids %}
|
||||
<ul class="list-group">
|
||||
{% for bid in bids %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<span>{{ bid.company.name }} - ${{ bid.amount }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>No bids yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
25
core/templates/core/tender_list.html
Normal file
25
core/templates/core/tender_list.html
Normal file
@ -0,0 +1,25 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-5">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>Tenders for {{ company.name }}</h1>
|
||||
<a href="{% url 'create_tender' company.id %}" class="btn btn-primary">Create Tender</a>
|
||||
</div>
|
||||
|
||||
{% if tenders %}
|
||||
<ul class="list-group">
|
||||
{% for tender in tenders %}
|
||||
<li class="list-group-item">
|
||||
<a href="{% url 'tender_detail' tender.id %}">{{ tender.title }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<div class="p-5 bg-light rounded">
|
||||
<h2>No tenders yet.</h2>
|
||||
<p class="lead">Get started by creating a new tender.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
17
core/urls.py
17
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/<int:company_id>/tenders/', views.tender_list, name='tender_list'),
|
||||
path('tender/<int:tender_id>/', views.tender_detail, name='tender_detail'),
|
||||
path('company/<int:company_id>/create_tender/', views.create_tender, name='create_tender'),
|
||||
path('tender/<int:tender_id>/create_bid/', views.create_bid, name='create_bid'),
|
||||
path('', views.home, name='home'),
|
||||
]
|
||||
133
core/views.py
133
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")
|
||||
|
||||
63
static/css/custom.css
Normal file
63
static/css/custom.css
Normal file
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user