- update pyproject.toml and models/clients.py
This commit is contained in:
1 parent
8b50ff5412
commit
4afe6eaa3a
8 files changed
+590
-11
No files matched your search
@@ -17,6 +17,7 @@ dependencies = [
|
||||
"django-htmx>=1.27.0",
|
||||
"django-nepali-datetime-field>=0.8.0",
|
||||
"django-phonenumber-field[phonenumbers]>=8.4.0",
|
||||
"babel>=2.17.0",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
|
||||
+34
-10
@@ -1,19 +1,32 @@
|
||||
from django.contrib import admin
|
||||
from django.forms import ModelForm
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from phonenumber_field.formfields import SplitPhoneNumberField
|
||||
|
||||
# Assuming these imports are correct based on your setup
|
||||
from src.admin.base import admin_site
|
||||
from src.models.clients import Client
|
||||
|
||||
|
||||
class ClientAdminForm(ModelForm):
|
||||
phone = SplitPhoneNumberField(required=True)
|
||||
|
||||
class Meta:
|
||||
model = Client
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
@admin.register(Client, site=admin_site)
|
||||
class ClientAdmin(admin.ModelAdmin):
|
||||
form = ClientAdminForm
|
||||
|
||||
list_display = (
|
||||
"name_display",
|
||||
"short_code",
|
||||
"company_name",
|
||||
"email",
|
||||
"phone",
|
||||
"formatted_phone",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
@@ -42,7 +55,7 @@ class ClientAdmin(admin.ModelAdmin):
|
||||
_("Timestamps"),
|
||||
{
|
||||
"fields": ("created_at", "updated_at"),
|
||||
"classes": ("collapse",), # collapsible section for cleaner UI
|
||||
"classes": ("collapse",),
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -59,15 +72,26 @@ class ClientAdmin(admin.ModelAdmin):
|
||||
|
||||
name_display.short_description = "Client"
|
||||
|
||||
def formatted_phone(self, obj):
|
||||
"""Displays the phone number in International format on the list view."""
|
||||
if obj.phone:
|
||||
# Uses the .as_international property from the PhoneNumber wrapper
|
||||
return obj.phone.as_international
|
||||
return "—"
|
||||
|
||||
formatted_phone.short_description = "Phone"
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
"""
|
||||
Ensure short_code generation happens gracefully and show a message.
|
||||
"""
|
||||
if not obj.short_code:
|
||||
generated = obj.generate_short_code()
|
||||
obj.short_code = generated
|
||||
# Check if the short_code was empty before this save operation
|
||||
short_code_was_empty = not obj.short_code
|
||||
|
||||
# Call the parent save method, which calls obj.save() and triggers
|
||||
# the short_code generation logic you put in the model.
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
# Check if it was generated and display a message
|
||||
if short_code_was_empty and obj.short_code:
|
||||
self.message_user(
|
||||
request,
|
||||
f"Short code '{generated}' was automatically generated for client '{obj.name}'.",
|
||||
f"Short code '{obj.short_code}' was automatically generated for client '{obj.name}'.",
|
||||
)
|
||||
super().save_model(request, obj, form, change)
|
||||
@@ -0,0 +1,521 @@
|
||||
# Generated by Django 6.0 on 2025-12-09 07:58
|
||||
|
||||
import django.db.models.deletion
|
||||
import phonenumber_field.modelfields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Note",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("title", models.CharField(db_index=True, max_length=255)),
|
||||
("content", models.TextField()),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
"ordering": ["-created_at"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Client",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
("name", models.CharField(db_index=True, max_length=255)),
|
||||
("short_code", models.CharField(blank=True, max_length=6, unique=True)),
|
||||
("email", models.EmailField(max_length=254)),
|
||||
(
|
||||
"phone",
|
||||
phonenumber_field.modelfields.PhoneNumberField(
|
||||
max_length=128, region=None
|
||||
),
|
||||
),
|
||||
("address", models.TextField()),
|
||||
("company_name", models.CharField(blank=True, max_length=255)),
|
||||
],
|
||||
options={
|
||||
"ordering": ["name"],
|
||||
"indexes": [
|
||||
models.Index(fields=["name"], name="src_client_name_ffc910_idx"),
|
||||
models.Index(
|
||||
fields=["short_code"], name="src_client_short_c_058134_idx"
|
||||
),
|
||||
],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Invoice",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"invoice_number",
|
||||
models.CharField(editable=False, max_length=20, unique=True),
|
||||
),
|
||||
("amount", models.DecimalField(decimal_places=2, max_digits=10)),
|
||||
("date_issued", models.DateField(auto_now_add=True)),
|
||||
("due_date", models.DateField(auto_now=True)),
|
||||
(
|
||||
"status",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("DRAFT", "Draft"),
|
||||
("SENT", "Sent"),
|
||||
("PAID", "Paid"),
|
||||
("OVERDUE", "Overdue"),
|
||||
("CANCELLED", "Cancelled"),
|
||||
],
|
||||
default="DRAFT",
|
||||
max_length=10,
|
||||
),
|
||||
),
|
||||
(
|
||||
"client",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="invoices",
|
||||
to="src.client",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Payment",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("amount", models.DecimalField(decimal_places=2, max_digits=10)),
|
||||
("date", models.DateField()),
|
||||
("method", models.CharField(blank=True, max_length=50)),
|
||||
("transaction_id", models.CharField(blank=True, max_length=100)),
|
||||
(
|
||||
"invoice",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="payments",
|
||||
to="src.invoice",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Project",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
("name", models.CharField(db_index=True, max_length=255)),
|
||||
("description", models.TextField()),
|
||||
("start_date", models.DateField(blank=True, null=True)),
|
||||
("deadline", models.DateField(blank=True, null=True)),
|
||||
(
|
||||
"status",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("PLANNING", "Planning"),
|
||||
("IN_PROGRESS", "In Progress"),
|
||||
("ON_HOLD", "On Hold"),
|
||||
("COMPLETED", "Completed"),
|
||||
("CANCELLED", "Cancelled"),
|
||||
],
|
||||
default="PLANNING",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
(
|
||||
"budget",
|
||||
models.DecimalField(
|
||||
blank=True, decimal_places=2, max_digits=10, null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"client",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="projects",
|
||||
to="src.client",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"ordering": ["-created_at"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Milestone",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
("title", models.CharField(max_length=255)),
|
||||
("due_date", models.DateField(blank=True, null=True)),
|
||||
("is_completed", models.BooleanField(default=False)),
|
||||
(
|
||||
"project",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="milestones",
|
||||
to="src.project",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"ordering": ["due_date"],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="invoice",
|
||||
name="project",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="invoices",
|
||||
to="src.project",
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Service",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"service_type",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("DOMAIN", "Domain"),
|
||||
("HOSTING", "Hosting"),
|
||||
("VPS", "VPS"),
|
||||
("SSL", "SSL"),
|
||||
("OTHER", "Other"),
|
||||
],
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
(
|
||||
"name",
|
||||
models.CharField(
|
||||
db_index=True,
|
||||
help_text="e.g. example.com or VPS - 4GB",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
("provider", models.CharField(blank=True, max_length=255)),
|
||||
(
|
||||
"cost",
|
||||
models.DecimalField(decimal_places=2, default=0.0, max_digits=10),
|
||||
),
|
||||
(
|
||||
"renewal_price",
|
||||
models.DecimalField(decimal_places=2, default=0.0, max_digits=10),
|
||||
),
|
||||
("start_date", models.DateField(blank=True, null=True)),
|
||||
("expiry_date", models.DateField(blank=True, null=True)),
|
||||
("auto_renew", models.BooleanField(default=False)),
|
||||
(
|
||||
"client",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="services",
|
||||
to="src.client",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"ordering": ["-created_at"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Expense",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("description", models.CharField(max_length=255)),
|
||||
("amount", models.DecimalField(decimal_places=2, max_digits=10)),
|
||||
("date", models.DateField()),
|
||||
("category", models.CharField(blank=True, max_length=100)),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"service",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="expenses",
|
||||
to="src.service",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Credential",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("url", models.URLField(blank=True)),
|
||||
("username", models.CharField(blank=True, max_length=255)),
|
||||
("password", models.CharField(blank=True, max_length=255)),
|
||||
("notes", models.TextField(blank=True)),
|
||||
(
|
||||
"service",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="credentials",
|
||||
to="src.service",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"ordering": ["service"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Task",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
("title", models.CharField(db_index=True, max_length=255)),
|
||||
("description", models.TextField(blank=True)),
|
||||
(
|
||||
"status",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("TODO", "To Do"),
|
||||
("IN_PROGRESS", "In Progress"),
|
||||
("REVIEW", "Review"),
|
||||
("DONE", "Done"),
|
||||
],
|
||||
default="TODO",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
("due_date", models.DateField(blank=True, null=True)),
|
||||
(
|
||||
"project",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="tasks",
|
||||
to="src.project",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"ordering": ["status", "due_date"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="TimeEntry",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("description", models.CharField(max_length=255)),
|
||||
("start_time", models.DateTimeField()),
|
||||
("end_time", models.DateTimeField(blank=True, null=True)),
|
||||
(
|
||||
"duration",
|
||||
models.DurationField(blank=True, editable=False, null=True),
|
||||
),
|
||||
(
|
||||
"project",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="time_entries",
|
||||
to="src.project",
|
||||
),
|
||||
),
|
||||
(
|
||||
"task",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="time_entries",
|
||||
to="src.task",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"ordering": ["-start_time"],
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="project",
|
||||
index=models.Index(fields=["status"], name="src_project_status_58968d_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="project",
|
||||
index=models.Index(
|
||||
fields=["client"], name="src_project_client__f7a74d_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="milestone",
|
||||
index=models.Index(
|
||||
fields=["project"], name="src_milesto_project_a28344_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="milestone",
|
||||
index=models.Index(
|
||||
fields=["due_date"], name="src_milesto_due_dat_c4307d_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="service",
|
||||
index=models.Index(
|
||||
fields=["service_type"], name="src_service_service_a91261_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="service",
|
||||
index=models.Index(
|
||||
fields=["client"], name="src_service_client__283c36_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="service",
|
||||
index=models.Index(
|
||||
fields=["expiry_date"], name="src_service_expiry__bec77a_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="credential",
|
||||
index=models.Index(
|
||||
fields=["service"], name="src_credent_service_0fdfd4_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="task",
|
||||
index=models.Index(fields=["project"], name="src_task_project_1c66bb_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="task",
|
||||
index=models.Index(fields=["status"], name="src_task_status_4e81ec_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="task",
|
||||
index=models.Index(fields=["due_date"], name="src_task_due_dat_68af9f_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="timeentry",
|
||||
index=models.Index(
|
||||
fields=["start_time"], name="src_timeent_start_t_0e6952_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="timeentry",
|
||||
index=models.Index(
|
||||
fields=["end_time"], name="src_timeent_end_tim_faabc6_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="timeentry",
|
||||
index=models.Index(
|
||||
fields=["project"], name="src_timeent_project_142fd7_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="timeentry",
|
||||
index=models.Index(fields=["task"], name="src_timeent_task_id_c75729_idx"),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 6.0 on 2025-12-09 08:21
|
||||
|
||||
import phonenumber_field.modelfields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("src", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="client",
|
||||
name="phone",
|
||||
field=phonenumber_field.modelfields.PhoneNumberField(
|
||||
max_length=128, region="NP"
|
||||
),
|
||||
),
|
||||
]
|
||||
Whitespace-only changes.
@@ -9,7 +9,7 @@ class Client(TimeStampedModel):
|
||||
name = models.CharField(max_length=255, db_index=True)
|
||||
short_code = models.CharField(max_length=6, unique=True, blank=True)
|
||||
email = models.EmailField()
|
||||
phone = PhoneNumberField()
|
||||
phone = PhoneNumberField(region="NP")
|
||||
address = models.TextField()
|
||||
company_name = models.CharField(max_length=255, blank=True)
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ THIRD_PARTY_APPS = [
|
||||
"django_htmx",
|
||||
"nepali_datetime_field",
|
||||
"phonenumber_field",
|
||||
"babel",
|
||||
]
|
||||
|
||||
USER_DEFINED_APPS = [
|
||||
|
||||
@@ -11,11 +11,21 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/91/be/317c2c55b8bbec407257d45f5c8d1b6867abc76d12043f2d3d58c538a4ea/asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d", size = 24096, upload-time = "2025-11-19T15:32:19.004Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "babel"
|
||||
version = "2.17.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backend"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "babel" },
|
||||
{ name = "django" },
|
||||
{ name = "django-components" },
|
||||
{ name = "django-environ" },
|
||||
@@ -36,6 +46,7 @@ dev = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "babel", specifier = ">=2.17.0" },
|
||||
{ name = "django" },
|
||||
{ name = "django-components", specifier = ">=0.143.2" },
|
||||
{ name = "django-environ" },
|
||||
|
||||
Reference in new issue
Block a user