Completed
Push — master ( 88a74d...531db1 )
by Andrea
01:12
created

Federation.compute_new_stats()   B

Complexity

Conditions 3

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 25
rs 8.8571
cc 3
1
#################################################################
2
# MET v2 Metadate Explorer Tool
3
#
4
# This Software is Open Source. See License: https://github.com/TERENA/met/blob/master/LICENSE.md
5
# Copyright (c) 2012, TERENA All rights reserved.
6
#
7
# This Software is based on MET v1 developed for TERENA by Yaco Sistemas, http://www.yaco.es/
8
# MET v2 was developed for TERENA by Tamim Ziai, DAASI International GmbH, http://www.daasi.de
9
# Current version of MET has been revised for performance improvements by Andrea Biancini,
10
# Consortium GARR, http://www.garr.it
11
#########################################################################################
12
13
import sys
14
import requests
15
import simplejson as json
16
17
from os import path
18
from urlparse import urlsplit, urlparse
19
from urllib import quote_plus
20
from datetime import datetime
21
22
from django.conf import settings
23
from django.contrib import messages
24
from django.contrib.auth.models import User
25
from django.core import validators
26
from django.core.cache import cache
27
from django.core.files.base import ContentFile
28
from django.core.urlresolvers import reverse
29
from django.db import models, transaction
30
from django.db.models import Count
31
from django.db.models.signals import pre_save
32
from django.db.models.query import QuerySet
33
from django.dispatch import receiver
34
from django.template.defaultfilters import slugify
35
from django.utils.safestring import mark_safe
36
from django.utils.translation import ugettext_lazy as _
37
from django.utils import timezone
38
39
from lxml import etree
40
41
from pyff.mdrepo import MDRepository
42
from pyff.pipes import Plumbing
43
44
from met.metadataparser.utils import compare_filecontents
45
from met.metadataparser.xmlparser import MetadataParser, DESCRIPTOR_TYPES_DISPLAY
46
from met.metadataparser.templatetags import attributemap
47
48
49
TOP_LENGTH = getattr(settings, "TOP_LENGTH", 5)
50
stats = getattr(settings, "STATS")
51
52
FEDERATION_TYPES = (
53
    (None, ''),
54
    ('hub-and-spoke', 'Hub and Spoke'),
55
    ('mesh', 'Full Mesh'),
56
)
57
58
59
def update_obj(mobj, obj, attrs=None):
60
    for_attrs = attrs or getattr(mobj, 'all_attrs', [])
61
    for attrb in attrs or for_attrs:
62
        if (getattr(mobj, attrb, None) and
63
            getattr(obj, attrb, None) and
64
            getattr(mobj, attrb) != getattr(obj, attrb)):
65
            setattr(obj, attrb,  getattr(mobj, attrb))
66
67
class JSONField(models.CharField):
68
    """JSONField is a generic textfield that neatly serializes/unserializes
69
    JSON objects seamlessly
70
71
    The json spec claims you must use a collection type at the top level of
72
    the data structure.  However the simplesjon decoder and Firefox both encode
73
    and decode non collection types that do not exist inside a collection.
74
    The to_python method relies on the value being an instance of basestring
75
    to ensure that it is encoded.  If a string is the sole value at the
76
    point the field is instanced, to_python attempts to decode the sting because
77
    it is derived from basestring but cannot be encodeded and throws the
78
    exception ValueError: No JSON object could be decoded.
79
    """
80
81
    # Used so to_python() is called
82
    __metaclass__ = models.SubfieldBase
83
    description = _("JSON object")
84
85
    def __init__(self, *args, **kwargs):
86
        super(JSONField, self).__init__(*args, **kwargs)
87
        self.validators.append(validators.MaxLengthValidator(self.max_length))
88
89
    @classmethod
90
    def get_internal_type(cls):
91
        return "TextField"
92
93
    @classmethod
94
    def to_python(cls, value):
95
        """Convert our string value to JSON after we load it from the DB"""
96
        if value == "":
97
            return None
98
99
        try:
100
            if isinstance(value, basestring):
101
                return json.loads(value)
102
        except ValueError:
103
            return value
104
105
        return value
106
107
    def get_prep_value(self, value):
108
        """Convert our JSON object to a string before we save"""
109
110
        if not value or value == "":
111
            return None
112
113
        db_value = json.dumps(value)
114
        return super(JSONField, self).get_prep_value(db_value)
115
116
    def get_db_prep_value(self, value, connection, prepared=False):
117
        """Convert our JSON object to a string before we save"""
118
119
        if not value or value == "":
120
            return None
121
122
        db_value = json.dumps(value)
123
        return super(JSONField, self).get_db_prep_value(db_value, connection, prepared)
124
125
126
class Base(models.Model):
127
    file_url = models.CharField(verbose_name='Metadata url',
128
                                max_length=1000,
129
                                blank=True, null=True,
130
                                help_text=_(u'Url to fetch metadata file'))
131
    file = models.FileField(upload_to='metadata', blank=True, null=True,
132
                            verbose_name=_(u'metadata xml file'),
133
                            help_text=_("if url is set, metadata url will be "
134
                                        "fetched and replace file value"))
135
    file_id = models.CharField(blank=True, null=True, max_length=500,
136
                               verbose_name=_(u'File ID'))
137
138
    registration_authority = models.CharField(verbose_name=_('Registration Authority'),
139
                                              max_length=200, blank=True, null=True)
140
141
    editor_users = models.ManyToManyField(User, null=True, blank=True,
142
                                          verbose_name=_('editor users'))
143
144
    class Meta:
145
        abstract = True
146
147
    class XmlError(Exception):
148
        pass
149
150
    def __unicode__(self):
151
        return self.url or u"Metadata %s" % self.id
152
153
    def load_file(self):
154
        if not hasattr(self, '_loaded_file'):
155
            """Only load file and parse it, don't create/update any objects"""
156
            if not self.file:
157
                return None
158
            self._loaded_file = MetadataParser(filename=self.file.path)
159
        return self._loaded_file
160
161
    def _get_metadata_stream(self, load_streams):
162
        try:
163
            load = []
164
            select = []
165
166
            count = 1
167
            for stream in load_streams:
168
                curid = "%s%d" % (self.slug, count)
169
                load.append("%s as %s" % (stream[0], curid))
170
                if stream[1] == 'SP' or stream[1] == 'IDP':
171
                    select.append("%s!//md:EntityDescriptor[md:%sSSODescriptor]" % (curid, stream[1]))
172
                else:
173
                    select.append("%s" % curid)
174
                count = count + 1
175
176
            if len(select) > 0:
177
                pipeline = [{'load': load}, {'select': select}]
178
            else:
179
                pipeline = [{'load': load}, 'select']
180
181
            md = MDRepository()
182
            entities = Plumbing(pipeline=pipeline, id=self.slug).process(md, state={'batch': True, 'stats': {}})
183
            return etree.tostring(entities)
184
        except Exception, e:
185
            raise Exception('Getting metadata from %s failed.\nError: %s' % (load_streams, e))
186
187
    def fetch_metadata_file(self, file_name):
188
        file_url = self.file_url
189
        if not file_url or file_url == '':
190
            return
191
192
        metadata_files = []
193
        files = file_url.split("|")
194
        for curfile in files:
195
            cursource = curfile.split(";")
196
            if len(cursource) == 1:
197
                cursource.append("All")
198
            metadata_files.append(cursource)
199
200
        req = self._get_metadata_stream(metadata_files)
201
202
        try:
203
            self.file.seek(0)
204
            original_file_content = self.file.read()
205
            if compare_filecontents(original_file_content, req):
206
                return False
207
        except:
208
            pass
209
210
        filename = path.basename("%s-metadata.xml" % file_name)
211
        self.file.delete(save=False)
212
        self.file.save(filename, ContentFile(req), save=False)
213
        return True
214
215
    @classmethod
216
    def process_metadata(cls):
217
        raise NotImplementedError()
218
219
220
class XmlDescriptionError(Exception):
221
    pass
222
223
224
class Federation(Base):
225
226
    name = models.CharField(blank=False, null=False, max_length=200,
227
                            unique=True, verbose_name=_(u'Name'))
228
229
    type = models.CharField(blank=True, null=True, max_length=100,
230
                            unique=False, verbose_name=_(u'Type'), choices=FEDERATION_TYPES)
231
232
    url = models.URLField(verbose_name='Federation url',
233
                          blank=True, null=True)
234
    
235
    fee_schedule_url = models.URLField(verbose_name='Fee schedule url',
236
                                       max_length=150, blank=True, null=True)
237
238
    logo = models.ImageField(upload_to='federation_logo', blank=True,
239
                             null=True, verbose_name=_(u'Federation logo'))
240
    is_interfederation = models.BooleanField(default=False, db_index=True,
241
                                         verbose_name=_(u'Is interfederation'))
242
    slug = models.SlugField(max_length=200, unique=True)
243
244
    country = models.CharField(blank=True, null=True, max_length=100,
245
                               unique=False, verbose_name=_(u'Country'))
246
247
    metadata_update = models.DateField(blank=True, null=True,
248
                                       unique=False, verbose_name=_(u'Metadata update date'))
249
250
    @property
251
    def _metadata(self):
252
        if not hasattr(self, '_metadata_cache'):
253
            self._metadata_cache = self.load_file()
254
        return self._metadata_cache
255
256
    def __unicode__(self):
257
        return self.name
258
259
    def get_entity_metadata(self, entityid):
260
        return self._metadata.get_entity(entityid)
261
262
    def get_entity(self, entityid):
263
        return self.entity_set.get(entityid=entityid)
264
265
    def process_metadata(self):
266
        metadata = self.load_file()
267
268
        if self.file_id and metadata.file_id and metadata.file_id == self.file_id:
269
            return
270
        else:
271
            self.file_id = metadata.file_id
272
273
        if not metadata:
274
            return
275
        if not metadata.is_federation:
276
            raise XmlDescriptionError("XML Haven't federation form")
277
278
        update_obj(metadata.get_federation(), self)
279
280
    def _remove_deleted_entities(self, entities_from_xml, request):
281
        entities_to_remove = []
282
        for entity in self.entity_set.all():
283
            """Remove entity relation if does not exist in metadata"""
284
            if not entity.entityid in entities_from_xml:
285
                entities_to_remove.append(entity)
286
287
        if len(entities_to_remove) > 0:
288
            self.entity_set.remove(*entities_to_remove)
289
290
            if request and not entity.federations.exists():
291
                for entity in entities_to_remove:
292
                    messages.warning(request,
293
                                     mark_safe(_("Orphan entity: <a href='%s'>%s</a>" %
294
                                     (entity.get_absolute_url(), entity.entityid))))
295
296
        return len(entities_to_remove)
297
298
    def _update_entities(self, entities_to_update, entities_to_add):
299
        for e in entities_to_update:
300
            e.save()
301
302
        self.entity_set.add(*entities_to_add)
303
304
    @staticmethod
305
    def _entity_has_changed(entity, entityid, name, registration_authority):
306
        if entity.entityid != entityid:
307
            return True
308
        if entity.name != name:
309
            return True
310
        if entity.registration_authority != registration_authority:
311
            return True
312
313
        return False
314
315
    def _add_new_entities(self, entities, entities_from_xml, request, federation_slug):
316
        db_entity_types = EntityType.objects.all()
317
        cached_entity_types = { entity_type.xmlname: entity_type for entity_type in db_entity_types }
318
319
        entities_to_add = []
320
        entities_to_update = []
321
322
        for m_id in entities_from_xml:
323
            if request and federation_slug:
324
                request.session['%s_cur_entities' % federation_slug] += 1
325
                request.session.save()
326
327
            created = False
328
            if m_id in entities:
329
                entity = entities[m_id]
330
            else:
331
                entity, created = Entity.objects.get_or_create(entityid=m_id)
332
333
            entityid = entity.entityid
334
            name = entity.name
335
            registration_authority = entity.registration_authority
336
 
337
            entity_from_xml = self._metadata.get_entity(m_id, True)
338
            entity.process_metadata(False, entity_from_xml, cached_entity_types)
339
340
            if created or self._entity_has_changed(entity, entityid, name, registration_authority):
341
                entities_to_update.append(entity)
342
343
            entities_to_add.append(entity)
344
345
        self._update_entities(entities_to_update, entities_to_add)
346
        return len(entities_to_update) 
347
348
    def compute_new_stats(self, timestamp=timezone.now()):
349
        entities_from_xml = self._metadata.get_entities()
350
351
        entities = Entity.objects.filter(entityid__in=entities_from_xml)
352
        entities = entities.prefetch_related('types')
353
354
        computed = {}
355
        not_computed = []
356
        entity_stats = []
357
        for feature in stats['features'].keys():
358
            fun = getattr(self, 'get_%s' % feature, None)
359
360
            if callable(fun):
361
                stat = EntityStat()
362
                stat.feature = feature
363
                stat.time = timestamp
364
                stat.federation = self
365
                stat.value = fun(entities, stats['features'][feature])
366
                entity_stats.append(stat)
367
                computed[feature] = stat.value
368
            else:
369
                not_computed.append(feature)
370
371
        EntityStat.objects.bulk_create(entity_stats)
372
        return (computed, not_computed)
373
374
    def process_metadata_entities(self, request=None, federation_slug=None, timestamp=timezone.now()):
375
        entities_from_xml = self._metadata.get_entities()
376
        removed = self._remove_deleted_entities(entities_from_xml, request)
377
378
        entities = {}
379
        db_entities = Entity.objects.filter(entityid__in=entities_from_xml)
380
        db_entities = db_entities.prefetch_related('types')
381
382
        for entity in db_entities.all():
383
            entities[entity.entityid] = entity
384
385
        if request and federation_slug:
386
            request.session['%s_num_entities' % federation_slug] = len(entities_from_xml)
387
            request.session['%s_cur_entities' % federation_slug] = 0
388
            request.session['%s_process_done' % federation_slug] = False
389
            request.session.save()
390
391
        updated = self._add_new_entities(entities, entities_from_xml, request, federation_slug)
392
393
        if request and federation_slug:
394
            request.session['%s_process_done' % federation_slug] = True
395
            request.session.save()
396
397
        return removed, updated
398
399
    def get_absolute_url(self):
400
        return reverse('federation_view', args=[self.slug])
401
402
    @classmethod
403
    def get_sp(cls, entities, xml_name):
404
        count = 0
405
        for entity in entities:
406
            cur_cached_types = [t.xmlname for t in entity.types.all()]
407
            if xml_name in cur_cached_types:
408
                count += 1
409
        return count
410
411
    @classmethod
412
    def get_idp(cls, entities, xml_name):
413
        count = 0
414
        for entity in entities:
415
            cur_cached_types = [t.xmlname for t in entity.types.all()]
416
            if xml_name in cur_cached_types:
417
                count += 1
418
        return count
419
420
    def get_sp_saml1(self, entities, xml_name):
421
        return self.get_stat_protocol(entities, xml_name, 'SPSSODescriptor')
422
423
    def get_sp_saml2(self, entities, xml_name):
424
        return self.get_stat_protocol(entities, xml_name, 'SPSSODescriptor')
425
426
    def get_sp_shib1(self, entities, xml_name):
427
        return self.get_stat_protocol(entities, xml_name, 'SPSSODescriptor')
428
429
    def get_idp_saml1(self, entities, xml_name):
430
        return self.get_stat_protocol(entities, xml_name, 'IDPSSODescriptor')
431
432
    def get_idp_saml2(self, entities, xml_name):
433
        return self.get_stat_protocol(entities, xml_name, 'IDPSSODescriptor')
434
435
    def get_idp_shib1(self, entities, xml_name):
436
        return self.get_stat_protocol(entities, xml_name, 'IDPSSODescriptor')
437
438
    def get_stat_protocol(self, entities, xml_name, service_type):
439
        count = 0
440
        for entity in entities:
441
            try:
442
                cur_cached_types = [t.xmlname for t in entity.types.all()]
443
                if service_type in cur_cached_types and Entity.READABLE_PROTOCOLS[xml_name] in entity.display_protocols:
444
                    count += 1
445
            except Exception, e:
446
                pass
447
        return count
448
449
    def can_edit(self, user, delete):
450
        if user.is_superuser:
451
            return True
452
453
        permission = 'delete_federation' if delete else 'change_federation'
454
        if user.has_perm('metadataparser.%s' % permission) and user in self.editor_users.all():
455
            return True
456
        return False
457
458
459
class EntityQuerySet(QuerySet):
460
    def iterator(self):
461
        cached_federations = {}
462
        for entity in super(EntityQuerySet, self).iterator():
463
            if entity.file:
464
                continue
465
466
            federations = entity.federations.all()
467
            if federations:
468
                federation = federations[0]
469
            else:
470
                raise ValueError("Can't find entity metadata")
471
472
            for federation in federations:
473
                if not federation.id in cached_federations:
474
                    cached_federations[federation.id] = federation
475
476
                cached_federation = cached_federations[federation.id]
477
                try:
478
                    entity.load_metadata(federation=cached_federation)
479
                except ValueError:
480
                    # Allow entity in federation but not in federation file
481
                    continue
482
                else:
483
                    break
484
485
            yield entity
486
487
488
class EntityManager(models.Manager):
489
    def get_queryset(self):
490
        return EntityQuerySet(self.model, using=self._db)
491
492
493
class EntityType(models.Model):
494
    name = models.CharField(blank=False, max_length=20, unique=True,
495
                            verbose_name=_(u'Name'), db_index=True)
496
    xmlname = models.CharField(blank=False, max_length=20, unique=True,
497
                            verbose_name=_(u'Name in XML'), db_index=True)
498
499
    def __unicode__(self):
500
        return self.name
501
502
503
class Entity(Base):
504
    READABLE_PROTOCOLS = {
505
        'urn:oasis:names:tc:SAML:1.1:protocol': 'SAML 1.1',
506
        'urn:oasis:names:tc:SAML:2.0:protocol': 'SAML 2.0',
507
        'urn:mace:shibboleth:1.0': 'Shiboleth 1.0',
508
    }
509
510
    entityid = models.CharField(blank=False, max_length=200, unique=True,
511
                                verbose_name=_(u'EntityID'), db_index=True)
512
    federations = models.ManyToManyField(Federation,
513
                                         verbose_name=_(u'Federations'))
514
515
    types = models.ManyToManyField(EntityType, verbose_name=_(u'Type'))
516
517
    name = JSONField(blank=True, null=True, max_length=2000,
518
                     verbose_name=_(u'Display Name'))
519
520
    objects = models.Manager()
521
    longlist = EntityManager()
522
523
    curfed = None
524
525
    @property
526
    def registration_authority_xml(self):
527
        return self._get_property('registration_authority')
528
529
    @property
530
    def registration_policy(self):
531
        return self._get_property('registration_policy')
532
533
    @property
534
    def registration_instant(self):
535
        reginstant = self._get_property('registration_instant')
536
        if reginstant is None:
537
            return None
538
        reginstant = "%sZ" % reginstant[0:19]
539
        return datetime.strptime(reginstant, '%Y-%m-%dT%H:%M:%SZ')
540
541
    @property
542
    def protocols(self):
543
        return ' '.join(self._get_property('protocols'))
544
545
    @property
546
    def languages(self):
547
        return ' '.join(self._get_property('languages'))
548
549
    @property
550
    def scopes(self):
551
        return ' '.join(self._get_property('scopes'))
552
553
    @property
554
    def attributes(self):
555
        attributes = self._get_property('attr_requested')
556
        if not attributes:
557
            return []
558
        return attributes['required']
559
560
    @property
561
    def attributes_optional(self):
562
        attributes = self._get_property('attr_requested')
563
        if not attributes:
564
            return []
565
        return attributes['optional']
566
567
    @property
568
    def organization(self):
569
        organization = self._get_property('organization')
570
        if not organization:
571
            return []
572
573
        vals = []
574
        for lang, data in organization.items():
575
            data['lang'] = lang
576
            vals.append(data)
577
578
        return vals
579
580
    @property
581
    def display_name(self):
582
        return self._get_property('displayName')
583
584
    @property
585
    def federations_count(self):
586
        return str(self.federations.all().count())
587
        
588
    @property
589
    def description(self):
590
        return self._get_property('description')
591
592
    @property
593
    def info_url(self):
594
        return self._get_property('infoUrl')
595
596
    @property
597
    def privacy_url(self):
598
        return self._get_property('privacyUrl')
599
600
    @property
601
    def xml(self):
602
         return self._get_property('xml')
603
604
    @property
605
    def xml_types(self):
606
         return self._get_property('entity_types')
607
608
    @property
609
    def display_protocols(self):
610
        protocols = []
611
612
        xml_protocols = self._get_property('protocols')
613
        if xml_protocols:
614
            for proto in xml_protocols:
615
                protocols.append(self.READABLE_PROTOCOLS.get(proto, proto))
616
617
        return protocols
618
619
    def display_attributes(self):
620
        attributes = {}
621
        for [attr, friendly] in self.attributes:
622
            if friendly:
623
                attributes[attr] = friendly
624
            elif attr in attributemap.MAP['fro']:
625
                attributes[attr] = attributemap.MAP['fro'][attr]
626
            else:
627
                attributes[attr] = '?'
628
        return attributes
629
630
    def display_attributes_optional(self):
631
        attributes = {}
632
        for [attr, friendly] in self.attributes_optional:
633
            if friendly:
634
                attributes[attr] = friendly
635
            elif attr in attributemap.MAP['fro']:
636
                attributes[attr] = attributemap.MAP['fro'][attr]
637
            else:
638
                attributes[attr] = '?'
639
        return attributes
640
641
    @property
642
    def contacts(self):
643
        contacts = []
644
        for cur_contact in self._get_property('contacts'):
645
            if cur_contact['name'] and cur_contact['surname']:
646
                contact_name = '%s %s' % (cur_contact['name'], cur_contact['surname'])
647
            elif cur_contact['name']:
648
                contact_name = cur_contact['name']
649
            elif cur_contact['surname']:
650
                contact_name = cur_contact['surname']
651
            else:
652
                contact_name = urlparse(cur_contact['email']).path.partition('?')[0]
653
            c_type = 'undefined'
654
            if cur_contact['type']:
655
                c_type = cur_contact['type']
656
            contacts.append({ 'name': contact_name, 'email': cur_contact['email'], 'type': c_type })
657
        return contacts
658
659
    @property
660
    def logos(self):
661
        logos = []
662
        for cur_logo in self._get_property('logos'):
663
            cur_logo['external'] = True
664
            logos.append(cur_logo)
665
666
        return logos
667
668
    class Meta:
669
        verbose_name = _(u'Entity')
670
        verbose_name_plural = _(u'Entities')
671
672
    def __unicode__(self):
673
        return self.entityid
674
675
    def load_metadata(self, federation=None, entity_data=None):
676
        if hasattr(self, '_entity_cached'):
677
            return
678
679
        if self.file:
680
            self._entity_cached = self.load_file().get_entity(self.entityid)
681
        elif federation:
682
            self._entity_cached = federation.get_entity_metadata(self.entityid)
683
        elif entity_data:
684
            self._entity_cached = entity_data
685
        else:
686
            right_fed = None
687
            first_fed = None
688
            for fed in self.federations.all():
689
                if fed.registration_authority == self.registration_authority:
690
                    right_fed = fed
691
                if first_fed is None:
692
                    first_fed = fed
693
694
            if right_fed is not None:
695
                entity_cached = right_fed.get_entity_metadata(self.entityid)
696
                self._entity_cached = entity_cached
697
            else:
698
                entity_cached = first_fed.get_entity_metadata(self.entityid)
699
                self._entity_cached = entity_cached
700
701
        if not hasattr(self, '_entity_cached'):
702
            raise ValueError("Can't find entity metadata")
703
704
    def _get_property(self, prop, federation=None):
705
        try:
706
            self.load_metadata(federation or self.curfed)
707
        except ValueError:
708
            return None
709
710
        if hasattr(self, '_entity_cached'):
711
            return self._entity_cached.get(prop, None)
712
        else:
713
            raise ValueError("Not metadata loaded")
714
715
    def _get_or_create_etypes(self, cached_entity_types):
716
        entity_types = []
717
        cur_cached_types = [t.xmlname for t in self.types.all()]
718
        for etype in self.xml_types:
719
            if etype in cur_cached_types:
720
               break
721
722
            if cached_entity_types is None:
723
                entity_type, created = EntityType.objects.get_or_create(xmlname=etype,
724
                                                                        name=DESCRIPTOR_TYPES_DISPLAY[etype])
725
            else:
726
                if etype in cached_entity_types:
727
                    entity_type = cached_entity_types[etype]
728
                else:
729
                    entity_type = EntityType.objects.create(xmlname=etype,
730
                                                                name=DESCRIPTOR_TYPES_DISPLAY[etype])
731
            entity_types.append(entity_type)
732
        return entity_types
733
734
    def process_metadata(self, auto_save=True, entity_data=None, cached_entity_types=None):
735
        if not entity_data:
736
            self.load_metadata()
737
738
        if self.entityid.lower() != entity_data.get('entityid').lower():
739
            raise ValueError("EntityID is not the same: %s != %s" % (self.entityid.lower(), entity_data.get('entityid').lower()))
740
741
        self._entity_cached = entity_data
742
        if self.xml_types:
743
            entity_types = self._get_or_create_etypes(cached_entity_types)
744
745
            if len(entity_types) > 0:
746
                self.types.add(*entity_types)
747
748
            newname = self._get_property('displayName')
749
            if newname and newname != '':
750
                self.name = newname
751
752
            if str(self._get_property('registration_authority')) != '':
753
                self.registration_authority = self._get_property('registration_authority')
754
755
            if auto_save:
756
                self.save()
757
758
    def to_dict(self):
759
        self.load_metadata()
760
761
        entity = self._entity_cached.copy()
762
        entity["types"] = [unicode(f) for f in self.types.all()]
763
        entity["federations"] = [{u"name": unicode(f), u"url": f.get_absolute_url()}
764
                                  for f in self.federations.all()]
765
766
        if self.registration_authority:
767
            entity["registration_authority"] = self.registration_authority
768
        if self.registration_instant:
769
            entity["registration_instant"] = '%s' % self.registration_instant
770
771
        if "file_id" in entity.keys():
772
            del entity["file_id"]
773
        if "entity_types" in entity.keys():
774
            del entity["entity_types"]
775
776
        return entity
777
778
    def display_etype(value, separator=', '):
779
            return separator.join([unicode(item) for item in value.all()])
780
781
    @classmethod
782
    def get_most_federated_entities(self, maxlength=TOP_LENGTH, cache_expire=None):
783
        entities = None
784
        if cache_expire:
785
            entities = cache.get("most_federated_entities")
786
787
        if not entities or len(entities) < maxlength:
788
            # Entities with count how many federations belongs to, and sorted by most first
789
            ob_entities = Entity.objects.all().annotate(federationslength=Count("federations")).order_by("-federationslength")
790
            ob_entities = ob_entities.prefetch_related('types', 'federations')
791
            ob_entities = ob_entities[:maxlength]
792
793
            entities = []
794
            for entity in ob_entities:
795
                entities.append({
796
                    'entityid': entity.entityid,
797
                    'name': entity.name,
798
                    'absolute_url': entity.get_absolute_url(),
799
                    'types': [unicode(item) for item in entity.types.all()],
800
                    'federations': [(unicode(item.name), item.get_absolute_url()) for item in entity.federations.all()],
801
                })
802
803
        if cache_expire:
804
            cache.set("most_federated_entities", entities, cache_expire)
805
806
        return entities[:maxlength]
807
808
    def get_absolute_url(self):
809
        return reverse('entity_view', args=[quote_plus(self.entityid)])
810
811
    def can_edit(self, user, delete):
812
        permission = 'delete_entity' if delete else 'change_entity'
813
        if user.is_superuser or (user.has_perm('metadataparser.%s' % permission) and user in self.editor_users.all()):
814
            return True
815
816
        for federation in self.federations.all():
817
            if federation.can_edit(user, False):
818
                return True
819
820
        return False
821
822
class EntityStat(models.Model):
823
    time = models.DateTimeField(blank=False, null=False, 
824
                           verbose_name=_(u'Metadata time stamp'))
825
    feature = models.CharField(max_length=100, blank=False, null=False, db_index=True,
826
                           verbose_name=_(u'Feature name'))
827
828
    value = models.PositiveIntegerField(max_length=100, blank=False, null=False,
829
                           verbose_name=_(u'Feature value'))
830
831
    federation = models.ForeignKey(Federation, blank = False,
832
                                         verbose_name=_(u'Federations'))
833
834
    def __unicode__(self):
835
        return self.feature
836
837
838
class Dummy(models.Model):
839
    pass
840
841
842
@receiver(pre_save, sender=Federation, dispatch_uid='federation_pre_save')
843
def federation_pre_save(sender, instance, **kwargs):
844
    # Skip pre_save if only file name is saved 
845
    if kwargs.has_key('update_fields') and kwargs['update_fields'] == set(['file']):
846
        return
847
848
    slug = slugify(unicode(instance.name))[:200]
849
    if instance.file_url and instance.file_url != '':
850
        instance.fetch_metadata_file(slug)
851
    if instance.name:
852
        instance.slug = slugify(unicode(instance))[:200]
853
854
855
@receiver(pre_save, sender=Entity, dispatch_uid='entity_pre_save')
856
def entity_pre_save(sender, instance, **kwargs):
857
    if instance.file_url:
858
        slug = slugify(unicode(instance.name))[:200]
859
        instance.fetch_metadata_file(slug)
860
        instance.process_metadata()
861