1 # -*- coding: utf-8 -*-
4 # Copyright (c) 2009-2010, Bolloré telecom
7 # See AUTHORS file for a full list of contributors.
9 # Redistribution and use in source and binary forms, with or without modification,
10 # are permitted provided that the following conditions are met:
12 # 1. Redistributions of source code must retain the above copyright notice,
13 # this list of conditions and the following disclaimer.
15 # 2. Redistributions in binary form must reproduce the above copyright
16 # notice, this list of conditions and the following disclaimer in the
17 # documentation and/or other materials provided with the distribution.
19 # 3. Neither the name of Bolloré telecom nor the names of its contributors
20 # may be used to endorse or promote products derived from this software
21 # without specific prior written permission.
23 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
24 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
25 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
26 # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
27 # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
28 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
29 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
30 # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
31 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
32 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
38 import django.db.models
39 from django.db import connections, router
40 from django.db.models import signals
43 from ldapdb.models.query import QuerySet
45 class ModelBase(django.db.models.base.ModelBase):
47 Metaclass for all LDAP models.
49 def __new__(cls, name, bases, attrs):
50 super_new = super(ModelBase, cls).__new__
51 new_class = super_new(cls, name, bases, attrs)
53 # patch manager to use our own QuerySet class
54 if not new_class._meta.abstract:
56 return QuerySet(new_class)
57 new_class.objects.get_query_set = get_query_set
58 new_class._default_manager.get_query_set = get_query_set
62 class Model(django.db.models.base.Model):
64 Base class for all LDAP models.
66 __metaclass__ = ModelBase
68 dn = django.db.models.fields.CharField(max_length=200)
72 search_scope = ldap.SCOPE_SUBTREE
73 object_classes = ['top']
75 def __init__(self, *args, **kwargs):
76 super(Model, self).__init__(*args, **kwargs)
77 self.saved_pk = self.pk
79 def _collect_sub_objects(self, seen_objs, parent=None, nullable=False):
81 This private API seems to be called by the admin interface in django 1.2
83 pk_val = self._get_pk_val()
84 seen_objs.add(self.__class__, pk_val, self,
85 type(parent), parent, nullable)
89 Build the Relative Distinguished Name for this entry.
92 for field in self._meta.fields:
93 if field.db_column and field.primary_key:
94 bits.append("%s=%s" % (field.db_column, getattr(self, field.name)))
96 raise Exception("Could not build Distinguished Name")
101 Build the Distinguished Name for this entry.
103 return "%s,%s" % (self.build_rdn(), self.base_dn)
104 raise Exception("Could not build Distinguished Name")
106 def delete(self, using=None):
110 using = using or router.db_for_write(self.__class__, instance=self)
111 connection = connections[using]
112 logging.debug("Deleting LDAP entry %s" % self.dn)
113 connection.delete_s(self.dn)
114 signals.post_delete.send(sender=self.__class__, instance=self)
116 def save(self, using=None):
118 Saves the current instance.
120 using = using or router.db_for_write(self.__class__, instance=self)
121 connection = connections[using]
124 record_exists = False
125 entry = [('objectClass', self.object_classes)]
126 new_dn = self.build_dn()
128 for field in self._meta.fields:
129 if not field.db_column:
131 value = getattr(self, field.name)
133 entry.append((field.db_column, field.get_db_prep_save(value, connection=connection)))
135 logging.debug("Creating new LDAP entry %s" % new_dn)
136 connection.add_s(new_dn, entry)
142 # update an existing entry
145 orig = self.__class__.objects.get(pk=self.saved_pk)
146 for field in self._meta.fields:
147 if not field.db_column:
149 old_value = getattr(orig, field.name, None)
150 new_value = getattr(self, field.name, None)
151 if old_value != new_value:
153 modlist.append((ldap.MOD_REPLACE, field.db_column, field.get_db_prep_save(new_value, connection=connection)))
155 modlist.append((ldap.MOD_DELETE, field.db_column, None))
159 new_dn = self.build_dn()
160 if new_dn != self.dn:
161 logging.debug("Renaming LDAP entry %s to %s" % (self.dn, new_dn))
162 connection.rename_s(self.dn, self.build_rdn())
165 logging.debug("Modifying existing LDAP entry %s" % self.dn)
166 connection.modify_s(self.dn, modlist)
168 logging.debug("No changes to be saved to LDAP entry %s" % self.dn)
171 self.saved_pk = self.pk
172 signals.post_save.send(sender=self.__class__, instance=self, created=(not record_exists))
175 def scoped(base_class, base_dn):
177 Returns a copy of the current class with a different base_dn.
181 suffix = re.sub('[=,]', '_', base_dn)
182 name = "%s_%s" % (base_class.__name__, str(suffix))
183 new_class = new.classobj(name, (base_class,), {'base_dn': base_dn, '__module__': base_class.__module__})