0

I have 2 groups of 5 employees. In each group, one of the employees is that group's supervisor.

In the create view, once the supervisor field is populated, I would like the employee foreign key field to show only those employees belonging to that supervisor.

It would be nice to have the appropriate employees displayed based on the user (supervisor) without the supervisor field having to be populated first.

I have tried model forms to try to appropriately modify the employee foreign key field query set, obviously to no avail. Please help!

The code is as follows:

class Instruction(models.Model):

    supervisor = models.ForeignKey(
        Profile, on_delete=models.CASCADE,
        related_name="supervisorinstructions"
    )
    employee = models.ForeignKey(
        Profile, on_delete=models.CASCADE,
        related_name="employeeinstructions"
    )
    instruction = models.CharField(max_length=300)
        
    def __str__(self):
        return f"{self.instruction}"

    def get_absolute_url(self):
        return reverse("myapp:instructiondetail", kwargs={"pk": self.pk})

class InstructionCreate(CreateView):

    model = models.Instruction
    fields = [
        "supervisor",
        "employee",
        "instruction",
    ]
    template_name = "myapp/instruction_create_form.html"

More information.

For more context (pardon the pun)...

I have already been able to get a list of employees reporting to a particular supervisor.

Each employee reports to only one supervisor, so none of these employees would appear in any other list of employees reporting to another supervisor.

From this, I have been able to put a link to a (template) view where the context is a chosen employee and use the context to produce a report specific to that employee as follows:

def get_context_data(self, **kwargs):
    context = super().get_context_data(**kwargs)
    profile = models.Profile.objects.get(**kwargs)
    context["profile"] = profile
    employee = profile.employee
    context["employee"] = employee
    supervisor = profile.supervisor
    context["supervisor"] = supervisor

    # report queries

    return context

Since only that supervisor can generate that report, it should also be possible for only that supervisor to be able to create an instruction for the employee, without even having to specify in the form who (the supervisor) is creating the instruction for who (the employee), and for the appropriate attributes required by my model to be set automatically from the context data.

This is where I have a problem. I am using CBVs and only a supervisor can create an object for an employee.

When the supervisor wants to create an instruction for the employee, I want to be able to use the context (as done in the case of the report) to fix who the instruction is for and who is giving the instruction.

At the moment, when the create form opens, regardless of whether the supervisor is identified, all the employees appear, making it possible for a supervisor to create instructions for employees of the other supervisor and that's what I am trying to prevent.

I am too new in Django to figure this out on my own. In my novice opinion, it should be possible to prepopulate the create form with the supervisor and employee details from the context which negates the drop-down problem.

New, the employee model as requested ---`

class Profile(models.Model):

    user = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.CASCADE, 
        related_name="userprofiles")
    employee = models.ForeignKey(
        "self", on_delete=models.CASCADE, null=True, blank=True,
        related_name="employeeprofiles",
    )
    supervisor1 = models.ForeignKey(
        "self", on_delete=models.CASCADE, null=True, blank=True,
        related_name="supervisorprofiles",
    )

    def __str__(self):
        return f"{self.user}" 

    def get_absolute_url(self):
        return reverse("myapp:profiledetail", kwargs={"pk": self.pk})`

Corrected the name of the last model, was Employee but should have been Profile as indicated in the Instruction model.

Hope this is the answer:

class InstructionCreateForm(ModelForm):
    class Meta:
        model = models.Instruction
        fields = ["supervisor", "employee", "instruction"]

    def clean(self):
        cleaned_data = super(InstructionCreateForm, self).clean()
        supervisor = cleaned_data.get('supervisor')
        employee = cleaned_data.get('employee')
        # check if the supervisor is the employee's supervisor and raise error if not
        if supervisor != models.Profile.objects.filter(employee=employee).supervisor:
            self.add_error(None, ValidationError('The employee is not your direct subordinate.'))
        return cleaned_data
    

class InstructionCreate(CreateView):

    model = models.Instruction
    template_name = "internalcontrol/instruction_create_form.html"
    form_class = forms.InstructionCreateForm

It worked! But only after this change:

I replaced if supervisor != models.Profile.objects.filter(employee=employee).supervisor:

with if employee != models.Profile.objects.filter(employee=employee, supervisor=supervisor):

for it to work.

The CBV create template:

    <form method="post">

    {% csrf_token %}

    {{ form|crispy }}

    <div class="btn-group">
        <input type="submit" class="btn btn-primary btn-sm" value="Add">
        <a href="{% url 'company:instructionlist' %}" class="btn btn-secondary btn-sm">List</a>
    </div>

</form>
  • 1
    Does this answer your question? [how to display dependent drop-down in django form](https://stackoverflow.com/questions/55878568/how-to-display-dependent-drop-down-in-django-form) – Abdul Aziz Barkat Sep 30 '21 at 04:24
  • Unfortunately no, Abdul. I have added more information to clarify the help I need. – Kaya Kwinana Oct 01 '21 at 15:07
  • Ah, I misunderstood due to the "_once the supervisor field is populated, I would like the employee foreign key field to show only those employees belonging to that supervisor._" but you instead want the supervisor to not even be a field on that form and only want to show filtered choices? See [How do I filter ForeignKey choices in a Django ModelForm?](https://stackoverflow.com/questions/291945/how-do-i-filter-foreignkey-choices-in-a-django-modelform) – Abdul Aziz Barkat Oct 01 '21 at 16:01
  • Will you add the Supervisor and Employee models? – Code-Apprentice Oct 01 '21 at 17:01
  • I tried to answer your question below, but I still feel like I don't understand what you are asking. Will you add some screenshots or mock ups of the forms and explain the exact work flow from a user's point of view? – Code-Apprentice Oct 01 '21 at 17:08
  • I see that you added the `InstructionCreate` view to your code example, but that still doesn't answer my previous question. – Code-Apprentice Oct 02 '21 at 08:39
  • Hi Abdul, I have had a look at the link you suggested. No, my model does not have choice fields so that solution does not apply. – Kaya Kwinana Oct 02 '21 at 09:31
  • 1
    @KayaKwinana `ModelChoiceField` is the default field (gives you a select input by default) for foreign keys, you want to limit those choices to the employees under the current supervisor, that linked question would show you how to filter the forms choices... – Abdul Aziz Barkat Oct 02 '21 at 11:17
  • Thanks, Abdul. I'll have another look at it. – Kaya Kwinana Oct 03 '21 at 03:49
  • Thanks a lot, Abdul and Code-Apprentice! You guys helped a lot! Being new to Django (and coding) I misled you. Wanted a sophisticated answer to a simple problem. Reminds me of the millions NASA spent to design a pen for use in space. The cosmonauts solved the problem by using a lead pencil! Both your insights were helpful. I can't wait for tomorrow. – Kaya Kwinana Oct 08 '21 at 20:52

3 Answers3

1

The problem was to prevent a supervisor from giving instructions to someone else's subordinates.

Because I am new in Django, I focused on methodology I am not yet good enough in and forgot to focus on the desired outcome.

Using the code below achieves the objective in the first sentence:

class Instruction(models.Model):
    
    employee = models.OneToOneField(Profile, on_delete=models.CASCADE, blank=True, null=True,
        related_name="employeeinstructions")
    supervisor = models.ForeignKey(Profile, on_delete=models.CASCADE, blank=True, null=True,
        related_name="supervisorinstructions")

class InstructionCreateForm(ModelForm):
    
    class Meta:
        model = models.Instruction
        fields = ["employee", "instruction"]

    def clean(self):
        cleaned_data = super(InstructionCreateForm, self).clean()
        supervisor = cleaned_data.get('supervisor')
        employee = cleaned_data.get('employee')
        if supervisor != models.Profile.objects.filter(employee=employee).supervisor:
            self.add_error(None, ValidationError('The employee is not a subordinate of this supervisor.'))
        return cleaned_data



class InstructionCreate(CreateView):

    model = models.Instruction
    template_name = "internalcontrol/instruction_create_form.html"
    form_class = forms.InstructionCreateForm

    def form_valid(self, form):
        user = self.request.user.id
        profile = models.Profile.objects.get(user=user, employee=True)
        form.instance.supervisor = profile
        return super().form_valid(form)

The CBV create template is:

    
    <form method="post">

        {% csrf_token %}

        {{ form|crispy }}

        <div class="btn-group">
            <input type="submit" class="btn btn-primary btn-sm" value="Add">
            <a href="{% url 'company:instructionlist' %}" class="btn btn-secondary btn-sm">List</a>
        </div>

    </form>
1

The correct answer to my original question - the first prize I was looking for - is in Django docs (https://docs.djangoproject.com/en/3.2/ref/forms/fields/#fields-which-handle-relationships) and in Django-Filter (https://django-filter.readthedocs.io/en/stable/guide/usage.html#filtering-the-related-queryset-for-modelchoicefilter)

That closes this chapter.

  • 1
    Glad you found a solution. When you have some time, please add some code to this answer to show exactly what you did. This will help future visitors that encounter the same problem. – Code-Apprentice Nov 08 '21 at 19:13
0

Until you update your question, I will assume you have a model similar to this:

class Employee(models.Model):
    # some fields not related to the current question
    supervisor = models.ForeignKey(
        'Employee', on_delete=models.CASCADE,
        related_name="direct_reports"
    )

Now if you have an Employee object named supervisor, you can do supervisor.direct_reports to get a queryset of Employee objects who's supervisor is supervisor. For your purposes, you probably want to send this in the context of your form in order to populate a dropdown list.

Alternatively, only pass the supervisor in the context and then access supervisor.direct_reports directly in the template.

Code-Apprentice
  • 81,660
  • 23
  • 145
  • 268
  • @KayaKwinana In fact, you can just pass the supervisor into the template context and use `supervisor.employ_direct_reports` directly in the template. – Code-Apprentice Oct 02 '21 at 08:37
  • Thank you, guys, for taking the time to try to help me out. Given that I can get the appropriate supervisor and employee attributes from the context as shown, that seems to be the appropriate route to take. If I already have these attributes, it seems pointless to pursue the dropdown route. I don't use model forms and the employee model only has "user", "employee" and "supervisor" fields, the latter 2 being foreign keys to self. Filtering that model by supervisor yields the employees reporting to that supervisor and an href on each sends the data to the context of the report view as shown. – Kaya Kwinana Oct 02 '21 at 08:43
  • Hi Code-Apprentice. Something I'm noticing now. I get the context attributes I need from the get_context_data method I used for the report. The CBV CreateViews do accept the get_context_data method. How can I incorporate context data,( essentially the same way as I have done for the report) in the CBV Create View to prepopulate the create form, maybe even displaying only the instruction field? – Kaya Kwinana Oct 02 '21 at 09:05
  • My last suggestion doesn't work. CBV CreateView does not allow id, pk or any second parameter. I am wondering if an FBV CreateView would do the trick. Unfortunately, I am totally clueless on that score. I am guessing it should be possible given the tighter control and that it is possible on edit and delete views. In short, is it possible to create a FBV CreateView which takes a pk as a parameter? – Kaya Kwinana Oct 02 '21 at 11:23
  • Obviously I do not know what pk will be created, my reference was to the employee profile pk. In any case, even I do not think I am making sense. – Kaya Kwinana Oct 02 '21 at 11:43
  • @KayaKwinana Take a step back. What are you actually trying to accomplish here? Describe it from the user's point of view. When they load this page, what should they see? What actions can they take? And what are the results of those actions? It may help to draw the UI with MS Paint or a similar program to show us what you want. – Code-Apprentice Oct 02 '21 at 14:54
  • Hi Code-Apprentice, thank you again for taking the time to try to help me. Abdul Aziz Barkat seems to be up to something and I am having a look at it. All the alternative solutions I was considering were to achieve one thing: "In the create view, once the supervisor field is populated, I would like the employee foreign key field to show only those employees belonging to that supervisor." Abdul Aziz Barkat's proposed solution points to exactly that. I'll report back when I am through with it. – Kaya Kwinana Oct 03 '21 at 06:25
  • I think the key is the question - "@KayaKwinana Take a step back. What are you actually trying to accomplish here?" Never mind what I was trying to do, all I wanted was to ensure that whenever a supervisor issued an instruction, it was only to his/her subordinates. Checking whether this is so, in a ModelForm seems to be the answer. I'll add the code in my question and confirm tomorrow if it works. – Kaya Kwinana Oct 08 '21 at 20:45
  • @KayaKwinana Is the supervisor the user that is currently logged in? Or is another user logged in and selecting a supervisor from a dropdown? These are two different scenarios with different solutions. In the first, you can just get the subordinates based on the current user. In the second, you will need to make a request to get subordinates based on the selected supervisor. – Code-Apprentice Oct 11 '21 at 20:29
  • Hi Code-Apprentice The assigned supervisor is the currently logged in user. The form itself does not display the supervisor field. One can therefore not fake who is making the entry. The form then checks if the assigned supervisor is the supervisor of the employee selected from the drop-down list and throws a validation error if not. The form check should say if supervisor (not the employee as I had it previously in my answer) is not the employee's supervisor. I have corrected that mistake now. Thanks for all the help. – Kaya Kwinana Oct 17 '21 at 09:53
  • @KayaKwinana So you can avoid needing the validation by passing in a list of employees filtered on the current user as their supervisor. – Code-Apprentice Oct 18 '21 at 16:12
  • Code-Apprentice, that would be my first prize still, but I don't know how to do it. – Kaya Kwinana Oct 18 '21 at 17:09
  • @KayaKwinana The key is that you can follow the relationships from a `User` to that user's direct reports. My answer shows a simplified version where you start with an `Employee` and can directly get `employee.direct_reports`. In your more complex situation, you start from a `User` and have to navigate to the `Profile` to find the direct reports. I'm having trouble figuring out all the relationships at this moment to give you a more exact solution. Hopefully what I have written will help you figure it out. – Code-Apprentice Oct 18 '21 at 19:53
  • Code-Apprentice, the 11th scenario - "you can just get the subordinates based on the current user" - is what I want and is also reflected in your comment of the 18th. Both reflect the 2nd sentence of my original question. My validation code, bearing in my that the user is the supervisor, was confirming whether that supervisor is the supervisor of the employee selected. Yours seems to check if the employee is the supervisor. Since both are employees, it might be easier to think of the selected employee to be a supervisor.employee (supervisor.direct_report). – Kaya Kwinana Oct 19 '21 at 10:10
  • Either way, the question remains, how do I get the supervisor.direct_reports, and only those direct_reports into the form drop-down list for the supervisor to choose from and obviate the need for validation? – Kaya Kwinana Oct 19 '21 at 10:14
  • @KayaKwinana Since you already add `supervisor` to the context, you can use `supervisor.direct_reports` in the template. – Code-Apprentice Oct 19 '21 at 15:07
  • Code Apprentice, first apologies for misunderstanding your code. I think it would also work. I am new in Django and appreciate the time you have given me. I don't know how to do what you suggest. Could we please use supervisor.employee,just so I don't get confused further. I'll add the template to my code. Please show me how to add the supervisor.employee (or supervisor.direct_reports) to get generate the drop-down list of just the direct reports. – Kaya Kwinana Oct 19 '21 at 18:53
  • @KayaKwinana The name can be whatever you want as long as it matches what you declare in the code. – Code-Apprentice Oct 20 '21 at 14:51
  • Code-Apprentice, found my first prize! See the answer! Thanks for pointing me in the right way. – Kaya Kwinana Nov 08 '21 at 13:10