1

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!

  • This post seems related https://stackoverflow.com/questions/37839867/django-error-cannot-assign-must-be-an-instance – John Gordon Dec 27 '22 at 23:19
  • Can you please post the full details of your form and view? – Willem Van Onsem Dec 27 '22 at 23:55
  • @WillemVanOnsem I just posted my complete form and view classes. Hope this helps. Thanks! – user8801701 Dec 28 '22 at 15:14
  • @JohnGordon - I believe I understand the idea in that post BUT it isn't clear in that post WHERE a code change like that should be made. I believe the change I made (above) in my form_valid() method was an attempt to do that very same thing. – user8801701 Dec 28 '22 at 15:35

1 Answers1

0

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.

assignee = models.ForeignKey(
    to=CustomUser,
    on_delete=models.RESTRICT,
    blank=True,
    null=True,
)

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!