mark base model as abstract
[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 import connections, router
40 from django.db.models import signals
41
42 import ldapdb
43
44 class Model(django.db.models.base.Model):
45     """
46     Base class for all LDAP models.
47     """
48     dn = django.db.models.fields.CharField(max_length=200)
49
50     # meta-data
51     base_dn = None
52     search_scope = ldap.SCOPE_SUBTREE
53     object_classes = ['top']
54
55     def __init__(self, *args, **kwargs):
56         super(Model, self).__init__(*args, **kwargs)
57         self.saved_pk = self.pk
58
59     def _collect_sub_objects(self, seen_objs, parent=None, nullable=False):
60         """
61         This private API seems to be called by the admin interface in django 1.2
62         """
63         pk_val = self._get_pk_val()
64         seen_objs.add(self.__class__, pk_val, self,
65                       type(parent), parent, nullable)
66
67     def build_rdn(self):
68         """
69         Build the Relative Distinguished Name for this entry.
70         """
71         bits = []
72         for field in self._meta.fields:
73             if field.db_column and field.primary_key:
74                 bits.append("%s=%s" % (field.db_column, getattr(self, field.name)))
75         if not len(bits):
76             raise Exception("Could not build Distinguished Name")
77         return '+'.join(bits)
78
79     def build_dn(self):
80         """
81         Build the Distinguished Name for this entry.
82         """
83         return "%s,%s" % (self.build_rdn(), self.base_dn)
84         raise Exception("Could not build Distinguished Name")
85
86     def delete(self, using=None):
87         """
88         Delete this entry.
89         """
90         using = using or router.db_for_write(self.__class__, instance=self)
91         connection = connections[using]
92         logging.debug("Deleting LDAP entry %s" % self.dn)
93         connection.delete_s(self.dn)
94         signals.post_delete.send(sender=self.__class__, instance=self)
95
96     def save(self, using=None):
97         """
98         Saves the current instance.
99         """
100         using = using or router.db_for_write(self.__class__, instance=self)
101         connection = connections[using]
102         if not self.dn:
103             # create a new entry
104             record_exists = False 
105             entry = [('objectClass', self.object_classes)]
106             new_dn = self.build_dn()
107
108             for field in self._meta.fields:
109                 if not field.db_column:
110                     continue
111                 value = getattr(self, field.name)
112                 if value:
113                     entry.append((field.db_column, field.get_db_prep_save(value, connection=connection)))
114
115             logging.debug("Creating new LDAP entry %s" % new_dn)
116             connection.add_s(new_dn, entry)
117
118             # update object
119             self.dn = new_dn
120
121         else:
122             # update an existing entry
123             record_exists = True
124             modlist = []
125             orig = self.__class__.objects.get(pk=self.saved_pk)
126             for field in self._meta.fields:
127                 if not field.db_column:
128                     continue
129                 old_value = getattr(orig, field.name, None)
130                 new_value = getattr(self, field.name, None)
131                 if old_value != new_value:
132                     if new_value:
133                         modlist.append((ldap.MOD_REPLACE, field.db_column, field.get_db_prep_save(new_value, connection=connection)))
134                     elif old_value:
135                         modlist.append((ldap.MOD_DELETE, field.db_column, None))
136
137             if len(modlist):
138                 # handle renaming
139                 new_dn = self.build_dn()
140                 if new_dn != self.dn:
141                     logging.debug("Renaming LDAP entry %s to %s" % (self.dn, new_dn))
142                     connection.rename_s(self.dn, self.build_rdn())
143                     self.dn = new_dn
144             
145                 logging.debug("Modifying existing LDAP entry %s" % self.dn)
146                 connection.modify_s(self.dn, modlist)
147             else:
148                 logging.debug("No changes to be saved to LDAP entry %s" % self.dn)
149
150         # done
151         self.saved_pk = self.pk
152         signals.post_save.send(sender=self.__class__, instance=self, created=(not record_exists))
153
154     @classmethod
155     def scoped(base_class, base_dn):
156         """
157         Returns a copy of the current class with a different base_dn.
158         """
159         import new
160         import re
161         suffix = re.sub('[=,]', '_', base_dn)
162         name = "%s_%s" % (base_class.__name__, str(suffix))
163         new_class = new.classobj(name, (base_class,), {'base_dn': base_dn, '__module__': base_class.__module__})
164         return new_class
165
166     class Meta:
167         abstract = True