diff --git a/apps/authentication/admin.py b/apps/authentication/admin.py index 6b03a4d..81f8db9 100644 --- a/apps/authentication/admin.py +++ b/apps/authentication/admin.py @@ -1,8 +1,8 @@ # -*- encoding: utf-8 -*- from django.contrib import admin -from django.contrib.auth.admin import UserAdmin -from .models import User +from .models import User, Department -admin.site.register(User, UserAdmin) +admin.site.register(User) +admin.site.register(Department) diff --git a/apps/authentication/forms.py b/apps/authentication/forms.py index b7b977b..af30835 100644 --- a/apps/authentication/forms.py +++ b/apps/authentication/forms.py @@ -5,14 +5,15 @@ Copyright (c) 2019 - present AppSeed.us from django import forms from django.contrib.auth.forms import UserCreationForm -from django.contrib.auth.models import User + +from .models import User class LoginForm(forms.Form): - username = forms.CharField( - widget=forms.TextInput( + email = forms.EmailField( + widget=forms.EmailInput( attrs={ - "placeholder": "Username", + "placeholder": "Email Address", "class": "form-control" } )) @@ -26,10 +27,17 @@ class LoginForm(forms.Form): class SignUpForm(UserCreationForm): - username = forms.CharField( + forename = forms.CharField( widget=forms.TextInput( attrs={ - "placeholder": "Username", + "placeholder": "Forename", + "class": "form-control" + } + )) + surname = forms.CharField( + widget=forms.TextInput( + attrs={ + "placeholder": "Surname", "class": "form-control" } )) @@ -50,11 +58,11 @@ class SignUpForm(UserCreationForm): password2 = forms.CharField( widget=forms.PasswordInput( attrs={ - "placeholder": "Password check", + "placeholder": "Password (Again)", "class": "form-control" } )) class Meta: model = User - fields = ('username', 'email', 'password1', 'password2') + fields = ('forename', 'surname', 'email', 'password1', 'password2') diff --git a/apps/authentication/migrations/0001_initial.py b/apps/authentication/migrations/0001_initial.py index fe3e7eb..0b58d69 100644 --- a/apps/authentication/migrations/0001_initial.py +++ b/apps/authentication/migrations/0001_initial.py @@ -1,8 +1,7 @@ -# Generated by Django 3.2.16 on 2024-01-04 14:05 +# Generated by Django 3.2.16 on 2024-01-05 12:04 -import django.contrib.auth.models -import django.contrib.auth.validators from django.db import migrations, models +import django.db.models.deletion import django.utils.timezone import uuid @@ -16,30 +15,36 @@ class Migration(migrations.Migration): ] operations = [ + migrations.CreateModel( + name='Department', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=150)), + ('icon', models.CharField(blank=True, max_length=32, null=True)), + ], + ), migrations.CreateModel( name='User', fields=[ ('password', models.CharField(max_length=128, verbose_name='password')), ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), - ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), - ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), - ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('icon', models.ImageField(default='users/default.webp', upload_to='users', verbose_name='profile picture')), + ('email', models.EmailField(error_messages={'unique': 'A user with this email address already exists.'}, max_length=254, unique=True, verbose_name='email address')), + ('forename', models.CharField(help_text='This should be your real first name.', max_length=150, verbose_name='first name')), + ('surname', models.CharField(help_text='This should be your real last name.', max_length=150, verbose_name='last name')), + ('create_timestamp', models.DateTimeField(default=django.utils.timezone.now, help_text='When the user was created.', verbose_name='Creation Date')), + ('edit_timestamp', models.DateTimeField(default=django.utils.timezone.now, help_text='When the user was last edited.', verbose_name='Last Edited')), + ('is_active', models.BooleanField(default=True, help_text='Use as a "soft delete" rather than deleting the user.', verbose_name='active status')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates whether the user has unrestricted site control.', verbose_name='superuser status')), + ('department', models.ForeignKey(blank=True, help_text='Which department does this user belong to?', null=True, on_delete=django.db.models.deletion.SET_NULL, to='authentication.department', verbose_name='department')), ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), ], options={ 'verbose_name': 'user', 'verbose_name_plural': 'users', - 'abstract': False, }, - managers=[ - ('objects', django.contrib.auth.models.UserManager()), - ], ), ] diff --git a/apps/authentication/migrations/0002_user_icon.py b/apps/authentication/migrations/0002_user_icon.py deleted file mode 100644 index d927614..0000000 --- a/apps/authentication/migrations/0002_user_icon.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.16 on 2024-01-04 14:17 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('authentication', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='user', - name='icon', - field=models.ImageField(default='users/default.webp', upload_to='users'), - ), - ] diff --git a/apps/authentication/migrations/0003_auto_20240104_1447.py b/apps/authentication/migrations/0003_auto_20240104_1447.py deleted file mode 100644 index c861b29..0000000 --- a/apps/authentication/migrations/0003_auto_20240104_1447.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 3.2.16 on 2024-01-04 14:47 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('authentication', '0002_user_icon'), - ] - - operations = [ - migrations.AddField( - model_name='user', - name='name', - field=models.CharField(default='Corban-Lee Jones', max_length=150), - preserve_default=False, - ), - migrations.AlterField( - model_name='user', - name='email', - field=models.EmailField(max_length=254, unique=True), - ), - ] diff --git a/apps/authentication/migrations/0004_alter_user_managers.py b/apps/authentication/migrations/0004_alter_user_managers.py deleted file mode 100644 index 283e41d..0000000 --- a/apps/authentication/migrations/0004_alter_user_managers.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.16 on 2024-01-04 22:15 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('authentication', '0003_auto_20240104_1447'), - ] - - operations = [ - migrations.AlterModelManagers( - name='user', - managers=[ - ], - ), - ] diff --git a/apps/authentication/migrations/default_departments.py b/apps/authentication/migrations/default_departments.py new file mode 100644 index 0000000..a804292 --- /dev/null +++ b/apps/authentication/migrations/default_departments.py @@ -0,0 +1,35 @@ +# Generated by Django 3.2.16 on 2024-01-05 10:44 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +from django.apps import apps +import uuid + + +def create_departments(app, schema_editor): + + Department = apps.get_model("authentication", "Department") + + data = [ + {"title": "Development", "icon": None}, + {"title": "Sales", "icon": None}, + {"title": "Marketing", "icon": None}, + {"title": "Management", "icon": None}, + {"title": "Business Strategy", "icon": None}, + ] + + for item in data: + priority = Department.objects.create(title=item["title"], icon=item["icon"]) + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentication", "0001_initial"), + ] + + operations = [ + migrations.RunPython(create_departments) + ] diff --git a/apps/authentication/models.py b/apps/authentication/models.py index 46190b8..4beac25 100644 --- a/apps/authentication/models.py +++ b/apps/authentication/models.py @@ -3,23 +3,39 @@ import uuid from django.db import models -from django.contrib.auth.models import AbstractUser, BaseUserManager +from django.utils import timezone +from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin +from django.utils.translation import gettext_lazy as _ + + +class Department(models.Model): + title = models.CharField(max_length=150) + icon = models.CharField(max_length=32, null=True, blank=True) + + def __str__(self): + return self.title + + def serialize(self) -> dict: + return { + "title": self.title, + "icon": self.icon + } class UserManager(BaseUserManager): - def create_user(self, email: str, name: str, password: str=None, **extra_fields): + def create_user(self, email: str, forename: str, surname: str, password: str=None, **extra_fields): if not email: raise ValueError("Please provide an email address") email = self.normalize_email(email) - user = self.model(email=email, name=name, **extra_fields) + user = self.model(email=email, forename=forename, surname=surname, **extra_fields) user.set_password(password) user.save() return user - def create_superuser(self, email: str, name: str, password: str, **extra_fields): + def create_superuser(self, email: str, forename: str, surname: str, password: str, **extra_fields): extra_fields.setdefault("is_staff", True) extra_fields.setdefault("is_superuser", True) extra_fields.setdefault("is_active", True) @@ -30,27 +46,108 @@ class UserManager(BaseUserManager): if not extra_fields.get("is_superuser"): raise ValueError("Superuser must have is_superuser=True") - return self.create_user(email, name, password, **extra_fields) + return self.create_user(email, forename, surname, password, **extra_fields) -class User(AbstractUser): +class User(AbstractBaseUser, PermissionsMixin): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - icon = models.ImageField(upload_to="users", default="users/default.webp") - name = models.CharField(max_length=150) - email = models.EmailField(unique=True) + + icon = models.ImageField(_("profile picture"), upload_to="users", default="users/default.webp") + email = models.EmailField( + _("email address"), + unique=True, + error_messages = { + "unique": _("A user with this email address already exists.") + } + ) + forename = models.CharField( + _("first name"), + max_length=150, + help_text=_("This should be your real first name.") + ) + surname = models.CharField( + _("last name"), + max_length=150, + help_text=_("This should be your real last name.") + ) + + department = models.ForeignKey( + Department, + blank=True, + null=True, + on_delete=models.SET_NULL, + verbose_name=_("department"), + help_text=_("Which department does this user belong to?") + ) + + create_timestamp = models.DateTimeField( + _("Creation Date"), + editable=True, + default=timezone.now, + help_text=_("When the user was created.") + ) + edit_timestamp = models.DateTimeField( + _("Last Edited"), + editable=True, + default=timezone.now, + help_text=_("When the user was last edited.") + ) + + is_active = models.BooleanField( + _("active status"), + default=True, + help_text=_('Use as a "soft delete" rather than deleting the user.') + ) + is_staff = models.BooleanField( + _("staff status"), + default=False, + help_text=_("Designates whether the user can log into this admin site.") + ) + is_superuser = models.BooleanField( + _("superuser status"), + default=False, + help_text=_("Designates whether the user has unrestricted site control.") + ) USERNAME_FIELD = "email" - REQUIRED_FIELDS = ["name"] + EMAIL_FIELD = "email" + REQUIRED_FIELDS = ["forename", "surname"] objects = UserManager() + class Meta: + verbose_name = _('user') + verbose_name_plural = _('users') + def __str__(self): - return self.name + return f"{self.id} • {self.email} • {self.formal_fullname}" + + def save(self, *args, **kwargs): + self.edit_timestamp = timezone.now() + super().save(*args, **kwargs) + + def clean(self): + super().clean() + self.email = self.__class__.objects.normalize_email(self.email) + + @property + def fullname(self) -> str: + return f"{self.forename} {self.surname}" + + @property + def formal_fullname(self) -> str: + return f"{self.surname}, {self.forename}" def serialize(self) -> dict: + department = self.department.serialize() if self.department else None + return { "id": self.id, "icon": self.icon.url, - "name": self.name, - "email": self.email + "email": self.email, + "forename": self.forename, + "surname": self.surname, + "department": department, + "create_timestamp": self.create_timestamp, + "edit_timestamp": self.edit_timestamp } diff --git a/apps/authentication/views.py b/apps/authentication/views.py index 9aa0fba..d95784c 100644 --- a/apps/authentication/views.py +++ b/apps/authentication/views.py @@ -15,9 +15,9 @@ def login_view(request): if request.method == "POST": if form.is_valid(): - username = form.cleaned_data.get("username") + email = form.cleaned_data.get("email") password = form.cleaned_data.get("password") - user = authenticate(username=username, password=password) + user = authenticate(username=email, password=password) if user is not None: login(request, user) return redirect("/") @@ -36,10 +36,16 @@ def register_user(request): if request.method == "POST": form = SignUpForm(request.POST) if form.is_valid(): - form.save() - username = form.cleaned_data.get("username") + user = form.save(commit=False) + + # Develepment, give all new users admin + user.is_staff = True + user.is_superuser = True + user.save() + + email = form.cleaned_data.get("email") raw_password = form.cleaned_data.get("password1") - user = authenticate(username=username, password=raw_password) + user = authenticate(username=email, password=raw_password) msg = 'User created successfully.' success = True diff --git a/apps/home/admin.py b/apps/home/admin.py index e3660ff..9b8a4a8 100644 --- a/apps/home/admin.py +++ b/apps/home/admin.py @@ -2,6 +2,8 @@ from django.contrib import admin -from .models import Ticket +from .models import Ticket, TicketPriority, TicketTag admin.site.register(Ticket) +admin.site.register(TicketPriority) +admin.site.register(TicketTag) diff --git a/apps/home/migrations/0001_initial.py b/apps/home/migrations/0001_initial.py index 95342b2..e3027d6 100644 --- a/apps/home/migrations/0001_initial.py +++ b/apps/home/migrations/0001_initial.py @@ -1,9 +1,10 @@ -# Generated by Django 3.2.16 on 2024-01-04 14:05 +# Generated by Django 3.2.16 on 2024-01-05 10:44 from django.conf import settings from django.db import migrations, models import django.db.models.deletion import django.utils.timezone +import uuid class Migration(migrations.Migration): @@ -34,12 +35,14 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Ticket', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=60)), - ('description', models.TextField(max_length=650)), - ('create_timestamp', models.DateTimeField(default=django.utils.timezone.now)), - ('edit_timestamp', models.DateTimeField(default=django.utils.timezone.now)), - ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('title', models.CharField(help_text='An extremely short summary of the ticket subject.', max_length=100, verbose_name='title')), + ('description', models.TextField(help_text='Detailed description of the ticket subject.', max_length=650, verbose_name='description')), + ('create_timestamp', models.DateTimeField(default=django.utils.timezone.now, help_text='When the user was created.', verbose_name='Creation Date')), + ('edit_timestamp', models.DateTimeField(default=django.utils.timezone.now, help_text='When the user was last edited.', verbose_name='Last Edited')), + ('author', models.ForeignKey(help_text='The creator of the ticket.', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author')), + ('priority', models.ForeignKey(help_text='The importance level of this ticket.', on_delete=django.db.models.deletion.CASCADE, to='home.ticketpriority', verbose_name='priority')), + ('tags', models.ManyToManyField(blank=True, help_text='Categories of the ticket.', to='home.TicketTag', verbose_name='tags')), ], ), ] diff --git a/apps/home/migrations/0002_alter_ticket_id.py b/apps/home/migrations/0002_alter_ticket_id.py deleted file mode 100644 index c04b358..0000000 --- a/apps/home/migrations/0002_alter_ticket_id.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.2.16 on 2024-01-04 23:51 - -from django.db import migrations, models -import uuid - - -class Migration(migrations.Migration): - - dependencies = [ - ('home', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='ticket', - name='id', - field=models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False), - ), - ] diff --git a/apps/home/migrations/default_priorities.py b/apps/home/migrations/default_priorities.py new file mode 100644 index 0000000..b881950 --- /dev/null +++ b/apps/home/migrations/default_priorities.py @@ -0,0 +1,34 @@ +# Generated by Django 3.2.16 on 2024-01-05 10:44 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +from django.apps import apps +import uuid + + +def create_priorities(app, schema_editor): + + TicketPriority = apps.get_model("home", "TicketPriority") + + data = [ + {"title": "Urgent", "colour": "#FF0000"}, + {"title": "High", "colour": "#FFA500"}, + {"title": "Normal", "colour": "#FFFF00"}, + {"title": "Low", "colour": "#008000"} + ] + + for item in data: + priority = TicketPriority.objects.create(title=item["title"], colour=item["colour"]) + + +class Migration(migrations.Migration): + + dependencies = [ + ("home", "0001_initial"), + ] + + operations = [ + migrations.RunPython(create_priorities) + ] diff --git a/apps/home/migrations/default_tags.py b/apps/home/migrations/default_tags.py new file mode 100644 index 0000000..5435ba3 --- /dev/null +++ b/apps/home/migrations/default_tags.py @@ -0,0 +1,36 @@ +# Generated by Django 3.2.16 on 2024-01-05 10:44 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +from django.apps import apps +import uuid + + +def create_tags(app, schema_editor): + + TicketTag = apps.get_model("home", "TicketTag") + + data = [ + {"title": "Network", "colour": "#0000FF"}, + {"title": "Software", "colour": "#FFA500"}, + {"title": "Hardware", "colour": "#808080"}, + {"title": "Question", "colour": "#FFFF00"}, + {"title": "Require's Help", "colour": "#00FF00"}, + {"title": "Issue", "colour": "#FF0000"} + ] + + for item in data: + priority = TicketTag.objects.create(title=item["title"], colour=item["colour"]) + + +class Migration(migrations.Migration): + + dependencies = [ + ("home", "default_priorities"), + ] + + operations = [ + migrations.RunPython(create_tags) + ] diff --git a/apps/home/models.py b/apps/home/models.py index 40b5399..ce80382 100644 --- a/apps/home/models.py +++ b/apps/home/models.py @@ -7,28 +7,86 @@ from datetime import timedelta, datetime from django.db import models from django.conf import settings from django.utils import timezone +from django.utils.translation import gettext_lazy as _ class TicketPriority(models.Model): title = models.CharField(max_length=32) colour = models.CharField(max_length=7) + def __str__(self): + return self.title + + def serialize(self) -> dict: + return { + "title": self.title, + "colour": self.colour + } + class TicketTag(models.Model): title = models.CharField(max_length=32) colour = models.CharField(max_length=7) + def __str__(self): + return self.title + + def serialize(self) -> dict: + return { + "title": self.title, + "colour": self.colour + } + class Ticket(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - title = models.CharField(max_length=60) - description = models.TextField(max_length=650) - author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) - create_timestamp = models.DateTimeField(editable=True, default=timezone.now) - edit_timestamp = models.DateTimeField(editable=True, default=timezone.now) + + title = models.CharField( + _("title"), + max_length=100, + help_text=_("An extremely short summary of the ticket subject.") + ) + description = models.TextField( + _("description"), + max_length=650, + help_text=_("Detailed description of the ticket subject.") + ) + author = models.ForeignKey( + settings.AUTH_USER_MODEL, + verbose_name=_("author"), + on_delete=models.CASCADE, + help_text=_("The creator of the ticket.") + ) + + priority = models.ForeignKey( + TicketPriority, + verbose_name=_("priority"), + on_delete=models.CASCADE, + help_text=_("The importance level of this ticket.") + ) + tags = models.ManyToManyField( + TicketTag, + verbose_name=_("tags"), + blank=True, + help_text=_("Categories of the ticket.") + + ) + + create_timestamp = models.DateTimeField( + _("Creation Date"), + editable=True, + default=timezone.now, + help_text=_("When the user was created.") + ) + edit_timestamp = models.DateTimeField( + _("Last Edited"), + editable=True, + default=timezone.now, + help_text=_("When the user was last edited.") + ) def __str__(self): - return f"#{self.id} • {self.title} • {self.author}" + return f"#{self.id} • {self.title} •{f' {self.author.department.title} •' if self.author.department else ''} {self.author.formal_fullname}" def clean_description(self): cleaned_description = bleach.clean( @@ -40,6 +98,7 @@ class Ticket(models.Model): def save(self, *args, **kwargs): self.description = self.clean_description() + self.edit_timestamp = timezone.now() super().save(*args, **kwargs) @property @@ -91,5 +150,7 @@ class Ticket(models.Model): "edit_timestamp": self.edit_timestamp, "is_edited": self.is_edited, "is_older_than_day": self.is_older_than_day, - "timestamp": self.timestamp + "timestamp": self.timestamp, + "priority": self.priority.serialize(), + "tags": [tag.serialize() for tag in self.tags.all()] } diff --git a/apps/home/views.py b/apps/home/views.py index 622278a..278eb6c 100644 --- a/apps/home/views.py +++ b/apps/home/views.py @@ -11,7 +11,8 @@ from django.shortcuts import render from django.urls import reverse from django.forms.models import model_to_dict -from .models import Ticket +from ..authentication.models import Department +from .models import Ticket, TicketPriority, TicketTag @login_required() @@ -22,8 +23,15 @@ def dashboard(request): @login_required() def tickets(request): tickets = Ticket.objects.all().order_by("-create_timestamp") + priorities = TicketPriority.objects.all() + tags = TicketTag.objects.all() + departments = Department.objects.all() + context = { "tickets": tickets, + "priorities": priorities, + "tags": tags, + "departments": departments, "dayago": datetime.now() - timedelta(hours=24) } diff --git a/apps/templates/accounts/login.html b/apps/templates/accounts/login.html index 3b77931..d34d277 100644 --- a/apps/templates/accounts/login.html +++ b/apps/templates/accounts/login.html @@ -32,10 +32,10 @@ {% csrf_token %}