简体   繁体   中英

Create a project management app in Django - DAGs and Dependencies

I'm working in a Project management app. One of the points that the app needs is the possibility to create Tasks that have dependencies, ie Task 2 depends on Task 1, Task 3 depends on Task 2 and Task B, etc. This would generate a DAG (directed acyclic graph).

The problem is how to store this "graph" using the Django default ORM.

I've come with a solution that uses Many-to-Many relationship (Task with self) and a trigger to avoid creating cycles, but I still don't know how to implement some operations such as getting the whole graph from a single node.

Does anyone have an idea on how to implement it nicely? This would allow to perform an implementation of the Critical Path Method, for example.

EDIT:

Using the solution proposed by @Daniel I ended up with the function below. We can see that in order to get the whole graph multiple calls are being made to the database. This function also goes into an infinite loop during cycles.

def get_task_graph(task, visited_nodes = [], graph = {}):
    # Visited nodes are nodes that all parents were visited
    # print(task)

    
    
    previous_tasks = task.previous_tasks.all()

    
    still_have_unmarked_parents = set(previous_tasks).difference(set(visited_nodes))
    if still_have_unmarked_parents:
        print(f"Task {task} still have unmarked parents")
        
        for previous_task in still_have_unmarked_parents:
            

            visited_nodes, graph =  get_task_graph(previous_task, visited_nodes=visited_nodes, graph=graph)
    
    next_tasks = task.next_tasks.all()
 
    visited_nodes.append(task)
    print(f"Marking {task} as visited")
    graph[task] = []
    for next_task in next_tasks:
        if next_task not in graph[task]:
            graph[task].append(next_task)
    
        
        visited_nodes, graph = get_task_graph(next_task, visited_nodes=visited_nodes, graph=graph)
        print(f"\t Adding {next_task} to {task}")

    return visited_nodes, graph

Thanks

Original Answer: ManyToMany Relations

Let's create a simple Task model:

class Task(models.Model):
   name = models.CharField(...)
   previous_tasks = models.ManyToMany('Task', ..., related_name='next_tasks')

Now let's create taks_a and task_b - task_b will depend on task_a :

# create task_a, task_b:
task_a = Task.objects.create(name='First Task')
task_b = Task.objects.create(name='Second task')

# add task_a as a dependency for task_b:
task_b.previous_tasks.add([task_a])
task_b.save()

Now we can access these tasks like so:

# get the tasks to do after task_a is complete:
task_a = Task.objects.get(id='<task-a-id>')
tasks_to_do_after_task_a = task_a.next_tasks.all() # returns a queryset with <task_b>

# get the tasks we need to complete before task_b:
task_b = Task.objects.get(id='<task-b-id>')
tasks_that_task_b_depends_on = task_b.previous_tasks.all() # returns a queryset with <task a>

Finally, let's assume task_a actually has a few different tasks that depend on it:

# iterate over all the tasks that depend on task a:
for tasks in task_a.next_tasks.all():

     # do some logic, i.e. find longest task:
     ...

# filter for a specific next_task on task_a:
some_task = task_a.next_tasks.filter(some_keyword='some_value')

# assume we have a duration time-field on tasks:      
next_longest_task = task_a.next_tasks.order_by('-duration')[0] # gets the next task with the longest duration

From there you can implement your path-finding logic / other algorithms as needed.

EDIT: Recursively Accessing Fields

Okay - using the structure above we can define a function like so:

def get_task_list(task):

    # get the next_tasks queryset, it will either contain tasks or be an empty queryset:
    next_tasks = task.next_tasks.all()

    # check if the current task has any children:
    if next_tasks:

        # do some logic, check for longest tasks / add next tasks to a list
        ...

        # pass each next task back into the function:
        for next_task in next_tasks:
            return get_task_list(next_task)

    # exit if there are no more next tasks:
    else:

        # do some logic on exiting, calculate the time to complete all tasks:
        ...

        return <optional-content>

You can do some more research into recursive functions but this should get you started. You can get clever about looking up at previous tasks or down at next tasks in your function. You can also add this as a method to your Task model so that it can be called on a task instance.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM