Auto commit: 2025-11-14T09:41:35.163Z

This commit is contained in:
Flatlogic Bot 2025-11-14 09:41:35 +00:00
parent cd996ba56a
commit f24f716001
36 changed files with 803 additions and 212 deletions

0
.perm_test_apache Normal file
View File

0
.perm_test_exec Normal file
View File

3
ai/__init__.py Normal file
View 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
View 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.

After

Width:  |  Height:  |  Size: 409 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

@ -1,8 +1,8 @@
from django.contrib import admin
from .models import Ticket
from .models import Consumption
@admin.register(Ticket)
class TicketAdmin(admin.ModelAdmin):
list_display = ('subject', 'status', 'priority', 'requester_email', 'created_at')
list_filter = ('status', 'priority')
search_fields = ('subject', 'requester_email', 'description')
@admin.register(Consumption)
class ConsumptionAdmin(admin.ModelAdmin):
list_display = ('drink_type', 'size', 'timestamp', 'session_key')
list_filter = ('drink_type', 'size', 'timestamp')
search_fields = ('session_key',)

View File

@ -1,7 +1,22 @@
from django import forms
from .models import Ticket
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from .models import Consumption
class TicketForm(forms.ModelForm):
class ConsumptionForm(forms.ModelForm):
class Meta:
model = Ticket
fields = ['subject', 'requester_email', 'priority', 'description']
model = Consumption
fields = ['drink_type', 'size']
labels = {
'drink_type': 'Drink Type',
'size': 'Size'
}
widgets = {
'drink_type': forms.Select(attrs={'class': 'form-select form-select-lg mb-3'}),
'size': forms.Select(attrs={'class': 'form-select form-select-lg mb-3'}),
}
class SignUpForm(UserCreationForm):
class Meta(UserCreationForm.Meta):
model = User
fields = ('username', 'email')

View File

@ -0,0 +1,26 @@
# Generated by Django 5.2.7 on 2025-11-14 09:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Consumption',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('drink_type', models.CharField(choices=[('coffee', 'Coffee'), ('tea', 'Tea'), ('decaf', 'Decaf'), ('energy', 'Energy Drink')], default='coffee', max_length=10)),
('size', models.CharField(choices=[('small', 'Small (240ml)'), ('medium', 'Medium (350ml)'), ('large', 'Large (470ml)')], default='medium', max_length=10)),
('timestamp', models.DateTimeField(auto_now_add=True)),
('session_key', models.CharField(blank=True, db_index=True, max_length=40, null=True)),
],
),
migrations.DeleteModel(
name='Ticket',
),
]

View File

@ -0,0 +1,21 @@
# Generated by Django 5.2.7 on 2025-11-14 09:34
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0002_consumption_delete_ticket'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='consumption',
name='user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
]

View File

@ -1,25 +1,25 @@
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'),
class Consumption(models.Model):
DRINK_CHOICES = [
('coffee', 'Coffee'),
('tea', 'Tea'),
('decaf', 'Decaf'),
('energy', 'Energy Drink'),
]
SIZE_CHOICES = [
('small', 'Small (240ml)'),
('medium', 'Medium (350ml)'),
('large', 'Large (470ml)'),
]
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()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True)
drink_type = models.CharField(max_length=10, choices=DRINK_CHOICES, default='coffee')
size = models.CharField(max_length=10, choices=SIZE_CHOICES, default='medium')
timestamp = models.DateTimeField(auto_now_add=True)
session_key = models.CharField(max_length=40, db_index=True, null=True, blank=True)
def __str__(self):
return self.subject
return f"{self.get_drink_type_display()} ({self.get_size_display()}) at {self.timestamp}"

55
core/templates/base.html Normal file
View File

@ -0,0 +1,55 @@
{% 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 %}Caffeine Tracker{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<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=Lato:wght@400;700&family=Playfair+Display:wght@700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v=1">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light shadow-sm">
<div class="container">
<a class="navbar-brand" href="/">Caffeine Tracker</a>
<div class="collapse navbar-collapse">
<ul class="navbar-nav ms-auto">
{% if user.is_authenticated %}
<li class="nav-item">
<span class="nav-link">Hi, {{ user.username }}!</span>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'core:profile' %}">Profile</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'core:logout' %}">Logout</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{% url 'core:login' %}">Login</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'core:signup' %}">Sign Up</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<main>
{% block content %}{% endblock %}
</main>
<footer class="text-center py-4 mt-5">
<div class="container">
<p class="text-muted">&copy; {% now "Y" %} Caffeine Tracker. All Rights Reserved.</p>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View 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 %}

View File

@ -1,157 +1,69 @@
<!doctype html>
<html lang="en">
{% extends "base.html" %}
{% load static %}
<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 title %}Caffeine Tracker - Your Daily Coffee Companion{% endblock %}
* {
box-sizing: border-box;
}
{% block content %}
<div class="container-fluid hero-section">
<div class="container text-center py-5">
<h1 class="display-4 fw-bold">Stay Energized, Not Overloaded.</h1>
<p class="lead col-lg-8 mx-auto">Mindfully track your caffeine intake for better sleep, health, and productivity. Start with one tap.</p>
</div>
</div>
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 class="container my-5">
<div class="row justify-content-center">
<div class="col-lg-6 mb-4 mb-lg-0">
<div class="card shadow-lg glass-card h-100">
<div class="card-body p-4 p-md-5">
<h2 class="card-title text-center mb-4">Log a Drink</h2>
<form method="post">
{% csrf_token %}
<div class="mb-3">
<label for="{{ form.drink_type.id_for_label }}" class="form-label">{{ form.drink_type.label }}</label>
{{ form.drink_type }}
</div>
<div class="mb-4">
<label for="{{ form.size.id_for_label }}" class="form-label">{{ form.size.label }}</label>
{{ form.size }}
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg">Add Cup</button>
</div>
</form>
</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>
<div class="col-lg-6">
<div class="card shadow-lg summary-card h-100">
<div class="card-body p-4 p-md-5">
<h2 class="card-title text-center mb-4">Today's Summary</h2>
<div class="text-center">
<p class="display-1 fw-bold text-primary mb-0">{{ total_caffeine }}</p>
<p class="lead text-muted">mg of Caffeine</p>
<hr class="my-4">
<p class="display-5 fw-bold">{{ total_drinks }}</p>
<p class="lead text-muted">Drinks Logged</p>
</div>
<h4 class="mt-5 mb-3 text-center">Today's Log:</h4>
<ul class="list-group list-group-flush">
{% for item in consumptions_today %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<span>
<strong class="text-capitalize">{{ item.get_drink_type_display }}</strong>
<small class="text-muted ms-2">({{ item.get_size_display }})</small>
</span>
<span class="badge bg-light text-dark rounded-pill border">
{{ item.timestamp|time:"H:i" }}
</span>
</li>
{% empty %}
<li class="list-group-item text-center text-muted">No drinks logged yet today.</li>
{% endfor %}
</ul>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,54 @@
{% extends 'base.html' %}
{% block content %}
<div class="container mt-5">
<h1 class="text-center mb-4">Your Profile</h1>
<div class="row mb-4">
<div class="col-md-6">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title">Total Drinks</h5>
<p class="card-text display-4">{{ total_drinks }}</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title">Total Caffeine (mg)</h5>
<p class="card-text display-4">{{ total_caffeine }}</p>
</div>
</div>
</div>
</div>
<h2 class="mt-5 mb-3">Your Consumption History</h2>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Drink</th>
<th>Size</th>
<th>Caffeine (mg)</th>
<th>Date & Time</th>
</tr>
</thead>
<tbody>
{% for consumption in consumptions %}
<tr>
<td>{{ consumption.get_drink_type_display }}</td>
<td>{{ consumption.get_size_display }}</td>
<td>{{ consumption.caffeine_amount }}</td>
<td>{{ consumption.timestamp|date:"Y-m-d H:i" }}</td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="text-center">You haven't logged any drinks yet.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,20 @@
{% extends 'base.html' %}
{% block content %}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-body">
<h2 class="card-title text-center mb-4">Login</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary w-100">Login</button>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,20 @@
{% extends 'base.html' %}
{% block content %}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-body">
<h2 class="card-title text-center mb-4">Sign Up</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary w-100">Sign Up</button>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,7 +1,12 @@
from django.urls import path
from . import views
from .views import home
app_name = 'core'
urlpatterns = [
path("", home, name="home"),
]
path('', views.index, name='index'),
path('signup/', views.signup, name='signup'),
path('login/', views.login_view, name='login'),
path('logout/', views.logout_view, name='logout'),
path('profile/', views.profile, name='profile'),
]

View File

@ -1,37 +1,109 @@
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.shortcuts import render, redirect
from django.utils import timezone
from django.views.generic.edit import CreateView
from .models import Consumption
from .forms import ConsumptionForm, SignUpForm
from datetime import timedelta
from django.contrib.auth import login, logout, authenticate
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import AuthenticationForm
from .forms import TicketForm
from .models import Ticket
# A simple mapping for caffeine content. This can be made more complex later.
CAFFEINE_MAP = {
'coffee': {'small': 95, 'medium': 140, 'large': 210},
'tea': {'small': 26, 'medium': 40, 'large': 55},
'energy': {'small': 80, 'medium': 120, 'large': 160},
'decaf': {'small': 2, 'medium': 4, 'large': 7},
}
def index(request):
if not request.session.session_key:
request.session.create()
session_key = request.session.session_key
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()
if request.method == 'POST':
form = ConsumptionForm(request.POST)
if form.is_valid():
consumption = form.save(commit=False)
if request.user.is_authenticated:
consumption.user = request.user
else:
consumption.session_key = session_key
consumption.save()
return redirect('index')
else:
form = ConsumptionForm()
# Get today's consumption
today = timezone.now().date()
start_of_day = timezone.make_aware(timezone.datetime.combine(today, timezone.datetime.min.time()))
end_of_day = start_of_day + timedelta(days=1)
if request.user.is_authenticated:
consumptions_today = Consumption.objects.filter(
user=request.user,
timestamp__gte=start_of_day,
timestamp__lt=end_of_day
).order_by('-timestamp')
else:
consumptions_today = Consumption.objects.filter(
session_key=session_key,
timestamp__gte=start_of_day,
timestamp__lt=end_of_day
).order_by('-timestamp')
total_caffeine = sum(
CAFFEINE_MAP.get(c.drink_type, {}).get(c.size, 0) for c in consumptions_today
)
total_drinks = consumptions_today.count()
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", ""),
'form': form,
'consumptions_today': consumptions_today,
'total_caffeine': total_caffeine,
'total_drinks': total_drinks,
}
return render(request, "core/index.html", context)
return render(request, 'core/index.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('index')
else:
form = SignUpForm()
return render(request, 'registration/signup.html', {'form': form})
class TicketCreateView(CreateView):
model = Ticket
form_class = TicketForm
template_name = "core/ticket_create.html"
success_url = reverse_lazy("home")
def login_view(request):
if request.method == 'POST':
form = AuthenticationForm(request, data=request.POST)
if form.is_valid():
username = form.cleaned_data.get('username')
password = form.cleaned_data.get('password')
user = authenticate(username=username, password=password)
if user is not None:
login(request, user)
return redirect('index')
else:
form = AuthenticationForm()
return render(request, 'registration/login.html', {'form': form})
def logout_view(request):
logout(request)
return redirect('index')
@login_required
def profile(request):
consumptions = Consumption.objects.filter(user=request.user).order_by('-timestamp')
# Calculate statistics
total_drinks = consumptions.count()
total_caffeine = sum(CAFFEINE_MAP.get(c.drink_type, {}).get(c.size, 0) for c in consumptions)
context = {
'consumptions': consumptions,
'total_drinks': total_drinks,
'total_caffeine': total_caffeine,
}
return render(request, 'core/profile.html', context)

92
static/css/custom.css Normal file
View File

@ -0,0 +1,92 @@
/*
Palette:
Primary (Dark Brown): #4A2C2A
Secondary (Cream): #F5F5DC
Accent (Vibrant Orange): #FF7043
Background (Light Beige): #FDFBF7
Text: #333333
*/
body {
font-family: 'Lato', sans-serif;
background-color: #FDFBF7;
color: #333333;
}
h1, h2, h3, h4, h5, h6, .navbar-brand, .display-4 {
font-family: 'Playfair Display', serif;
}
.navbar {
background-color: #fff !important;
border-bottom: 1px solid #eee;
}
.navbar-brand {
color: #4A2C2A !important;
font-weight: 700;
}
.hero-section {
background: linear-gradient(135deg, #fdfbf7 0%, #f5f5dc 100%);
padding: 4rem 0;
color: #4A2C2A;
}
.hero-section .display-4 {
color: #4A2C2A;
}
.btn-primary {
background-color: #FF7043;
border-color: #FF7043;
padding: 0.75rem 1.5rem;
font-weight: bold;
transition: background-color 0.3s ease, transform 0.2s ease;
}
.btn-primary:hover {
background-color: #e65a31;
border-color: #e65a31;
transform: translateY(-2px);
}
.text-primary {
color: #FF7043 !important;
}
.card {
border: none;
border-radius: 1rem;
}
.glass-card {
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.25);
}
.summary-card {
background-color: #fff;
}
.form-label {
font-weight: bold;
color: #4A2C2A;
}
.form-select-lg {
padding: 0.75rem 1.25rem;
font-size: 1.1rem;
background-color: rgba(255, 255, 255, 0.8);
}
.list-group-item {
background-color: transparent;
border-color: #eee !important;
}
footer {
background-color: #F5F5DC;
}