source: trunk/sara_cmt/sara_cmt/django_cli.py @ 14127

Last change on this file since 14127 was 14127, checked in by sil, 12 years ago

Made dependency on south conditional, since it shouldn't be available to all users. See #5

File size: 17.2 KB
Line 
1from django.db.models.fields import FieldDoesNotExist
2from django.db.models.fields.related import ForeignKey, ManyToManyField, \
3    OneToOneField, RelatedField
4
5from types import StringTypes
6
7import sqlite3
8from sqlite3 import IntegrityError
9
10from sara_cmt.logger import Logger
11logger = Logger().getLogger()
12
13from sara_cmt.parser import Parser
14parser = Parser().getParser()
15
16from django.db import models
17
18import tagging
19from tagging.fields import TagField
20from django_extensions.db.fields import CreationDateTimeField, \
21                                        ModificationDateTimeField
22
23import settings
24if not settings.CLIENT_ONLY:
25    # To be able to migrate fields of 3rd party app django-extensions
26    from south.modelsinspector import add_introspection_rules
27    add_introspection_rules([], ["^django_extensions\.db\.fields"])
28
29
30class ModelExtension(models.Model):
31    """
32        The ModelExtension of Django-CLI is meant as a Mixin for a Django
33        Model.
34    """
35    tags = TagField()
36    created_on = CreationDateTimeField()
37    updated_on = ModificationDateTimeField()
38    note = models.TextField(blank=True)
39
40    class Meta:
41        abstract = True
42
43
44#####
45#
46# <STATIC METHODS>
47#
48
49    @staticmethod
50    def display(instance):
51        """
52            Print all values given in list_display of the model's admin
53        """
54        # First get access to the admin
55        admin_class_name = instance._meta.object_name + 'Admin'
56        import sara_cmt.cluster.admin
57        admin_list_display = eval('sara_cmt.cluster.admin.' \
58                                + admin_class_name + '.list_display')
59
60        # Determine longest value-string to display
61        longest_key = 0
62        for val in admin_list_display:
63            if len(val) > longest_key:
64                longest_key = len(val)
65
66        # Print the values
67        print ' .---[  %s  ]---' % instance
68        for key in admin_list_display:
69            if key not in ('__unicode__', '__str__'):
70                print ' : %s : %s' % (key.ljust(longest_key), \
71                                      instance.__getattribute__(key))
72        print " '---\n"
73#
74# </STATIC METHODS>
75#
76#####
77
78    def _required_fields(self):
79        """
80            Checks which fields are required, and returns these fields in a
81            set.
82        """
83        fields = [fld for fld in self._meta.fields if not fld.blank]
84        return fields
85
86    def _required_local_fields(self):
87        """
88            Checks which local fields are required, and returns these fields
89            in a set. A local field can be of any type except ForeignKey and
90            ManyToManyField.
91        """
92        fields = [fld for fld in self._required_fields() if \
93            not isinstance(fld, RelatedField)]
94        return fields
95
96    def _required_refering_fields(self):
97        """
98            Checks which refering fields are required, and returns these
99            fields in a set. A refering field is of the type ForeignKey or
100            ManyToManyField.
101        """
102        fields = [fld for fld in self._required_fields() if \
103            isinstance(fld, RelatedField)]
104        return fields
105
106    def _is_fk(self, field):
107        """
108            Checks if a given field is a ForeignKey.
109            Returns a boolean value.
110        """
111        # First be sure that we check a field
112        if isinstance(field, StringTypes):
113            field = self._meta.get_field(field)
114            logger.warning('Checking for a ForeignKey with the \
115                string-representation of a field')
116        retval = isinstance(field, ForeignKey)
117        return retval
118
119    def _is_m2m(self, field):
120        """
121            Checks if a given field is a ManyToManyField.
122            Returns a boolean value.
123        """
124        # First be sure that we check a field
125        if isinstance(field, StringTypes):
126            field = self._meta.get_field(field)
127            logger.warning('Checking for a ManyToMany with the \
128                string-representation of a field')
129        retval = isinstance(field, ManyToManyField)
130        return retval
131
132    def is_complete(self):
133        """
134            Check if all the required fields has been assigned.
135        """
136        return not self._missing_fields()
137
138    def setattrs_from_dict(self, arg_dict):
139        """
140            Set attributes according to the arguments, given in a dictionary.
141            Each key in the dictionary should match a fieldname in the model.
142        """
143        m2ms = [] # to collect M2Ms (which should be done at last)
144
145        for arg in arg_dict:
146            field = self._meta.get_field(arg)
147            logger.debug("Have to assign %s to attribute '%s' (%s)" \
148                % (arg_dict[arg], arg, field.__class__.__name__))
149
150            # In case of an id-field: Just ignore it.
151            if arg == 'id':
152                logger.error("Better to not set an id-field, so I'll skip \
153                    this one")
154                continue
155
156            # Leave M2Ms for later, because they need an object's id
157            elif type(field) == ManyToManyField:
158                m2ms.append([field, arg_dict[arg]])
159
160            self._setattr(field=arg, value=arg_dict[arg])
161
162        # Save object to give it an id, and make the M2M relations
163        if not parser.values.DRYRUN:
164            try:
165                self.save()
166            except Exception as err:
167                logger.warning('%s: %s. Maybe not enough (unique) data provided?' % (type(err), err[-1] ))
168
169        for m2m in m2ms:
170            self._setm2m(m2m[0], m2m[1])
171
172        if self.is_complete():
173            save_msg = 'Saved %s %s' % (self.__class__.__name__, self)
174            if not parser.values.DRYRUN:
175                try:
176                    self.save()
177                    logger.info(save_msg)
178                except (sqlite3.IntegrityError, ValueError), err: # ??? what if using non-sqlite db? ???
179                    logger.error(err)
180            else:
181                logger.info('[DRYRUN] %s' % save_msg)
182
183        logger.debug('attrs_from_dict(%s) => %s' % (arg_dict, self.__dict__))
184
185    def _missing_fields(self):
186        """
187            Checks if the required fields all have a value assigned to it, to
188            be sure it can be saved to the database.
189            Returns the set of missing editable fields.
190        """
191        required, missing = self._required_fields(), []
192
193        # Isolate all missing fields from required fields.
194        for field in required:
195            try:
196                if not self.__getattribute__(field.name) and field.editable:
197                    # Field hasn't been set
198                    missing.append(field)
199            except:
200                # FK hasn't been set
201                missing.append(field)
202        return missing
203
204    def interactive_completion(self):
205        """
206            Lets the user assign values to the missing fields, by iterating
207            over the missing fields. Iteration will stop when all required
208            fields are given.
209        """
210        # !!! Note: This function should only be called in INTERACTIVE mode !!!
211
212        # ??? TODO: Validation via forms:
213        #     http://docs.djangoproject.com/en/dev/ref/forms/validation/ ???
214
215        missing = self._missing_fields()
216
217        while missing:
218            logger.info('Missing required attributes: %s'
219                % ' '.join([field.name for field in missing]))
220
221            current_field = missing[0]
222            interactive_input = [raw_input(current_field.verbose_name + ': ')]
223            logger.debug('INTERACTIVE INPUT: %s'%interactive_input)
224            try:
225                assert bool(interactive_input), 'Field cannot be left blank'
226                # Input for missing attribute is now stored in var 'input'.
227                #i = self._setattr(current_field, input)
228                #self.save()
229                self._setattr(current_field, interactive_input)
230                missing.remove(current_field)
231            except AssertionError, err:
232                logger.error(err)
233            except sqlite3.IntegrityError, err: # ??? what if using non-sqlite db? ???
234                logger.error('IntegrityError:', err)
235
236            logger.debug('Current values: %s' % self.__dict__)
237            missing = self._missing_fields()
238
239    def _setfk(self, field, value, subfields=None):
240        """
241            Set the FK of the given field to the id of an object with the
242            given value in one of its required fields (minus 'id' and FKs).
243        """
244        to_model = field.rel.to
245        logger.debug("Trying to save '%s' (type:%s) in FK to %s"
246            % (value, type(value), to_model.__name__))
247
248        if isinstance(value, StringTypes):
249            value = [value]
250            logger.debug("Transformed value '%s' in list '%s'"%(value[0], value))
251
252        # determine which fields should be searched for
253        if not subfields:
254            subfields = to_model()._required_local_fields() # exclude FKs
255
256        logger.debug('Searching a %s matching on fields %s'
257            % (to_model.__name__, [f.name for f in subfields]))
258
259        qset = models.query.EmptyQuerySet(model=to_model)
260        # OR-filtering QuerySets
261        # !!! TODO: have to use Q-objects for this !!!
262        # !!! TODO: have to write a Custom Manager for this !!!
263        for subfield in subfields:
264            # !!! TODO: support multiple values[] !!!
265            try:
266                found = to_model.objects.filter(
267                    **{'%s__in' % subfield.name: value})
268                logger.debug('Iteration done, found: %s (%s)'%(found, type(found)))
269                # higher priority on the label-field:
270                if len(found) == 1 and (subfield.name == 'label' or subfield.name == 'name'):
271                    qset = found
272                    break
273                qset |= found
274            except ValueError, e:
275                logger.warning(e)
276        logger.debug('Found the following matching objects: %s' % qset)
277
278        objects = [_object for _object in qset]
279        object_count = len(objects)
280        if object_count is 0:
281            logger.warning('No matching object found; Change your query.')
282        elif object_count is 1:
283            _object = objects[0]
284            logger.debug('Found 1 match: %s' % _object)
285            self.__setattr__(field.attname, _object.id)
286            logger.debug('%s now references to %s' % (field.name, _object))
287        else:
288            # !!! TODO: let the user refine the search !!!
289            logger.warning('To many matching objects; Refine your query.')
290            # Try the match with the highest number of matches, ...
291
292    def _setm2m(self, field, values, subfields=None):
293        """
294            Set a ManyToMany-relation.
295        """
296        to_model = field.rel.to
297        logger.debug("Trying to make M2M-relations to %s based on '%s'" \
298            % (to_model.__name__, values))
299
300        # determine which fields should be searched for
301        if not subfields:
302            #subfields = to_model()._required_fields()
303            subfields = to_model()._required_local_fields() # exclude FKs
304
305        qset = models.query.EmptyQuerySet(model=to_model)
306        # OR-filtering QuerySets
307        # !!! TODO: have to write a Custom Manager for this !!!
308        for subfield in subfields:
309            logger.debug("Searching in field '%s'" % subfield.name)
310            qset |= to_model.objects.filter(
311                **{'%s__in' % subfield.name: values})
312        logger.debug('Found the following matching objects: %s' % qset)
313
314        objects = [_object for _object in qset]
315        object_count = len(objects)
316        if object_count is 0:
317            logger.warning('No matching object found; Change your query.')
318            pass
319        else:
320            for _object in objects:
321                # !!! TODO: make options to add (+=), remove(-=), and set (=)
322                self.__getattribute__(field.name).add(_object)
323
324        pass
325
326
327    def _setattr(self, field, value):
328        """
329            Assign the given value to the attribute belonging to the given
330            field. The field can be given as a field in the model, or as the
331            string matching its name.
332            When given as a string, it may be followed by '__<attr>', to
333            search for already existing entities with the <attr>-field equal
334            to the given value.
335            If the search results to a single match, the id of the matching
336            entity is assigned to the given ForeignKey-field.
337        """
338        # First init the field itself if it's given as a string
339        if isinstance(field, StringTypes):
340            field = self._meta.get_field(field)
341
342        logger.debug("Trying to set attribute '%s' (%s) to %s" \
343            % (field.name, field.__class__.__name__, value.__repr__()))
344
345        if isinstance(field, ForeignKey):
346            self._setfk(field, value)
347        elif isinstance(field, ManyToManyField):
348            logger.debug("""Found an M2M field. Can't assign it as long as the" \
349                object doesn't have an id, so leave this for later""")
350        else:
351            logger.debug('Trying to set attribute of %s'%type(field))
352            for e in value: # iterate through all elements
353                self.__setattr__(field.name, e)
354            if len(value) > 1:
355                # !!! TODO: append values, instead of overwrite !!!
356                # like: for v in value: self.<append>(v)
357                logger.debug('Functionality to append values is still missing')
358
359
360class ObjectManager():
361    """
362        The ObjectManager is responsible for operations on objects in the
363        database.
364        Operations are based on a given Query-object.
365    """
366
367    def __init__(self):
368        logger.debug('Initializing ObjectManager')
369
370    def get_objects(self, query):
371        """
372            Retrieve objects from the database, corresponding to the entity
373            and terms in the given query. The terms are OR-ed by default.
374        """
375        # !!! TODO: Implement AND !!!
376        kwargs = {}
377
378        logger.debug('CHECK query: %s' % query)
379        for attr, val in query['get'].items():
380            try:
381                fld = query['ent']._meta.get_field(attr)
382                logger.debug('CHECK %s: %s' \
383                    % (fld.__class__.__name__, fld.name))
384                if type(fld) in (ForeignKey, ManyToManyField):
385                    # Default field to search in
386                    if 'label' in fld.rel.to().__dict__:
387                        label = 'label'
388                    else:
389                        label = 'name'
390                    # ??? TODO: maybe use %s__str ???
391                    attr = '%s__%s' % (attr, label)
392                kwargs['%s__in' % attr] = val
393            except FieldDoesNotExist, err:
394                logger.error(err)
395
396        objects = query['ent'].objects.filter(**kwargs)
397        return objects.distinct()
398
399    def save_objects(self, qset):
400        """
401            Save all objects of the given QuerySet.
402        """
403        # TODO: implement
404        for _object in qset:
405            try:
406                self._save_object(_object)
407            except:
408                logger.error('Error saving %s %s' \
409                    % (_object.__class__.__name__, _object))
410
411    def display(self, instance):
412        """
413            Print all values
414        """
415        # TODO: implement
416        pass
417
418
419class QueryManager():
420    """
421        The QueryManager has knowledge about building Queries based on
422        arguments that are given on the commandline. Those arguments can be
423        pushed to the QueryManager with push_args(), which on its turn will
424        build a new Query, which can be retrieved with get_query().
425    """
426
427    def __init__(self):
428        logger.debug('Initializing QueryManager')
429        self.query = self.Query()
430
431    class Query(dict):
432        """
433            Query holds a dictionary of the given args.
434        """
435
436        def __init__(self, ent=None):
437            logger.debug('Initializing new Query')
438            if ent:
439                self._new(ent)
440
441        def _new(self, ent=None):
442            self['ent'] = ent
443            self['get'] = {}
444            self['set'] = {}
445            # ??? TODO: maybe implement something like `self['fields'] = {}`
446            #     to narrow the searchspace ???
447
448    def push_args(self, args, entity, keys=['default']):
449        """
450            # args = list of args from cli, like:
451            #     ['get', 'label=fs7', 'label=fs6']
452            # entity = class of given entity, like:
453            #     <class 'sara_cmt.cluster.models.HardwareUnit'>
454            # keys = the key(s) to use (which depends on the given option),
455            # like:
456            #     ['get']
457        """
458        self.query = self.Query(entity)
459
460        key = keys[0]
461
462        for arg in args:
463            #logger.debug("checking arg '%s' of args '%s'"%(arg,args))
464            if arg in keys: # it's a key like 'get', 'set'
465                key = arg
466            else: # it's an assignment like 'label=fs6'
467                attr, val = arg.split('=', 1)
468
469                if attr in self.query[key]:
470                    # this isn't the first time we see this attribute, so
471                    # assign an extra value to it
472                    self.query[key][attr].append(val)
473                else:
474                    # this is the first time we see this attribute
475                    self.query[key][attr] = [val]
476
477        logger.debug("push_args built query '%s'" % self.query)
478
479    def get_query(self):
480        return self.query
Note: See TracBrowser for help on using the repository browser.