diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index a5ed392..90e9ac7 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/apps.cpython-311.pyc b/core/__pycache__/apps.cpython-311.pyc index 6f131d4..357c7c8 100644 Binary files a/core/__pycache__/apps.cpython-311.pyc and b/core/__pycache__/apps.cpython-311.pyc differ diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc new file mode 100644 index 0000000..1ba2fec Binary files /dev/null and b/core/__pycache__/forms.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index e061640..dd19605 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/signals.cpython-311.pyc b/core/__pycache__/signals.cpython-311.pyc new file mode 100644 index 0000000..a7aa534 Binary files /dev/null and b/core/__pycache__/signals.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 5a69659..4869d19 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 2a36fd6..4ee1212 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 8c38f3f..8a74819 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,3 +1,35 @@ from django.contrib import admin +from .models import Company, Profile, JobStatus, RequiredFolder, Job, JobFolderCompletion, JobFile -# Register your models here. +@admin.register(Company) +class CompanyAdmin(admin.ModelAdmin): + list_display = ('name', 'is_uprn_required', 'created_at') + +@admin.register(Profile) +class ProfileAdmin(admin.ModelAdmin): + list_display = ('user', 'company', 'role') + list_filter = ('role', 'company') + +@admin.register(JobStatus) +class JobStatusAdmin(admin.ModelAdmin): + list_display = ('name', 'company', 'is_starting_status', 'order') + list_filter = ('company',) + +@admin.register(RequiredFolder) +class RequiredFolderAdmin(admin.ModelAdmin): + list_display = ('name', 'company') + list_filter = ('company',) + +@admin.register(Job) +class JobAdmin(admin.ModelAdmin): + list_display = ('job_ref', 'company', 'status', 'postcode') + list_filter = ('company', 'status') + search_fields = ('job_ref', 'uprn', 'address_line_1', 'postcode') + +@admin.register(JobFolderCompletion) +class JobFolderCompletionAdmin(admin.ModelAdmin): + list_display = ('job', 'folder', 'is_completed') + +@admin.register(JobFile) +class JobFileAdmin(admin.ModelAdmin): + list_display = ('job', 'folder', 'file', 'uploaded_at') \ No newline at end of file diff --git a/core/apps.py b/core/apps.py index 8115ae6..068ea9e 100644 --- a/core/apps.py +++ b/core/apps.py @@ -1,6 +1,8 @@ from django.apps import AppConfig - class CoreConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'core' + + def ready(self): + import core.signals \ No newline at end of file diff --git a/core/forms.py b/core/forms.py new file mode 100644 index 0000000..f26ed7b --- /dev/null +++ b/core/forms.py @@ -0,0 +1,67 @@ +from django import forms +from .models import Company, JobStatus, RequiredFolder, Job + +class CompanyForm(forms.ModelForm): + class Meta: + model = Company + fields = ['name', 'is_uprn_required'] + widgets = { + 'name': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'e.g. Acme Repairs Ltd'}), + 'is_uprn_required': forms.CheckboxInput(attrs={'class': 'form-check-input'}), + } + +class JobStatusForm(forms.ModelForm): + class Meta: + model = JobStatus + fields = ['name', 'is_starting_status'] + widgets = { + 'name': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Status Name'}), + 'is_starting_status': forms.CheckboxInput(attrs={'class': 'form-check-input'}), + } + +class RequiredFolderForm(forms.ModelForm): + class Meta: + model = RequiredFolder + fields = ['name'] + widgets = { + 'name': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Folder Name (e.g. Photos)'}), + } + +class JobForm(forms.ModelForm): + class Meta: + model = Job + fields = ['job_ref', 'uprn', 'address_line_1', 'address_line_2', 'address_line_3', 'postcode', 'description', 'action', 'notes', 'status'] + widgets = { + 'job_ref': forms.TextInput(attrs={'class': 'form-control'}), + 'uprn': forms.TextInput(attrs={'class': 'form-control'}), + 'address_line_1': forms.TextInput(attrs={'class': 'form-control'}), + 'address_line_2': forms.TextInput(attrs={'class': 'form-control'}), + 'address_line_3': forms.TextInput(attrs={'class': 'form-control'}), + 'postcode': forms.TextInput(attrs={'class': 'form-control'}), + 'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), + 'action': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), + 'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), + 'status': forms.Select(attrs={'class': 'form-control'}), + } + + def __init__(self, *args, **kwargs): + company = kwargs.pop('company', None) + super().__init__(*args, **kwargs) + if company: + self.fields['status'].queryset = JobStatus.objects.filter(company=company) + # Set initial status to starting status if creating new job + if not self.instance.pk: + starting_status = JobStatus.objects.filter(company=company, is_starting_status=True).first() + if starting_status: + self.fields['status'].initial = starting_status + + if company.is_uprn_required: + self.fields['uprn'].required = True + else: + self.fields['uprn'].required = False + + def clean_uprn(self): + uprn = self.cleaned_data.get('uprn') + if not uprn: + return None + return uprn diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..a9b0c67 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,102 @@ +# Generated by Django 5.2.7 on 2026-01-21 22:39 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Company', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('is_uprn_required', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name='JobStatus', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('is_starting_status', models.BooleanField(default=False)), + ('order', models.PositiveIntegerField(default=0)), + ('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='statuses', to='core.company')), + ], + options={ + 'verbose_name_plural': 'Job Statuses', + 'ordering': ['order'], + }, + ), + migrations.CreateModel( + name='Job', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('job_ref', models.CharField(max_length=100)), + ('uprn', models.CharField(blank=True, max_length=100, null=True)), + ('address_line_1', models.CharField(max_length=255)), + ('address_line_2', models.CharField(blank=True, max_length=255, null=True)), + ('address_line_3', models.CharField(blank=True, max_length=255, null=True)), + ('postcode', models.CharField(max_length=20)), + ('description', models.TextField(blank=True, null=True)), + ('action', models.TextField(blank=True, null=True)), + ('notes', models.TextField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='jobs', to='core.company')), + ('status', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='jobs', to='core.jobstatus')), + ], + options={ + 'unique_together': {('company', 'job_ref'), ('company', 'uprn')}, + }, + ), + migrations.CreateModel( + name='Profile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('role', models.CharField(choices=[('ADMIN', 'Admin'), ('STANDARD', 'Standard User')], default='STANDARD', max_length=20)), + ('company', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='users', to='core.company')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='RequiredFolder', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='required_folders', to='core.company')), + ], + ), + migrations.CreateModel( + name='JobFile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file', models.FileField(upload_to='job_files/')), + ('uploaded_at', models.DateTimeField(auto_now_add=True)), + ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='files', to='core.job')), + ('uploaded_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ('folder', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='files', to='core.requiredfolder')), + ], + ), + migrations.CreateModel( + name='JobFolderCompletion', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_completed', models.BooleanField(default=False)), + ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='folder_completions', to='core.job')), + ('folder', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.requiredfolder')), + ], + options={ + 'unique_together': {('job', 'folder')}, + }, + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..d53ba5d Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a8362..5ea4af8 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,82 @@ from django.db import models +from django.contrib.auth.models import User +from django.core.validators import MinLengthValidator -# Create your models here. +class Company(models.Model): + name = models.CharField(max_length=255) + is_uprn_required = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return self.name + +class Profile(models.Model): + ROLE_CHOICES = [ + ('ADMIN', 'Admin'), + ('STANDARD', 'Standard User'), + ] + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') + company = models.ForeignKey(Company, on_delete=models.CASCADE, related_name='users', null=True, blank=True) + role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='STANDARD') + + def __str__(self): + return f"{self.user.username} - {self.company.name if self.company else 'No Company'}" + +class JobStatus(models.Model): + company = models.ForeignKey(Company, on_delete=models.CASCADE, related_name='statuses') + name = models.CharField(max_length=100) + is_starting_status = models.BooleanField(default=False) + order = models.PositiveIntegerField(default=0) + + class Meta: + verbose_name_plural = "Job Statuses" + ordering = ['order'] + + def __str__(self): + return f"{self.name} ({self.company.name})" + +class RequiredFolder(models.Model): + company = models.ForeignKey(Company, on_delete=models.CASCADE, related_name='required_folders') + name = models.CharField(max_length=100) + + def __str__(self): + return f"{self.name} ({self.company.name})" + +class Job(models.Model): + company = models.ForeignKey(Company, on_delete=models.CASCADE, related_name='jobs') + job_ref = models.CharField(max_length=100) + uprn = models.CharField(max_length=100, null=True, blank=True) + address_line_1 = models.CharField(max_length=255) + address_line_2 = models.CharField(max_length=255, null=True, blank=True) + address_line_3 = models.CharField(max_length=255, null=True, blank=True) + postcode = models.CharField(max_length=20) + description = models.TextField(null=True, blank=True) + action = models.TextField(null=True, blank=True) + notes = models.TextField(null=True, blank=True) + status = models.ForeignKey(JobStatus, on_delete=models.PROTECT, related_name='jobs') + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + unique_together = [['company', 'job_ref'], ['company', 'uprn']] + + def __str__(self): + return f"{self.job_ref} - {self.address_line_1}" + +class JobFolderCompletion(models.Model): + job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name='folder_completions') + folder = models.ForeignKey(RequiredFolder, on_delete=models.CASCADE) + is_completed = models.BooleanField(default=False) + + class Meta: + unique_together = ['job', 'folder'] + +class JobFile(models.Model): + job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name='files') + folder = models.ForeignKey(RequiredFolder, on_delete=models.CASCADE, related_name='files') + file = models.FileField(upload_to='job_files/') + uploaded_at = models.DateTimeField(auto_now_add=True) + uploaded_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) + + def __str__(self): + return f"{self.file.name} in {self.folder.name}" \ No newline at end of file diff --git a/core/signals.py b/core/signals.py new file mode 100644 index 0000000..2d84210 --- /dev/null +++ b/core/signals.py @@ -0,0 +1,13 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver +from django.contrib.auth.models import User +from .models import Profile + +@receiver(post_save, sender=User) +def create_user_profile(sender, instance, created, **kwargs): + if created: + Profile.objects.get_or_create(user=instance) + +@receiver(post_save, sender=User) +def save_user_profile(sender, instance, **kwargs): + instance.profile.save() diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..2a17761 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,25 +1,146 @@ -
- -