I am working on a simple "issue tracking" web application as way to learn more about Django.
I am using Django 4.1.4 and Python 3.9.2.
I have the following classes in models.py (which may look familiar to people familiar with JIRA):
- Components
- Issues
- IssueStates
- IssueTypes
- Priorities
- Projects
- Releases
- Sprints
Originally I also had a Users class in models.py but now am trying to switch to using the Django User model. (The User class no longer exists in my models.py)
I have been studying the following pages to learn how best to migrate to using the Django Users model.
All of my List/Detail/Create/Delete view classes worked fine with all of the above models until I started working on using the Django User class.
-- models.py --
from django.conf import settings
class Issues(models.Model):
id = models.BigAutoField(primary_key=True)
project = models.ForeignKey(
to=Projects, on_delete=models.RESTRICT, blank=True, null=True
)
summary = models.CharField(max_length=80, blank=False, null=False, default="")
issue_type = models.ForeignKey(
to=IssueTypes, on_delete=models.RESTRICT, blank=True, null=True
)
issue_state = models.ForeignKey(
to=IssueStates, on_delete=models.RESTRICT, blank=True, null=True, default="New"
)
# https://learndjango.com/tutorials/django-best-practices-referencing-user-model
# https://docs.djangoproject.com/en/4.0/topics/auth/customizing/#referencing-the-user-model
reporter = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.RESTRICT,
related_name="reporter_id",
)
priority = models.ForeignKey(
to=Priorities, on_delete=models.RESTRICT, blank=True, null=True
)
component = models.ForeignKey(
to=Components, on_delete=models.RESTRICT, blank=True, null=True
)
description = models.TextField(blank=True, null=True)
planned_release = models.ForeignKey(
to=Releases, on_delete=models.RESTRICT, blank=True, null=True
)
# https://learndjango.com/tutorials/django-best-practices-referencing-user-model
# https://docs.djangoproject.com/en/4.0/topics/auth/customizing/#referencing-the-user-model
assignee = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.RESTRICT,
related_name="assignee_id",
)
slug = models.ForeignKey(
to="IssueSlugs", on_delete=models.RESTRICT, blank=True, null=True
)
sprint = models.ForeignKey(
to=Sprints, on_delete=models.RESTRICT, blank=True, null=True
)
def save(self, *args, **kwargs):
if not self.slug:
# generate slug for this new Issue
slug = IssueSlugs()
slug.project_id = self.project.id
slug.save()
self.slug = slug
super().save(*args, **kwargs)
def __str__(self):
return self.slug.__str__() + " - " + self.summary.__str__()
class Meta:
managed = True
db_table = "issues"
class IssueSlugs(models.Model):
"""
This table is used to generate unique identifiers for records in the
Issues table. My goal was to model the default behavior found in JIRA
where each Issue has a unique identifier that is a combination of:
1) the project abbreviation
2) a sequential number for the project
So here when creating a new Issue record, if it is the first record for
a particular project, the sequential number starts at 100, otherwise it
is the next sequential number for the project.
"""
id = models.BigAutoField(primary_key=True)
project = models.ForeignKey(
to=Projects, on_delete=models.RESTRICT, blank=True, null=True
)
slug_id = models.IntegerField(default=100)
slug = models.CharField(
max_length=80,
blank=False,
null=False,
unique=True,
)
def __str__(self):
return self.slug.__str__()
def save(self, *args, **kwargs):
if not self.slug:
result = IssueSlugs.objects.filter(
project_id__exact=self.project.id
).aggregate(Max("slug_id"))
# The first issue being created for the project
# {'slug_id__max': None}
if not result["slug_id__max"]:
self.slug_id = 100
self.slug = self.project.abbreviation + "-" + str(100)
else:
logging.debug(result)
next_slug_id = result["slug_id__max"] + 1
self.slug_id = next_slug_id
self.slug = self.project.abbreviation + "-" + str(next_slug_id)
super().save(*args, **kwargs)
class Meta:
managed = True
db_table = "issue_slugs"
-- issues.py --
class CreateUpdateIssueForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# save for IssueCreateView.form_valid()
self.kwargs = kwargs
font_size = "12pt"
for field_name in self.fields:
if field_name in ("summary", "description"):
self.fields[field_name].widget.attrs.update(
{
"size": self.fields[field_name].max_length,
"style": "font-size: {0}".format(font_size),
}
)
elif field_name in ("reporter", "assignee"):
# https://docs.djangoproject.com/en/4.0/topics/auth/customizing/#referencing-the-user-model
User = get_user_model()
choices = list()
choices.append(("", ""))
for element in [
{
"id": getattr(row, "id"),
"display": row.get_full_name(),
}
for row in User.objects.exclude(is_superuser__exact="t")
]:
choices.append((element["id"], element["display"]))
self.fields[field_name] = forms.fields.ChoiceField(
choices=choices,
# I had to specify required=False here to eliminate a very
# strange error:
# An invalid form control with name='assignee' is not focusable.
required=False,
)
else:
# all the <select> fields ...
self.fields[field_name].widget.attrs.update(
{
"class": ".my-select",
}
)
class Meta:
model = Issues
fields = [
"project",
"summary",
"component",
"description",
"issue_type",
"issue_state",
"reporter",
"priority",
"planned_release",
"assignee",
"sprint",
]
class IssueCreateView(LoginRequiredMixin, PermissionRequiredMixin, generic.CreateView):
"""
A view that displays a form for creating an object, redisplaying the form
with validation errors (if there are any) and saving the object.
https://docs.djangoproject.com/en/4.1/ref/class-based-views/generic-editing/#createview
"""
model = Issues
permission_required = "ui.add_{0}".format(model.__name__.lower())
template_name = "ui/issues/issue_create.html"
success_url = "/ui/issue_list"
form_class = CreateUpdateIssueForm
def form_valid(self, form):
User = get_user_model()
if "reporter" in self.kwargs:
form.instance.reporter = User.objects.get(id__exact=self.kwargs["reporter"])
if not form.is_valid():
messages.add_message(
self.request, messages.ERROR, "ERROR: '{0}'.".format(form.errors)
)
return super().form_valid(form)
action = self.request.POST["action"]
if action == "Cancel":
# https://docs.djangoproject.com/en/4.1/topics/http/shortcuts/#django.shortcuts.redirect
return redirect("/ui/issue_list")
return super().form_valid(form)
def get_initial(self):
"""
When creating a new Issue I'm setting default values for a few
fields on the Create Issue page.
"""
# https://docs.djangoproject.com/en/4.0/topics/auth/customizing/#referencing-the-user-model
User = get_user_model()
from ui.models import IssueStates, Priorities, IssueTypes
issue_state = IssueStates.objects.get(state__exact="New")
priority = Priorities.objects.get(priority__exact="Medium")
issue_type = IssueTypes.objects.get(issue_type__exact="Task")
reporter = User.objects.get(username__exact=self.request.user)
return {
"issue_state": issue_state.id,
"priority": priority.id,
"issue_type": issue_type.id,
"reporter": reporter.id,
}
When I try to create a new Issue, the "new Issue" form is displayed normally, but when I save the form I get a Django error with a stack trace I don't understand because it does not have a reference to any of my code, so I have no idea where to start debugging.
16:22:48 ERROR Internal Server Error: /ui/issue/create
Traceback (most recent call last):
File "/Users/a0r470/git/issue_tracker/env/lib/python3.9/site-packages/django/core/handlers/exception.py", line 55, in inner
response = get_response(request)
File "/Users/a0r470/git/issue_tracker/env/lib/python3.9/site-packages/django/core/handlers/base.py", line 197, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/Users/a0r470/git/issue_tracker/env/lib/python3.9/site-packages/django/views/generic/base.py", line 103, in view
return self.dispatch(request, *args, **kwargs)
File "/Users/a0r470/git/issue_tracker/env/lib/python3.9/site-packages/django/contrib/auth/mixins.py", line 73, in dispatch
return super().dispatch(request, *args, **kwargs)
File "/Users/a0r470/git/issue_tracker/env/lib/python3.9/site-packages/django/contrib/auth/mixins.py", line 109, in dispatch
return super().dispatch(request, *args, **kwargs)
File "/Users/a0r470/git/issue_tracker/env/lib/python3.9/site-packages/django/views/generic/base.py", line 142, in dispatch
return handler(request, *args, **kwargs)
File "/Users/a0r470/git/issue_tracker/env/lib/python3.9/site-packages/django/views/generic/edit.py", line 184, in post
return super().post(request, *args, **kwargs)
File "/Users/a0r470/git/issue_tracker/env/lib/python3.9/site-packages/django/views/generic/edit.py", line 152, in post
if form.is_valid():
File "/Users/a0r470/git/issue_tracker/env/lib/python3.9/site-packages/django/forms/forms.py", line 205, in is_valid
return self.is_bound and not self.errors
File "/Users/a0r470/git/issue_tracker/env/lib/python3.9/site-packages/django/forms/forms.py", line 200, in errors
self.full_clean()
File "/Users/a0r470/git/issue_tracker/env/lib/python3.9/site-packages/django/forms/forms.py", line 439, in full_clean
self._post_clean()
File "/Users/a0r470/git/issue_tracker/env/lib/python3.9/site-packages/django/forms/models.py", line 485, in _post_clean
self.instance = construct_instance(
File "/Users/a0r470/git/issue_tracker/env/lib/python3.9/site-packages/django/forms/models.py", line 82, in construct_instance
f.save_form_data(instance, cleaned_data[f.name])
File "/Users/a0r470/git/issue_tracker/env/lib/python3.9/site-packages/django/db/models/fields/__init__.py", line 1006, in save_form_data
setattr(instance, self.name, data)
File "/Users/a0r470/git/issue_tracker/env/lib/python3.9/site-packages/django/db/models/fields/related_descriptors.py", line 237, in __set__
raise ValueError(
ValueError: Cannot assign "'2'": "Issues.reporter" must be a "User" instance.
[27/Dec/2022 16:22:48] "POST /ui/issue/create HTTP/1.1" 500 120153
Generally I understand that under the covers, Django creates two fields in the Issues model for me:
- reporter
- reporter_id
and I understand that the reporter field needs to contain a User instance instead of an integer (2). BUT I don't know WHERE in my code I should do this assignment.
I have tried overriding a few methods in my CreateUpdateIssueForm and IssueCreateView as a way to try to find where my code is causing problems - no luck so far.
In my IssueCreateView(generic.CreateView) class, I added the following to my form_valid() method, intending to retrieve the correct User record and assign it to form.instance.reporter, but the code appears to be failing before it gets to my form_valid() method.
def form_valid(self, form):
User = get_user_model()
if "reporter" in self.kwargs:
form.instance.reporter = User.objects.get(id__exact=self.kwargs["reporter"])
Clearly I do not fully understand the flow of control in these Generic View classes.
Thank you for any help you can provide!
I discovered that trying to migrate my own Users model to a CustomUser model is a non-trivial undertaking! I learned this from Will Vincent and his excellent post on this very topic!
Django Best Practices: Custom User Model
The Django documentation also states that migrating to the Django User in the midst of an existing project is non-trivial.
Changing to a custom user model mid-project
So, to solve my problem I started with a new empty project with only the CustomUser in my models.py as Mr. Vincent described, which worked perfectly.
After that, I setup the rest of my model classes in models.py, referencing the CustomUser model as needed.
And copied the rest of my template files, view source files, static files, etc. from my original project into this new project.
My codebase is now working as expected using the Django User model.
Huge Thanks to Mr. Will Vincent's excellent article on this issue!