Files
tecvico-website/apps/projects/models.py
T
2026-07-19 15:03:27 +03:30

331 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from django.conf import settings
from django.db import models
from django.urls import reverse
from .uploads import project_attachment_storage, project_attachment_upload_to
class ProjectBrief(models.Model):
STATUS_SUBMITTED = "submitted"
STATUS_UNDER_REVIEW = "under_review"
STATUS_QUOTED = "quoted"
STATUS_ACCEPTED = "accepted"
STATUS_DECLINED = "declined"
STATUS_IN_PROGRESS = "in_progress"
STATUS_DELIVERED = "delivered"
STATUS_CLOSED = "closed"
STATUS_CHOICES = [
(STATUS_SUBMITTED, "Submitted"),
(STATUS_UNDER_REVIEW, "Under review"),
(STATUS_QUOTED, "Quoted"),
(STATUS_ACCEPTED, "Accepted"),
(STATUS_DECLINED, "Declined"),
(STATUS_IN_PROGRESS, "In progress"),
(STATUS_DELIVERED, "Delivered"),
(STATUS_CLOSED, "Closed"),
]
CATEGORY_WEB = "web"
CATEGORY_DATA = "data"
CATEGORY_RESEARCH = "research"
CATEGORY_MEDICAL_IMAGING = "medical_imaging"
CATEGORY_WORKFLOW = "workflow"
CATEGORY_OTHER = "other"
CATEGORY_CHOICES = [
(CATEGORY_WEB, "Web development"),
(CATEGORY_DATA, "Data & analytics"),
(CATEGORY_RESEARCH, "Research"),
(CATEGORY_MEDICAL_IMAGING, "Medical imaging"),
(CATEGORY_WORKFLOW, "Workflow automation"),
(CATEGORY_OTHER, "Other"),
]
BUDGET_UNDER_5K = "under_5k"
BUDGET_5K_15K = "5k_15k"
BUDGET_15K_50K = "15k_50k"
BUDGET_50K_PLUS = "50k_plus"
BUDGET_NOT_SURE = "not_sure"
BUDGET_CHOICES = [
(BUDGET_UNDER_5K, "Under $5,000"),
(BUDGET_5K_15K, "$5,000 $15,000"),
(BUDGET_15K_50K, "$15,000 $50,000"),
(BUDGET_50K_PLUS, "$50,000+"),
(BUDGET_NOT_SURE, "Not sure yet"),
]
LOCK_FIELD_MAP = {
"title": "lock_title",
"category": "lock_category",
"description": "lock_description",
"budget_range": "lock_budget_range",
"desired_deadline": "lock_desired_deadline",
"reference_links": "lock_reference_links",
"attachments": "lock_attachments",
}
client = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="project_briefs",
)
title = models.CharField(max_length=300)
category = models.CharField(max_length=32, choices=CATEGORY_CHOICES, blank=True, default="")
description = models.TextField(
blank=True,
default="",
help_text="Project scope, context, and goals combined.",
)
budget_range = models.CharField(max_length=32, choices=BUDGET_CHOICES, blank=True, default="")
desired_deadline = models.DateField(blank=True, null=True)
reference_links = models.TextField(blank=True)
status = models.CharField(
max_length=32,
choices=STATUS_CHOICES,
default=STATUS_SUBMITTED,
)
quote_text = models.TextField(blank=True)
website_notes = models.TextField(
blank=True,
help_text="Notes from Tecvico shown to the client in the portal.",
)
locked = models.BooleanField(
default=False,
verbose_name="Lock project",
help_text="Lock the whole project so the client cannot edit it at all.",
)
lock_title = models.BooleanField(default=False, help_text="Lock title for the client.")
lock_category = models.BooleanField(default=False, help_text="Lock category for the client.")
lock_description = models.BooleanField(
default=False, help_text="Lock description for the client."
)
lock_budget_range = models.BooleanField(
default=False, help_text="Lock budget range for the client."
)
lock_desired_deadline = models.BooleanField(
default=False, help_text="Lock desired deadline for the client."
)
lock_reference_links = models.BooleanField(
default=False, help_text="Lock reference links for the client."
)
lock_attachments = models.BooleanField(
default=False, help_text="Lock attachments for the client."
)
show_on_website = models.BooleanField(
default=False,
help_text="Display this project in the public website showcase.",
)
show_title_on_website = models.BooleanField(
default=True,
help_text="Show the title on the website card.",
)
show_category_on_website = models.BooleanField(
default=True,
help_text="Show category as a tag on the website card.",
)
show_description_on_website = models.BooleanField(
default=True,
help_text="Use the description as the website card summary when no summary is set.",
)
show_budget_on_website = models.BooleanField(
default=False,
help_text="Show the budget range on the website card.",
)
show_deadline_on_website = models.BooleanField(
default=False,
help_text="Show the desired deadline on the website card.",
)
show_references_on_website = models.BooleanField(
default=False,
help_text="Show reference links on the website card.",
)
show_attachments_on_website = models.BooleanField(
default=False,
help_text="Show attachment filenames on the website card.",
)
public_title = models.CharField(
max_length=300,
blank=True,
help_text="Optional showcase title. Defaults to the brief title.",
)
public_summary = models.TextField(
blank=True,
help_text="Short summary for the website project card.",
)
public_tags = models.CharField(
max_length=300,
blank=True,
help_text="Comma-separated tags for the website card.",
)
public_image = models.ImageField(
upload_to="projects/showcase/",
blank=True,
null=True,
help_text="Image for the website project card.",
)
public_project_status = models.CharField(
max_length=10,
choices=[
("", ""),
("new", "New"),
("ongoing", "Ongoing"),
("done", "Done"),
],
blank=True,
help_text="Filter category for the website showcase.",
)
public_url = models.CharField(
max_length=300,
blank=True,
help_text="Optional link for the website project card.",
)
submitted_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["-submitted_at"]
verbose_name = "Project Brief"
verbose_name_plural = "Project Briefs"
def __str__(self):
return f"{self.title} ({self.get_status_display()})"
@property
def is_editable_by_client(self):
return self.status == self.STATUS_SUBMITTED and not self.locked
def is_field_locked(self, field_name):
if self.locked:
return True
attr = self.LOCK_FIELD_MAP.get(field_name)
if attr is None:
return False
return getattr(self, attr)
@property
def attachments_locked(self):
return self.is_field_locked("attachments")
@property
def shows_quote_to_client(self):
return self.status in {
self.STATUS_QUOTED,
self.STATUS_ACCEPTED,
self.STATUS_DECLINED,
self.STATUS_IN_PROGRESS,
self.STATUS_DELIVERED,
self.STATUS_CLOSED,
} and bool(self.quote_text.strip())
@property
def has_website_notes(self):
return bool(self.website_notes.strip())
@property
def public_display_title(self):
if not self.show_title_on_website:
return ""
return self.public_title.strip() or self.title
@property
def public_tag_list(self):
tags = []
if self.show_category_on_website and self.category:
tags.append(self.get_category_display())
if self.public_tags.strip():
tags.extend(
tag.strip()
for tag in self.public_tags.split(",")
if tag.strip()
)
return tags
@property
def public_display_summary(self):
if self.public_summary.strip():
return self.public_summary.strip()
if self.show_description_on_website and self.description.strip():
return self.description.strip()
return ""
@property
def public_budget_display(self):
if not self.show_budget_on_website or not self.budget_range:
return ""
return self.get_budget_range_display()
@property
def public_deadline_display(self):
if not self.show_deadline_on_website or not self.desired_deadline:
return ""
return self.desired_deadline.strftime("%b %-d, %Y")
@property
def public_reference_link_list(self):
if not self.show_references_on_website or not self.reference_links.strip():
return []
return [
link.strip()
for link in self.reference_links.splitlines()
if link.strip()
]
@property
def public_attachment_filename_list(self):
if not self.show_attachments_on_website:
return []
return list(self.attachments.values_list("original_filename", flat=True))
def get_absolute_url(self):
return reverse("projects:detail", kwargs={"pk": self.pk})
class ProjectBriefAttachment(models.Model):
brief = models.ForeignKey(
ProjectBrief,
on_delete=models.CASCADE,
related_name="attachments",
)
file = models.FileField(
upload_to=project_attachment_upload_to,
storage=project_attachment_storage,
)
original_filename = models.CharField(max_length=255)
uploaded_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["uploaded_at"]
verbose_name = "Project Attachment"
verbose_name_plural = "Project Attachments"
def __str__(self):
return self.original_filename
class ProjectInternalNote(models.Model):
brief = models.ForeignKey(
ProjectBrief,
on_delete=models.CASCADE,
related_name="internal_notes",
)
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="project_internal_notes",
)
note = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["-created_at"]
verbose_name = "Internal Note"
verbose_name_plural = "Internal Notes"
def __str__(self):
preview = self.note[:60]
return f"{self.brief.title}{preview}"