Skip to content

Commit 6040c77

Browse files
authored
Merge pull request #11 from toggle-corp/feature/admin-panel-fixes
Feature/admin panel fixes
2 parents 5980e7b + 3b6e89f commit 6040c77

File tree

15 files changed

+603
-161
lines changed

15 files changed

+603
-161
lines changed

apps/common/admin.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from django.contrib import admin
2+
from reversion.admin import VersionAdmin as OgVersionAdmin
3+
4+
from .models import UserResource
5+
6+
7+
class VersionAdmin(OgVersionAdmin):
8+
history_latest_first = True
9+
10+
11+
class UserResourceAdmin(admin.ModelAdmin):
12+
def get_readonly_fields(self, *args, **kwargs):
13+
readonly_fields = super().get_readonly_fields(*args, **kwargs) # type: ignore[reportAttributeAccessIssue]
14+
return [
15+
# To maintain order
16+
*dict.fromkeys(
17+
[
18+
*readonly_fields,
19+
"created_at",
20+
"created_by",
21+
"modified_at",
22+
"modified_by",
23+
]
24+
)
25+
]
26+
27+
def save_model(self, request, obj, form, change):
28+
if not change:
29+
obj.created_by = request.user
30+
obj.modified_by = request.user
31+
return super().save_model(request, obj, form, change) # type: ignore[reportAttributeAccessIssue]
32+
33+
def save_formset(self, request, form, formset, change):
34+
if not issubclass(formset.model, UserResource):
35+
return super().save_formset(request, form, formset, change)
36+
# https://docs.djangoproject.com/en/4.2/ref/contrib/admin/#django.contrib.admin.ModelAdmin.save_formset
37+
instances = formset.save(commit=False)
38+
for obj in formset.deleted_objects:
39+
obj.delete()
40+
for instance in instances:
41+
# UserResource changes
42+
if instance.pk is None:
43+
instance.created_by = request.user
44+
instance.modified_by = request.user
45+
instance.save()
46+
47+
48+
class UserResourceTabularInline(admin.TabularInline):
49+
def get_readonly_fields(self, *args, **kwargs):
50+
readonly_fields = super().get_readonly_fields(*args, **kwargs) # type: ignore[reportAttributeAccessIssue]
51+
return [
52+
# To maintain order
53+
*dict.fromkeys(
54+
[
55+
*readonly_fields,
56+
"created_at",
57+
"created_by",
58+
"modified_at",
59+
"modified_by",
60+
]
61+
)
62+
]
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import dataclasses
2+
import functools
3+
import json
4+
from argparse import FileType
5+
from sys import stdin
6+
7+
from dacite import from_dict
8+
from django.conf import settings
9+
from django.core.management.base import BaseCommand
10+
11+
from apps.project.models import Client, Contractor, Project
12+
from apps.track.models import Contract, Task
13+
from apps.user.models import User
14+
15+
16+
@dataclasses.dataclass
17+
class ImportTask:
18+
name: str
19+
hours: float
20+
21+
22+
@dataclasses.dataclass
23+
class ImportContract:
24+
name: str
25+
hours: float
26+
tasks: list[ImportTask]
27+
28+
29+
@dataclasses.dataclass
30+
class ImportProject:
31+
name: str
32+
project_manager_email: str
33+
client: str
34+
contractor: str
35+
contracts: list[ImportContract]
36+
37+
38+
@dataclasses.dataclass
39+
class ImportData:
40+
projects: list[ImportProject]
41+
user_emails: list[str]
42+
43+
44+
def cache_with_args(*cache_args):
45+
cache = {}
46+
47+
def decorator(func):
48+
@functools.wraps(func)
49+
def wrapper(*args, **kwargs):
50+
# Extract the cache-relevant arguments
51+
cache_key = tuple(kwargs[arg] if arg in kwargs else args[arg] for arg in cache_args)
52+
if cache_key in cache:
53+
return cache[cache_key]
54+
result = func(*args, **kwargs)
55+
cache[cache_key] = result
56+
return result
57+
58+
return wrapper
59+
60+
return decorator
61+
62+
63+
# NOTE: This is not meant to be run on PRODUCTION!!
64+
class Command(BaseCommand):
65+
help = "Generate data for development"
66+
67+
def add_arguments(self, parser):
68+
parser.add_argument("input_file", nargs="?", type=FileType("r"), default=stdin)
69+
# TODO: Generate time track data
70+
# TODO: Generate journal data
71+
72+
def handle(self, **options):
73+
if not (settings.DEBUG and settings.ALLOW_DUMMY_DATA_SCRIPT):
74+
self.stdout.write(
75+
self.style.ERROR(
76+
"Enable DJANGO_DEBUG and ALLOW_DUMMY_DATA_SCRIPT using environment variable to run this",
77+
)
78+
)
79+
return
80+
81+
try:
82+
import_raw_data = json.load(options["input_file"])
83+
except json.decoder.JSONDecodeError as e:
84+
self.stdout.write(
85+
self.style.ERROR(
86+
f"Invalid JSON file: {e}",
87+
)
88+
)
89+
return
90+
91+
import_data: ImportData = from_dict(
92+
data_class=ImportData,
93+
data=import_raw_data,
94+
)
95+
self.run(import_data)
96+
97+
def get_user_resource_kwargs(self, user) -> dict:
98+
return {
99+
"created_by": user,
100+
"modified_by": user,
101+
}
102+
103+
def create_user(self, email: str, is_admin: bool = False) -> User:
104+
insecure_password = "password123" # XXX: Insecure password - Don't use this in production
105+
if user := User.objects.filter(email=email).first():
106+
return user
107+
self.stdout.write(
108+
self.style.SUCCESS(
109+
f"Creating user with email: {email=} {insecure_password=} as {"Admin" if is_admin else "Normal"} User",
110+
)
111+
)
112+
if is_admin:
113+
return User.objects.create_superuser(
114+
email=email,
115+
password=insecure_password,
116+
)
117+
return User.objects.create_user(
118+
email=email,
119+
password=insecure_password,
120+
)
121+
122+
@cache_with_args(1)
123+
def get_user(self, email: str) -> User | None:
124+
if user := User.objects.filter(email=email).first():
125+
return user
126+
127+
@cache_with_args(2)
128+
def get_or_create_client(self, creator: User, name: str) -> Client:
129+
client, created = Client.objects.get_or_create(
130+
name=name,
131+
defaults=self.get_user_resource_kwargs(creator),
132+
)
133+
if created:
134+
self.stdout.write(self.style.SUCCESS(f"Created client {name=}"))
135+
return client
136+
137+
@cache_with_args(2)
138+
def get_or_create_contractor(self, creator: User, name: str) -> Contractor:
139+
contractor, created = Contractor.objects.get_or_create(
140+
name=name,
141+
defaults=self.get_user_resource_kwargs(creator),
142+
)
143+
if created:
144+
self.stdout.write(self.style.SUCCESS(f"Created contractor {name=}"))
145+
return contractor
146+
147+
@cache_with_args(2, 3, 4)
148+
def get_or_create_project(self, creator: User, name: str, client: Client, contractor: Contractor) -> Project:
149+
project, created = Project.objects.get_or_create(
150+
name=name,
151+
client=client,
152+
contractor=contractor,
153+
defaults=self.get_user_resource_kwargs(creator),
154+
)
155+
if created:
156+
self.stdout.write(self.style.SUCCESS(f"Created Project {name=} {client=} {contractor=}"))
157+
return project
158+
159+
@cache_with_args(2, 3)
160+
def get_or_create_contract(self, creator: User, name: str, project: Project, hours: float) -> Contract:
161+
contract, created = Contract.objects.get_or_create(
162+
name=name,
163+
project=project,
164+
total_estimated_hours=hours,
165+
defaults=self.get_user_resource_kwargs(creator),
166+
)
167+
if created:
168+
self.stdout.write(self.style.SUCCESS(f"Created Contract {name=} {project=}"))
169+
return contract
170+
171+
@cache_with_args(2, 3)
172+
def get_or_create_task(self, creator: User, name: str, contract: Contract, hours: float) -> Task:
173+
task, created = Task.objects.get_or_create(
174+
name=name,
175+
contract=contract,
176+
estimated_hours=hours,
177+
defaults=self.get_user_resource_kwargs(creator),
178+
)
179+
if created:
180+
self.stdout.write(self.style.SUCCESS(f"Created Task {name=} {contract=}"))
181+
return task
182+
183+
def run(self, import_data: ImportData):
184+
self.stdout.write("---- Generating tasks")
185+
pm_not_admin_users = set()
186+
187+
for project_data in import_data.projects:
188+
pm_user = self.get_user(project_data.project_manager_email)
189+
if pm_user is None:
190+
pm_user = self.create_user(project_data.project_manager_email, is_admin=True)
191+
192+
if not pm_user.is_superuser:
193+
pm_not_admin_users.add(pm_user)
194+
195+
project = self.get_or_create_project(
196+
pm_user,
197+
project_data.name,
198+
self.get_or_create_client(pm_user, project_data.client),
199+
self.get_or_create_contractor(pm_user, project_data.contractor),
200+
)
201+
for contract_data in project_data.contracts:
202+
contract = self.get_or_create_contract(
203+
pm_user,
204+
contract_data.name,
205+
project,
206+
contract_data.hours,
207+
)
208+
for task_data in contract_data.tasks:
209+
self.get_or_create_task(
210+
pm_user,
211+
task_data.name,
212+
contract,
213+
contract_data.hours,
214+
)
215+
216+
self.stdout.write("---- Generating Dev users")
217+
for email in import_data.user_emails:
218+
self.create_user(email, is_admin=False)
219+
220+
self.stdout.write("---- Grant missing admin access to new PM")
221+
for user in pm_not_admin_users:
222+
print(f"- {user.email}")
223+
user.is_superuser = True
224+
user.save(update_fields=("is_superuser",))

apps/project/admin.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,41 @@
11
from admin_auto_filters.filters import AutocompleteFilterFactory
22
from django.contrib import admin
3+
from django.db import models
4+
from django.http import HttpRequest
5+
6+
from apps.common.admin import UserResourceAdmin, VersionAdmin
37

48
from .models import Client, Contractor, Project
59

610

711
@admin.register(Client)
8-
class ClientAdmin(admin.ModelAdmin):
12+
class ClientAdmin(VersionAdmin, UserResourceAdmin):
913
search_fields = ("name",)
14+
list_display = ("name", "created_by", "modified_by")
15+
16+
def get_queryset(self, request: HttpRequest) -> models.QuerySet[Client]:
17+
return super().get_queryset(request).select_related("created_by", "modified_by")
1018

1119

1220
@admin.register(Contractor)
13-
class ContractorAdmin(admin.ModelAdmin):
21+
class ContractorAdmin(VersionAdmin, UserResourceAdmin):
1422
search_fields = ("name",)
1523

24+
list_display = ("name", "created_by", "modified_by")
25+
26+
def get_queryset(self, request: HttpRequest) -> models.QuerySet[Client]:
27+
return super().get_queryset(request).select_related("created_by", "modified_by")
28+
1629

1730
@admin.register(Project)
18-
class ProjectAdmin(admin.ModelAdmin):
31+
class ProjectAdmin(VersionAdmin, UserResourceAdmin):
1932
search_fields = ("name",)
2033
list_filter = (
2134
AutocompleteFilterFactory("Client", "client"),
2235
AutocompleteFilterFactory("Contractor", "contractor"),
2336
)
37+
38+
list_display = ("name", "created_by", "modified_by")
39+
40+
def get_queryset(self, request: HttpRequest) -> models.QuerySet[Client]:
41+
return super().get_queryset(request).select_related("created_by", "modified_by")

0 commit comments

Comments
 (0)