Completed
Push — master ( 13b617...40e09b )
by Andrea
01:18
created

EntityCategory   A

Complexity

Total Complexity 1

Size/Duplication

Total Lines 12
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 1
c 0
b 0
f 0
dl 0
loc 12
rs 10

1 Method

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