Statistics
| Branch: | Tag: | Revision:

root / edumanage / models.py @ 3cdee1de

History | View | Annotate | Download (22.1 kB)

1
# -*- coding: utf-8 -*- vim:fileencoding=utf-8:
2
# vim: tabstop=4:shiftwidth=4:softtabstop=4:expandtab
3

    
4
'''
5
TODO main description
6
'''
7

    
8
from django.db import models
9
from django.utils.translation import ugettext_lazy as _
10

    
11
from django.contrib.contenttypes.models import ContentType
12
from django.contrib.contenttypes import generic
13
from django.db import models
14
from django import forms
15
from django import forms
16
from django.db import models
17
from django.utils.text import capfirst
18
from django.core import exceptions
19
from django.conf import settings
20
from django.contrib.auth.models import User
21

    
22
class MultiSelectFormField(forms.MultipleChoiceField):
23
    widget = forms.CheckboxSelectMultiple
24

    
25
    def __init__(self, *args, **kwargs):
26
        self.max_choices = 4
27
        super(MultiSelectFormField, self).__init__(*args, **kwargs)
28

    
29
    def clean(self, value):
30
        if not value and self.required:
31
            raise forms.ValidationError(self.error_messages['required'])
32
        # if value and self.max_choices and len(value) > self.max_choices:
33
        #     raise forms.ValidationError('You must select a maximum of %s choice%s.'
34
        #             % (apnumber(self.max_choices), pluralize(self.max_choices)))
35
        return value
36

    
37

    
38
class MultiSelectField(models.Field):
39
    __metaclass__ = models.SubfieldBase
40

    
41
    def get_internal_type(self):
42
        return "CharField"
43

    
44
    def get_choices_default(self):
45
        return self.get_choices(include_blank=False)
46

    
47
    def _get_FIELD_display(self, field):
48
        value = getattr(self, field.attname)
49
        choicedict = dict(field.choices)
50

    
51
    def formfield(self, **kwargs):
52
        # don't call super, as that overrides default widget if it has choices
53
        defaults = {'required': not self.blank, 'label': capfirst(self.verbose_name),
54
                    'help_text': self.help_text, 'choices': self.choices}
55
        if self.has_default():
56
            defaults['initial'] = self.get_default()
57
        defaults.update(kwargs)
58
        return MultiSelectFormField(**defaults)
59

    
60
    def get_prep_value(self, value):
61
        return value
62

    
63
    def get_db_prep_value(self, value, connection=None, prepared=False):
64
        if isinstance(value, basestring):
65
            return value
66
        elif isinstance(value, list):
67
            return ",".join(value)
68

    
69
    def to_python(self, value):
70
        if value is not None:
71
            return value if isinstance(value, list) else value.split(',')
72
        return ''
73

    
74
    def contribute_to_class(self, cls, name):
75
        super(MultiSelectField, self).contribute_to_class(cls, name)
76
        if self.choices:
77
            func = lambda self, fieldname = name, choicedict = dict(self.choices): ",".join([choicedict.get(value, value) for value in getattr(self, fieldname)])
78
            setattr(cls, 'get_%s_display' % self.name, func)
79

    
80
    def validate(self, value, model_instance):
81
        arr_choices = self.get_choices_selected(self.get_choices_default())
82
        for opt_select in value:
83
            if (opt_select not in arr_choices):  # the int() here is for comparing with integer choices
84
                raise exceptions.ValidationError(self.error_messages['invalid_choice'] % value)
85
        return
86

    
87
    def get_choices_selected(self, arr_choices=''):
88
        if not arr_choices:
89
            return False
90
        list = []
91
        for choice_selected in arr_choices:
92
            list.append(choice_selected[0])
93
        return list
94

    
95
    def value_to_string(self, obj):
96
        value = self._get_val_from_obj(obj)
97
        return self.get_db_prep_value(value)
98

    
99
# needed for South compatibility
100

    
101
from south.modelsinspector import add_introspection_rules
102
add_introspection_rules([], ["^edumanage\.models\.MultiSelectField"])
103

    
104

    
105

    
106

    
107
ERTYPES = (
108
        (1, 'IdP only' ),
109
        (2, 'SP only'),
110
        (3, 'IdP and SP'),
111
    )
112

    
113
RADPROTOS = (
114
        ('radius', 'traditional RADIUS over UDP' ),
115
#        ('radius-tcp', 'RADIUS over TCP (RFC6613)'),
116
#        ('radius-tls', 'RADIUS over TLS (RFC6614)'),
117
#        ('radius-dtls', 'RADIUS over datagram TLS (RESERVED)'),
118
    )
119

    
120

    
121
ADDRTYPES = (
122
        ('any', 'Default'),
123
        ('ipv4', 'IPv4 only'),
124
        #('ipv6', 'IPv6 only'), # Commented for the time...not yet in use
125
    )
126

    
127
RADTYPES = (
128
            ('auth', 'Handles Access-Request packets only'),
129
            ('acct', 'Handles Accounting-Request packets only'),
130
            ('auth+acct', 'Handles both Access-Request and Accounting-Request packets'),
131
            )
132

    
133
class Name_i18n(models.Model):
134
    '''
135
    Name in a particular language
136
    '''
137

    
138
    name = models.CharField(max_length=80)
139
    lang = models.CharField(max_length=5, choices=settings.URL_NAME_LANGS)
140
    content_type = models.ForeignKey(ContentType, blank=True, null=True)
141
    object_id = models.PositiveIntegerField(blank=True, null=True)
142
    content_object = generic.GenericForeignKey('content_type', 'object_id')
143

    
144
    def __unicode__(self):
145
        return self.name
146

    
147
    class Meta:
148
        verbose_name = "Name (i18n)"
149
        verbose_name_plural = "Names (i18n)"
150

    
151

    
152
class Contact(models.Model):
153
    '''
154
    Contact
155
    '''
156

    
157
    name = models.CharField(max_length=255, db_column='contact_name')
158
    email = models.CharField(max_length=80, db_column='contact_email')
159
    phone = models.CharField(max_length=80, db_column='contact_phone')
160

    
161
    def __unicode__(self):
162
        return '%s <%s> (%s)' % (self.name, self.email, self.phone)
163

    
164
    class Meta:
165
        verbose_name = "Contact"
166
        verbose_name_plural = "Contacts"
167

    
168

    
169
class InstitutionContactPool(models.Model):
170
    contact = models.OneToOneField(Contact)
171
    institution = models.ForeignKey("Institution")
172

    
173
    def __unicode__(self):
174
        return u"%s:%s" %(self.contact, self.institution)
175

    
176
    class Meta:
177
        verbose_name = "Instutution Contact (Pool)"
178
        verbose_name_plural = "Instutution Contacts (Pool)"
179

    
180
class URL_i18n(models.Model):
181
    '''
182
    URL of a particular type in a particular language
183
    '''
184

    
185
    URLTYPES = (
186
                ('info', 'Info' ),
187
                ('policy', 'Policy'),
188
               )
189
    url = models.CharField(max_length=180, db_column='URL')
190
    lang = models.CharField(max_length=5, choices=settings.URL_NAME_LANGS)
191
    urltype = models.CharField(max_length=10, choices=URLTYPES, db_column='type')
192
    content_type = models.ForeignKey(ContentType, blank=True, null=True)
193
    object_id = models.PositiveIntegerField(blank=True, null=True)
194
    content_object = generic.GenericForeignKey('content_type', 'object_id')
195

    
196
    class Meta:
197
        verbose_name = "Url (i18n)"
198
        verbose_name_plural = "Urls (i18n)"
199

    
200
    def __unicode__(self):
201
        return self.url
202

    
203
class InstRealm(models.Model):
204
    '''
205
    Realm of an IdP Institution
206
    '''
207
    # accept if instid.ertype: 1 (idp) or 3 (idpsp)
208
    realm = models.CharField(max_length=160)
209
    instid = models.ForeignKey("Institution",verbose_name="Institution")
210
    proxyto = models.ManyToManyField("InstServer", help_text=_("Only IdP and IdP/SP server types are allowed"))
211

    
212
    class Meta:
213
        verbose_name = "Institution Realm"
214
        verbose_name_plural = "Institutions' Realms"
215

    
216
    def __unicode__(self):
217
        return '%s' % self.realm
218

    
219

    
220
    def get_servers(self):
221
        return ",".join(["%s"%x for x in self.proxyto.all()])
222

    
223

    
224
class InstServer(models.Model):
225
    '''
226
    Server of an Institution
227
    '''
228
    instid = models.ForeignKey("Institution")
229
    ertype = models.PositiveIntegerField(max_length=1, choices=ERTYPES, db_column='type')
230
    # ertype:
231
    # 1: accept if instid.ertype: 1 (idp) or 3 (idpsp)
232
    # 2: accept if instid.ertype: 2 (sp) or 3 (idpsp)
233
    # 3: accept if instid.ertype: 3 (idpsp)
234

    
235
    # hostname/ipaddr or descriptive label of server
236
    name = models.CharField(max_length=80, help_text=_("Descriptive label"),  null=True, blank=True) # ** (acts like a label)
237
    # hostname/ipaddr of server, overrides name
238
    addr_type = models.CharField(max_length=16, choices=ADDRTYPES, default='ipv4')
239
    host = models.CharField(max_length=80, help_text=_("IP address | FQDN hostname")) # Handling with FQDN parser or ipaddr (google lib) * !!! Add help text to render it in template (mandatory, unique)
240
    #TODO: Add description field or label field
241
    # accept if type: 1 (idp) or 3 (idpsp) (for the folowing 4 fields)
242
    rad_pkt_type = models.CharField(max_length=48, choices=RADTYPES, default='auth+acct', null=True, blank=True,)
243
    auth_port = models.PositiveIntegerField(max_length=5, null=True, blank=True, default=1812, help_text=_("Default for RADIUS: 1812")) # TODO: Also ignore while exporting XML
244
    acct_port = models.PositiveIntegerField(max_length=5, null=True, blank=True, default=1813, help_text=_("Default for RADIUS: 1813"))
245
    status_server = models.BooleanField(help_text=_("Do you accept Status-Server requests?"))
246

    
247
    secret = models.CharField(max_length=80)
248
    proto = models.CharField(max_length=12, choices=RADPROTOS, default='radius')
249
    ts = models.DateTimeField(auto_now=True)
250

    
251
    class Meta:
252
        verbose_name = "Institution Server"
253
        verbose_name_plural = "Institutions' Servers"
254

    
255
    def __unicode__(self):
256
        return _('Server: %(servername)s, Type: %(ertype)s') % {
257
        # but name is many-to-many from institution
258
            #'inst': self.instid,
259
            'servername': self.get_name(),
260
        # the human-readable name would be nice here
261
            'ertype': self.get_ertype_display(),
262
            }
263

    
264
    def get_name(self):
265
        if self.name:
266
            return self.name
267
        return self.host
268

    
269

    
270
class InstRealmMon(models.Model):
271
    '''
272
    Realm of an IdP Institution to be monitored
273
    '''
274

    
275
    MONTYPES = (
276
                ('localauthn', 'Institution provides account for the NRO to monitor the realm' ),
277
                #('loopback', 'Institution proxies the realm back to the NRO'),
278
               )
279

    
280
    realm = models.ForeignKey(InstRealm)
281
    mon_type = models.CharField(max_length=16, choices=MONTYPES)
282

    
283
    class Meta:
284
        unique_together = ('realm','mon_type')
285
        verbose_name = "Institution Monitored Realm"
286
        verbose_name_plural = "Institution Monitored Realms"
287

    
288
    def __unicode__(self):
289
        return "%s-%s" %(self.realm.realm, self.mon_type)
290
#    def __unicode__(self):
291
#        return _('Institution: %(inst)s, Monitored Realm: %(monrealm)s, Monitoring Type: %(montype)s') % {
292
#        # but name is many-to-many from institution
293
#            'inst': self.instid.name,
294
#            'monrealm': self.realm,
295
#            'montype': self.mon_type,
296
#            }
297

    
298
class MonProxybackClient(models.Model):
299
    '''
300
    Server of an Institution that will be proxying back requests for a monitored realm
301
    '''
302

    
303
    instrealmmonid = models.ForeignKey("InstRealmMon")
304
    # hostname/ipaddr or descriptive label of server
305
    name = models.CharField(max_length=80, help_text=_("Descriptive label"),  null=True, blank=True) # ** (acts like a label)
306
    # hostname/ipaddr of server, overrides name
307
    host = models.CharField(max_length=80, help_text=_("IP address | FQDN hostname")) # Handling with FQDN parser or ipaddr (google lib) * !!! Add help text to render it in template (mandatory, unique)
308
    status_server = models.BooleanField()
309
    secret = models.CharField(max_length=80)
310
    proto = models.CharField(max_length=12, choices=RADPROTOS)
311
    ts = models.DateTimeField(auto_now=True)
312

    
313
    class Meta:
314
        verbose_name = "Instituion Proxyback Client"
315
        verbose_name_plural = "Instituion Proxyback Clients"
316

    
317
    def __unicode__(self):
318
        return _('Monitored Realm: %(monrealm)s, Proxyback Client: %(servername)s') % {
319
        # but name is many-to-many from institution
320
            'monrealm': self.instrealmmonid.realm,
321
            'servername': self.name,
322
            }
323

    
324
class MonLocalAuthnParam(models.Model):
325
    '''
326
    Parameters for an old-style monitored realm
327
    '''
328

    
329
    EAPTYPES = (
330
                ('PEAP', 'EAP-PEAP' ),
331
                ('TTLS', 'EAP-TTLS'),
332
#                ('TLS', 'EAP-TLS'),
333
               )
334
    EAP2TYPES = (
335
                ('PAP', 'PAP' ),
336
                ('CHAP', 'CHAP'),
337
                ('MS-CHAPv2', 'MS-CHAPv2'),
338
               )
339
#    MONRESPTYPES = (
340
#                ('accept', 'Access-Accept expected' ),
341
#                ('reject', 'Access-Reject expected'),
342
#                ('both', 'RESERVED'),
343
#               )
344

    
345
    instrealmmonid = models.OneToOneField("InstRealmMon")
346
    eap_method = models.CharField(max_length=16, choices=EAPTYPES)
347
    phase2 = models.CharField(max_length=16, choices=EAP2TYPES)
348
    # only local-part, no realm
349
    username = models.CharField(max_length=36)
350
    passwp = models.CharField(max_length=80, db_column='pass')
351
    # TODO: In next releast change it to TextField and add a key field
352
    #cert = models.CharField(max_length=32)
353
    #exp_response = models.CharField(max_length=6, choices=MONRESPTYPES)
354

    
355
    class Meta:
356
        verbose_name = "Monitored Realm (local authn)"
357
        verbose_name_plural = "Monitored Realms (local authn)"
358

    
359
    def __unicode__(self):
360
        return _('Monitored Realm: %(monrealm)s, EAP Method: %(eapmethod)s, Phase 2: %(phase2)s, Username: %(username)s') % {
361
        # but name is many-to-many from institution
362
            'monrealm': self.instrealmmonid.realm,
363
            'eapmethod': self.eap_method,
364
            'phase2': self.phase2,
365
            'username': self.username,
366
            }
367

    
368
class ServiceLoc(models.Model):
369
    '''
370
    Service Location of an SP/IdPSP Institution
371
    '''
372

    
373
    ENCTYPES = (
374
                ('WPA/TKIP', 'WPA-TKIP' ),
375
                ('WPA/AES', 'WPA-AES'),
376
                ('WPA2/TKIP', 'WPA2-TKIP'),
377
                ('WPA2/AES', 'WPA2-AES'),
378
               )
379

    
380
    # accept if institutionid.ertype: 2 (sp) or 3 (idpsp)
381
    institutionid = models.ForeignKey("Institution", verbose_name="Institution")
382
    longitude = models.DecimalField(max_digits=12, decimal_places=8)
383
    latitude = models.DecimalField(max_digits=12, decimal_places=8)
384
    # TODO: multiple names can be specified [...] name in English is required
385
    loc_name = generic.GenericRelation(Name_i18n)
386
    address_street = models.CharField(max_length=96)
387
    address_city = models.CharField(max_length=64)
388
    contact = models.ManyToManyField(Contact, blank=True, null=True)
389
    SSID = models.CharField(max_length=16)
390
    enc_level = MultiSelectField(max_length=64, choices=ENCTYPES, blank=True, null=True)
391
    port_restrict = models.BooleanField()
392
    transp_proxy = models.BooleanField()
393
    IPv6 = models.BooleanField()
394
    NAT = models.BooleanField()
395
    AP_no = models.PositiveIntegerField(max_length=3)
396
    wired = models.BooleanField()
397
    # only urltype = 'info' should be accepted here
398
    url = generic.GenericRelation(URL_i18n, blank=True, null=True)
399
    ts = models.DateTimeField(auto_now=True)
400

    
401
    class Meta:
402
        verbose_name = "Service Location"
403
        verbose_name_plural = "Service Locations"
404

    
405
    def __unicode__(self):
406
        return _('Institution: %(inst)s, Service Location: %(locname)s') % {
407
        # but name is many-to-many from institution
408
            'inst': self.institutionid,
409
        # but locname is many-to-many
410
            'locname': self.get_name(),
411
            }
412

    
413

    
414
    def get_name(self, lang=None):
415
        name = ', '.join([i.name for i in self.loc_name.all()])
416
        if not lang:
417
            return name
418
        else:
419
            try:
420
                name = self.loc_name.get(lang=lang)
421
                return name
422
            except Exception as e:
423
                return name
424
    get_name.short_description = 'Location Name'
425

    
426

    
427
class Institution(models.Model):
428
    '''
429
    Institution
430
    '''
431

    
432
    realmid = models.ForeignKey("Realm")
433
    org_name = generic.GenericRelation(Name_i18n)
434
    ertype = models.PositiveIntegerField(max_length=1, choices=ERTYPES, db_column='type')
435

    
436
    def __unicode__(self):
437
        return "%s" % ', '.join([i.name for i in self.org_name.all()])
438

    
439

    
440
    def get_name(self, lang=None):
441
        name = ', '.join([i.name for i in self.org_name.all()])
442
        if not lang:
443
            return name
444
        else:
445
            try:
446
                name = self.org_name.get(lang=lang)
447
                return name
448
            except Exception as e:
449
                return name
450

    
451
    def get_active_cat_enrl(self):
452
        urls = []
453
        active_cat_enrl = self.catenrollment_set.filter(url='ACTIVE', cat_instance='production')
454
        for catenrl in active_cat_enrl:
455
            if catenrl.cat_configuration_url:
456
                urls.append(catenrl.cat_configuration_url)
457
        return urls
458

    
459

    
460
class InstitutionDetails(models.Model):
461
    '''
462
    Institution Details
463
    '''
464
    institution = models.OneToOneField(Institution)
465
    # TODO: multiple names can be specified [...] name in English is required
466
    address_street = models.CharField(max_length=96)
467
    address_city = models.CharField(max_length=64)
468
    contact = models.ManyToManyField(Contact)
469
    url = generic.GenericRelation(URL_i18n)
470
    # accept if ertype: 2 (sp) or 3 (idpsp) (Applies to the following field)
471
    oper_name = models.CharField(max_length=24, null=True, blank=True, help_text=_('The primary, registered domain name for your institution, eg. example.com.<br>This is used to derive the Operator-Name attribute according to RFC5580, par.4.1, using the REALM namespace.'))
472
    # accept if ertype: 1 (idp) or 3 (idpsp) (Applies to the following field)
473
    number_user = models.PositiveIntegerField(max_length=6, null=True, blank=True, help_text=_("Number of users (individuals) that are eligible to participate in eduroam service"))
474
    number_id = models.PositiveIntegerField(max_length=6, null=True, blank=True, help_text=_("Number of issued e-identities (credentials) that may be used for authentication in eduroam service"))
475
    ts = models.DateTimeField(auto_now=True)
476

    
477
    class Meta:
478
        verbose_name = "Institutions' Details"
479
        verbose_name_plural = "Institution Details"
480

    
481
    def __unicode__(self):
482
        return _('Institution: %(inst)s, Type: %(ertype)s') % {
483
        # but name is many-to-many from institution
484
            'inst': ', '.join([i.name for i in self.institution.org_name.all()]),
485
            'ertype': self.institution.get_ertype_display(),
486
            }
487
    def get_inst_name(self):
488
        return join([i.name for i in self.institution.org_name.all()])
489
    get_inst_name.short_description = "Institution Name"
490

    
491

    
492

    
493
class Realm(models.Model):
494
    '''
495
    Realm
496
    '''
497

    
498
    country = models.CharField(max_length=5, choices=settings.REALM_COUNTRIES)
499
    stype = models.PositiveIntegerField(max_length=1, default=0, editable=False)
500
    # TODO: multiple names can be specified [...] name in English is required
501
    org_name = generic.GenericRelation(Name_i18n)
502
    address_street = models.CharField(max_length=32)
503
    address_city = models.CharField(max_length=24)
504
    contact = models.ManyToManyField(Contact)
505
    url = generic.GenericRelation(URL_i18n)
506
    ts = models.DateTimeField(auto_now=True)
507

    
508
    class Meta:
509
        verbose_name = "Realm"
510
        verbose_name_plural = "Realms"
511

    
512
    def __unicode__(self):
513
        return _('Country: %(country)s, NRO: %(orgname)s') % {
514
        # but name is many-to-many from institution
515
            'orgname': ', '.join([i.name for i in self.org_name.all()]),
516
            'country': self.country,
517
            }
518

    
519

    
520
# TODO: this represents a *database view* "realm_data", find a better way to write it
521
class RealmData(models.Model):
522
    '''
523
    Realm statistics
524
    '''
525

    
526
    realmid = models.OneToOneField(Realm)
527
    # db: select count(institution.id) as number_inst from institution, realm where institution.realmid == realm.realmid
528
    number_inst = models.PositiveIntegerField(max_length=5, editable=False)
529
    # db: select sum(institution.number_user) as number_user from institution, realm where institution.realmid == realm.realmid
530
    number_user = models.PositiveIntegerField(max_length=9, editable=False)
531
    # db: select sum(institution.number_id) as number_id from institution, realm where institution.realmid == realm.realmid
532
    number_id = models.PositiveIntegerField(max_length=9, editable=False)
533
    # db: select count(institution.id) as number_IdP from institution, realm where institution.realmid == realm.realmid and institution.type == 1
534
    number_IdP = models.PositiveIntegerField(max_length=5, editable=False)
535
    # db: select count(institution.id) as number_SP from institution, realm where institution.realmid == realm.realmid and institution.type == 2
536
    number_SP = models.PositiveIntegerField(max_length=5, editable=False)
537
    # db: select count(institution.id) as number_IdPSP from institution, realm where institution.realmid == realm.realmid and institution.type == 3
538
    number_IdPSP = models.PositiveIntegerField(max_length=5, editable=False)
539
    # db: select greatest(max(realm.ts), max(institution.ts)) as ts from institution, realm where institution.realmid == realm.realmid
540
    ts = models.DateTimeField(editable=False)
541

    
542
    def __unicode__(self):
543
        return _('Country: %(country)s, NRO: %(orgname)s, Institutions: %(inst)s, IdPs: %(idp)s, SPs: %(sp)s, IdPSPs: %(idpsp)s, Users: %(numuser)s, Identities: %(numid)s') % {
544
        # but name is many-to-many from institution
545
            'orgname': self.org_name,
546
            'country': self.country,
547
            'inst': self.number_inst,
548
            'idp': self.number_IdP,
549
            'sp': self.number_SP,
550
            'idpsp': self.number_IdPSP,
551
            'numuser': self.number_user,
552
            'numid': self.number_id,
553
            }
554

    
555

    
556
class CatEnrollment(models.Model):
557
    ''' Eduroam CAT enrollment '''
558

    
559
    ACTIVE = u"ACTIVE"
560

    
561
    cat_inst_id = models.PositiveIntegerField(max_length=10)
562
    inst = models.ForeignKey(Institution)
563
    url = models.CharField(max_length=255, blank=True, null=True, help_text="Set to ACTIVE if institution has CAT profiles")
564
    cat_instance = models.CharField(max_length=50, choices=settings.CAT_INSTANCES)
565
    applier = models.ForeignKey(User)
566
    ts = models.DateTimeField(auto_now=True)
567

    
568
    class Meta:
569
        unique_together = ['inst', 'cat_instance']
570

    
571
    def __unicode__(self):
572
        return "%s: %s" % (self.cat_inst_id, ', '.join([i.name for i in self.inst.org_name.all()]))
573

    
574
    def cat_active(self):
575
        return self.url == self.ACTIVE
576

    
577
    def cat_configuration_url(self):
578
        if self.cat_active():
579
            try:
580
                return "%s?idp=%s"%(settings.CAT_AUTH[self.cat_instance]['CAT_PROFILES_URL'],self.cat_inst_id)
581
            except:
582
                return False
583
        return False
584

    
585
    cat_active.boolean = True
586
    cat_active.short_description = "CAT profiles"
587

    
588

    
589