Completed
Push — master ( 354484...099b04 )
by Andrea
01:26
created

Federation.get_sp()   B

Complexity

Conditions 6

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 12
rs 8
cc 6
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
        selected = entities.filter(types__xmlname=xml_name)
437
        count = 0
438
        for entity in selected:
439
            reginst = None
440
            if entity.registration_instant:
441
                reginst = pytz.utc.localize(entity.registration_instant)
442
            if not ref_date or (reginst and reginst > ref_date):
443
                continue
444
            count += 1
445
        return count
446
447
    @classmethod
448
    def get_idp(cls, entities, xml_name, ref_date=None):
449
        selected = entities.filter(types__xmlname=xml_name)
450
        count = 0
451
        for entity in selected:
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
            count += 1
458
        return count
459
460
    @classmethod
461
    def get_aa(cls, entities, xml_name, ref_date=None):
462
        selected = entities.filter(types__xmlname=xml_name)
463
        count = 0
464
        for entity in selected:
465
            reginst = None
466
            if entity.registration_instant:
467
                reginst = pytz.utc.localize(entity.registration_instant)
468
            if not ref_date or (reginst and reginst > ref_date):
469
                continue
470
            count += 1
471
        return count
472
473
    def get_sp_saml1(self, entities, xml_name, ref_date = None):
474
        return self.get_stat_protocol(entities, xml_name, 'SPSSODescriptor', ref_date)
475
476
    def get_sp_saml2(self, entities, xml_name, ref_date = None):
477
        return self.get_stat_protocol(entities, xml_name, 'SPSSODescriptor', ref_date)
478
479
    def get_sp_shib1(self, entities, xml_name, ref_date = None):
480
        return self.get_stat_protocol(entities, xml_name, 'SPSSODescriptor', ref_date)
481
482
    def get_idp_saml1(self, entities, xml_name, ref_date = None):
483
        return self.get_stat_protocol(entities, xml_name, 'IDPSSODescriptor', ref_date)
484
485
    def get_idp_saml2(self, entities, xml_name, ref_date = None):
486
        return self.get_stat_protocol(entities, xml_name, 'IDPSSODescriptor', ref_date)
487
488
    def get_idp_shib1(self, entities, xml_name, ref_date = None):
489
        return self.get_stat_protocol(entities, xml_name, 'IDPSSODescriptor', ref_date)
490
491
    def get_stat_protocol(self, entities, xml_name, service_type, ref_date):
492
        selected = entities.filter(types__xmlname=service_type)
493
        count = 0
494
        for entity in selected:
495
            reginst = None
496
            if entity.registration_instant:
497
                reginst = pytz.utc.localize(entity.registration_instant)
498
            if not ref_date or (reginst and reginst > ref_date):
499
                continue
500
501
            try:
502
                if Entity.READABLE_PROTOCOLS[xml_name] in entity.display_protocols:
503
                    count += 1
504
            except Exception:
505
                pass
506
        return count
507
508
    def can_edit(self, user, delete):
509
        if user.is_superuser:
510
            return True
511
512
        permission = 'delete_federation' if delete else 'change_federation'
513
        if user.has_perm('metadataparser.%s' % permission) and user in self.editor_users.all():
514
            return True
515
        return False
516
517
518
class EntityQuerySet(QuerySet):
519
    def iterator(self):
520
        cached_federations = {}
521
        for entity in super(EntityQuerySet, self).iterator():
522
            if entity.file:
523
                continue
524
525
            federations = entity.federations.all()
526
            if federations:
527
                federation = federations[0]
528
            else:
529
                raise ValueError("Can't find entity metadata")
530
531
            for federation in federations:
532
                if not federation.id in cached_federations:
533
                    cached_federations[federation.id] = federation
534
535
                cached_federation = cached_federations[federation.id]
536
                try:
537
                    entity.load_metadata(federation=cached_federation)
538
                except ValueError:
539
                    # Allow entity in federation but not in federation file
540
                    continue
541
                else:
542
                    break
543
544
            yield entity
545
546
547
class EntityManager(models.Manager):
548
    def get_queryset(self):
549
        return EntityQuerySet(self.model, using=self._db)
550
551
552
class EntityType(models.Model):
553
    name = models.CharField(blank=False, max_length=20, unique=True,
554
                            verbose_name=_(u'Name'), db_index=True)
555
    xmlname = models.CharField(blank=False, max_length=20, unique=True,
556
                            verbose_name=_(u'Name in XML'), db_index=True)
557
558
    def __unicode__(self):
559
        return self.name
560
561
562
class EntityCategory(models.Model):
563
    category_id = models.CharField(verbose_name='Entity category ID',
564
                                max_length=1000,
565
                                blank=False, null=False,
566
                                help_text=_(u'The ID of the entity category'))
567
    name = models.CharField(verbose_name='Entity category name',
568
                                max_length=1000,
569
                                blank=True, null=True,
570
                                help_text=_(u'The name of the entity category'))
571
572
    def __unicode__(self):
573
        return self.name or self.category_id
574
575
576
class Entity(Base):
577
    READABLE_PROTOCOLS = {
578
        'urn:oasis:names:tc:SAML:1.1:protocol': 'SAML 1.1',
579
        'urn:oasis:names:tc:SAML:2.0:protocol': 'SAML 2.0',
580
        'urn:mace:shibboleth:1.0': 'Shiboleth 1.0',
581
    }
582
583
    entityid = models.CharField(blank=False, max_length=200, unique=True,
584
                                verbose_name=_(u'EntityID'), db_index=True)
585
586
    federations = models.ManyToManyField(Federation,
587
                                         verbose_name=_(u'Federations'))
588
589
    types = models.ManyToManyField(EntityType, verbose_name=_(u'Type'))
590
591
    name = JSONField(blank=True, null=True, max_length=2000,
592
                     verbose_name=_(u'Display Name'))
593
594
    certstats = models.CharField(blank=True, null=True, max_length=200,
595
                                 unique=False, verbose_name=_(u'Certificate Stats'))
596
597
    entity_categories = models.ManyToManyField(EntityCategory,
598
                                               verbose_name=_(u'Entity categories'))
599
600
    objects = models.Manager()
601
602
    longlist = EntityManager()
603
604
    curfed = None
605
606
    @property
607
    def certificates(self):
608
        return json.loads(self.certstats)
609
610
    @property
611
    def registration_authority_xml(self):
612
        return self._get_property('registration_authority')
613
614
    @property
615
    def registration_policy(self):
616
        return self._get_property('registration_policy')
617
618
    @property
619
    def registration_instant(self):
620
        reginstant = self._get_property('registration_instant')
621
        if reginstant is None:
622
            return None
623
        reginstant = "%sZ" % reginstant[0:19]
624
        return datetime.strptime(reginstant, '%Y-%m-%dT%H:%M:%SZ')
625
626
    @property
627
    def protocols(self):
628
        return ' '.join(self._get_property('protocols'))
629
630
    @property
631
    def languages(self):
632
        return ' '.join(self._get_property('languages'))
633
634
    @property
635
    def scopes(self):
636
        return ' '.join(self._get_property('scopes'))
637
638
    @property
639
    def attributes(self):
640
        attributes = self._get_property('attr_requested')
641
        if not attributes:
642
            return []
643
        return attributes['required']
644
645
    @property
646
    def attributes_optional(self):
647
        attributes = self._get_property('attr_requested')
648
        if not attributes:
649
            return []
650
        return attributes['optional']
651
652
    @property
653
    def organization(self):
654
        organization = self._get_property('organization')
655
        if not organization:
656
            return []
657
658
        vals = []
659
        for lang, data in organization.items():
660
            data['lang'] = lang
661
            vals.append(data)
662
663
        return vals
664
665
    @property
666
    def display_name(self):
667
        return self._get_property('displayName')
668
669
    @property
670
    def federations_count(self):
671
        return str(self.federations.all().count())
672
        
673
    @property
674
    def description(self):
675
        return self._get_property('description')
676
677
    @property
678
    def info_url(self):
679
        return self._get_property('infoUrl')
680
681
    @property
682
    def privacy_url(self):
683
        return self._get_property('privacyUrl')
684
685
    @property
686
    def xml(self):
687
        return self._get_property('xml')
688
689
    @property
690
    def xml_types(self):
691
        return self._get_property('entity_types')
692
693
    @property
694
    def xml_categories(self):
695
        return self._get_property('entity_categories')
696
697
    @property
698
    def display_protocols(self):
699
        protocols = []
700
701
        xml_protocols = self._get_property('protocols')
702
        if xml_protocols:
703
            for proto in xml_protocols:
704
                protocols.append(self.READABLE_PROTOCOLS.get(proto, proto))
705
706
        return protocols
707
708
    def display_attributes(self):
709
        attributes = {}
710
        for [attr, friendly] in self.attributes:
711
            if friendly:
712
                attributes[attr] = friendly
713
            elif attr in attributemap.MAP['fro']:
714
                attributes[attr] = attributemap.MAP['fro'][attr]
715
            else:
716
                attributes[attr] = '?'
717
        return attributes
718
719
    def display_attributes_optional(self):
720
        attributes = {}
721
        for [attr, friendly] in self.attributes_optional:
722
            if friendly:
723
                attributes[attr] = friendly
724
            elif attr in attributemap.MAP['fro']:
725
                attributes[attr] = attributemap.MAP['fro'][attr]
726
            else:
727
                attributes[attr] = '?'
728
        return attributes
729
730
    @property
731
    def contacts(self):
732
        contacts = []
733
        for cur_contact in self._get_property('contacts'):
734
            if cur_contact['name'] and cur_contact['surname']:
735
                contact_name = '%s %s' % (cur_contact['name'], cur_contact['surname'])
736
            elif cur_contact['name']:
737
                contact_name = cur_contact['name']
738
            elif cur_contact['surname']:
739
                contact_name = cur_contact['surname']
740
            else:
741
                contact_name = urlparse(cur_contact['email']).path.partition('?')[0]
742
            c_type = 'undefined'
743
            if cur_contact['type']:
744
                c_type = cur_contact['type']
745
            contacts.append({ 'name': contact_name, 'email': cur_contact['email'], 'type': c_type })
746
        return contacts
747
748
    @property
749
    def logos(self):
750
        logos = []
751
        for cur_logo in self._get_property('logos'):
752
            cur_logo['external'] = True
753
            logos.append(cur_logo)
754
755
        return logos
756
757
    class Meta(object):
758
        verbose_name = _(u'Entity')
759
        verbose_name_plural = _(u'Entities')
760
761
    def __unicode__(self):
762
        return self.entityid
763
764
    def load_metadata(self, federation=None, entity_data=None):
765
        if hasattr(self, '_entity_cached'):
766
            return
767
768
        if self.file:
769
            self._entity_cached = self.load_file().get_entity(self.entityid)
770
        elif federation:
771
            self._entity_cached = federation.get_entity_metadata(self.entityid)
772
        elif entity_data:
773
            self._entity_cached = entity_data
774
        else:
775
            right_fed = None
776
            first_fed = None
777
            for fed in self.federations.all():
778
                if fed.registration_authority == self.registration_authority:
779
                    right_fed = fed
780
                if first_fed is None:
781
                    first_fed = fed
782
783
            if right_fed is not None:
784
                entity_cached = right_fed.get_entity_metadata(self.entityid)
785
                self._entity_cached = entity_cached
786
            else:
787
                entity_cached = first_fed.get_entity_metadata(self.entityid)
788
                self._entity_cached = entity_cached
789
790
        if not hasattr(self, '_entity_cached'):
791
            raise ValueError("Can't find entity metadata")
792
793
    def _get_property(self, prop, federation=None):
794
        try:
795
            self.load_metadata(federation or self.curfed)
796
        except ValueError:
797
            return None
798
799
        if hasattr(self, '_entity_cached'):
800
            return self._entity_cached.get(prop, None)
801
        else:
802
            raise ValueError("Not metadata loaded")
803
804
    def _get_or_create_etypes(self, cached_entity_types):
805
        entity_types = []
806
        cur_cached_types = [t.xmlname for t in self.types.all()]
807
        for etype in self.xml_types:
808
            if etype in cur_cached_types:
809
               break
810
811
            if cached_entity_types is None:
812
                entity_type, _ = EntityType.objects.get_or_create(xmlname=etype,
813
                                                                  name=DESCRIPTOR_TYPES_DISPLAY[etype])
814
            else:
815
                if etype in cached_entity_types:
816
                    entity_type = cached_entity_types[etype]
817
                else:
818
                    entity_type = EntityType.objects.create(xmlname=etype,
819
                                                            name=DESCRIPTOR_TYPES_DISPLAY[etype])
820
            entity_types.append(entity_type)
821
        return entity_types
822
823
    def _get_or_create_ecategories(self, cached_entity_categories):
824
        entity_categories = []
825
        cur_cached_categories = [t.category_id for t in self.entity_categories.all()]
826
        for ecategory in self.xml_categories:
827
            if ecategory in cur_cached_categories:
828
                break
829
830
            if cached_entity_categories is None:
831
                entity_category, _ = EntityCategory.objects.get_or_create(category_id=ecategory)
832
            else:
833
                if ecategory in cached_entity_categories:
834
                    entity_category = cached_entity_categories[ecategory]
835
                else:
836
                    entity_category = EntityCategory.objects.create(category_id=ecategory)
837
            entity_categories.append(entity_category)
838
        return entity_categories
839
840
    def process_metadata(self, auto_save=True, entity_data=None, cached_entity_types=None):
841
        if not entity_data:
842
            self.load_metadata()
843
844
        if self.entityid.lower() != entity_data.get('entityid').lower():
845
            raise ValueError("EntityID is not the same: %s != %s" % (self.entityid.lower(), entity_data.get('entityid').lower()))
846
847
        self._entity_cached = entity_data
848
849
        if self.xml_types:
850
            entity_types = self._get_or_create_etypes(cached_entity_types)
851
            if len(entity_types) > 0:
852
                self.types.add(*entity_types)
853
854
        if self.xml_categories:
855
            db_entity_categories = EntityCategory.objects.all()
856
            cached_entity_categories = { entity_category.category_id: entity_category for entity_category in db_entity_categories }
857
858
            # Delete categories no more present in XML
859
            category_to_delete = []
860
            for category in cached_entity_categories:
861
                if not category in self.xml_categories:
862
                    category_to_delete.append(cached_entity_categories[category])
863
            self.entity_categories.remove(*category_to_delete)
864
865
            # Create all entities, if not alread in database
866
            entity_categories = self._get_or_create_ecategories(cached_entity_categories)
867
868
            # Add categories to entity
869
            if len(entity_categories) > 0:
870
                self.entity_categories.add(*entity_categories)
871
        else:
872
            # No categories in XML, delete eventual categorie sin DB
873
            self.entity_categories.all().delete()
874
875
        newname = self._get_property('displayName')
876
        if newname and newname != '':
877
            self.name = newname
878
879
        self.certstats = self._get_property('certstats')
880
881
        if str(self._get_property('registration_authority')) != '':
882
            self.registration_authority = self._get_property('registration_authority')
883
884
        if auto_save:
885
            self.save()
886
887
    def to_dict(self):
888
        self.load_metadata()
889
890
        entity = self._entity_cached.copy()
891
        entity["types"] = [unicode(f) for f in self.types.all()]
892
        entity["federations"] = [{u"name": unicode(f), u"url": f.get_absolute_url()}
893
                                  for f in self.federations.all()]
894
895
        if self.registration_authority:
896
            entity["registration_authority"] = self.registration_authority
897
        if self.registration_instant:
898
            entity["registration_instant"] = '%s' % self.registration_instant
899
900
        if "file_id" in entity.keys():
901
            del entity["file_id"]
902
        if "entity_types" in entity.keys():
903
            del entity["entity_types"]
904
905
        return entity
906
907
    def display_etype(value, separator=', '):
908
            return separator.join([unicode(item) for item in value.all()])
909
910
    @classmethod
911
    def get_most_federated_entities(self, maxlength=TOP_LENGTH, cache_expire=None):
912
        entities = None
913
        if cache_expire:
914
            entities = cache.get("most_federated_entities")
915
916
        if not entities or len(entities) < maxlength:
917
            # Entities with count how many federations belongs to, and sorted by most first
918
            ob_entities = Entity.objects.all().annotate(federationslength=Count("federations")).order_by("-federationslength")
919
            ob_entities = ob_entities.prefetch_related('types', 'federations')
920
            ob_entities = ob_entities[:maxlength]
921
922
            entities = []
923
            for entity in ob_entities:
924
                entities.append({
925
                    'entityid': entity.entityid,
926
                    'name': entity.name,
927
                    'absolute_url': entity.get_absolute_url(),
928
                    'types': [unicode(item) for item in entity.types.all()],
929
                    'federations': [(unicode(item.name), item.get_absolute_url()) for item in entity.federations.all()],
930
                })
931
932
        if cache_expire:
933
            cache.set("most_federated_entities", entities, cache_expire)
934
935
        return entities[:maxlength]
936
937
    def get_absolute_url(self):
938
        return reverse('entity_view', args=[quote_plus(self.entityid.encode('utf-8'))])
939
940
    def can_edit(self, user, delete):
941
        permission = 'delete_entity' if delete else 'change_entity'
942
        if user.is_superuser or (user.has_perm('metadataparser.%s' % permission) and user in self.editor_users.all()):
943
            return True
944
945
        for federation in self.federations.all():
946
            if federation.can_edit(user, False):
947
                return True
948
949
        return False
950
951
952
class EntityStat(models.Model):
953
    time = models.DateTimeField(blank=False, null=False, 
954
                           verbose_name=_(u'Metadata time stamp'))
955
    feature = models.CharField(max_length=100, blank=False, null=False, db_index=True,
956
                           verbose_name=_(u'Feature name'))
957
958
    value = models.PositiveIntegerField(max_length=100, blank=False, null=False,
959
                           verbose_name=_(u'Feature value'))
960
961
    federation = models.ForeignKey(Federation, blank = False,
962
                                         verbose_name=_(u'Federations'))
963
964
    def __unicode__(self):
965
        return self.feature
966
967
968
class Dummy(models.Model):
969
    pass
970
971
972
@receiver(pre_save, sender=Federation, dispatch_uid='federation_pre_save')
973
def federation_pre_save(sender, instance, **kwargs):
974
    # Skip pre_save if only file name is saved 
975
    if kwargs.has_key('update_fields') and kwargs['update_fields'] == set(['file']):
976
        return
977
978
    #slug = slugify(unicode(instance.name))[:200]
979
    #if instance.file_url and instance.file_url != '':
980
    #    try:
981
    #        instance.fetch_metadata_file(slug)
982
    #    except Exception, e:
983
    #        pass
984
985
    if instance.name:
986
        instance.slug = slugify(unicode(instance))[:200]
987
988
989
@receiver(pre_save, sender=Entity, dispatch_uid='entity_pre_save')
990
def entity_pre_save(sender, instance, **kwargs):
991
    #if refetch and instance.file_url:
992
    #    slug = slugify(unicode(instance.name))[:200]
993
    #    instance.fetch_metadata_file(slug)
994
    #    instance.process_metadata()
995
    pass
996