Completed
Push — master ( 3ea300...72debe )
by Andrea
01:09
created

Federation.get_stat_protocol()   B

Complexity

Conditions 7

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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