├── .gitignore ├── MANIFEST ├── MANIFEST.in ├── README ├── __init__.py ├── django_dag ├── __init__.py ├── models.py ├── templates │ └── tree.html ├── templatetags │ ├── __init__.py │ └── dag_tags.py ├── tests │ ├── __init__.py │ ├── models.py │ └── test.py └── tree_test_output.py ├── manage.py ├── settings.py ├── setup.py └── urls.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | MANIFEST.in 2 | README 3 | setup.py 4 | django_dag/__init__.py 5 | django_dag/models.py 6 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGES LICENSE NOTICE README UPDATING MANIFEST.in 2 | recursive-include README *.py *.rst 3 | recursive-include django_dag *.py *.html 4 | #recursive-include django_dag_test *.py *.html 5 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Django DAG 2 | ---------- 3 | 4 | Django-dag is a small reusable app which implements a Directed Acyclic Graph. 5 | 6 | Usage 7 | ..... 8 | 9 | Django-dag uses abstract base classes, to use it you must create your own 10 | concrete classes that inherit from Django-dag classes. 11 | 12 | The `dag_test` app contains a simple example and a unit test to show 13 | you its usage. 14 | 15 | Example:: 16 | 17 | class ConcreteNode(node_factory('ConcreteEdge')): 18 | """ 19 | Test node, adds just one field 20 | """ 21 | name = models.CharField(max_length = 32) 22 | 23 | class ConcreteEdge(edge_factory(ConcreteNode, concrete = False)): 24 | """ 25 | Test edge, adds just one field 26 | """ 27 | name = models.CharField(max_length = 32, blank = True, null = True) 28 | 29 | 30 | Tests 31 | ..... 32 | 33 | Unit tests can be run with just django installed at the base directory by running 34 | `python manage.py test` 35 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpaso/django-dag/374c8da38c99dc737ec6fa4b4a4aa8032196b405/__init__.py -------------------------------------------------------------------------------- /django_dag/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpaso/django-dag/374c8da38c99dc737ec6fa4b4a4aa8032196b405/django_dag/__init__.py -------------------------------------------------------------------------------- /django_dag/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | A class to model hierarchies of objects following 3 | Directed Acyclic Graph structure. 4 | 5 | Some ideas stolen from: from https://github.com/stdbrouw/django-treebeard-dag 6 | 7 | """ 8 | 9 | from django.db import models 10 | from django.core.exceptions import ValidationError 11 | 12 | 13 | class NodeNotReachableException (Exception): 14 | """ 15 | Exception for node distance and path 16 | """ 17 | pass 18 | 19 | 20 | class NodeBase(object): 21 | """ 22 | Main node abstract model 23 | """ 24 | 25 | class Meta: 26 | ordering = ('-id',) 27 | 28 | def __unicode__(self): 29 | return u"# %s" % self.pk 30 | 31 | def __str__(self): 32 | return self.__unicode__() 33 | 34 | def add_child(self, descendant, **kwargs): 35 | """ 36 | Adds a child 37 | """ 38 | args = kwargs 39 | args.update({'parent' : self, 'child' : descendant }) 40 | disable_check = args.pop('disable_circular_check', False) 41 | cls = self.children.through(**kwargs) 42 | return cls.save(disable_circular_check=disable_check) 43 | 44 | 45 | def add_parent(self, parent, *args, **kwargs): 46 | """ 47 | Adds a parent 48 | """ 49 | return parent.add_child(self, **kwargs) 50 | 51 | def remove_child(self, descendant): 52 | """ 53 | Removes a child 54 | """ 55 | self.children.through.objects.get(parent = self, child = descendant).delete() 56 | 57 | def remove_parent(self, parent): 58 | """ 59 | Removes a parent 60 | """ 61 | parent.children.through.objects.get(parent = parent, child = self).delete() 62 | 63 | def parents(self): 64 | """ 65 | Returns all elements which have 'self' as a direct descendant 66 | """ 67 | return self.__class__.objects.filter(children = self) 68 | 69 | def descendants_tree(self): 70 | """ 71 | Returns a tree-like structure with progeny 72 | """ 73 | tree = {} 74 | for f in self.children.all(): 75 | tree[f] = f.descendants_tree() 76 | return tree 77 | 78 | def ancestors_tree(self): 79 | """ 80 | Returns a tree-like structure with ancestors 81 | """ 82 | tree = {} 83 | for f in self.parents(): 84 | tree[f] = f.ancestors_tree() 85 | return tree 86 | 87 | def descendants_set(self, cached_results=None): 88 | """ 89 | Returns a set of descendants 90 | """ 91 | if cached_results is None: 92 | cached_results = dict() 93 | if self in cached_results.keys(): 94 | return cached_results[self] 95 | else: 96 | res = set() 97 | for f in self.children.all(): 98 | res.add(f) 99 | res.update(f.descendants_set(cached_results=cached_results)) 100 | cached_results[self] = res 101 | return res 102 | 103 | def ancestors_set(self, cached_results=None): 104 | """ 105 | Returns a set of ancestors 106 | """ 107 | if cached_results is None: 108 | cached_results = dict() 109 | if self in cached_results.keys(): 110 | return cached_results[self] 111 | else: 112 | res = set() 113 | for f in self.parents(): 114 | res.add(f) 115 | res.update(f.ancestors_set(cached_results=cached_results)) 116 | cached_results[self] = res 117 | return res 118 | 119 | def descendants_edges_set(self, cached_results=None): 120 | """ 121 | Returns a set of descendants edges 122 | """ 123 | if cached_results is None: 124 | cached_results = dict() 125 | if self in cached_results.keys(): 126 | return cached_results[self] 127 | else: 128 | res = set() 129 | for f in self.children.all(): 130 | res.add((self, f)) 131 | res.update(f.descendants_edges_set(cached_results=cached_results)) 132 | cached_results[self] = res 133 | return res 134 | 135 | def ancestors_edges_set(self, cached_results=None): 136 | """ 137 | Returns a set of ancestors edges 138 | """ 139 | if cached_results is None: 140 | cached_results = dict() 141 | if self in cached_results.keys(): 142 | return cached_results[self] 143 | else: 144 | res = set() 145 | for f in self.parents(): 146 | res.add((f, self)) 147 | res.update(f.ancestors_edges_set(cached_results=cached_results)) 148 | cached_results[self] = res 149 | return res 150 | 151 | def nodes_set(self): 152 | """ 153 | Retrun a set of all nodes 154 | """ 155 | nodes = set() 156 | nodes.add(self) 157 | nodes.update(self.ancestors_set()) 158 | nodes.update(self.descendants_set()) 159 | return nodes 160 | 161 | def edges_set(self): 162 | """ 163 | Returns a set of all edges 164 | """ 165 | edges = set() 166 | edges.update(self.descendants_edges_set()) 167 | edges.update(self.ancestors_edges_set()) 168 | return edges 169 | 170 | def distance(self, target): 171 | """ 172 | Returns the shortest hops count to the target vertex 173 | """ 174 | return len(self.path(target)) 175 | 176 | def path(self, target): 177 | """ 178 | Returns the shortest path 179 | """ 180 | if self == target: 181 | return [] 182 | if target in self.children.all(): 183 | return [target] 184 | if target in self.descendants_set(): 185 | path = None 186 | for d in self.children.all(): 187 | try: 188 | desc_path = d.path(target) 189 | if not path or len(desc_path) < len(path): 190 | path = [d] + desc_path 191 | except NodeNotReachableException: 192 | pass 193 | else: 194 | raise NodeNotReachableException 195 | return path 196 | 197 | def is_root(self): 198 | """ 199 | Check if has children and not ancestors 200 | """ 201 | return bool(self.children.exists() and not self._parents.exists()) 202 | 203 | def is_leaf(self): 204 | """ 205 | Check if has ancestors and not children 206 | """ 207 | return bool(self._parents.exists() and not self.children.exists()) 208 | 209 | def is_island(self): 210 | """ 211 | Check if has no ancestors nor children 212 | """ 213 | return bool(not self.children.exists() and not self._parents.exists()) 214 | 215 | def _get_roots(self, at): 216 | """ 217 | Works on objects: no queries 218 | """ 219 | if not at: 220 | return set([self]) 221 | roots = set() 222 | for a2 in at: 223 | roots.update(a2._get_roots(at[a2])) 224 | return roots 225 | 226 | def get_roots(self): 227 | """ 228 | Returns roots nodes, if any 229 | """ 230 | at = self.ancestors_tree() 231 | roots = set() 232 | for a in at: 233 | roots.update(a._get_roots(at[a])) 234 | return roots 235 | 236 | def _get_leaves(self, dt): 237 | """ 238 | Works on objects: no queries 239 | """ 240 | if not dt: 241 | return set([self]) 242 | leaves = set() 243 | for d2 in dt: 244 | leaves.update(d2._get_leaves(dt[d2])) 245 | return leaves 246 | 247 | def get_leaves(self): 248 | """ 249 | Returns leaves nodes, if any 250 | """ 251 | dt = self.descendants_tree() 252 | leaves = set() 253 | for d in dt: 254 | leaves.update(d._get_leaves(dt[d])) 255 | return leaves 256 | 257 | 258 | @staticmethod 259 | def circular_checker(parent, child): 260 | """ 261 | Checks that the object is not an ancestor, avoid self links 262 | """ 263 | if parent == child: 264 | raise ValidationError('Self links are not allowed.') 265 | if child in parent.ancestors_set(): 266 | raise ValidationError('The object is an ancestor.') 267 | 268 | 269 | def edge_factory(node_model, child_to_field = "id", parent_to_field = "id", concrete = True, base_model = models.Model): 270 | """ 271 | Dag Edge factory 272 | """ 273 | try: 274 | basestring 275 | except NameError: 276 | basestring = str 277 | if isinstance(node_model, basestring): 278 | try: 279 | node_model_name = node_model.split('.')[1] 280 | except IndexError: 281 | node_model_name = node_model 282 | else: 283 | node_model_name = node_model._meta.model_name 284 | 285 | class Edge(base_model): 286 | class Meta: 287 | abstract = not concrete 288 | 289 | parent = models.ForeignKey(node_model, related_name = "%s_child" % node_model_name, to_field = parent_to_field, on_delete=models.CASCADE) 290 | child = models.ForeignKey(node_model, related_name = "%s_parent" % node_model_name, to_field = child_to_field, on_delete=models.CASCADE) 291 | 292 | def __unicode__(self): 293 | return u"%s is child of %s" % (self.child, self.parent) 294 | 295 | def save(self, *args, **kwargs): 296 | if not kwargs.pop('disable_circular_check', False): 297 | self.parent.__class__.circular_checker(self.parent, self.child) 298 | super(Edge, self).save(*args, **kwargs) # Call the "real" save() method. 299 | 300 | return Edge 301 | 302 | def node_factory(edge_model, children_null = True, base_model = models.Model): 303 | """ 304 | Dag Node factory 305 | """ 306 | class Node(base_model, NodeBase): 307 | class Meta: 308 | abstract = True 309 | 310 | children = models.ManyToManyField( 311 | 'self', 312 | blank = children_null, 313 | symmetrical = False, 314 | through = edge_model, 315 | related_name = '_parents') # NodeBase.parents() is a function 316 | 317 | return Node 318 | -------------------------------------------------------------------------------- /django_dag/templates/tree.html: -------------------------------------------------------------------------------- 1 | {% load dag_tags %}{% for dag_instance in dag_list %}{{ dag_instance }} 2 | {% if dag_instance.is_root %}Descendants: 3 | {% recursedict dag_instance.descendants_tree %}{% loop %}{% if key %}{{ key }} 4 | {% endif %}{% value %} 5 | {% endloop %}{% endrecursedict %}{% endif %} 6 | {% if dag_instance.is_leaf %}Ancestors: 7 | {% recursedict dag_instance.ancestors_tree %}{% loop %}{% if key %}{{ key }}{% endif %} 8 | {% value %} 9 | {% endloop %}{% endrecursedict %}{% endif %}{% endfor %} 10 | -------------------------------------------------------------------------------- /django_dag/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpaso/django-dag/374c8da38c99dc737ec6fa4b4a4aa8032196b405/django_dag/templatetags/__init__.py -------------------------------------------------------------------------------- /django_dag/templatetags/dag_tags.py: -------------------------------------------------------------------------------- 1 | # dict recurse template tag for django 2 | # from http://djangosnippets.org/snippets/1974/ 3 | 4 | from django import template 5 | 6 | register = template.Library() 7 | 8 | 9 | class RecurseDictNode(template.Node): 10 | def __init__(self, var, nodeList): 11 | self.var = var 12 | self.nodeList = nodeList 13 | 14 | def __repr__(self): 15 | return '' 16 | 17 | def renderCallback(self, context, vals, level): 18 | if len(vals) == 0: 19 | return '' 20 | 21 | output = [] 22 | 23 | if 'loop' in self.nodeList: 24 | output.append(self.nodeList['loop'].render(context)) 25 | 26 | for k, v in vals: 27 | context.push() 28 | 29 | context['level'] = level 30 | context['key'] = k 31 | 32 | if 'value' in self.nodeList: 33 | output.append(self.nodeList['value'].render(context)) 34 | 35 | if type(v) == list or type(v) == tuple: 36 | child_items = [ (None, x) for x in v ] 37 | output.append(self.renderCallback(context, child_items, level + 1)) 38 | else: 39 | try: 40 | child_items = v.items() 41 | output.append(self.renderCallback(context, child_items, level + 1)) 42 | except: 43 | output.append(unicode(v)) 44 | 45 | if 'endloop' in self.nodeList: 46 | output.append(self.nodeList['endloop'].render(context)) 47 | else: 48 | output.append(self.nodeList['endrecursedict'].render(context)) 49 | 50 | context.pop() 51 | 52 | if 'endloop' in self.nodeList: 53 | output.append(self.nodeList['endrecursedict'].render(context)) 54 | 55 | return ''.join(output) 56 | 57 | def render(self, context): 58 | vals = self.var.resolve(context).items() 59 | output = self.renderCallback(context, vals, 1) 60 | return output 61 | 62 | 63 | def recursedict_tag(parser, token): 64 | bits = list(token.split_contents()) 65 | if len(bits) != 2 and bits[0] != 'recursedict': 66 | raise template.TemplateSyntaxError("Invalid tag syntax expected '{% recursedict [dictVar] %}'") 67 | 68 | var = parser.compile_filter(bits[1]) 69 | nodeList = {} 70 | while len(nodeList) < 4: 71 | temp = parser.parse(('value','loop','endloop','endrecursedict')) 72 | tag = parser.tokens[0].contents 73 | nodeList[tag] = temp 74 | parser.delete_first_token() 75 | if tag == 'endrecursedict': 76 | break 77 | 78 | return RecurseDictNode(var, nodeList) 79 | 80 | recursedict_tag = register.tag('recursedict', recursedict_tag) 81 | -------------------------------------------------------------------------------- /django_dag/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpaso/django-dag/374c8da38c99dc737ec6fa4b4a4aa8032196b405/django_dag/tests/__init__.py -------------------------------------------------------------------------------- /django_dag/tests/models.py: -------------------------------------------------------------------------------- 1 | 2 | from django.db.models import CharField 3 | from django_dag.models import node_factory, edge_factory 4 | 5 | 6 | class ConcreteNode(node_factory('ConcreteEdge')): 7 | """ 8 | Test node, adds just one field 9 | """ 10 | name = CharField(max_length=32) 11 | 12 | def __str__(self): 13 | return '# %s' % self.name 14 | 15 | class Meta: 16 | app_label = 'django_dag' 17 | 18 | 19 | class ConcreteEdge(edge_factory('ConcreteNode', concrete=False)): 20 | """ 21 | Test edge, adds just one field 22 | """ 23 | name = CharField(max_length=32, blank=True, null=True) 24 | 25 | class Meta: 26 | app_label = 'django_dag' 27 | 28 | 29 | -------------------------------------------------------------------------------- /django_dag/tests/test.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | 3 | from django.test import TestCase 4 | from django.shortcuts import render_to_response 5 | from django.core.exceptions import ValidationError 6 | from django_dag.tree_test_output import expected_tree_output 7 | from .models import ConcreteNode, ConcreteEdge 8 | 9 | 10 | 11 | class DagTestCase(TestCase): 12 | 13 | def setUp(self): 14 | for i in range(1, 11): 15 | ConcreteNode(name="%s" % i).save() 16 | 17 | def test_01_objects_were_created(self): 18 | for i in range(1, 11): 19 | self.assertEqual(ConcreteNode.objects.get(name="%s" % i).name, "%s" % i) 20 | 21 | def test_02_dag(self): 22 | # Get nodes 23 | for i in range(1, 11): 24 | globals()["p%s" % i] = ConcreteNode.objects.get(pk=i) 25 | 26 | # Creates a DAG 27 | p1.add_child(p5) 28 | p5.add_child(p7) 29 | 30 | tree = p1.descendants_tree() 31 | # {: {: {}}} 32 | self.assertIn(p5, tree) 33 | self.assertEqual(len(tree), 1) 34 | self.assertIn(p7, tree[p5]) 35 | self.assertEqual(tree[p5][p7], {}) 36 | 37 | l = [p.pk for p in p1.descendants_set()] 38 | l.sort() 39 | self.assertEqual(l, [5, 7]) 40 | 41 | p1.add_child(p6) 42 | p2.add_child(p6) 43 | p3.add_child(p7) 44 | p6.add_child(p7) 45 | p6.add_child(p8) 46 | l = [p.pk for p in p2.descendants_set()] 47 | l.sort() 48 | self.assertEqual(l, [6, 7, 8]) 49 | 50 | # ValidationError: [u'The object is a descendant.'] 51 | # self.assertRaises(ValidationError, p2.add_child, p8) 52 | 53 | try: 54 | p2.add_child(p8) 55 | except ValidationError as e: 56 | self.assertEqual(e.message, 'The object is a descendant.') 57 | 58 | # Checks that p8 was not added two times 59 | l = [p.pk for p in p2.descendants_set()] 60 | l.sort() 61 | self.assertEqual(l, [6, 7, 8]) 62 | 63 | p6.add_parent(p4) 64 | p9.add_parent(p3) 65 | p9.add_parent(p6) 66 | self.assertRaises(ValidationError, p9.add_child, p2) 67 | try: 68 | p9.add_child(p2) 69 | except ValidationError as e: 70 | self.assertEqual(e.message, 'The object is an ancestor.') 71 | 72 | tree = p1.descendants_tree() 73 | self.assertIn(p5, tree) 74 | self.assertIn(p6, tree) 75 | self.assertIn(p7, tree[p5]) 76 | self.assertIn(p7, tree[p6]) 77 | self.assertIn(p8, tree[p6]) 78 | self.assertIn(p9, tree[p6]) 79 | self.assertEqual(len(tree), 2) 80 | self.assertEqual(len(tree[p5]), 1) 81 | self.assertEqual(len(tree[p6]), 3) 82 | 83 | l = [p.pk for p in p1.descendants_set()] 84 | l.sort() 85 | self.assertEqual(l, [5, 6, 7, 8, 9]) 86 | self.assertEqual(p1.distance(p8), 2) 87 | 88 | # Test additional fields for edge 89 | p9.add_child(p10, name='test_name') 90 | self.assertEqual(p9.children.through.objects.filter(child=p10)[0].name, u'test_name') 91 | 92 | self.assertEqual([p.name for p in p1.path(p7)], ['6', '7']) 93 | self.assertEqual([p.name for p in p1.path(p10)], ['6', '9', '10']) 94 | self.assertEqual(p1.distance(p7), 2) 95 | 96 | self.assertEqual([p.name for p in p1.get_leaves()], ['8', '10', '7']) 97 | self.assertEqual([p.name for p in p8.get_roots()], ['1', '2', '4']) 98 | 99 | self.assertTrue(p1.is_root()) 100 | self.assertFalse(p1.is_leaf()) 101 | self.assertFalse(p10.is_root()) 102 | self.assertTrue(p10.is_leaf()) 103 | self.assertFalse(p6.is_leaf()) 104 | self.assertFalse(p6.is_root()) 105 | 106 | self.assertRaises(ValidationError, p6.add_child, p6) 107 | try: 108 | p6.add_child(p6) 109 | except ValidationError as e: 110 | self.assertEqual(e.message, 'Self links are not allowed.') 111 | 112 | # Remove a link and test island 113 | p10.remove_parent(p9) 114 | self.assertFalse(p10 in p9.descendants_set()) 115 | self.assertTrue(p10.is_island()) 116 | 117 | self.assertEqual([p.name for p in p6.ancestors_set()], ['1', '2', '4']) 118 | 119 | p1.remove_child(p6) 120 | self.assertEqual([p.name for p in p6.ancestors_set()], ['2', '4']) 121 | 122 | self.assertFalse(p1 in p6.ancestors_set()) 123 | 124 | # Testing the view 125 | response = render_to_response('tree.html', { 'dag_list': ConcreteNode.objects.all()}) 126 | self.assertEqual(response.content.decode('utf-8'), expected_tree_output) 127 | 128 | def test_03_deep_dag(self): 129 | """ 130 | Create a deep graph and check that graph operations run in a 131 | reasonable amount of time (linear in size of graph, not 132 | exponential). 133 | """ 134 | def run_test(): 135 | # There are on the order of 1 million paths through the graph, so 136 | # results for intermediate nodes need to be cached 137 | n = 20 138 | 139 | for i in range(2*n): 140 | ConcreteNode(pk=i).save() 141 | 142 | # Create edges 143 | for i in range(0, 2*n - 2, 2): 144 | p1 = ConcreteNode.objects.get(pk=i) 145 | p2 = ConcreteNode.objects.get(pk=i+1) 146 | p3 = ConcreteNode.objects.get(pk=i+2) 147 | p4 = ConcreteNode.objects.get(pk=i+3) 148 | 149 | p1.add_child(p3) 150 | p1.add_child(p4) 151 | p2.add_child(p3) 152 | p2.add_child(p4) 153 | 154 | # Compute descendants of a root node 155 | ConcreteNode.objects.get(pk=0).descendants_set() 156 | 157 | # Compute ancestors of a leaf node 158 | ConcreteNode.objects.get(pk=2*n - 1).ancestors_set() 159 | 160 | ConcreteNode.objects.get(pk=0).add_child(ConcreteNode.objects.get(pk=2*n - 1)) 161 | 162 | # Run the test, raising an error if the code times out 163 | p = multiprocessing.Process(target=run_test) 164 | p.start() 165 | p.join(10) 166 | if p.is_alive(): 167 | p.terminate() 168 | p.join() 169 | raise RuntimeError('Graph operations take too long!') 170 | -------------------------------------------------------------------------------- /django_dag/tree_test_output.py: -------------------------------------------------------------------------------- 1 | expected_tree_output = '''# 1 2 | Descendants: 3 | # 5 4 | # 7 5 | 6 | 7 | 8 | # 2 9 | Descendants: 10 | # 8 11 | 12 | # 6 13 | # 8 14 | 15 | # 9 16 | 17 | # 7 18 | 19 | 20 | 21 | # 3 22 | Descendants: 23 | # 9 24 | 25 | # 7 26 | 27 | 28 | # 4 29 | Descendants: 30 | # 6 31 | # 8 32 | 33 | # 9 34 | 35 | # 7 36 | 37 | 38 | 39 | # 5 40 | 41 | # 6 42 | 43 | # 7 44 | 45 | Ancestors: 46 | # 3 47 | 48 | # 5 49 | # 1 50 | 51 | 52 | # 6 53 | # 2 54 | 55 | # 4 56 | 57 | 58 | # 8 59 | 60 | Ancestors: 61 | # 2 62 | 63 | # 6 64 | # 2 65 | 66 | # 4 67 | 68 | 69 | # 9 70 | 71 | Ancestors: 72 | # 3 73 | 74 | # 6 75 | # 2 76 | 77 | # 4 78 | 79 | 80 | # 10 81 | 82 | 83 | ''' 84 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for dag project. 2 | import os 3 | 4 | DEBUG = True 5 | 6 | ADMINS = ( 7 | # ('Your Name', 'your_email@domain.com'), 8 | ) 9 | 10 | MANAGERS = ADMINS 11 | 12 | DATABASES = { 13 | 'default': { 14 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. 15 | 'NAME': 'sqlite.db', # Or path to database file if using sqlite3. 16 | 'USER': '', # Not used with sqlite3. 17 | 'PASSWORD': '', # Not used with sqlite3. 18 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 19 | 'PORT': '', # Set to empty string for default. Not used with sqlite3. 20 | } 21 | } 22 | 23 | # Local time zone for this installation. Choices can be found here: 24 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 25 | # although not all choices may be available on all operating systems. 26 | # On Unix systems, a value of None will cause Django to use the same 27 | # timezone as the operating system. 28 | # If running in a Windows environment this must be set to the same as your 29 | # system time zone. 30 | TIME_ZONE = 'America/Chicago' 31 | 32 | # Language code for this installation. All choices can be found here: 33 | # http://www.i18nguy.com/unicode/language-identifiers.html 34 | LANGUAGE_CODE = 'en-us' 35 | 36 | SITE_ID = 1 37 | 38 | # If you set this to False, Django will make some optimizations so as not 39 | # to load the internationalization machinery. 40 | USE_I18N = True 41 | 42 | # If you set this to False, Django will not format dates, numbers and 43 | # calendars according to the current locale 44 | USE_L10N = True 45 | 46 | # Absolute filesystem path to the directory that will hold user-uploaded files. 47 | # Example: "/home/media/media.lawrence.com/" 48 | MEDIA_ROOT = '' 49 | 50 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 51 | # trailing slash if there is a path component (optional in other cases). 52 | # Examples: "http://media.lawrence.com", "http://example.com/media/" 53 | MEDIA_URL = '' 54 | 55 | # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a 56 | # trailing slash. 57 | # Examples: "http://foo.com/media/", "/media/". 58 | ADMIN_MEDIA_PREFIX = '/media/' 59 | 60 | # Make this unique, and don't share it with anybody. 61 | SECRET_KEY = 'okwy(ibut6w&9xuhaqi4k(wq5odg5t(_l@$ni&3!9@k%4n1n28' 62 | 63 | # List of callables that know how to import templates from various sources. 64 | 65 | MIDDLEWARE_CLASSES = ( 66 | 'django.middleware.common.CommonMiddleware', 67 | 'django.contrib.sessions.middleware.SessionMiddleware', 68 | 'django.middleware.csrf.CsrfViewMiddleware', 69 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 70 | 'django.contrib.messages.middleware.MessageMiddleware', 71 | ) 72 | 73 | ROOT_URLCONF = 'urls' 74 | TEMPLATES = [ 75 | { 76 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 77 | 'DIRS': [], 78 | 'APP_DIRS': True, 79 | 'OPTIONS': { 80 | # ... some options here ... 81 | }, 82 | }, 83 | ] 84 | INSTALLED_APPS = ( 85 | #'django.contrib.auth', 86 | #'django.contrib.contenttypes', 87 | #'django.contrib.sessions', 88 | #'django.contrib.sites', 89 | #'django.contrib.messages', 90 | # Uncomment the next line to enable the admin: 91 | # 'django.contrib.admin', 92 | # Uncomment the next line to enable admin documentation: 93 | # 'django.contrib.admindocs', 94 | 95 | 'django_dag', 96 | 'django_dag.tests', 97 | ) 98 | TEST_RUNNER = 'django.test.runner.DiscoverRunner' 99 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | from distutils.core import setup 5 | 6 | version = '1.4.1' 7 | 8 | classifiers = [ 9 | "Development Status :: 4 - Beta", 10 | "Intended Audience :: Developers", 11 | "License :: OSI Approved :: GNU Affero General Public License v3", 12 | "Programming Language :: Python", 13 | "Operating System :: OS Independent", 14 | "Topic :: Software Development :: Libraries", 15 | "Topic :: Utilities", 16 | "Environment :: Web Environment", 17 | "Framework :: Django", 18 | ] 19 | 20 | root_dir = os.path.dirname(__file__) 21 | if not root_dir: 22 | root_dir = '.' 23 | long_desc = open(root_dir + '/README').read() 24 | 25 | setup( 26 | name='django-dag', 27 | version=version, 28 | url='https://github.com/elpaso/django-dag', 29 | author='Alessandro Pasotti', 30 | author_email='apasotti@gmail.com', 31 | license='GNU Affero General Public License v3', 32 | packages=['django_dag'], 33 | package_dir={'django_dag': 'django_dag'}, 34 | #package_data={'dag': ['templates/admin/*.html']}, 35 | description='Directed Acyclic Graph implementation for Django 1.6+', 36 | classifiers=classifiers, 37 | long_description=long_desc, 38 | ) 39 | -------------------------------------------------------------------------------- /urls.py: -------------------------------------------------------------------------------- 1 | urlpatterns =[] 2 | --------------------------------------------------------------------------------