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

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