allow dynamically adding object classes to types
[matthijs/upstream/django-ldapdb.git] / ldapdb / models / base.py
1 # -*- coding: utf-8 -*-
2
3 # django-ldapdb
4 # Copyright (c) 2009-2011, 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 import connections, router
40 from django.db.models import signals
41
42 import ldapdb
43 import fields
44
45 # Dict containing 
46 object_classes = {}
47
48 class Model(django.db.models.base.Model):
49     """
50     Base class for all LDAP models.
51     """
52     dn = django.db.models.fields.CharField(max_length=200)
53
54     # meta-data
55     base_dn = None
56     search_scope = ldap.SCOPE_SUBTREE
57     object_classes = ['top']
58
59     def __init__(self, *args, **kwargs):
60         super(Model, self).__init__(*args, **kwargs)
61         self.saved_pk = self.pk
62
63     def build_rdn(self):
64         """
65         Build the Relative Distinguished Name for this entry.
66         """
67         bits = []
68         for field in self._meta.fields:
69             if field.db_column and field.primary_key:
70                 bits.append("%s=%s" % (field.db_column, getattr(self, field.name)))
71         if not len(bits):
72             raise Exception("Could not build Distinguished Name")
73         return '+'.join(bits)
74
75     def build_dn(self):
76         """
77         Build the Distinguished Name for this entry.
78         """
79         return "%s,%s" % (self.build_rdn(), self.base_dn)
80         raise Exception("Could not build Distinguished Name")
81
82     def delete(self, using=None):
83         """
84         Delete this entry.
85         """
86         using = using or router.db_for_write(self.__class__, instance=self)
87         connection = connections[using]
88         logging.debug("Deleting LDAP entry %s" % self.dn)
89         connection.delete_s(self.dn)
90         signals.post_delete.send(sender=self.__class__, instance=self)
91
92     def save(self, using=None):
93         """
94         Saves the current instance.
95         """
96         using = using or router.db_for_write(self.__class__, instance=self)
97         connection = connections[using]
98         if not self.dn:
99             # create a new entry
100             record_exists = False 
101             entry = [('objectClass', self.object_classes)]
102             new_dn = self.build_dn()
103
104             for field in self._meta.fields:
105                 if not field.db_column:
106                     continue
107                 value = getattr(self, field.name)
108                 if value:
109                     entry.append((field.db_column, field.get_db_prep_save(value, connection=connection)))
110
111             logging.debug("Creating new LDAP entry %s" % new_dn)
112             connection.add_s(new_dn, entry)
113
114             # update object
115             self.dn = new_dn
116
117         else:
118             # update an existing entry
119             record_exists = True
120             modlist = []
121             modlist.append((ldap.MOD_REPLACE, 'objectClass', [x.encode(connection.charset) for x in self.object_classes]))
122             orig = self.__class__.objects.get(pk=self.saved_pk)
123             for field in self._meta.fields:
124                 if not field.db_column:
125                     continue
126                 old_value = getattr(orig, field.name, None)
127                 new_value = getattr(self, field.name, None)
128                 if old_value != new_value:
129                     if new_value:
130                         modlist.append((ldap.MOD_REPLACE, field.db_column, field.get_db_prep_save(new_value, connection=connection)))
131                     elif old_value:
132                         modlist.append((ldap.MOD_DELETE, field.db_column, None))
133
134             if len(modlist):
135                 # handle renaming
136                 new_dn = self.build_dn()
137                 if new_dn != self.dn:
138                     logging.debug("Renaming LDAP entry %s to %s" % (self.dn, new_dn))
139                     connection.rename_s(self.dn, self.build_rdn())
140                     self.dn = new_dn
141             
142                 logging.debug("Modifying existing LDAP entry %s" % self.dn)
143                 connection.modify_s(self.dn, modlist)
144             else:
145                 logging.debug("No changes to be saved to LDAP entry %s" % self.dn)
146
147         # done
148         self.saved_pk = self.pk
149         signals.post_save.send(sender=self.__class__, instance=self, created=(not record_exists))
150
151     def add_object_class(self, oc):
152         """
153         Add an extra object class to this object. The added objectclass
154         must be defined as a subclass of ObjectClass. This changes the
155         type of this object to add the fields of the new
156         objectclass.
157
158         The objectclass passed can be a string naming the objectclass,
159         or an ObjectClass subclass.
160         """
161
162         # The new class is a subclass of Model
163         bases = [Model] + list(Model.__bases__)
164
165         object_classes = self.__class__.object_classes
166         object_classes.append(oc)
167
168         dict_ = {
169             # Copy the module from Model
170             '__module__': Model.__module__,
171             # Generate some documentation
172             '__doc__': 'Automatically generated class for LDAP objects with objectclasses: ' + (', '.join(object_classes)),
173             # Add a class variable containing the object_classes
174             'object_classes': object_classes,
175             # Copy the base_dn from the old class
176             'base_dn': self.__class__.base_dn,
177         }
178
179         # Add all current fields, but leave out fields that Model
180         # already has (these will be added by Django later, since we
181         # make the new class a subclass of Model).
182         dict_.update([(f.name, f) for f in self._meta.fields if not f in Model._meta.fields])
183
184         # If the passed object class is a string, resolve it to an
185         # ObjectClass object.
186         if (isinstance(oc, basestring)):
187             oc = ObjectClass.object_classes[oc]
188
189         # Copy all fields (e.g., subclasses of Field) from the new
190         # ObjectClass
191         for k,v in oc.__dict__.items():
192             if isinstance(v, django.db.models.fields.Field):
193                 dict_[k] = v
194
195         # Generate a name for the class (gotta have something...)
196         name = '_'.join(object_classes)
197
198         # And finally, generate the type
199         self.__class__ = type(name, tuple(bases), dict_)
200
201     # TODO: remove_object_class
202
203     @classmethod
204     def scoped(base_class, base_dn):
205         """
206         Returns a copy of the current class with a different base_dn.
207         """
208         import new
209         import re
210         suffix = re.sub('[=,]', '_', base_dn)
211         name = "%s_%s" % (base_class.__name__, str(suffix))
212         new_class = new.classobj(name, (base_class,), {'base_dn': base_dn, '__module__': base_class.__module__})
213         return new_class
214
215     class Meta:
216         abstract = True
217
218 class ObjectClass(object):
219     """
220     Superclass for LDAP objectclass descriptors.
221     """
222
223     # Mapping from objectclass name (string) to the ObjectClass subclass
224     # that describes it. Used when loading items from the database or
225     # when adding object classes.
226     #
227     # TODO: Automatically fill this list from a fancy metaclass
228     object_classes = {}
229