| 1 | from django.db import models, connection
|
|---|
| 2 | from django.db.models.query import Q
|
|---|
| 3 | # -*- coding: utf-8 -*-
|
|---|
| 4 |
|
|---|
| 5 | # Implementation of a Modified Preorder Tree Traversal algorithm
|
|---|
| 6 | # Theory taken from http://www.sitepoint.com/print/hierarchical-data-database
|
|---|
| 7 | # More examples at: http://code.djangoproject.com/wiki/ModifiedPreorderTreeTraversal
|
|---|
| 8 | # http://dev.mysql.com/tech-resources/articles/hierarchical-data.html
|
|---|
| 9 |
|
|---|
| 10 | CHOICES = (
|
|---|
| 11 | ('before','Before'),
|
|---|
| 12 | ('after','After'),
|
|---|
| 13 | ('below','Below')
|
|---|
| 14 | )
|
|---|
| 15 |
|
|---|
| 16 | #set table name here fixme, detect table automatically
|
|---|
| 17 | TABLE = 'pages_page'
|
|---|
| 18 |
|
|---|
| 19 | class TreeManager(models.Manager):
|
|---|
| 20 |
|
|---|
| 21 | def get_query_set(self):
|
|---|
| 22 |
|
|---|
| 23 | sql = "SELECT node.id, count(parent.lft) AS level " \
|
|---|
| 24 | "FROM %s AS node, " \
|
|---|
| 25 | "%s AS parent " \
|
|---|
| 26 | "WHERE node.lft BETWEEN parent.lft AND parent.rgt " \
|
|---|
| 27 | "GROUP BY node.lft, node.id " \
|
|---|
| 28 | "ORDER BY node.lft" % (TABLE, TABLE)
|
|---|
| 29 |
|
|---|
| 30 | cursor = connection.cursor()
|
|---|
| 31 | cursor.execute(sql)
|
|---|
| 32 | rows = cursor.fetchall()
|
|---|
| 33 |
|
|---|
| 34 | qc = Q()
|
|---|
| 35 | self.leveldict = {}
|
|---|
| 36 | for i in rows:
|
|---|
| 37 | id,level = i
|
|---|
| 38 | self.leveldict[id] = level
|
|---|
| 39 | qc = qc|Q(id__exact=id)
|
|---|
| 40 |
|
|---|
| 41 | self.model.add_to_class('levels', self.leveldict)
|
|---|
| 42 |
|
|---|
| 43 | return super(TreeManager, self).get_query_set().filter(qc)
|
|---|
| 44 |
|
|---|
| 45 | def all(self): # gets children
|
|---|
| 46 |
|
|---|
| 47 | sql = "SELECT node.id " \
|
|---|
| 48 | "FROM %s AS node, " \
|
|---|
| 49 | "%s AS parent " \
|
|---|
| 50 | "WHERE node.lft BETWEEN 1 AND 5000 " \
|
|---|
| 51 | "AND node.lft BETWEEN parent.lft AND parent.rgt " \
|
|---|
| 52 | "GROUP BY node.lft, node.id " \
|
|---|
| 53 | "HAVING (count(parent.lft)-1) = 0 " \
|
|---|
| 54 | "ORDER BY node.lft" % (TABLE, TABLE)
|
|---|
| 55 |
|
|---|
| 56 | cursor = connection.cursor()
|
|---|
| 57 | cursor.execute(sql)
|
|---|
| 58 | rows = cursor.fetchall()
|
|---|
| 59 |
|
|---|
| 60 | top = []
|
|---|
| 61 | for i in rows:
|
|---|
| 62 | top.append(super(TreeManager, self).get_query_set().get(pk=i[0]))
|
|---|
| 63 |
|
|---|
| 64 | return top
|
|---|
| 65 |
|
|---|
| 66 | class Page(models.Model):
|
|---|
| 67 |
|
|---|
| 68 | label = models.CharField(maxlength=255)
|
|---|
| 69 |
|
|---|
| 70 | # To create categories for a website menu, we won't probably need to record
|
|---|
| 71 | # what user created the node. But if nodes are used for threaded comments
|
|---|
| 72 | # or forums posts, it's a good idea to have.
|
|---|
| 73 | where = models.CharField(maxlength=6,choices=CHOICES,blank=True,null=True)
|
|---|
| 74 | wich = models.ForeignKey("self",blank=True,null=True)
|
|---|
| 75 |
|
|---|
| 76 | lft = models.PositiveIntegerField(blank=True,null=True,db_index = True)
|
|---|
| 77 | rgt = models.PositiveIntegerField(blank=True,null=True)
|
|---|
| 78 |
|
|---|
| 79 | manager = TreeManager()
|
|---|
| 80 |
|
|---|
| 81 | def __repr__(self):
|
|---|
| 82 | # Page.objects.all() should get whole list as tree with level , repeat --- per level
|
|---|
| 83 | # rertun'---' times level + self.label
|
|---|
| 84 | try:
|
|---|
| 85 | level = self.levels[self.id]
|
|---|
| 86 | except:
|
|---|
| 87 | level=1
|
|---|
| 88 |
|
|---|
| 89 | return ('----' * level) + ' ' + self.label
|
|---|
| 90 |
|
|---|
| 91 | def before(self,related_node):
|
|---|
| 92 |
|
|---|
| 93 | # adds node before picked node self.wich
|
|---|
| 94 | cursor = connection.cursor()
|
|---|
| 95 |
|
|---|
| 96 | target_rgt = related_node.rgt - 1
|
|---|
| 97 | target_lft = related_node.lft - 1
|
|---|
| 98 |
|
|---|
| 99 | cursor.execute("UPDATE %s " \
|
|---|
| 100 | "SET rgt = rgt + 2 " \
|
|---|
| 101 | "WHERE rgt > %i " % (self.tbl(), int(target_rgt)))
|
|---|
| 102 |
|
|---|
| 103 | cursor.execute("UPDATE %s " \
|
|---|
| 104 | "SET lft = lft + 2 " \
|
|---|
| 105 | "WHERE lft > %i " % (self.tbl(), int(target_lft)))
|
|---|
| 106 |
|
|---|
| 107 | self.rgt = related_node.rgt
|
|---|
| 108 | self.lft = related_node.lft
|
|---|
| 109 |
|
|---|
| 110 | def after(self,related_node):
|
|---|
| 111 |
|
|---|
| 112 | # adds node after picked node self.wich
|
|---|
| 113 |
|
|---|
| 114 | cursor = connection.cursor()
|
|---|
| 115 |
|
|---|
| 116 | target_rgt = related_node.rgt
|
|---|
| 117 |
|
|---|
| 118 | cursor.execute("UPDATE %s " \
|
|---|
| 119 | "SET rgt = rgt + 2 " \
|
|---|
| 120 | "WHERE rgt > %i " % (self.tbl(), int(target_rgt)))
|
|---|
| 121 |
|
|---|
| 122 | cursor.execute("UPDATE %s " \
|
|---|
| 123 | "SET lft = lft + 2 " \
|
|---|
| 124 | "WHERE lft > %i " % (self.tbl(), int(target_rgt)))
|
|---|
| 125 |
|
|---|
| 126 | self.lft = related_node.lft + 2
|
|---|
| 127 | self.rgt = related_node.rgt + 2
|
|---|
| 128 |
|
|---|
| 129 | def below(self,related_node):
|
|---|
| 130 |
|
|---|
| 131 | # *fixme* should make sure below does not work on nodes already having child nodes
|
|---|
| 132 | # because that does not have a happy ending
|
|---|
| 133 | if related_node.rgt == related_node.lft + 1:
|
|---|
| 134 |
|
|---|
| 135 | cursor = connection.cursor()
|
|---|
| 136 |
|
|---|
| 137 | target_rgt = related_node.rgt - 1
|
|---|
| 138 |
|
|---|
| 139 | cursor.execute("UPDATE %s " \
|
|---|
| 140 | "SET rgt = rgt + 2 " \
|
|---|
| 141 | "WHERE rgt > %i " % (self.tbl(), int(target_rgt)))
|
|---|
| 142 |
|
|---|
| 143 | cursor.execute("UPDATE %s " \
|
|---|
| 144 | "SET lft = lft + 2 " \
|
|---|
| 145 | "WHERE lft > %i " % (self.tbl(), int(target_rgt)))
|
|---|
| 146 |
|
|---|
| 147 | self.rgt = related_node.rgt + 1
|
|---|
| 148 | self.lft = related_node.lft + 1
|
|---|
| 149 |
|
|---|
| 150 | else:
|
|---|
| 151 |
|
|---|
| 152 | sql = "SELECT node.lft, node.rgt " \
|
|---|
| 153 | "FROM %s AS node, " \
|
|---|
| 154 | "%s AS parent " \
|
|---|
| 155 | "WHERE node.lft BETWEEN %s AND %s " \
|
|---|
| 156 | "AND node.lft BETWEEN parent.lft AND parent.rgt " \
|
|---|
| 157 | "GROUP BY node.lft, node.rgt " \
|
|---|
| 158 | "HAVING (count(parent.lft)-1) = 1 " \
|
|---|
| 159 | "ORDER BY node.lft" % (self.tbl(), self.tbl(), related_node.lft, related_node.rgt)
|
|---|
| 160 |
|
|---|
| 161 | cursor = connection.cursor()
|
|---|
| 162 | cursor.execute(sql)
|
|---|
| 163 | rows = cursor.fetchall()
|
|---|
| 164 |
|
|---|
| 165 | related_node.lft, related_node.rgt = rows[0]
|
|---|
| 166 |
|
|---|
| 167 | self.before(related_node)
|
|---|
| 168 |
|
|---|
| 169 | def top(self):
|
|---|
| 170 |
|
|---|
| 171 | # adds node to bottom of top level
|
|---|
| 172 | # If there are zero nodes for this tree so far, we will just
|
|---|
| 173 | # insert with lft = 1 and rgt = 2
|
|---|
| 174 |
|
|---|
| 175 | cursor = connection.cursor()
|
|---|
| 176 | cursor.execute("SELECT MAX(rgt) FROM %s LIMIT 1" % self.tbl() )
|
|---|
| 177 |
|
|---|
| 178 | row = cursor.fetchone()
|
|---|
| 179 |
|
|---|
| 180 | current_max_rgt = row[0]
|
|---|
| 181 |
|
|---|
| 182 | if current_max_rgt is None:
|
|---|
| 183 | self.lft = 1
|
|---|
| 184 | self.rgt = 2
|
|---|
| 185 | # But if there are already pages attached to the tree we will put
|
|---|
| 186 | # this new one at the top level (to the right of the 'tree')
|
|---|
| 187 | else:
|
|---|
| 188 | self.lft = current_max_rgt + 1
|
|---|
| 189 | self.rgt = current_max_rgt + 2
|
|---|
| 190 |
|
|---|
| 191 | def skip(self):
|
|---|
| 192 | u=False
|
|---|
| 193 | # *fixme* reset parentnode to avoid future errors
|
|---|
| 194 |
|
|---|
| 195 | def save(self):
|
|---|
| 196 |
|
|---|
| 197 | where = self.where # where to place node ralative to related_node
|
|---|
| 198 |
|
|---|
| 199 |
|
|---|
| 200 | if self.id:
|
|---|
| 201 | dont_trust_the_form = Page.manager.get(pk=self.id)
|
|---|
| 202 | self.lft = dont_trust_the_form.lft
|
|---|
| 203 | self.rgt = dont_trust_the_form.rgt
|
|---|
| 204 |
|
|---|
| 205 | try: # test for related node
|
|---|
| 206 | related_node = self.wich
|
|---|
| 207 | except:
|
|---|
| 208 | related_node = False
|
|---|
| 209 |
|
|---|
| 210 | if where:
|
|---|
| 211 | where = "skip" # don't do anything if no node is selected
|
|---|
| 212 | else: # add to toplevel if no node is selected
|
|---|
| 213 | where = "top"
|
|---|
| 214 |
|
|---|
| 215 | if self.rgt: # node exists just change content, fix this to allow moving og nodes and child nodes
|
|---|
| 216 | where = "skip"
|
|---|
| 217 |
|
|---|
| 218 | # done testing for now, execute!
|
|---|
| 219 |
|
|---|
| 220 | if where=="before":
|
|---|
| 221 | self.before(related_node)
|
|---|
| 222 |
|
|---|
| 223 | if where=="after":
|
|---|
| 224 | self.after(related_node)
|
|---|
| 225 |
|
|---|
| 226 | if where=="below":
|
|---|
| 227 | self.below(related_node)
|
|---|
| 228 |
|
|---|
| 229 | if where=="skip":
|
|---|
| 230 | self.skip()
|
|---|
| 231 |
|
|---|
| 232 | if where=="top":
|
|---|
| 233 | self.top()
|
|---|
| 234 |
|
|---|
| 235 | self.where = ''
|
|---|
| 236 | self.wich = ''
|
|---|
| 237 |
|
|---|
| 238 | super(Page, self).save()
|
|---|
| 239 |
|
|---|
| 240 | def delete(self):
|
|---|
| 241 | # delete nodes and child nodes
|
|---|
| 242 | if self.rgt and self.lft:
|
|---|
| 243 | width = self.rgt - self.lft + 1
|
|---|
| 244 |
|
|---|
| 245 | del_sql = "DELETE FROM %s WHERE lft BETWEEN %s AND %s" % (self.tbl(), self.lft, self.rgt)
|
|---|
| 246 | upd_sql_1 = "UPDATE %s SET rgt = rgt - %s WHERE rgt > %s" % (self.tbl(), width, self.rgt)
|
|---|
| 247 | upd_sql_2 = "UPDATE %s SET lft = lft - %s WHERE rgt > %s" % (self.tbl(), width, self.rgt)
|
|---|
| 248 |
|
|---|
| 249 | cursor = connection.cursor()
|
|---|
| 250 | cursor.execute(del_sql)
|
|---|
| 251 | cursor.execute(upd_sql_1)
|
|---|
| 252 | cursor.execute(upd_sql_2)
|
|---|
| 253 |
|
|---|
| 254 | super(Page, self).delete()
|
|---|
| 255 |
|
|---|
| 256 | def get_parent(self):
|
|---|
| 257 |
|
|---|
| 258 | parentid = 5000
|
|---|
| 259 | if self.where == 'below':
|
|---|
| 260 | parentid = self.wich.id
|
|---|
| 261 | else:
|
|---|
| 262 | sql = "SELECT parent.id " \
|
|---|
| 263 | "FROM %s AS node, " \
|
|---|
| 264 | "%s AS parent " \
|
|---|
| 265 | "WHERE node.lft BETWEEN parent.lft AND parent.rgt" \
|
|---|
| 266 | "AND node.id = %s " \
|
|---|
| 267 | "ORDER BY node.lft" % (self.tbl(), self.tbl(), self.wich.id)
|
|---|
| 268 |
|
|---|
| 269 | cursor = connection.cursor()
|
|---|
| 270 | cursor.execute(sql)
|
|---|
| 271 |
|
|---|
| 272 | row = cursor.fetchall()
|
|---|
| 273 |
|
|---|
| 274 | parentid=row[0][0]
|
|---|
| 275 |
|
|---|
| 276 | return Page.manager.get(pk=parentid)
|
|---|
| 277 |
|
|---|
| 278 | def get_children(self): # gets children
|
|---|
| 279 |
|
|---|
| 280 | sql = "SELECT node.id " \
|
|---|
| 281 | "FROM %s AS node, " \
|
|---|
| 282 | "%s AS parent " \
|
|---|
| 283 | "WHERE node.lft BETWEEN %s AND %s " \
|
|---|
| 284 | "AND node.lft BETWEEN parent.lft AND parent.rgt " \
|
|---|
| 285 | "GROUP BY node.lft, node.id " \
|
|---|
| 286 | "HAVING (count(parent.lft)-1) = 1 " \
|
|---|
| 287 | "ORDER BY node.lft" % (self.tbl(), self.tbl(), self.lft, self.rgt)
|
|---|
| 288 |
|
|---|
| 289 | cursor = connection.cursor()
|
|---|
| 290 | cursor.execute(sql)
|
|---|
| 291 | rows = cursor.fetchall()
|
|---|
| 292 |
|
|---|
| 293 | children = []
|
|---|
| 294 | for i in rows:
|
|---|
| 295 | children.append(Page.manager.get(pk=i[0]))
|
|---|
| 296 |
|
|---|
| 297 | return children
|
|---|
| 298 |
|
|---|
| 299 |
|
|---|
| 300 |
|
|---|
| 301 | def tbl(self): # finds table name how do i do this in the TreeManager
|
|---|
| 302 |
|
|---|
| 303 | name = self.__class__.__name__.lower()
|
|---|
| 304 | app = __name__.split('.')[1]
|
|---|
| 305 |
|
|---|
| 306 | return app + '_' + name
|
|---|
| 307 |
|
|---|
| 308 | class Meta:
|
|---|
| 309 | ordering = ['lft',]
|
|---|
| 310 |
|
|---|
| 311 | class Admin:
|
|---|
| 312 | list_display = ('__repr__','lft','rgt')
|
|---|
| 313 | fields = (
|
|---|
| 314 | (None, { 'fields' : ('label',) }),
|
|---|
| 315 | ('Position', { 'fields': ('where','wich') }),
|
|---|
| 316 | ('Advanced', { 'fields': ('lft','rgt') , 'classes': 'collapse' }),
|
|---|
| 317 | )
|
|---|
| 318 | manager = TreeManager()
|
|---|