Compare commits

..

No commits in common. "ai-dev" and "master" have entirely different histories.

57 changed files with 235 additions and 1572 deletions

View File

View File

View File

@ -1,3 +0,0 @@
"""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

View File

@ -1,282 +0,0 @@
"""
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

View File

@ -141,9 +141,6 @@ USE_TZ = True
STATIC_URL = '/static/' STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles' STATIC_ROOT = BASE_DIR / 'staticfiles'
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
STATICFILES_DIRS = [ STATICFILES_DIRS = [
BASE_DIR / 'static', BASE_DIR / 'static',

View File

@ -16,13 +16,8 @@ Including another URLconf
""" """
from django.contrib import admin from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [ urlpatterns = [
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path("", include("core.urls")), path("", include("core.urls")),
] ]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@ -1,10 +1,8 @@
from django.contrib import admin from django.contrib import admin
from .models import Company, Membership, Tender, Bid, Document, Note, Approval from .models import Ticket
admin.site.register(Company) @admin.register(Ticket)
admin.site.register(Membership) class TicketAdmin(admin.ModelAdmin):
admin.site.register(Tender) list_display = ('subject', 'status', 'priority', 'requester_email', 'created_at')
admin.site.register(Bid) list_filter = ('status', 'priority')
admin.site.register(Document) search_fields = ('subject', 'requester_email', 'description')
admin.site.register(Note)
admin.site.register(Approval)

View File

@ -1,12 +0,0 @@
from django.contrib.auth.models import Group
from django.http import HttpResponseForbidden
def group_required(group_names):
def decorator(view_func):
def _wrapped_view(request, *args, **kwargs):
if request.user.is_authenticated:
if request.user.groups.filter(name__in=group_names).exists() or request.user.is_superuser:
return view_func(request, *args, **kwargs)
return HttpResponseForbidden("You don't have permission to access this page.")
return _wrapped_view
return decorator

View File

@ -1,18 +0,0 @@
import requests
ETENDERS_API_URL = "https://www.etenders.gov.za/api/v1/tenders"
def get_tenders():
"""
Fetches a list of tenders from the eTenders API.
Returns:
list: A list of tenders as dictionaries, or None if an error occurs.
"""
try:
response = requests.get(ETENDERS_API_URL)
response.raise_for_status() # Raise an exception for bad status codes (4xx or 5xx)
return response.json()
except requests.exceptions.RequestException as e:
print(f"Error fetching tenders from eTenders API: {e}")
return None

View File

@ -1,37 +1,7 @@
from django import forms from django import forms
from django.contrib.auth.forms import UserCreationForm from .models import Ticket
from .models import Company, Tender, Bid, Document, Note, Approval
class SignUpForm(UserCreationForm): class TicketForm(forms.ModelForm):
class Meta(UserCreationForm.Meta):
fields = UserCreationForm.Meta.fields + ('email', 'first_name', 'last_name',)
class CompanyForm(forms.ModelForm):
class Meta: class Meta:
model = Company model = Ticket
fields = ['name'] fields = ['subject', 'requester_email', 'priority', 'description']
class TenderForm(forms.ModelForm):
class Meta:
model = Tender
fields = ['title', 'description', 'deadline']
class BidForm(forms.ModelForm):
class Meta:
model = Bid
fields = ['amount']
class DocumentForm(forms.ModelForm):
class Meta:
model = Document
fields = ['file', 'description']
class NoteForm(forms.ModelForm):
class Meta:
model = Note
fields = ['note']
class ApprovalForm(forms.ModelForm):
class Meta:
model = Approval
fields = ['status', 'comments']

View File

@ -1,52 +0,0 @@
from django.core.management.base import BaseCommand
from core.etenders_api import get_tenders
from core.models import Tender, Company
from datetime import datetime
class Command(BaseCommand):
help = 'Imports tenders from the eTenders API'
def handle(self, *args, **options):
self.stdout.write('Importing tenders from eTenders API...')
tenders_data = get_tenders()
if not tenders_data:
self.stdout.write(self.style.WARNING('No tenders found or API is unavailable.'))
return
# For now, let's assume a default company exists.
# A more robust implementation would be to get the company from the tender data.
default_company, created = Company.objects.get_or_create(name='Default Company')
imported_count = 0
for tender_data in tenders_data:
# Assuming the API returns these fields. This will need to be adjusted
# based on the actual API response.
title = tender_data.get('title')
description = tender_data.get('description')
deadline_str = tender_data.get('deadline')
if not all([title, description, deadline_str]):
self.stdout.write(self.style.WARNING(f'Skipping tender with incomplete data: {tender_data}'))
continue
try:
deadline = datetime.fromisoformat(deadline_str)
except ValueError:
self.stdout.write(self.style.WARNING(f'Skipping tender with invalid deadline format: {deadline_str}'))
continue
# Check if a tender with the same title and company already exists.
if Tender.objects.filter(title=title, company=default_company).exists():
self.stdout.write(self.style.NOTICE(f'Tender "{title}" already exists. Skipping.'))
continue
Tender.objects.create(
company=default_company,
title=title,
description=description,
deadline=deadline,
)
imported_count += 1
self.stdout.write(self.style.SUCCESS(f'Successfully imported {imported_count} new tenders.'))

View File

@ -1,7 +1,5 @@
# Generated by Django 5.2.7 on 2025-11-15 18:48 # Generated by Django 5.2.7 on 2025-10-23 10:09
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
@ -10,89 +8,20 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Bid', name='Ticket',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('amount', models.DecimalField(decimal_places=2, max_digits=10)), ('subject', models.CharField(max_length=255)),
('created_at', models.DateTimeField(auto_now_add=True)), ('status', models.CharField(choices=[('open', 'Open'), ('in_progress', 'In Progress'), ('closed', 'Closed')], default='open', max_length=20)),
('updated_at', models.DateTimeField(auto_now=True)), ('priority', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High')], default='medium', max_length=20)),
], ('requester_email', models.EmailField(max_length=254)),
),
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()), ('description', models.TextField()),
('deadline', models.DateTimeField()),
('created_at', models.DateTimeField(auto_now_add=True)), ('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=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'),
),
] ]

View File

@ -1,18 +0,0 @@
# Generated by Django 5.2.7 on 2025-11-15 18:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='document',
name='description',
field=models.TextField(blank=True),
),
]

View File

@ -1,21 +0,0 @@
# Generated by Django 5.2.7 on 2025-11-15 18:58
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0002_document_description'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='document',
name='uploaded_by',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 5.2.7 on 2025-11-15 19:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0003_document_uploaded_by'),
]
operations = [
migrations.AddField(
model_name='tender',
name='status',
field=models.CharField(choices=[('opportunity-discovery', 'Opportunity Discovery'), ('qualification', 'Qualification / GoNo Go'), ('tender-registration', 'Tender Registration'), ('bid-planning', 'Bid Planning'), ('team-task-assignment', 'Team & Task Assignment'), ('document-collection-drafting', 'Document Collection & Drafting'), ('internal-review-compliance-check', 'Internal Review & Compliance Check'), ('approvals-sign-off', 'Approvals & Signoff'), ('submission-confirmation', 'Submission & Confirmation'), ('post-bid-review-analytics', 'PostBid Review & Analytics')], default='opportunity-discovery', max_length=50),
),
]

View File

@ -1,93 +1,25 @@
from django.db import models from django.db import models
from django.contrib.auth.models import User
class Company(models.Model): class Ticket(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.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):
STATUS_CHOICES = [ STATUS_CHOICES = [
('opportunity-discovery', 'Opportunity Discovery'), ('open', 'Open'),
('qualification', 'Qualification / GoNo Go'), ('in_progress', 'In Progress'),
('tender-registration', 'Tender Registration'), ('closed', 'Closed'),
('bid-planning', 'Bid Planning'),
('team-task-assignment', 'Team & Task Assignment'),
('document-collection-drafting', 'Document Collection & Drafting'),
('internal-review-compliance-check', 'Internal Review & Compliance Check'),
('approvals-sign-off', 'Approvals & Signoff'),
('submission-confirmation', 'Submission & Confirmation'),
('post-bid-review-analytics', 'PostBid Review & Analytics'),
] ]
company = models.ForeignKey(Company, on_delete=models.CASCADE)
title = models.CharField(max_length=255) 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() description = models.TextField()
deadline = models.DateTimeField()
status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='opportunity-discovery')
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
def __str__(self): def __str__(self):
return self.title return self.subject
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)
uploaded_by = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
file = models.FileField(upload_to='documents/')
description = models.TextField(blank=True)
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}"

View File

@ -1,56 +0,0 @@
{% 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>

View File

@ -1,24 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-5">
<div class="card">
<div class="card-header">
<h1 class="card-title">Approve Bid</h1>
</div>
<div class="card-body">
<p><strong>Tender:</strong> {{ bid.tender.title }}</p>
<p><strong>Company:</strong> {{ bid.company.name }}</p>
<p><strong>Amount:</strong> ${{ bid.amount }}</p>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary">Submit Approval</button>
</form>
</div>
<div class="card-footer">
<a href="{% url 'tender_detail' bid.tender.id %}" class="btn btn-secondary">Back to Tender</a>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,14 +0,0 @@
{% 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 %}

View File

@ -1,24 +0,0 @@
{% 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 %}

View File

@ -1,24 +0,0 @@
{% 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 %}

View File

@ -1,24 +0,0 @@
{% 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 %}

View File

@ -1,29 +0,0 @@
{% 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 %}

View File

@ -1,19 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-5">
<div class="card">
<div class="card-header">
<h1 class="card-title">Delete Bid</h1>
</div>
<div class="card-body">
<p>Are you sure you want to delete this bid of ${{ bid.amount }}?</p>
<form method="post">
{% csrf_token %}
<button type="submit" class="btn btn-danger">Delete</button>
<a href="{% url 'tender_detail' bid.tender.id %}" class="btn btn-secondary">Cancel</a>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,19 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-5">
<div class="card">
<div class="card-header">
<h1 class="card-title">Delete Document</h1>
</div>
<div class="card-body">
<p>Are you sure you want to delete the document "{{ document.file.name }}"?</p>
<form method="post">
{% csrf_token %}
<button type="submit" class="btn btn-danger">Delete</button>
<a href="{% url 'tender_detail' document.tender.id %}" class="btn btn-secondary">Cancel</a>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,19 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-5">
<div class="card">
<div class="card-header">
<h1 class="card-title">Delete Tender</h1>
</div>
<div class="card-body">
<p>Are you sure you want to delete the tender "{{ tender.title }}"?</p>
<form method="post">
{% csrf_token %}
<button type="submit" class="btn btn-danger">Delete</button>
<a href="{% url 'tender_detail' tender.id %}" class="btn btn-secondary">Cancel</a>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,48 +1,157 @@
{% extends "base.html" %} <!doctype html>
<html lang="en">
{% block content %} <head>
<div class="container"> <meta charset="utf-8">
<section class="hero-section"> <meta name="viewport" content="width=device-width, initial-scale=1">
<h1 class="display-4">Win More Tenders with <span class="text-primary">AI-Powered</span> Management</h1> <title>{{ project_name }}</title>
<p class="lead"> {% if project_description %}
Complete tender lifecycle management from opportunity discovery to project delivery. <meta name="description" content="{{ project_description }}">
</p> <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);
}
* {
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>
</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> <p>
<a href="#" class="btn btn-primary btn-lg">Get Started</a> Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
</p> </p>
</section> </div>
<footer>
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
</footer>
</main>
</body>
<section class="features-section"> </html>
<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>
</div>
</section>
</div>
{% endblock %}

View File

@ -1,27 +0,0 @@
{% 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 %}

View File

@ -1,27 +0,0 @@
{% 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 %}

View File

@ -1,131 +0,0 @@
{% 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>
<p><strong>Status:</strong> <span class="badge bg-info">{{ tender.get_status_display }}</span></p>
</div>
<div class="card-footer">
<a href="{% url 'tender_list' tender.company.id %}" class="btn btn-secondary">Back to Tenders</a>
<a href="{% url 'update_tender' tender.id %}" class="btn btn-primary">Update</a>
<a href="{% url 'delete_tender' tender.id %}" class="btn btn-danger">Delete</a>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h2 class="h4">Workflow</h2>
</div>
<div class="card-body">
{% if request.user.is_authenticated and request.user.groups.all|length > 0 %}
{% if 'Head of Bids' in request.user.groups.all.0.name or 'Bid Administrator' in request.user.groups.all.0.name %}
{% if next_status %}
<p>The current step is complete. Ready to move to the next step.</p>
<a href="{% url 'update_tender_status' tender.id next_status.0 %}" class="btn btn-success">Approve and move to: {{ next_status.1 }}</a>
{% else %}
<p>This tender is in the final workflow step.</p>
{% endif %}
{% endif %}
{% else %}
<p>You do not have permission to advance the workflow.</p>
{% endif %}
</div>
</div>
<div class="card mb-4">
<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">
<div>
<span>{{ bid.company.name }} - ${{ bid.amount }}</span>
{% with approval=bid.approval_set.first %}
{% if approval %}
<span class="badge bg-{% if approval.status == 'approved' %}success{% elif approval.status == 'rejected' %}danger{% else %}secondary{% endif %}">{{ approval.get_status_display }}</span>
{% else %}
<span class="badge bg-secondary">Pending</span>
{% endif %}
{% endwith %}
</div>
<div>
<a href="{% url 'approve_bid' bid.id %}" class="btn btn-sm btn-outline-primary">Review</a>
{% if request.user.membership_set.first.company == bid.company %}
<a href="{% url 'update_bid' bid.id %}" class="btn btn-sm btn-outline-secondary">Update</a>
<a href="{% url 'delete_bid' bid.id %}" class="btn btn-sm btn-outline-danger">Delete</a>
{% endif %}
</div>
</li>
{% endfor %}
</ul>
{% else %}
<p>No bids yet.</p>
{% endif %}
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h2 class="h4">Documents</h2>
</div>
<div class="card-body">
<form method="post" enctype="multipart/form-data" class="mb-4">
{% csrf_token %}
{{ doc_form.as_p }}
<button type="submit" name="submit_document" class="btn btn-primary">Upload Document</button>
</form>
{% if documents %}
<ul class="list-group">
{% for doc in documents %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<a href="{{ doc.file.url }}" target="_blank">{{ doc.file.name }}</a>
<small class="text-muted">by {{ doc.uploaded_by.username }} on {{ doc.uploaded_at|date:"Y-m-d" }}</small>
</li>
{% endfor %}
</ul>
{% else %}
<p>No documents yet.</p>
{% endif %}
</div>
</div>
<div class="card">
<div class="card-header">
<h2 class="h4">Notes</h2>
</div>
<div class="card-body">
<form method="post" class="mb-4">
{% csrf_token %}
{{ note_form.as_p }}
<button type="submit" name="submit_note" class="btn btn-primary">Add Note</button>
</form>
{% if notes %}
<ul class="list-group">
{% for note in notes %}
<li class="list-group-item">
<p class="mb-1">{{ note.note }}</p>
<small class="text-muted">by {{ note.user.username }} on {{ note.created_at|date:"Y-m-d" }}</small>
</li>
{% endfor %}
</ul>
{% else %}
<p>No notes yet.</p>
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@ -1,29 +0,0 @@
{% 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 d-flex justify-content-between align-items-center">
<a href="{% url 'tender_detail' tender.id %}">{{ tender.title }}</a>
<div>
<a href="{% url 'update_tender' tender.id %}" class="btn btn-sm btn-outline-primary">Update</a>
<a href="{% url 'delete_tender' tender.id %}" class="btn btn-sm btn-outline-danger">Delete</a>
</div>
</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 %}

View File

@ -1,21 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-5">
<div class="card">
<div class="card-header">
<h1 class="card-title">Update Bid</h1>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary">Update Bid</button>
</form>
</div>
<div class="card-footer">
<a href="{% url 'tender_detail' bid.tender.id %}" class="btn btn-secondary">Back to Tender</a>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,21 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-5">
<div class="card">
<div class="card-header">
<h1 class="card-title">Update Tender</h1>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary">Update Tender</button>
</form>
</div>
<div class="card-footer">
<a href="{% url 'tender_detail' tender.id %}" class="btn btn-secondary">Back to Tender</a>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,22 +1,7 @@
from django.urls import path from django.urls import path
from django.contrib.auth import views as auth_views
from . import views from .views import home
urlpatterns = [ urlpatterns = [
path('signup/', views.signup, name='signup'), path("", home, name="home"),
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('tender/<int:tender_id>/update_status/<str:next_status>/', views.update_tender_status, name='update_tender_status'),
path('company/<int:company_id>/create_tender/', views.create_tender, name='create_tender'),
path('tender/<int:tender_id>/update/', views.update_tender, name='update_tender'),
path('tender/<int:tender_id>/delete/', views.delete_tender, name='delete_tender'),
path('tender/<int:tender_id>/create_bid/', views.create_bid, name='create_bid'),
path('bid/<int:bid_id>/update/', views.update_bid, name='update_bid'),
path('bid/<int:bid_id>/delete/', views.delete_bid, name='delete_bid'),
path('bid/<int:bid_id>/approve/', views.approve_bid, name='approve_bid'),
path('', views.home, name='home'),
] ]

View File

@ -1,251 +1,37 @@
from django.shortcuts import render, redirect, get_object_or_404 import os
from django.contrib.auth import login, authenticate, logout import platform
from django.contrib.auth.decorators import login_required
from .forms import SignUpForm, CompanyForm, TenderForm, BidForm, DocumentForm, NoteForm, ApprovalForm from django import get_version as django_version
from .models import Company, Membership, Tender, Bid, Document, Note, Approval from django.shortcuts import render
from .decorators import group_required from django.urls import reverse_lazy
from django.utils import timezone
from django.views.generic.edit import CreateView
from .forms import TicketForm
from .models import Ticket
def home(request): def home(request):
return render(request, "core/index.html") """Render the landing screen with loader and environment details."""
host_name = request.get_host().lower()
@login_required agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
def dashboard(request): now = timezone.now()
memberships = Membership.objects.filter(user=request.user)
companies = [m.company for m in memberships]
context = {
'companies': companies
}
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)
documents = Document.objects.filter(tender=tender)
notes = Note.objects.filter(tender=tender)
if request.method == 'POST':
if 'submit_document' in request.POST:
doc_form = DocumentForm(request.POST, request.FILES)
if doc_form.is_valid():
document = doc_form.save(commit=False)
document.tender = tender
document.uploaded_by = request.user
document.save()
return redirect('tender_detail', tender_id=tender.id)
elif 'submit_note' in request.POST:
note_form = NoteForm(request.POST)
if note_form.is_valid():
note = note_form.save(commit=False)
note.tender = tender
note.user = request.user
note.save()
return redirect('tender_detail', tender_id=tender.id)
doc_form = DocumentForm()
note_form = NoteForm()
# Workflow logic
current_status_index = [i for i, (value, display) in enumerate(Tender.STATUS_CHOICES) if value == tender.status][0]
next_status = Tender.STATUS_CHOICES[current_status_index + 1] if current_status_index + 1 < len(Tender.STATUS_CHOICES) else None
context = { context = {
'tender': tender, "project_name": "New Style",
'bids': bids, "agent_brand": agent_brand,
'documents': documents, "django_version": django_version(),
'notes': notes, "python_version": platform.python_version(),
'doc_form': doc_form, "current_time": now,
'note_form': note_form, "host_name": host_name,
'next_status': next_status, "project_description": os.getenv("PROJECT_DESCRIPTION", ""),
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
} }
return render(request, 'core/tender_detail.html', context) return render(request, "core/index.html", context)
@login_required
@group_required(['Head of Bids', 'Bid Administrator'])
def update_tender_status(request, tender_id, next_status):
tender = get_object_or_404(Tender, pk=tender_id)
# Find the index of the current status
current_status_index = [i for i, (value, display) in enumerate(Tender.STATUS_CHOICES) if value == tender.status][0]
# Find the index of the next status
next_status_index = [i for i, (value, display) in enumerate(Tender.STATUS_CHOICES) if value == next_status][0]
# Check if the next status is valid
if next_status_index == current_status_index + 1:
tender.status = next_status
tender.save()
return redirect('tender_detail', tender_id=tender.id)
@login_required class TicketCreateView(CreateView):
def create_tender(request, company_id): model = Ticket
company = get_object_or_404(Company, pk=company_id) form_class = TicketForm
if request.method == 'POST': template_name = "core/ticket_create.html"
form = TenderForm(request.POST) success_url = reverse_lazy("home")
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 update_tender(request, tender_id):
tender = get_object_or_404(Tender, pk=tender_id)
if request.method == 'POST':
form = TenderForm(request.POST, instance=tender)
if form.is_valid():
form.save()
return redirect('tender_detail', tender_id=tender.id)
else:
form = TenderForm(instance=tender)
context = {
'form': form,
'tender': tender
}
return render(request, 'core/update_tender.html', context)
@login_required
def delete_tender(request, tender_id):
tender = get_object_or_404(Tender, pk=tender_id)
if request.method == 'POST':
company_id = tender.company.id
tender.delete()
return redirect('tender_list', company_id=company_id)
context = {
'tender': tender
}
return render(request, 'core/delete_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)
@login_required
def update_bid(request, bid_id):
bid = get_object_or_404(Bid, pk=bid_id)
if request.method == 'POST':
form = BidForm(request.POST, instance=bid)
if form.is_valid():
form.save()
return redirect('tender_detail', tender_id=bid.tender.id)
else:
form = BidForm(instance=bid)
context = {
'form': form,
'bid': bid
}
return render(request, 'core/update_bid.html', context)
@login_required
def delete_bid(request, bid_id):
bid = get_object_or_404(Bid, pk=bid_id)
if request.method == 'POST':
tender_id = bid.tender.id
bid.delete()
return redirect('tender_detail', tender_id=tender_id)
context = {
'bid': bid
}
return render(request, 'core/delete_bid.html', context)
@login_required
def approve_bid(request, bid_id):
bid = get_object_or_404(Bid, pk=bid_id)
approval, created = Approval.objects.get_or_create(bid=bid, approver=request.user)
if request.method == 'POST':
form = ApprovalForm(request.POST, instance=approval)
if form.is_valid():
form.save()
return redirect('tender_detail', tender_id=bid.tender.id)
else:
form = ApprovalForm(instance=approval)
context = {
'form': form,
'bid': bid
}
return render(request, 'core/approve_bid.html', context)
@login_required
def delete_document(request, document_id):
document = get_object_or_404(Document, pk=document_id)
if request.method == 'POST':
tender_id = document.tender.id
document.delete()
return redirect('tender_detail', tender_id=tender_id)
context = {
'document': document
}
return render(request, 'core/delete_document.html', context)

View File

@ -1,4 +1,3 @@
Django==5.2.7 Django==5.2.7
mysqlclient==2.2.7 mysqlclient==2.2.7
python-dotenv==1.1.1 python-dotenv==1.1.1
requests==2.31.0

View File

@ -1,63 +0,0 @@
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;
}

View File

@ -1,63 +1,21 @@
: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 { body {
background-color: #f8f9fa; margin: 0;
color: #333;
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif;
} background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
.header { display: flex;
border-bottom: 1px solid #e9ecef; justify-content: center;
background-color: #fff; align-items: center;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); min-height: 100vh;
}
.hero-section {
padding: 4rem 0;
text-align: center; text-align: center;
background-color: #fff; overflow: hidden;
} position: relative;
.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;
} }