adjust license headers to reflect BSD license
[matthijs/upstream/django-ldapdb.git] / ldapdb / models / base.py
1 # -*- coding: utf-8 -*-
2
3 # django-ldapdb
4 # Copyright (c) 2009-2010, BollorĂ© telecom
5 # All rights reserved.
6
7 # See AUTHORS file for a full list of contributors.
8
9 # Redistribution and use in source and binary forms, with or without modification,
10 # are permitted provided that the following conditions are met:
11
12 #     1. Redistributions of source code must retain the above copyright notice, 
13 #        this list of conditions and the following disclaimer.
14 #     
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.
18
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.
22
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.
33 #
34
35 import ldap
36 import logging
37
38 import django.db.models
39 from django.db.models import signals
40
41 import ldapdb
42 from ldapdb.models.query import QuerySet
43
44 class ModelBase(django.db.models.base.ModelBase):
45     """
46     Metaclass for all LDAP models.
47     """
48     def __new__(cls, name, bases, attrs):
49         super_new = super(ModelBase, cls).__new__
50         new_class = super_new(cls, name, bases, attrs)
51
52         # patch manager to use our own QuerySet class
53         if not new_class._meta.abstract:
54             def get_query_set():
55                 return QuerySet(new_class)
56             new_class.objects.get_query_set = get_query_set
57             new_class._default_manager.get_query_set = get_query_set
58
59         return new_class
60
61 class Model(django.db.models.base.Model):
62     """
63     Base class for all LDAP models.
64     """
65     __metaclass__ = ModelBase
66
67     dn = django.db.models.fields.CharField(max_length=200)
68
69     # meta-data
70     base_dn = None
71     object_classes = ['top']
72
73     def __init__(self, *args, **kwargs):
74         super(Model, self).__init__(*args, **kwargs)
75         self.saved_pk = self.pk
76
77     def _collect_sub_objects(self, collector):
78         """
79         This private API seems to be called by the admin interface in django 1.2
80         """
81         pass
82
83     def build_rdn(self):
84         """
85         Build the Relative Distinguished Name for this entry.
86         """
87         bits = []
88         for field in self._meta.local_fields:
89             if field.primary_key:
90                 bits.append("%s=%s" % (field.db_column, getattr(self, field.name)))
91         if not len(bits):
92             raise Exception("Could not build Distinguished Name")
93         return '+'.join(bits)
94
95     def build_dn(self):
96         """
97         Build the Distinguished Name for this entry.
98         """
99         return "%s,%s" % (self.build_rdn(), self.base_dn)
100         raise Exception("Could not build Distinguished Name")
101
102     def delete(self):
103         """
104         Delete this entry.
105         """
106         logging.debug("Deleting LDAP entry %s" % self.dn)
107         ldapdb.connection.delete_s(self.dn)
108         signals.post_delete.send(sender=self.__class__, instance=self)
109         
110     def save(self):
111         if not self.dn:
112             # create a new entry
113             record_exists = False 
114             entry = [('objectClass', self.object_classes)]
115             new_dn = self.build_dn()
116
117             for field in self._meta.local_fields:
118                 if not field.db_column:
119                     continue
120                 value = getattr(self, field.name)
121                 if value:
122                     entry.append((field.db_column, field.get_db_prep_save(value, connection=ldapdb.connection)))
123
124             logging.debug("Creating new LDAP entry %s" % new_dn)
125             ldapdb.connection.add_s(new_dn, entry)
126
127             # update object
128             self.dn = new_dn
129
130         else:
131             # update an existing entry
132             record_exists = True
133             modlist = []
134             orig = self.__class__.objects.get(pk=self.saved_pk)
135             for field in self._meta.local_fields:
136                 if not field.db_column:
137                     continue
138                 old_value = getattr(orig, field.name, None)
139                 new_value = getattr(self, field.name, None)
140                 if old_value != new_value:
141                     if new_value:
142                         modlist.append((ldap.MOD_REPLACE, field.db_column, field.get_db_prep_save(new_value, connection=ldapdb.connection)))
143                     elif old_value:
144                         modlist.append((ldap.MOD_DELETE, field.db_column, None))
145
146             if len(modlist):
147                 # handle renaming
148                 new_dn = self.build_dn()
149                 if new_dn != self.dn:
150                     logging.debug("Renaming LDAP entry %s to %s" % (self.dn, new_dn))
151                     ldapdb.connection.rename_s(self.dn, self.build_rdn())
152                     self.dn = new_dn
153             
154                 logging.debug("Modifying existing LDAP entry %s" % self.dn)
155                 ldapdb.connection.modify_s(self.dn, modlist)
156             else:
157                 logging.debug("No changes to be saved to LDAP entry %s" % self.dn)
158
159         # done
160         self.saved_pk = self.pk
161         signals.post_save.send(sender=self.__class__, instance=self, created=(not record_exists))
162
163     @classmethod
164     def scoped(base_class, base_dn):
165         """
166         Returns a copy of the current class with a different base_dn.
167         """
168         import new
169         import re
170         suffix = re.sub('[=,]', '_', base_dn)
171         name = "%s_%s" % (base_class.__name__, str(suffix))
172         new_class = new.classobj(name, (base_class,), {'base_dn': base_dn, '__module__': base_class.__module__})
173         return new_class
174