feat: admin panel enrichment

This commit is contained in:
mohamad
2026-07-19 14:31:28 +03:30
parent d8d7819eca
commit 1042fba121
77 changed files with 4616 additions and 2 deletions
+329
View File
@@ -0,0 +1,329 @@
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,
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}"