Completed
Push — master ( aba28f...22db48 )
by Andrea
01:05
created

Entity.certificates()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 3
rs 10
cc 1
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):
312
        if entity.entityid != entityid:
313
            return True
314
        if entity.name != name:
315
            return True
316
        if entity.registration_authority != registration_authority:
317
            return True
318
        if entity.certstats != certstats:
319
            return True
320
321
        return False
322
323
    def _add_new_entities(self, entities, entities_from_xml, request, federation_slug):
324
        db_entity_types = EntityType.objects.all()
325
        cached_entity_types = { entity_type.xmlname: entity_type for entity_type in db_entity_types }
326
327
        entities_to_add = []
328
        entities_to_update = []
329
330
        for m_id in entities_from_xml:
331
            if request and federation_slug:
332
                request.session['%s_cur_entities' % federation_slug] += 1
333
                request.session.save()
334
335
            created = False
336
            if m_id in entities:
337
                entity = entities[m_id]
338
            else:
339
                entity, created = Entity.objects.get_or_create(entityid=m_id)
340
341
            entityid = entity.entityid
342
            name = entity.name
343
            registration_authority = entity.registration_authority
344
            certstats = entity.certstats
345
 
346
            entity_from_xml = self._metadata.get_entity(m_id, True)
347
            entity.process_metadata(False, entity_from_xml, cached_entity_types)
348
349
            if created or self._entity_has_changed(entity, entityid, name, registration_authority, certstats):
350
                entities_to_update.append(entity)
351
352
            entities_to_add.append(entity)
353
354
        self._update_entities(entities_to_update, entities_to_add)
355
        return len(entities_to_update) 
356
357
    @staticmethod
358
    def _daterange(start_date, end_date):
359
        for n in range(int ((end_date - start_date).days + 1)):
360
            yield start_date + timedelta(n)
361
362
    def compute_new_stats(self):
363
        entities_from_xml = self._metadata.get_entities()
364
365
        entities = Entity.objects.filter(entityid__in=entities_from_xml)
366
        entities = entities.prefetch_related('types')
367
368
        try:
369
            first_date = EntityStat.objects.filter(federation=self).aggregate(Max('time'))['time__max']
370
            if not first_date:
371
                raise Exception('Not able to find statistical data in the DB.')
372
        except Exception:
373
            first_date = datetime(2010, 1, 1)
374
            first_date = pytz.utc.localize(first_date)
375
      
376
        for curtimestamp in self._daterange(first_date, timezone.now()):
377
            computed = {}
378
            not_computed = []
379
            entity_stats = []
380
            for feature in stats['features'].keys():
381
                fun = getattr(self, 'get_%s' % feature, None)
382
    
383
                if callable(fun):
384
                    stat = EntityStat()
385
                    stat.feature = feature
386
                    stat.time = curtimestamp
387
                    stat.federation = self
388
                    stat.value = fun(entities, stats['features'][feature], curtimestamp)
389
                    entity_stats.append(stat)
390
                    computed[feature] = stat.value
391
                else:
392
                    not_computed.append(feature)
393
394
            from_time = datetime.combine(curtimestamp, time.min) 
395
            if timezone.is_naive(from_time):
396
                from_time = pytz.utc.localize(from_time)
397
            to_time = datetime.combine(curtimestamp, time.max)
398
            if timezone.is_naive(to_time):
399
                to_time = pytz.utc.localize(to_time)
400
401
            EntityStat.objects.filter(federation=self, time__gte=from_time, time__lte=to_time).delete()
402
            EntityStat.objects.bulk_create(entity_stats)
403
404
        return (computed, not_computed)
405
406
    def process_metadata_entities(self, request=None, federation_slug=None):
407
        entities_from_xml = self._metadata.get_entities()
408
        removed = self._remove_deleted_entities(entities_from_xml, request)
409
410
        entities = {}
411
        db_entities = Entity.objects.filter(entityid__in=entities_from_xml)
412
        db_entities = db_entities.prefetch_related('types', 'entity_categories')
413
414
        for entity in db_entities.all():
415
            entities[entity.entityid] = entity
416
417
        if request and federation_slug:
418
            request.session['%s_num_entities' % federation_slug] = len(entities_from_xml)
419
            request.session['%s_cur_entities' % federation_slug] = 0
420
            request.session['%s_process_done' % federation_slug] = False
421
            request.session.save()
422
423
        updated = self._add_new_entities(entities, entities_from_xml, request, federation_slug)
424
425
        if request and federation_slug:
426
            request.session['%s_process_done' % federation_slug] = True
427
            request.session.save()
428
429
        return removed, updated
430
431
    def get_absolute_url(self):
432
        return reverse('federation_view', args=[self.slug])
433
434
    @classmethod
435
    def get_sp(cls, entities, xml_name, ref_date=None):
436
        count = 0
437
        for entity in entities:
438
            reginst = None
439
            if entity.registration_instant:
440
                reginst = pytz.utc.localize(entity.registration_instant)
441
            if not ref_date or (reginst and reginst > ref_date):
442
                continue
443
            cur_cached_types = [t.xmlname for t in entity.types.all()]
444
            if xml_name in cur_cached_types:
445
                count += 1
446
        return count
447
448
    @classmethod
449
    def get_idp(cls, entities, xml_name, ref_date=None):
450
        count = 0
451
        for entity in entities:
452
            reginst = None
453
            if entity.registration_instant:
454
                reginst = pytz.utc.localize(entity.registration_instant)
455
            if not ref_date or (reginst and reginst > ref_date):
456
                continue
457
            cur_cached_types = [t.xmlname for t in entity.types.all()]
458
            if xml_name in cur_cached_types:
459
                count += 1
460
        return count
461
462
    @classmethod
463
    def get_aa(cls, entities, xml_name, ref_date=None):
464
        count = 0
465
        for entity in entities:
466
            reginst = None
467
            if entity.registration_instant:
468
                reginst = pytz.utc.localize(entity.registration_instant)
469
            if not ref_date or (reginst and reginst > ref_date):
470
                continue
471
            cur_cached_types = [t.xmlname for t in entity.types.all()]
472
            if xml_name in cur_cached_types:
473
                count += 1
474
        return count
475
476
    def get_sp_saml1(self, entities, xml_name, ref_date = None):
477
        return self.get_stat_protocol(entities, xml_name, 'SPSSODescriptor', ref_date)
478
479
    def get_sp_saml2(self, entities, xml_name, ref_date = None):
480
        return self.get_stat_protocol(entities, xml_name, 'SPSSODescriptor', ref_date)
481
482
    def get_sp_shib1(self, entities, xml_name, ref_date = None):
483
        return self.get_stat_protocol(entities, xml_name, 'SPSSODescriptor', ref_date)
484
485
    def get_idp_saml1(self, entities, xml_name, ref_date = None):
486
        return self.get_stat_protocol(entities, xml_name, 'IDPSSODescriptor', ref_date)
487
488
    def get_idp_saml2(self, entities, xml_name, ref_date = None):
489
        return self.get_stat_protocol(entities, xml_name, 'IDPSSODescriptor', ref_date)
490
491
    def get_idp_shib1(self, entities, xml_name, ref_date = None):
492
        return self.get_stat_protocol(entities, xml_name, 'IDPSSODescriptor', ref_date)
493
494
    def get_stat_protocol(self, entities, xml_name, service_type, ref_date):
495
        count = 0
496
        for entity in entities:
497
            reginst = None
498
            if entity.registration_instant:
499
                reginst = pytz.utc.localize(entity.registration_instant)
500
            if not ref_date or (reginst and reginst > ref_date):
501
                continue
502
503
            try:
504
                cur_cached_types = [t.xmlname for t in entity.types.all()]
505
                if service_type in cur_cached_types and Entity.READABLE_PROTOCOLS[xml_name] in entity.display_protocols:
506
                    count += 1
507
            except Exception:
508
                pass
509
        return count
510
511
    def can_edit(self, user, delete):
512
        if user.is_superuser:
513
            return True
514
515
        permission = 'delete_federation' if delete else 'change_federation'
516
        if user.has_perm('metadataparser.%s' % permission) and user in self.editor_users.all():
517
            return True
518
        return False
519
520
521
class EntityQuerySet(QuerySet):
522
    def iterator(self):
523
        cached_federations = {}
524
        for entity in super(EntityQuerySet, self).iterator():
525
            if entity.file:
526
                continue
527
528
            federations = entity.federations.all()
529
            if federations:
530
                federation = federations[0]
531
            else:
532
                raise ValueError("Can't find entity metadata")
533
534
            for federation in federations:
535
                if not federation.id in cached_federations:
536
                    cached_federations[federation.id] = federation
537
538
                cached_federation = cached_federations[federation.id]
539
                try:
540
                    entity.load_metadata(federation=cached_federation)
541
                except ValueError:
542
                    # Allow entity in federation but not in federation file
543
                    continue
544
                else:
545
                    break
546
547
            yield entity
548
549
550
class EntityManager(models.Manager):
551
    def get_queryset(self):
552
        return EntityQuerySet(self.model, using=self._db)
553
554
555
class EntityType(models.Model):
556
    name = models.CharField(blank=False, max_length=20, unique=True,
557
                            verbose_name=_(u'Name'), db_index=True)
558
    xmlname = models.CharField(blank=False, max_length=20, unique=True,
559
                            verbose_name=_(u'Name in XML'), db_index=True)
560
561
    def __unicode__(self):
562
        return self.name
563
564
565
class EntityCategory(models.Model):
566
    category_id = models.CharField(verbose_name='Entity category ID',
567
                                max_length=1000,
568
                                blank=False, null=False,
569
                                help_text=_(u'The ID of the entity category'))
570
    name = models.CharField(verbose_name='Entity category name',
571
                                max_length=1000,
572
                                blank=True, null=True,
573
                                help_text=_(u'The name of the entity category'))
574
575
    def __unicode__(self):
576
        return self.name or self.category_id
577
578
579
class Entity(Base):
580
    READABLE_PROTOCOLS = {
581
        'urn:oasis:names:tc:SAML:1.1:protocol': 'SAML 1.1',
582
        'urn:oasis:names:tc:SAML:2.0:protocol': 'SAML 2.0',
583
        'urn:mace:shibboleth:1.0': 'Shiboleth 1.0',
584
    }
585
586
    entityid = models.CharField(blank=False, max_length=200, unique=True,
587
                                verbose_name=_(u'EntityID'), db_index=True)
588
589
    federations = models.ManyToManyField(Federation,
590
                                         verbose_name=_(u'Federations'))
591
592
    types = models.ManyToManyField(EntityType, verbose_name=_(u'Type'))
593
594
    name = JSONField(blank=True, null=True, max_length=2000,
595
                     verbose_name=_(u'Display Name'))
596
597
    certstats = models.CharField(blank=True, null=True, max_length=200,
598
                                 unique=False, verbose_name=_(u'Certificate Stats'))
599
600
    entity_categories = models.ManyToManyField(EntityCategory,
601
                                               verbose_name=_(u'Entity categories'))
602
603
    objects = models.Manager()
604
605
    longlist = EntityManager()
606
607
    curfed = None
608
609
    @property
610
    def certificates(self):
611
        return json.loads(self.certstats)
612
613
    @property
614
    def registration_authority_xml(self):
615
        return self._get_property('registration_authority')
616
617
    @property
618
    def registration_policy(self):
619
        return self._get_property('registration_policy')
620
621
    @property
622
    def registration_instant(self):
623
        reginstant = self._get_property('registration_instant')
624
        if reginstant is None:
625
            return None
626
        reginstant = "%sZ" % reginstant[0:19]
627
        return datetime.strptime(reginstant, '%Y-%m-%dT%H:%M:%SZ')
628
629
    @property
630
    def protocols(self):
631
        return ' '.join(self._get_property('protocols'))
632
633
    @property
634
    def languages(self):
635
        return ' '.join(self._get_property('languages'))
636
637
    @property
638
    def scopes(self):
639
        return ' '.join(self._get_property('scopes'))
640
641
    @property
642
    def attributes(self):
643
        attributes = self._get_property('attr_requested')
644
        if not attributes:
645
            return []
646
        return attributes['required']
647
648
    @property
649
    def attributes_optional(self):
650
        attributes = self._get_property('attr_requested')
651
        if not attributes:
652
            return []
653
        return attributes['optional']
654
655
    @property
656
    def organization(self):
657
        organization = self._get_property('organization')
658
        if not organization:
659
            return []
660
661
        vals = []
662
        for lang, data in organization.items():
663
            data['lang'] = lang
664
            vals.append(data)
665
666
        return vals
667
668
    @property
669
    def display_name(self):
670
        return self._get_property('displayName')
671
672
    @property
673
    def federations_count(self):
674
        return str(self.federations.all().count())
675
        
676
    @property
677
    def description(self):
678
        return self._get_property('description')
679
680
    @property
681
    def info_url(self):
682
        return self._get_property('infoUrl')
683
684
    @property
685
    def privacy_url(self):
686
        return self._get_property('privacyUrl')
687
688
    @property
689
    def xml(self):
690
        return self._get_property('xml')
691
692
    @property
693
    def xml_types(self):
694
        return self._get_property('entity_types')
695
696
    @property
697
    def xml_categories(self):
698
        return self._get_property('entity_categories')
699
700
    @property
701
    def display_protocols(self):
702
        protocols = []
703
704
        xml_protocols = self._get_property('protocols')
705
        if xml_protocols:
706
            for proto in xml_protocols:
707
                protocols.append(self.READABLE_PROTOCOLS.get(proto, proto))
708
709
        return protocols
710
711
    def display_attributes(self):
712
        attributes = {}
713
        for [attr, friendly] in self.attributes:
714
            if friendly:
715
                attributes[attr] = friendly
716
            elif attr in attributemap.MAP['fro']:
717
                attributes[attr] = attributemap.MAP['fro'][attr]
718
            else:
719
                attributes[attr] = '?'
720
        return attributes
721
722
    def display_attributes_optional(self):
723
        attributes = {}
724
        for [attr, friendly] in self.attributes_optional:
725
            if friendly:
726
                attributes[attr] = friendly
727
            elif attr in attributemap.MAP['fro']:
728
                attributes[attr] = attributemap.MAP['fro'][attr]
729
            else:
730
                attributes[attr] = '?'
731
        return attributes
732
733
    @property
734
    def contacts(self):
735
        contacts = []
736
        for cur_contact in self._get_property('contacts'):
737
            if cur_contact['name'] and cur_contact['surname']:
738
                contact_name = '%s %s' % (cur_contact['name'], cur_contact['surname'])
739
            elif cur_contact['name']:
740
                contact_name = cur_contact['name']
741
            elif cur_contact['surname']:
742
                contact_name = cur_contact['surname']
743
            else:
744
                contact_name = urlparse(cur_contact['email']).path.partition('?')[0]
745
            c_type = 'undefined'
746
            if cur_contact['type']:
747
                c_type = cur_contact['type']
748
            contacts.append({ 'name': contact_name, 'email': cur_contact['email'], 'type': c_type })
749
        return contacts
750
751
    @property
752
    def logos(self):
753
        logos = []
754
        for cur_logo in self._get_property('logos'):
755
            cur_logo['external'] = True
756
            logos.append(cur_logo)
757
758
        return logos
759
760
    class Meta(object):
761
        verbose_name = _(u'Entity')
762
        verbose_name_plural = _(u'Entities')
763
764
    def __unicode__(self):
765
        return self.entityid
766
767
    def load_metadata(self, federation=None, entity_data=None):
768
        if hasattr(self, '_entity_cached'):
769
            return
770
771
        if self.file:
772
            self._entity_cached = self.load_file().get_entity(self.entityid)
773
        elif federation:
774
            self._entity_cached = federation.get_entity_metadata(self.entityid)
775
        elif entity_data:
776
            self._entity_cached = entity_data
777
        else:
778
            right_fed = None
779
            first_fed = None
780
            for fed in self.federations.all():
781
                if fed.registration_authority == self.registration_authority:
782
                    right_fed = fed
783
                if first_fed is None:
784
                    first_fed = fed
785
786
            if right_fed is not None:
787
                entity_cached = right_fed.get_entity_metadata(self.entityid)
788
                self._entity_cached = entity_cached
789
            else:
790
                entity_cached = first_fed.get_entity_metadata(self.entityid)
791
                self._entity_cached = entity_cached
792
793
        if not hasattr(self, '_entity_cached'):
794
            raise ValueError("Can't find entity metadata")
795
796
    def _get_property(self, prop, federation=None):
797
        try:
798
            self.load_metadata(federation or self.curfed)
799
        except ValueError:
800
            return None
801
802
        if hasattr(self, '_entity_cached'):
803
            return self._entity_cached.get(prop, None)
804
        else:
805
            raise ValueError("Not metadata loaded")
806
807
    def _get_or_create_etypes(self, cached_entity_types):
808
        entity_types = []
809
        cur_cached_types = [t.xmlname for t in self.types.all()]
810
        for etype in self.xml_types:
811
            if etype in cur_cached_types:
812
               break
813
814
            if cached_entity_types is None:
815
                entity_type, _ = EntityType.objects.get_or_create(xmlname=etype,
816
                                                                  name=DESCRIPTOR_TYPES_DISPLAY[etype])
817
            else:
818
                if etype in cached_entity_types:
819
                    entity_type = cached_entity_types[etype]
820
                else:
821
                    entity_type = EntityType.objects.create(xmlname=etype,
822
                                                            name=DESCRIPTOR_TYPES_DISPLAY[etype])
823
            entity_types.append(entity_type)
824
        return entity_types
825
826
    def _get_or_create_ecategories(self, cached_entity_categories):
827
        entity_categories = []
828
        cur_cached_categories = [t.category_id for t in self.entity_categories.all()]
829
        for ecategory in self.xml_categories:
830
            if ecategory in cur_cached_categories:
831
                break
832
833
            if cached_entity_categories is None:
834
                entity_category, _ = EntityCategory.objects.get_or_create(category_id=ecategory)
835
            else:
836
                if ecategory in cached_entity_categories:
837
                    entity_category = cached_entity_categories[ecategory]
838
                else:
839
                    entity_category = EntityCategory.objects.create(category_id=ecategory)
840
            entity_categories.append(entity_category)
841
        return entity_categories
842
843
    def process_metadata(self, auto_save=True, entity_data=None, cached_entity_types=None):
844
        if not entity_data:
845
            self.load_metadata()
846
847
        if self.entityid.lower() != entity_data.get('entityid').lower():
848
            raise ValueError("EntityID is not the same: %s != %s" % (self.entityid.lower(), entity_data.get('entityid').lower()))
849
850
        self._entity_cached = entity_data
851
852
        if self.xml_types:
853
            entity_types = self._get_or_create_etypes(cached_entity_types)
854
            if len(entity_types) > 0:
855
                self.types.add(*entity_types)
856
857
        if self.xml_categories:
858
            db_entity_categories = EntityCategory.objects.all()
859
            cached_entity_categories = { entity_category.category_id: entity_category for entity_category in db_entity_categories }
860
861
            entity_categories = self._get_or_create_ecategories(cached_entity_categories)
862
            #self.entity_categories.all().delete()
863
            if len(entity_categories) > 0:
864
                self.entity_categories.add(*entity_categories)
865
866
        newname = self._get_property('displayName')
867
        if newname and newname != '':
868
            self.name = newname
869
870
        self.certstats = self._get_property('certstats')
871
872
        if str(self._get_property('registration_authority')) != '':
873
            self.registration_authority = self._get_property('registration_authority')
874
875
        if auto_save:
876
            self.save()
877
878
    def to_dict(self):
879
        self.load_metadata()
880
881
        entity = self._entity_cached.copy()
882
        entity["types"] = [unicode(f) for f in self.types.all()]
883
        entity["federations"] = [{u"name": unicode(f), u"url": f.get_absolute_url()}
884
                                  for f in self.federations.all()]
885
886
        if self.registration_authority:
887
            entity["registration_authority"] = self.registration_authority
888
        if self.registration_instant:
889
            entity["registration_instant"] = '%s' % self.registration_instant
890
891
        if "file_id" in entity.keys():
892
            del entity["file_id"]
893
        if "entity_types" in entity.keys():
894
            del entity["entity_types"]
895
896
        return entity
897
898
    def display_etype(value, separator=', '):
899
            return separator.join([unicode(item) for item in value.all()])
900
901
    @classmethod
902
    def get_most_federated_entities(self, maxlength=TOP_LENGTH, cache_expire=None):
903
        entities = None
904
        if cache_expire:
905
            entities = cache.get("most_federated_entities")
906
907
        if not entities or len(entities) < maxlength:
908
            # Entities with count how many federations belongs to, and sorted by most first
909
            ob_entities = Entity.objects.all().annotate(federationslength=Count("federations")).order_by("-federationslength")
910
            ob_entities = ob_entities.prefetch_related('types', 'federations')
911
            ob_entities = ob_entities[:maxlength]
912
913
            entities = []
914
            for entity in ob_entities:
915
                entities.append({
916
                    'entityid': entity.entityid,
917
                    'name': entity.name,
918
                    'absolute_url': entity.get_absolute_url(),
919
                    'types': [unicode(item) for item in entity.types.all()],
920
                    'federations': [(unicode(item.name), item.get_absolute_url()) for item in entity.federations.all()],
921
                })
922
923
        if cache_expire:
924
            cache.set("most_federated_entities", entities, cache_expire)
925
926
        return entities[:maxlength]
927
928
    def get_absolute_url(self):
929
        return reverse('entity_view', args=[quote_plus(self.entityid.encode('utf-8'))])
930
931
    def can_edit(self, user, delete):
932
        permission = 'delete_entity' if delete else 'change_entity'
933
        if user.is_superuser or (user.has_perm('metadataparser.%s' % permission) and user in self.editor_users.all()):
934
            return True
935
936
        for federation in self.federations.all():
937
            if federation.can_edit(user, False):
938
                return True
939
940
        return False
941
942
943
class EntityStat(models.Model):
944
    time = models.DateTimeField(blank=False, null=False, 
945
                           verbose_name=_(u'Metadata time stamp'))
946
    feature = models.CharField(max_length=100, blank=False, null=False, db_index=True,
947
                           verbose_name=_(u'Feature name'))
948
949
    value = models.PositiveIntegerField(max_length=100, blank=False, null=False,
950
                           verbose_name=_(u'Feature value'))
951
952
    federation = models.ForeignKey(Federation, blank = False,
953
                                         verbose_name=_(u'Federations'))
954
955
    def __unicode__(self):
956
        return self.feature
957
958
959
class Dummy(models.Model):
960
    pass
961
962
963
@receiver(pre_save, sender=Federation, dispatch_uid='federation_pre_save')
964
def federation_pre_save(sender, instance, **kwargs):
965
    # Skip pre_save if only file name is saved 
966
    if kwargs.has_key('update_fields') and kwargs['update_fields'] == set(['file']):
967
        return
968
969
    slug = slugify(unicode(instance.name))[:200]
970
    if instance.file_url and instance.file_url != '':
971
        try:
972
            instance.fetch_metadata_file(slug)
973
        except Exception, e:
974
            pass
975
    if instance.name:
976
        instance.slug = slugify(unicode(instance))[:200]
977
978
979
@receiver(pre_save, sender=Entity, dispatch_uid='entity_pre_save')
980
def entity_pre_save(sender, instance, **kwargs):
981
    if instance.file_url:
982
        slug = slugify(unicode(instance.name))[:200]
983
        instance.fetch_metadata_file(slug)
984
        instance.process_metadata()
985