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!