• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

matijakolaric-com / django-music-publisher / 2734221357

pending completion
2734221357

push

github

matijakolaric
replacing assert with proper exceptions

6 of 6 new or added lines in 2 files covered. (100.0%)

4076 of 4165 relevant lines covered (97.86%)

1.96 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.19
/music_publisher/models.py
1
"""Concrete models.
2

3
They mostly inherit from classes in :mod:`.base`.
4

5
"""
6

7
import base64
2✔
8
import uuid
2✔
9
from collections import defaultdict
2✔
10
from datetime import datetime
2✔
11
from decimal import Decimal
2✔
12

13
from django.conf import settings
2✔
14
from django.core.exceptions import ValidationError
2✔
15
from django.core.validators import MaxValueValidator, MinValueValidator
2✔
16
from django.db import models
2✔
17
from django.db.models.signals import pre_save
2✔
18
from django.dispatch import receiver
2✔
19
from django.template import Context
2✔
20
from django.urls import reverse
2✔
21
from django.utils import timezone
2✔
22
from django.utils.duration import duration_string
2✔
23

24
from .base import (
2✔
25
    ArtistBase,
26
    IPIBase,
27
    LabelBase,
28
    LibraryBase,
29
    PersonBase,
30
    ReleaseBase,
31
    TitleBase,
32
    WriterBase,
33
    upload_to,
34
)
35
from .cwr_templates import (
2✔
36
    TEMPLATES_21,
37
    TEMPLATES_22,
38
    TEMPLATES_30,
39
    TEMPLATES_31,
40
)
41
from .societies import SOCIETIES, SOCIETY_DICT
2✔
42
from .validators import CWRFieldValidator
2✔
43

44
WORLD_DICT = {'tis-a': '2WL', 'tis-n': '2136', 'name': 'World'}
2✔
45

46

47
class Artist(ArtistBase):
2✔
48
    """Performing artist."""
49

50
    def get_dict(self):
2✔
51
        """Get the object in an internal dictionary format
52

53
        Returns:
54
            dict: internal dict format
55
        """
56
        return {
2✔
57
            'id': self.id,
58
            'code': self.artist_id,
59
            'last_name': self.last_name,
60
            'first_name': self.first_name or None,
61
            'isni': self.isni or None,
62
        }
63

64
    @property
2✔
65
    def artist_id(self):
2✔
66
        """Artist identifier
67

68
        Returns:
69
            str: Artist ID
70
        """
71
        return 'A{:06d}'.format(self.id)
2✔
72

73

74
class Label(LabelBase):
2✔
75
    """Music Label."""
76

77
    class Meta:
2✔
78
        verbose_name = 'Music Label'
2✔
79

80
    def __str__(self):
2✔
81
        return self.name.upper()
2✔
82

83
    @property
2✔
84
    def label_id(self):
2✔
85
        """Label identifier
86

87
        Returns:
88
            str: Label ID
89
        """
90
        return 'LA{:06d}'.format(self.id)
2✔
91

92
    def get_dict(self):
2✔
93
        """Get the object in an internal dictionary format
94

95
        Returns:
96
            dict: internal dict format
97
        """
98
        return {
2✔
99
            'id': self.id,
100
            'code': self.label_id,
101
            'name': self.name,
102
        }
103

104

105
class Library(LibraryBase):
2✔
106
    """Music Library."""
107

108
    class Meta:
2✔
109
        verbose_name = 'Music Library'
2✔
110
        verbose_name_plural = 'Music Libraries'
2✔
111
        ordering = ('name',)
2✔
112

113
    # name = models.CharField(
114
    #     max_length=60, unique=True,
115
    #     validators=(CWRFieldValidator('library'),))
116

117
    def __str__(self):
2✔
118
        return self.name.upper()
2✔
119

120
    @property
2✔
121
    def library_id(self):
2✔
122
        """Library identifier
123

124
        Returns:
125
            str: Library ID
126
        """
127
        return 'LI{:06d}'.format(self.id)
2✔
128

129
    def get_dict(self):
2✔
130
        """Get the object in an internal dictionary format
131

132
        Returns:
133
            dict: internal dict format
134
        """
135
        return {
2✔
136
            'id': self.id,
137
            'code': self.library_id,
138
            'name': self.name,
139
        }
140

141

142
class Release(ReleaseBase):
2✔
143
    """Music Release (album / other product)
144

145
    Attributes:
146
        library (django.db.models.ForeignKey): Foreign key to \
147
        :class:`.models.Library`
148
        release_label (django.db.models.ForeignKey): Foreign key to \
149
        :class:`.models.Label`
150
        recordings (django.db.models.ManyToManyField): M2M to \
151
        :class:`.models.Recording` through :class:`.models.Track`
152
    """
153

154
    class Meta:
2✔
155
        verbose_name = 'Release'
2✔
156

157
    library = models.ForeignKey(
2✔
158
        Library, null=True, blank=True, on_delete=models.PROTECT
159
    )
160
    release_label = models.ForeignKey(
2✔
161
        Label,
162
        verbose_name='Release (album) label',
163
        null=True,
164
        blank=True,
165
        on_delete=models.PROTECT,
166
    )
167
    recordings = models.ManyToManyField('Recording', through='Track')
2✔
168

169
    def __str__(self):
2✔
170
        if self.cd_identifier:
2✔
171
            if self.release_title:
2✔
172
                return '{}: {} ({})'.format(
2✔
173
                    self.cd_identifier,
174
                    self.release_title.upper(),
175
                    self.library,
176
                )
177
            else:
178
                return '{} ({})'.format(self.cd_identifier, self.library)
2✔
179
        else:
180
            if self.release_label:
2✔
181
                return '{} ({})'.format(
2✔
182
                    (self.release_title or '<no title>').upper(),
183
                    self.release_label,
184
                )
185
            return (self.release_title or '<no title>').upper()
2✔
186

187
    @property
2✔
188
    def release_id(self):
2✔
189
        """Release identifier.
190

191
        Returns:
192
            str: Release ID
193
        """
194
        return 'RE{:06d}'.format(self.id)
2✔
195

196
    def get_dict(self, with_tracks=False):
2✔
197
        """Get the object in an internal dictionary format
198

199
        Args:
200
            with_tracks (bool): add track data to the output
201

202
        Returns:
203
            dict: internal dict format
204

205
        """
206

207
        d = {
2✔
208
            'id': self.id,
209
            'code': self.release_id,
210
            'title': self.release_title or None,
211
            'date': self.release_date.strftime('%Y%m%d')
212
            if self.release_date
213
            else None,
214
            'label': self.release_label.get_dict()
215
            if self.release_label
216
            else None,
217
            'ean': self.ean,
218
        }
219
        if with_tracks:
2✔
220
            d['tracks'] = [track.get_dict() for track in self.tracks.all()]
2✔
221
        return d
2✔
222

223

224
class LibraryReleaseManager(models.Manager):
2✔
225
    """Manager for a proxy class :class:`.models.LibraryRelease`"""
226

227
    def get_queryset(self):
2✔
228
        """Return only library releases
229

230
        Returns:
231
            django.db.models.query.QuerySet: Queryset with instances of \
232
            :class:`.models.LibraryRelease`
233
        """
234
        return (
2✔
235
            super()
236
            .get_queryset()
237
            .filter(cd_identifier__isnull=False, library__isnull=False)
238
        )
239

240
    def get_dict(self, qs):
2✔
241
        """Get the object in an internal dictionary format
242

243
        Args:
244
            qs (django.db.models.query.QuerySet)
245

246
        Returns:
247
            dict: internal dict format
248
        """
249
        return {
2✔
250
            'releases': [release.get_dict(with_tracks=True) for release in qs]
251
        }
252

253

254
class LibraryRelease(Release):
2✔
255
    """Proxy class for Library Releases (AKA Library CDs)
256

257
    Attributes:
258
        objects (LibraryReleaseManager): Database Manager
259
    """
260

261
    class Meta:
2✔
262
        proxy = True
2✔
263
        verbose_name = 'Library Release'
2✔
264
        verbose_name_plural = 'Library Releases'
2✔
265

266
    objects = LibraryReleaseManager()
2✔
267

268
    def clean(self):
2✔
269
        """Make sure that release title is required if one of the other \
270
        "non-library" fields is present.
271

272
        Raises:
273
            ValidationError: If not compliant.
274
        """
275
        if (
2✔
276
            self.ean or self.release_date or self.release_label
277
        ) and not self.release_title:
278
            raise ValidationError(
2✔
279
                {'release_title': 'Required if other release data is set.'}
280
            )
281

282
    def get_origin_dict(self):
2✔
283
        """Get the object in an internal dictionary format.
284

285
        This is used for work origin, not release data.
286

287
        Returns:
288
            dict: internal dict format
289
        """
290
        return {
2✔
291
            'origin_type': {'code': 'LIB', 'name': 'Library Work'},
292
            'cd_identifier': self.cd_identifier,
293
            'library': self.library.get_dict(),
294
        }
295

296

297
class CommercialReleaseManager(models.Manager):
2✔
298
    """Manager for a proxy class :class:`.models.CommercialRelease`"""
299

300
    def get_queryset(self):
2✔
301
        """Return only commercial releases
302

303
        Returns:
304
            django.db.models.query.QuerySet: Queryset with instances of \
305
            :class:`.models.CommercialRelease`
306
        """
307
        return (
2✔
308
            super()
309
            .get_queryset()
310
            .filter(cd_identifier__isnull=True, library__isnull=True)
311
        )
312

313
    def get_dict(self, qs):
2✔
314
        """Get the object in an internal dictionary format
315

316
        Args:
317
            qs (django.db.models.query.QuerySet)
318

319
        Returns:
320
            dict: internal dict format
321
        """
322
        return {
2✔
323
            'releases': [release.get_dict(with_tracks=True) for release in qs]
324
        }
325

326

327
class CommercialRelease(Release):
2✔
328
    """Proxy class for Commercial Releases
329

330
    Attributes:
331
        objects (CommercialReleaseManager): Database Manager
332
    """
333

334
    class Meta:
2✔
335
        proxy = True
2✔
336
        verbose_name = 'Commercial Release'
2✔
337
        verbose_name_plural = 'Commercial Releases'
2✔
338

339
    objects = CommercialReleaseManager()
2✔
340

341

342
class PlaylistManager(models.Manager):
2✔
343
    """Manager for a proxy class :class:`.models.Playlist`"""
344

345
    def get_queryset(self):
2✔
346
        """Return only commercial releases
347

348
        Returns:
349
            django.db.models.query.QuerySet: Queryset with instances of \
350
            :class:`.models.CommercialRelease`
351
        """
352
        return (
2✔
353
            super()
354
            .get_queryset()
355
            .filter(cd_identifier__isnull=False, library__isnull=True)
356
        )
357

358
    def get_dict(self, qs):
2✔
359
        """Get the object in an internal dictionary format
360

361
        Args:
362
            qs (django.db.models.query.QuerySet)
363

364
        Returns:
365
            dict: internal dict format
366
        """
367
        return {
×
368
            'releases': [release.get_dict(with_tracks=True) for release in qs]
369
        }
370

371

372
class Playlist(Release):
2✔
373
    """Proxy class for Playlists
374

375
    Attributes:
376
        objects (CommercialReleaseManager): Database Manager
377
    """
378

379
    class Meta:
2✔
380
        proxy = True
2✔
381
        verbose_name = 'Playlist'
2✔
382
        verbose_name_plural = 'Playlists'
2✔
383

384
    objects = PlaylistManager()
2✔
385

386
    def __str__(self):
2✔
387
        return self.release_title or ''
2✔
388

389
    def clean(self, *args, **kwargs):
2✔
390
        if self.cd_identifier is None:
2✔
391
            self.cd_identifier = base64.urlsafe_b64encode(uuid.uuid4().bytes)
2✔
392
            self.cd_identifier = self.cd_identifier.decode().rstrip('=')[:15]
2✔
393
        return super().clean(*args, **kwargs)
2✔
394

395
    @property
2✔
396
    def secret_url(self):
2✔
397
        return reverse('secret_playlist', args=[self.cd_identifier])
2✔
398

399
    @property
2✔
400
    def secret_api_url(self):
2✔
401
        return reverse('playlist-detail', args=[self.cd_identifier])
2✔
402

403

404
class Writer(WriterBase):
2✔
405
    """Writers.
406

407
    Attributes:
408
        original_publishing_agreement (django.db.models.ForeignKey): \
409
        Foreign key to :class:`.models.OriginalPublishingAgreement`
410
    """
411

412
    class Meta:
2✔
413
        ordering = ('last_name', 'first_name', 'ipi_name', '-id')
2✔
414
        verbose_name = 'Writer'
2✔
415
        verbose_name_plural = 'Writers'
2✔
416

417
    def __str__(self):
2✔
418
        name = super().__str__()
2✔
419
        if self.generally_controlled:
2✔
420
            return name + ' (*)'
2✔
421
        return name
2✔
422

423
    def clean(self, *args, **kwargs):
2✔
424
        """Check if writer who is controlled still has enough data."""
425
        super().clean(*args, **kwargs)
2✔
426
        if self.pk is None or self._can_be_controlled:
2✔
427
            return
2✔
428
        # A controlled writer requires more data, so once a writer is in
429
        # that role, it is not allowed to remove required data."""
430
        if self.writerinwork_set.filter(controlled=True).exists():
2✔
431
            raise ValidationError(
2✔
432
                'This writer is controlled in at least one work. '
433
                + 'Required fields are: Last name, IPI name and PR society. '
434
                + 'See "Writers" in the user manual.'
435
            )
436

437
    @property
2✔
438
    def writer_id(self):
2✔
439
        """
440
        Writer ID for CWR
441

442
        Returns:
443
            str: formatted writer ID
444
        """
445
        if self.id:
2✔
446
            return 'W{:06d}'.format(self.id)
2✔
447
        return ''
2✔
448

449
    def get_dict(self):
2✔
450
        """Create a data structure that can be serialized as JSON.
451

452
        Returns:
453
            dict: JSON-serializable data structure
454
        """
455

456
        d = {
2✔
457
            'id': self.id,
458
            'code': self.writer_id,
459
            'first_name': self.first_name or None,
460
            'last_name': self.last_name or None,
461
            'ipi_name_number': self.ipi_name or None,
462
            'ipi_base_number': self.ipi_base or None,
463
            'account_number': self.account_number,
464
            'affiliations': [],
465
        }
466
        if self.pr_society:
2✔
467
            d['affiliations'].append(
2✔
468
                {
469
                    'organization': {
470
                        'code': self.pr_society,
471
                        'name': self.get_pr_society_display().split(',')[0],
472
                    },
473
                    'affiliation_type': {
474
                        'code': 'PR',
475
                        'name': 'Performance Rights',
476
                    },
477
                    'territory': WORLD_DICT,
478
                }
479
            )
480
        if self.mr_society:
2✔
481
            d['affiliations'].append(
2✔
482
                {
483
                    'organization': {
484
                        'code': self.mr_society,
485
                        'name': self.get_mr_society_display().split(',')[0],
486
                    },
487
                    'affiliation_type': {
488
                        'code': 'MR',
489
                        'name': 'Mechanical Rights',
490
                    },
491
                    'territory': WORLD_DICT,
492
                }
493
            )
494
        if self.sr_society:
2✔
495
            d['affiliations'].append(
2✔
496
                {
497
                    'organization': {
498
                        'code': self.sr_society,
499
                        'name': self.get_sr_society_display().split(',')[0],
500
                    },
501
                    'affiliation_type': {
502
                        'code': 'SR',
503
                        'name': 'Synchronization Rights',
504
                    },
505
                    'territory': WORLD_DICT,
506
                }
507
            )
508
        return d
2✔
509

510

511
class WorkManager(models.Manager):
2✔
512
    """Manager for class :class:`.models.Work`"""
513

514
    def get_queryset(self):
2✔
515
        """
516
        Get an optimized queryset.
517

518
        Returns:
519
            django.db.models.query.QuerySet: Queryset with instances of \
520
            :class:`.models.Work`
521
        """
522
        return super().get_queryset().prefetch_related('writers')
2✔
523

524
    def get_dict(self, qs):
2✔
525
        """
526
        Return a dictionary with works from the queryset
527

528
        Args:
529
            qs(django.db.models.query import QuerySet)
530

531
        Returns:
532
            dict: dictionary with works
533

534
        """
535
        qs = qs.prefetch_related('alternatetitle_set')
2✔
536
        qs = qs.prefetch_related('writerinwork_set__writer')
2✔
537
        qs = qs.prefetch_related('artistinwork_set__artist')
2✔
538
        qs = qs.prefetch_related('library_release__library')
2✔
539
        qs = qs.prefetch_related('recordings__record_label')
2✔
540
        qs = qs.prefetch_related('recordings__artist')
2✔
541
        qs = qs.prefetch_related('recordings__tracks__release__library')
2✔
542
        qs = qs.prefetch_related('recordings__tracks__release__release_label')
2✔
543
        qs = qs.prefetch_related('workacknowledgement_set')
2✔
544

545
        works = []
2✔
546

547
        for work in qs:
2✔
548
            j = work.get_dict()
2✔
549
            works.append(j)
2✔
550

551
        return {
2✔
552
            'works': works,
553
        }
554

555

556
class Work(TitleBase):
2✔
557
    """Concrete class, with references to foreign objects.
558

559
    Attributes:
560
        _work_id (django.db.models.CharField): permanent work id, either \
561
        imported or fixed when exports are created
562
        iswc (django.db.models.CharField): ISWC
563
        original_title (django.db.models.CharField): title of the original \
564
            work, implies modified work
565
        release_label (django.db.models.ForeignKey): Foreign key to \
566
            :class:`.models.LibraryRelease`
567
        last_change (django.db.models.DateTimeField):
568
            when the last change was made to this object or any of the child
569
            objects, basically used in filtering
570
        artists (django.db.models.ManyToManyField):
571
            Artists performing the work
572
        writers (django.db.models.ManyToManyField):
573
            Writers who created the work
574
        objects (WorkManager): Database Manager
575
    """
576

577
    class Meta:
2✔
578
        verbose_name = 'Musical Work'
2✔
579
        ordering = ('-id',)
2✔
580
        permissions = (
2✔
581
            ('can_process_royalties', 'Can perform royalty calculations'),
582
        )
583

584
    @staticmethod
2✔
585
    def persist_work_ids(qs):
2✔
586
        qs = qs.prefetch_related('recordings')
2✔
587
        for work in qs.filter(_work_id__isnull=True):
2✔
588
            work.work_id = work.work_id
2✔
589
            work.save()
2✔
590
            for rec in work.recordings.all():
2✔
591
                if rec._recording_id is None:
2✔
592
                    rec.recording_id = rec.recording_id
2✔
593

594
    _work_id = models.CharField(
2✔
595
        'Work ID',
596
        max_length=14,
597
        blank=True,
598
        null=True,
599
        unique=True,
600
        editable=False,
601
        validators=(CWRFieldValidator('name'),),
602
    )
603
    iswc = models.CharField(
2✔
604
        'ISWC',
605
        max_length=15,
606
        blank=True,
607
        null=True,
608
        unique=True,
609
        validators=(CWRFieldValidator('iswc'),),
610
    )
611
    original_title = models.CharField(
2✔
612
        verbose_name='Title of original work',
613
        max_length=60,
614
        db_index=True,
615
        blank=True,
616
        help_text='Use only for modification of existing works.',
617
        validators=(CWRFieldValidator('title'),),
618
    )
619
    library_release = models.ForeignKey(
2✔
620
        'LibraryRelease',
621
        on_delete=models.PROTECT,
622
        blank=True,
623
        null=True,
624
        related_name='works',
625
        verbose_name='Library release',
626
    )
627
    last_change = models.DateTimeField(
2✔
628
        'Last Edited', editable=False, null=True
629
    )
630
    artists = models.ManyToManyField('Artist', through='ArtistInWork')
2✔
631
    writers = models.ManyToManyField(
2✔
632
        'Writer', through='WriterInWork', related_name='works'
633
    )
634

635
    objects = WorkManager()
2✔
636

637
    @property
2✔
638
    def work_id(self):
2✔
639
        """Create Work ID used in registrations.
640

641
        Returns:
642
            str: Internal Work ID
643
        """
644
        if self._work_id:
2✔
645
            return self._work_id
2✔
646
        if self.id is None:
2✔
647
            return ''
2✔
648
        return '{}{:06}'.format(
2✔
649
            settings.PUBLISHER_CODE,
650
            self.id,
651
        )
652

653
    @work_id.setter
2✔
654
    def work_id(self, value):
2✔
655
        if self._work_id is not None:
2✔
656
            raise NotImplementedError('work_id can not be changed')
×
657
        if value:
2✔
658
            self._work_id = value
2✔
659

660
    def is_modification(self):
2✔
661
        """
662
        Check if the work is a modification.
663

664
        Returns:
665
            bool: True if modification, False if original
666
        """
667
        return bool(self.original_title)
2✔
668

669
    def clean_fields(self, *args, **kwargs):
2✔
670
        """Deal with various ways ISWC is written."""
671
        if self.iswc:
2✔
672
            # CWR 2.x holds ISWC in TNNNNNNNNNC format
673
            # CWR 3.0 holds ISWC in T-NNNNNNNNN-C format
674
            # sometimes it comes in T-NNN.NNN.NNN-C format
675
            self.iswc = self.iswc.replace('-', '').replace('.', '')
2✔
676
        return super().clean_fields(*args, **kwargs)
2✔
677

678
    def writer_last_names(self):
2✔
679
        writers = sorted(set(self.writers.all()), key=lambda w: w.last_name)
2✔
680
        return ' / '.join(w.last_name.upper() for w in writers)
2✔
681

682
    def __str__(self):
2✔
683
        return '{}: {} ({})'.format(
2✔
684
            self.work_id, self.title.upper(), self.writer_last_names()
685
        )
686

687
    @staticmethod
2✔
688
    def get_publisher_dict():
2✔
689
        """Create data structure for the publisher.
690

691
        Returns:
692
            dict: JSON-serializable data structure
693
        """
694
        j = {
2✔
695
            'id': 1,
696
            'code': settings.PUBLISHER_CODE,
697
            'name': settings.PUBLISHER_NAME,
698
            'ipi_name_number': settings.PUBLISHER_IPI_NAME,
699
            'ipi_base_number': settings.PUBLISHER_IPI_BASE,
700
            'affiliations': [
701
                {
702
                    'organization': {
703
                        'code': settings.PUBLISHER_SOCIETY_PR,
704
                        'name': SOCIETY_DICT.get(
705
                            settings.PUBLISHER_SOCIETY_PR, ''
706
                        ).split(',')[0],
707
                    },
708
                    'affiliation_type': {
709
                        'code': 'PR',
710
                        'name': 'Performance Rights',
711
                    },
712
                    'territory': WORLD_DICT,
713
                }
714
            ],
715
        }
716

717
        # append MR data to affiliations id needed
718
        if settings.PUBLISHER_SOCIETY_MR:
2✔
719
            j['affiliations'].append(
2✔
720
                {
721
                    'organization': {
722
                        'code': settings.PUBLISHER_SOCIETY_MR,
723
                        'name': SOCIETY_DICT.get(
724
                            settings.PUBLISHER_SOCIETY_MR, ''
725
                        ).split(',')[0],
726
                    },
727
                    'affiliation_type': {
728
                        'code': 'MR',
729
                        'name': 'Mechanical Rights',
730
                    },
731
                    'territory': WORLD_DICT,
732
                }
733
            )
734

735
        # append SR data to affiliations id needed
736
        if settings.PUBLISHER_SOCIETY_SR:
2✔
737
            j['affiliations'].append(
2✔
738
                {
739
                    'organization': {
740
                        'code': settings.PUBLISHER_SOCIETY_SR,
741
                        'name': SOCIETY_DICT.get(
742
                            settings.PUBLISHER_SOCIETY_SR, ''
743
                        ).split(',')[0],
744
                    },
745
                    'affiliation_type': {
746
                        'code': 'SR',
747
                        'name': 'Synchronization Rights',
748
                    },
749
                    'territory': WORLD_DICT,
750
                }
751
            )
752

753
        return j
2✔
754

755
    def get_dict(self, with_recordings=True):
2✔
756
        """Create a data structure that can be serialized as JSON.
757

758
        Normalize the structure if required.
759

760
        Returns:
761
            dict: JSON-serializable data structure
762
        """
763

764
        j = {
2✔
765
            'id': self.id,
766
            'code': self.work_id,
767
            'work_title': self.title,
768
            'last_change': self.last_change,
769
            'version_type': {
770
                'code': 'MOD',
771
                'name': 'Modified Version of a musical work',
772
            }
773
            if self.original_title
774
            else {
775
                'code': 'ORI',
776
                'name': 'Original Work',
777
            },
778
            'iswc': self.iswc,
779
            'other_titles': [
780
                at.get_dict() for at in self.alternatetitle_set.all()
781
            ],
782
            'origin': (
783
                self.library_release.get_origin_dict()
784
                if self.library_release
785
                else None
786
            ),
787
            'writers': [],
788
            'performing_artists': [],
789
            'original_works': [],
790
            'cross_references': [],
791
        }
792

793
        if self.original_title:
2✔
794
            d = {'work_title': self.original_title}
2✔
795
            j['original_works'].append(d)
2✔
796

797
        # add data for (live) artists in work, normalize of required
798
        for aiw in self.artistinwork_set.all():
2✔
799
            d = aiw.get_dict()
2✔
800
            j['performing_artists'].append(d)
2✔
801

802
        # add data for writers in work, normalize of required
803
        for wiw in self.writerinwork_set.all():
2✔
804
            d = wiw.get_dict()
2✔
805
            j['writers'].append(d)
2✔
806

807
        if with_recordings:
2✔
808
            j['recordings'] = [
2✔
809
                recording.get_dict(with_releases=True)
810
                for recording in self.recordings.all()
811
            ]
812

813
        # add cross references, currently only society work ids from ACKs
814
        used_society_codes = []
2✔
815
        for wa in self.workacknowledgement_set.all():
2✔
816
            if not wa.remote_work_id:
2✔
817
                continue
2✔
818
            if wa.society_code in used_society_codes:
2✔
819
                continue
2✔
820
            used_society_codes.append(wa.society_code)
2✔
821
            d = wa.get_dict()
2✔
822
            j['cross_references'].append(d)
2✔
823

824
        return j
2✔
825

826

827
class AlternateTitle(TitleBase):
2✔
828
    """Concrete class for alternate titles.
829

830
    Attributes:
831
        work (django.db.models.ForeignKey): Foreign key to Work model
832
        suffix (django.db.models.BooleanField): implies that the title should\
833
            be appended to the work title
834
    """
835

836
    work = models.ForeignKey(Work, on_delete=models.CASCADE)
2✔
837
    suffix = models.BooleanField(
2✔
838
        default=False,
839
        help_text='Select if this title is only a suffix to the main title.',
840
    )
841

842
    class Meta:
2✔
843
        unique_together = (('work', 'title'),)
2✔
844
        ordering = ('-suffix', 'title')
2✔
845
        verbose_name = 'Alternative Title'
2✔
846

847
    def get_dict(self):
2✔
848
        """Create a data structure that can be serialized as JSON.
849

850
        Returns:
851
            dict: JSON-serializable data structure
852
        """
853
        return {
2✔
854
            'title': str(self),
855
            'title_type': {
856
                'code': 'AT',
857
                'name': 'Alternative Title',
858
            },
859
        }
860

861
    def __str__(self):
2✔
862
        if self.suffix:
2✔
863
            return '{} {}'.format(self.work.title, self.title)
2✔
864
        return super().__str__()
2✔
865

866

867
class ArtistInWork(models.Model):
2✔
868
    """Artist performing the work (live in CWR 3).
869

870
    Attributes:
871
        artist (django.db.models.ForeignKey): FK to Artist
872
        work (django.db.models.ForeignKey): FK to Work
873
    """
874

875
    work = models.ForeignKey(Work, on_delete=models.CASCADE)
2✔
876
    artist = models.ForeignKey(Artist, on_delete=models.PROTECT)
2✔
877

878
    class Meta:
2✔
879
        verbose_name = 'Artist performing'
2✔
880
        verbose_name_plural = (
2✔
881
            'Artists performing (not mentioned in recordings section)'
882
        )
883
        unique_together = (('work', 'artist'),)
2✔
884
        ordering = ('artist__last_name', 'artist__first_name')
2✔
885

886
    def __str__(self):
2✔
887
        return str(self.artist)
2✔
888

889
    def get_dict(self):
2✔
890
        """
891

892
        Returns:
893
            dict: taken from :meth:`models.Artist.get_dict`
894
        """
895
        return {'artist': self.artist.get_dict()}
2✔
896

897

898
class WriterInWork(models.Model):
2✔
899
    """Writers who created this work.
900

901
    At least one writer in work must be controlled.
902
    Sum of relative shares must be (roughly) 100%.
903
    Capacity is limited to roles for original writers.
904

905
    Attributes:
906
        work (django.db.models.ForeignKey): FK to Work
907
        writer (django.db.models.ForeignKey): FK to Writer
908
        saan (django.db.models.CharField): Society-assigned agreement number
909
            between the writer and the original publisher, please note that
910
            this field is for SPECIFIC agreements, for a general agreement,
911
            use :attr:`.base.IPIBase.saan`
912
        controlled (django.db.models.BooleanField): A complete mistery field
913
        relative_share (django.db.models.DecimalField): Initial split among
914
            writers, prior to publishing
915
        capacity (django.db.models.CharField): Role of the writer in this work
916
        publisher_fee (django.db.models.DecimalField): Percentage of royalties
917
            kept by publisher
918
    """
919

920
    class Meta:
2✔
921
        verbose_name = 'Writer in Work'
2✔
922
        verbose_name_plural = 'Writers in Work'
2✔
923
        unique_together = (('work', 'writer', 'controlled'),)
2✔
924
        ordering = (
2✔
925
            '-controlled',
926
            'writer__last_name',
927
            'writer__first_name',
928
            '-id',
929
        )
930

931
    ROLES = (
2✔
932
        ('CA', 'Composer&Lyricist'),
933
        ('C ', 'Composer'),
934
        ('A ', 'Lyricist'),
935
        ('AR', 'Arranger'),
936
        ('AD', 'Adaptor'),
937
        ('TR', 'Translator'),
938
    )
939

940
    work = models.ForeignKey(Work, on_delete=models.CASCADE)
2✔
941
    writer = models.ForeignKey(
2✔
942
        Writer, on_delete=models.PROTECT, blank=True, null=True
943
    )
944
    saan = models.CharField(
2✔
945
        'Society-assigned specific agreement number',
946
        help_text='Use this field for specific agreements only.'
947
        'For general agreements use the field in the Writer form.',
948
        max_length=14,
949
        blank=True,
950
        null=True,
951
        validators=(CWRFieldValidator('saan'),),
952
    )
953
    controlled = models.BooleanField(default=False)
2✔
954
    relative_share = models.DecimalField(
2✔
955
        'Manuscript share', max_digits=5, decimal_places=2
956
    )
957
    capacity = models.CharField(
2✔
958
        'Role', max_length=2, blank=True, choices=ROLES
959
    )
960
    publisher_fee = models.DecimalField(
2✔
961
        max_digits=5,
962
        decimal_places=2,
963
        blank=True,
964
        null=True,
965
        validators=[MinValueValidator(0), MaxValueValidator(100)],
966
        help_text=(
967
            'Percentage of royalties kept by the publisher,\n'
968
            'in a specific agreement.'
969
        ),
970
    )
971

972
    def __str__(self):
2✔
973
        return str(self.writer)
2✔
974

975
    def clean_fields(self, *args, **kwargs):
2✔
976
        """Turn SAAN into uppercase.
977

978
        Args:
979
            *args: passing through
980
            **kwargs: passing through
981

982
        Returns:
983
            str: SAAN in uppercase
984
        """
985
        if self.saan:
2✔
986
            self.saan = self.saan.upper()
2✔
987
        return super().clean_fields(*args, **kwargs)
2✔
988

989
    def clean(self):
2✔
990
        """Make sure that controlled writers have all the required data.
991

992
        Also check that writers that are not controlled do not have data
993
        that can not apply to them."""
994

995
        if (
2✔
996
            self.writer
997
            and self.writer.generally_controlled
998
            and not self.controlled
999
        ):
1000
            raise ValidationError(
2✔
1001
                {
1002
                    'controlled': (
1003
                        'Must be set for a generally controlled writer.'
1004
                    )
1005
                }
1006
            )
1007
        d = {}
2✔
1008
        if self.controlled:
2✔
1009
            if not self.capacity:
2✔
1010
                d['capacity'] = 'Must be set for a controlled writer.'
2✔
1011
            if not self.writer:
2✔
1012
                d['writer'] = 'Must be set for a controlled writer.'
2✔
1013
            else:
1014
                if not self.writer._can_be_controlled:
2✔
1015
                    d['writer'] = (
2✔
1016
                        'IPI name and PR society must be set. '
1017
                        'See "Writers" in the user manual'
1018
                    )
1019
        else:
1020
            if self.saan:
2✔
1021
                d['saan'] = 'Must be empty if writer is not controlled.'
2✔
1022
            if self.publisher_fee:
2✔
1023
                d[
2✔
1024
                    'publisher_fee'
1025
                ] = 'Must be empty if writer is not controlled.'
1026
        if d:
2✔
1027
            raise ValidationError(d)
2✔
1028

1029
    def get_agreement_dict(self):
2✔
1030
        """Get agreement dictionary for this writer in work."""
1031

1032
        pub_pr_soc = settings.PUBLISHER_SOCIETY_PR
2✔
1033
        pub_pr_name = SOCIETY_DICT.get(pub_pr_soc, '').split(',')[0]
2✔
1034

1035
        if not self.controlled or not self.writer:
2✔
1036
            return None
2✔
1037
        if self.writer.generally_controlled and not self.saan:
2✔
1038
            # General
1039
            return {
2✔
1040
                'recipient_organization': {
1041
                    'code': pub_pr_soc,
1042
                    'name': pub_pr_name,
1043
                },
1044
                'recipient_agreement_number': self.writer.saan,
1045
                'agreement_type': {
1046
                    'code': 'OG',
1047
                    'name': 'Original General',
1048
                },
1049
            }
1050
        else:
1051
            return {
2✔
1052
                'recipient_organization': {
1053
                    'code': pub_pr_soc,
1054
                },
1055
                'recipient_agreement_number': self.saan,
1056
                'agreement_type': {
1057
                    'code': 'OS',
1058
                    'name': 'Original Specific',
1059
                },
1060
            }
1061

1062
    def get_dict(self):
2✔
1063
        """Create a data structure that can be serialized as JSON.
1064

1065
        Returns:
1066
            dict: JSON-serializable data structure
1067
        """
1068

1069
        j = {
2✔
1070
            'writer': self.writer.get_dict() if self.writer else None,
1071
            'controlled': self.controlled,
1072
            'relative_share': str(self.relative_share / 100),
1073
            'writer_role': {
1074
                'code': self.capacity.strip(),
1075
                'name': self.get_capacity_display(),
1076
            }
1077
            if self.capacity
1078
            else None,
1079
            'original_publishers': [
1080
                {
1081
                    'publisher': self.work.get_publisher_dict(),
1082
                    'publisher_role': {
1083
                        'code': 'E',
1084
                        'name': 'Original publisher',
1085
                    },
1086
                    'agreement': self.get_agreement_dict(),
1087
                }
1088
            ]
1089
            if self.controlled
1090
            else [],
1091
        }
1092
        return j
2✔
1093

1094

1095
class Recording(models.Model):
2✔
1096
    """Recording.
1097

1098
    Attributes:
1099
        release_date (django.db.models.DateField): Recording Release Date
1100
        duration (django.db.models.TimeField): Recording Duration
1101
        isrc (django.db.models.CharField):
1102
            International Standard Recording Code
1103
        record_label (django.db.models.CharField): Record Label
1104
    """
1105

1106
    class Meta:
2✔
1107
        verbose_name = 'Recording'
2✔
1108
        verbose_name_plural = 'Recordings'
2✔
1109
        ordering = ('-id',)
2✔
1110

1111
    _recording_id = models.CharField(
2✔
1112
        'Recording ID',
1113
        max_length=14,
1114
        blank=True,
1115
        null=True,
1116
        unique=True,
1117
        editable=False,
1118
        validators=(CWRFieldValidator('name'),),
1119
    )
1120
    recording_title = models.CharField(
2✔
1121
        blank=True, max_length=60, validators=(CWRFieldValidator('title'),)
1122
    )
1123
    recording_title_suffix = models.BooleanField(
2✔
1124
        default=False, help_text='A suffix to the WORK title.'
1125
    )
1126
    version_title = models.CharField(
2✔
1127
        blank=True, max_length=60, validators=(CWRFieldValidator('title'),)
1128
    )
1129
    version_title_suffix = models.BooleanField(
2✔
1130
        default=False, help_text='A suffix to the RECORDING title.'
1131
    )
1132
    release_date = models.DateField(blank=True, null=True)
2✔
1133
    duration = models.DurationField(blank=True, null=True)
2✔
1134
    isrc = models.CharField(
2✔
1135
        'ISRC',
1136
        max_length=15,
1137
        blank=True,
1138
        null=True,
1139
        unique=True,
1140
        validators=(CWRFieldValidator('isrc'),),
1141
    )
1142
    record_label = models.ForeignKey(
2✔
1143
        Label,
1144
        verbose_name='Record label',
1145
        null=True,
1146
        blank=True,
1147
        on_delete=models.PROTECT,
1148
    )
1149
    work = models.ForeignKey(
2✔
1150
        Work, on_delete=models.CASCADE, related_name='recordings'
1151
    )
1152
    artist = models.ForeignKey(
2✔
1153
        Artist,
1154
        verbose_name='Recording Artist',
1155
        related_name='recordings',
1156
        on_delete=models.PROTECT,
1157
        blank=True,
1158
        null=True,
1159
    )
1160

1161
    releases = models.ManyToManyField(Release, through='Track')
2✔
1162

1163
    audio_file = models.FileField(
2✔
1164
        upload_to=upload_to, max_length=255, blank=True
1165
    )
1166

1167
    def clean_fields(self, *args, **kwargs):
2✔
1168
        """
1169
        ISRC cleaning, just removing dots and dashes.
1170

1171
        Args:
1172
            *args: may be used in upstream
1173
            **kwargs: may be used in upstream
1174

1175
        Returns:
1176
            return from :meth:`django.db.models.Model.clean_fields`
1177

1178
        """
1179
        if not any(
2✔
1180
            [
1181
                self.recording_title,
1182
                self.version_title,
1183
                self.release_date,
1184
                self.isrc,
1185
                self.duration,
1186
                self.record_label,
1187
                self.artist,
1188
                self.audio_file,
1189
            ]
1190
        ):
1191
            raise ValidationError('No data left, please delete instead.')
×
1192
        if self.isrc:
2✔
1193
            # Removing all characters added for readability
1194
            self.isrc = self.isrc.replace('-', '').replace('.', '')
2✔
1195
        return super().clean_fields(*args, **kwargs)
2✔
1196

1197
    @property
2✔
1198
    def complete_recording_title(self):
2✔
1199
        """
1200
        Return complete recording title.
1201

1202
        Returns:
1203
            str
1204
        """
1205
        if self.recording_title_suffix:
2✔
1206
            return '{} {}'.format(
2✔
1207
                self.work.title, self.recording_title
1208
            ).strip()
1209
        return self.recording_title
2✔
1210

1211
    @property
2✔
1212
    def complete_version_title(self):
2✔
1213
        """
1214
        Return complete version title.
1215

1216
        Returns:
1217
            str
1218
        """
1219
        if self.version_title_suffix:
2✔
1220
            return '{} {}'.format(
2✔
1221
                self.complete_recording_title or self.work.title,
1222
                self.version_title,
1223
            ).strip()
1224
        return self.version_title
2✔
1225

1226
    @property
2✔
1227
    def title(self):
2✔
1228
        """Generate title from various fields."""
1229
        return (
2✔
1230
            self.complete_version_title
1231
            if self.version_title
1232
            else self.complete_recording_title
1233
            if self.recording_title
1234
            else self.work.title
1235
        )
1236

1237
    @property
2✔
1238
    def recording_id(self):
2✔
1239
        """Create Recording ID used in registrations
1240

1241
        Returns:
1242
            str: Internal Recording ID
1243
        """
1244
        if self._recording_id:
2✔
1245
            return self._recording_id
2✔
1246
        if self.id is None:
2✔
1247
            return ''
2✔
1248
        return '{}{:06}R'.format(
2✔
1249
            settings.PUBLISHER_CODE,
1250
            self.id,
1251
        )
1252

1253
    @recording_id.setter
2✔
1254
    def recording_id(self, value):
2✔
1255
        if self._recording_id is not None:
2✔
1256
            raise NotImplementedError('recording_id can not be changed')
×
1257
        if value:
2✔
1258
            self._recording_id = value
2✔
1259

1260
    def __str__(self):
2✔
1261
        """Return the most precise type of title"""
1262
        if self.artist:
2✔
1263
            return '{}: {} ({})'.format(
2✔
1264
                self.recording_id, self.title, self.artist
1265
            )
1266
        else:
1267
            return '{}: {}'.format(self.recording_id, self.title)
2✔
1268

1269
    def get_dict(self, with_releases=False, with_work=True):
2✔
1270
        """Create a data structure that can be serialized as JSON.
1271

1272
        Args:
1273
            with_releases (bool): add releases data (through tracks)
1274
            with_work (bool): add work data
1275

1276
        Returns:
1277
            dict: JSON-serializable data structure
1278

1279
        """
1280
        j = {
2✔
1281
            'id': self.id,
1282
            'code': self.recording_id,
1283
            'recording_title': self.complete_recording_title
1284
            or self.work.title,
1285
            'version_title': self.complete_version_title,
1286
            'release_date': self.release_date.strftime('%Y%m%d')
1287
            if self.release_date
1288
            else None,
1289
            'duration': duration_string(self.duration)
1290
            if self.duration
1291
            else None,
1292
            'isrc': self.isrc,
1293
            'recording_artist': self.artist.get_dict()
1294
            if self.artist
1295
            else None,
1296
            'record_label': self.record_label.get_dict()
1297
            if self.record_label
1298
            else None,
1299
        }
1300
        if with_releases:
2✔
1301
            j['tracks'] = []
2✔
1302
            for track in self.tracks.all():
2✔
1303
                d = track.release.get_dict()
2✔
1304
                j['tracks'].append(
2✔
1305
                    {
1306
                        'release': d,
1307
                        'cut_number': track.cut_number,
1308
                    }
1309
                )
1310
        if with_work:
2✔
1311
            j['works'] = [{'work': self.work.get_dict(with_recordings=False)}]
2✔
1312
        return j
2✔
1313

1314

1315
class Track(models.Model):
2✔
1316
    """Track, a recording on a release.
1317

1318
    Attributes:
1319
        recording (django.db.models.ForeignKey): Recording
1320
        release (django.db.models.ForeignKey): Release
1321
        cut_number (django.db.models.PositiveSmallIntegerField): Cut Number
1322

1323
    """
1324

1325
    class Meta:
2✔
1326
        verbose_name = 'Track'
2✔
1327
        unique_together = (('recording', 'release'), ('release', 'cut_number'))
2✔
1328
        ordering = (
2✔
1329
            'release',
1330
            'cut_number',
1331
        )
1332

1333
    recording = models.ForeignKey(
2✔
1334
        Recording, on_delete=models.PROTECT, related_name='tracks'
1335
    )
1336
    release = models.ForeignKey(
2✔
1337
        Release, on_delete=models.CASCADE, related_name='tracks'
1338
    )
1339
    cut_number = models.PositiveSmallIntegerField(
2✔
1340
        blank=True,
1341
        null=True,
1342
        validators=(MinValueValidator(1), MaxValueValidator(9999)),
1343
    )
1344

1345
    def get_dict(self):
2✔
1346
        """Create a data structure that can be serialized as JSON.
1347

1348
        Returns:
1349
            dict: JSON-serializable data structure
1350

1351
        """
1352
        return {
2✔
1353
            'cut_number': self.cut_number,
1354
            'recording': self.recording.get_dict(
1355
                with_releases=False, with_work=True
1356
            ),
1357
        }
1358

1359
    def __str__(self):
2✔
1360
        return self.recording.title
×
1361

1362

1363
class DeferCwrManager(models.Manager):
2✔
1364
    """Manager for CWR Exports and ACK Imports.
1365

1366
    Defers :attr:`CWRExport.cwr` and :attr:`AckImport.cwr` fields.
1367

1368
    """
1369

1370
    def get_queryset(self):
2✔
1371
        qs = super().get_queryset()
2✔
1372
        qs = qs.defer('cwr')
2✔
1373
        return qs
2✔
1374

1375

1376
class CWRExport(models.Model):
2✔
1377
    """Export in CWR format.
1378

1379
    Common Works Registration format is a standard format for registration of
1380
    musical works world-wide. Exports are available in CWR 2.1 revision 8 and
1381
    CWR 3.0 (experimental).
1382

1383
    Attributes:
1384
        nwr_rev (django.db.models.CharField): choice field where user can
1385
            select which version and type of CWR it is
1386
        cwr (django.db.models.TextField): contents of CWR file
1387
        year (django.db.models.CharField): 2-digit year format
1388
        num_in_year (django.db.models.PositiveSmallIntegerField): \
1389
        CWR sequential number in a year
1390
        works (django.db.models.ManyToManyField): included works
1391
        description (django.db.models.CharField): internal note
1392

1393
    """
1394

1395
    class Meta:
2✔
1396
        verbose_name = 'CWR Export'
2✔
1397
        verbose_name_plural = 'CWR Exports'
2✔
1398
        ordering = ('-id',)
2✔
1399

1400
    objects = DeferCwrManager()
2✔
1401

1402
    nwr_rev = models.CharField(
2✔
1403
        'CWR version/type',
1404
        max_length=3,
1405
        db_index=True,
1406
        default='NWR',
1407
        choices=(
1408
            ('NWR', 'CWR 2.1: New work registrations'),
1409
            ('REV', 'CWR 2.1: Revisions of registered works'),
1410
            ('NW2', 'CWR 2.2: New work registrations'),
1411
            ('RE2', 'CWR 2.2: Revisions of registered works'),
1412
            ('WRK', 'CWR 3.0: Work registration'),
1413
            ('ISR', 'CWR 3.0: ISWC request (EDI)'),
1414
            ('WR1', 'CWR 3.1 DRAFT: Work registration'),
1415
        ),
1416
    )
1417
    cwr = models.TextField(blank=True, editable=False)
2✔
1418
    created_on = models.DateTimeField(editable=False, null=True)
2✔
1419
    year = models.CharField(
2✔
1420
        max_length=2, db_index=True, editable=False, blank=True
1421
    )
1422
    num_in_year = models.PositiveSmallIntegerField(default=0)
2✔
1423
    works = models.ManyToManyField(Work, related_name='cwr_exports')
2✔
1424
    description = models.CharField('Internal Note', blank=True, max_length=60)
2✔
1425

1426
    publisher_code = None
2✔
1427
    agreement_pr = settings.PUBLISHING_AGREEMENT_PUBLISHER_PR
2✔
1428
    agreement_mr = settings.PUBLISHING_AGREEMENT_PUBLISHER_MR
2✔
1429
    agreement_sr = settings.PUBLISHING_AGREEMENT_PUBLISHER_SR
2✔
1430

1431
    @property
2✔
1432
    def version(self):
2✔
1433
        """Return CWR version."""
1434
        if self.nwr_rev in ['WRK', 'ISR']:
2✔
1435
            return '30'
2✔
1436
        elif self.nwr_rev == 'WR1':
2✔
1437
            return '31'
2✔
1438
        elif self.nwr_rev in ['NW2', 'RE2']:
2✔
1439
            return '22'
2✔
1440
        return '21'
2✔
1441

1442
    @property
2✔
1443
    def filename(self):
2✔
1444
        """Return CWR file name.
1445

1446
        Returns:
1447
            str: CWR file name
1448
        """
1449
        if self.version in ['30', '31']:
2✔
1450
            return self.filename3
2✔
1451
        return self.filename2
2✔
1452

1453
    @property
2✔
1454
    def filename3(self):
2✔
1455
        """Return proper CWR 3.x filename.
1456

1457
        Format is: CWYYnnnnSUB_REP_VM - m - r.EXT
1458

1459
        Returns:
1460
            str: CWR file name
1461
        """
1462
        if self.version == '30':
2✔
1463
            minor_version = '0-0'
2✔
1464
        else:
1465
            minor_version = '1-0'
2✔
1466
        if self.nwr_rev == 'ISR':
2✔
1467
            ext = 'ISR'
2✔
1468
        else:
1469
            ext = 'SUB'
2✔
1470
        return 'CW{}{:04}{}_0000_V3-{}.{}'.format(
2✔
1471
            self.year,
1472
            self.num_in_year,
1473
            self.publisher_code or settings.PUBLISHER_CODE,
1474
            minor_version,
1475
            ext,
1476
        )
1477

1478
    @property
2✔
1479
    def filename2(self):
2✔
1480
        """Return proper CWR 2.x filename.
1481

1482
        Returns:
1483
            str: CWR file name
1484
        """
1485
        return 'CW{}{:04}{}_000.V{}'.format(
2✔
1486
            self.year,
1487
            self.num_in_year,
1488
            self.publisher_code or settings.PUBLISHER_CODE,
1489
            self.version,
1490
        )
1491

1492
    def __str__(self):
2✔
1493
        return self.filename
2✔
1494

1495
    def get_record(self, key, record):
2✔
1496
        """Create CWR record (row) from the key and dict.
1497

1498
        Args:
1499
            key (str): type of record
1500
            record (dict): field values
1501

1502
        Returns:
1503
            str: CWR record (row)
1504
        """
1505
        if self.version == '30':
2✔
1506
            template = TEMPLATES_30.get(key)
2✔
1507
        elif self.version == '31':
2✔
1508
            template = TEMPLATES_31.get(key)
2✔
1509
        else:
1510
            if self.version == '22':
2✔
1511
                tdict = TEMPLATES_22
2✔
1512
            else:
1513
                tdict = TEMPLATES_21
2✔
1514
            if key == 'HDR' and len(record['ipi_name_number'].lstrip('0')) > 9:
2✔
1515
                # CWR 2.1 revision 8 "hack" for 10+ digit IPI name numbers
1516
                template = tdict.get('HDR_8')
2✔
1517
            else:
1518
                template = tdict.get(key)
2✔
1519
        record.update({'settings': settings})
2✔
1520
        return template.render(Context(record)).upper()
2✔
1521

1522
    def get_transaction_record(self, key, record):
2✔
1523
        """Create CWR transaction record (row) from the key and dict.
1524

1525
        This methods adds transaction and record sequences.
1526

1527
        Args:
1528
            key (str): type of record
1529
            record (dict): field values
1530

1531
        Returns:
1532
            str: CWR record (row)
1533
        """
1534
        record['transaction_sequence'] = self.transaction_count
2✔
1535
        record['record_sequence'] = self.record_sequence
2✔
1536
        line = self.get_record(key, record)
2✔
1537
        if line:
2✔
1538
            self.record_count += 1
2✔
1539
            self.record_sequence += 1
2✔
1540
        return line
2✔
1541

1542
    def yield_iswc_request_lines(self, works):
2✔
1543
        """Yield lines for an ISR (ISWC request) in CWR 3.x"""
1544

1545
        for work in works:
2✔
1546

1547
            # ISR
1548
            self.record_sequence = 0
2✔
1549
            if work['iswc']:
2✔
1550
                work['indicator'] = 'U'
2✔
1551
            yield self.get_transaction_record('ISR', work)
2✔
1552

1553
            # WRI
1554
            reported = set()
2✔
1555
            for wiw in work['writers']:
2✔
1556
                w = wiw['writer']
2✔
1557
                if not w:
2✔
1558
                    continue  # goes to OWR
2✔
1559
                tup = (w['code'], wiw['writer_role']['code'])
2✔
1560
                if tup in reported:
2✔
1561
                    continue
2✔
1562
                reported.add(tup)
2✔
1563
                w.update(
2✔
1564
                    {
1565
                        'writer_role': wiw['writer_role']['code'],
1566
                    }
1567
                )
1568
                yield self.get_transaction_record('WRI', w)
2✔
1569

1570
            self.transaction_count += 1
2✔
1571

1572
    def yield_publisher_lines(self, publisher, controlled_relative_share):
2✔
1573
        """Yield SPU/SPT lines.
1574

1575
        Args:
1576
            publisher (dict): dictionary with publisher data
1577
            controlled_relative_share (Decimal): sum of manuscript shares \
1578
            for controlled writers
1579

1580
        Yields:
1581
              str: CWR record (row/line)
1582
        """
1583
        affiliations = publisher.get('affiliations', [])
2✔
1584
        for aff in affiliations:
2✔
1585
            if aff['affiliation_type']['code'] == 'PR':
2✔
1586
                publisher['pr_society'] = aff['organization']['code']
2✔
1587
            elif aff['affiliation_type']['code'] == 'MR':
2✔
1588
                publisher['mr_society'] = aff['organization']['code']
2✔
1589
            elif aff['affiliation_type']['code'] == 'SR':
2✔
1590
                publisher['sr_society'] = aff['organization']['code']
2✔
1591

1592
        pr_share = controlled_relative_share * self.agreement_pr
2✔
1593
        mr_share = controlled_relative_share * self.agreement_mr
2✔
1594
        sr_share = controlled_relative_share * self.agreement_sr
2✔
1595
        yield self.get_transaction_record(
2✔
1596
            'SPU',
1597
            {
1598
                'chain_sequence': 1,
1599
                'name': publisher.get('name'),
1600
                'code': publisher.get('code'),
1601
                'ipi_name_number': publisher.get('ipi_name_number'),
1602
                'ipi_base_number': publisher.get('ipi_base_number'),
1603
                'pr_society': publisher.get('pr_society'),
1604
                'mr_society': publisher.get('mr_society'),
1605
                'sr_society': publisher.get('sr_society'),
1606
                'pr_share': pr_share,
1607
                'mr_share': mr_share,
1608
                'sr_share': sr_share,
1609
            },
1610
        )
1611
        if controlled_relative_share:
2✔
1612
            yield self.get_transaction_record(
2✔
1613
                'SPT',
1614
                {
1615
                    'code': publisher.get('code'),
1616
                    'pr_share': pr_share,
1617
                    'mr_share': mr_share,
1618
                    'sr_share': sr_share,
1619
                    'pr_society': publisher.get('pr_society'),
1620
                    'mr_society': publisher.get('mr_society'),
1621
                    'sr_society': publisher.get('sr_society'),
1622
                },
1623
            )
1624

1625
    def yield_registration_lines(self, works):
2✔
1626
        """Yield lines for CWR registrations (WRK in 3.x, NWR and REV in 2.x)
1627

1628
        Args:
1629
            works (list): list of work dicts
1630

1631
        Yields:
1632
            str: CWR record (row/line)
1633
        """
1634
        for work in works:
2✔
1635

1636
            # WRK
1637
            self.record_sequence = 0
2✔
1638
            if self.version == '22':
2✔
1639
                if self.nwr_rev == 'NW2':
2✔
1640
                    record_type = 'NWR'
2✔
1641
                elif self.nwr_rev == 'RE2':
2✔
1642
                    record_type = 'REV'
2✔
1643
            else:
1644
                record_type = self.nwr_rev
2✔
1645

1646
            d = {
2✔
1647
                'record_type': record_type,
1648
                'code': work['code'],
1649
                'work_title': work['work_title'],
1650
                'iswc': work['iswc'],
1651
                'recorded_indicator': 'Y' if work['recordings'] else 'U',
1652
                'version_type': (
1653
                    'MOD   UNSUNS'
1654
                    if work['version_type']['code'] == 'MOD'
1655
                    else 'ORI         '
1656
                ),
1657
            }
1658
            yield self.get_transaction_record('WRK', d)
2✔
1659
            yield from self.get_party_lines(work)
2✔
1660
            yield from self.get_other_lines(work)
2✔
1661
            self.transaction_count += 1
2✔
1662

1663
    def yield_other_publisher_lines(self, other_publisher_share):
2✔
1664
        if other_publisher_share:
2✔
1665
            pr_share = other_publisher_share * self.agreement_pr
2✔
1666
            mr_share = other_publisher_share * self.agreement_mr
2✔
1667
            sr_share = other_publisher_share * self.agreement_sr
2✔
1668
            yield self.get_transaction_record(
2✔
1669
                'OPU',
1670
                {
1671
                    'sequence': 2,
1672
                    'pr_share': pr_share,
1673
                    'mr_share': mr_share,
1674
                    'sr_share': sr_share,
1675
                },
1676
            )
1677
            yield self.get_transaction_record(
2✔
1678
                'OPT',
1679
                {
1680
                    'pr_share': pr_share,
1681
                    'mr_share': mr_share,
1682
                    'sr_share': sr_share,
1683
                },
1684
            )
1685

1686
    def calculate_publisher_shares(self, work):
2✔
1687
        controlled_relative_share = Decimal(0)  # total pub share
2✔
1688
        other_publisher_share = Decimal(0)  # used for co-publishing
2✔
1689
        controlled_writer_ids = set()  # used for co-publishing
2✔
1690
        copublished_writer_ids = set()  # used for co-publishing
2✔
1691
        controlled_shares = defaultdict(Decimal)
2✔
1692
        for wiw in work['writers']:
2✔
1693
            if wiw['controlled']:
2✔
1694
                controlled_writer_ids.add(wiw['writer']['code'])
2✔
1695
        for wiw in work['writers']:
2✔
1696
            writer = wiw['writer']
2✔
1697
            share = Decimal(wiw['relative_share'])
2✔
1698
            if wiw['controlled']:
2✔
1699
                key = writer['code']
2✔
1700
                controlled_relative_share += share
2✔
1701
                controlled_shares[key] += share
2✔
1702
            elif writer and writer['code'] in controlled_writer_ids:
2✔
1703
                key = writer['code']
2✔
1704
                copublished_writer_ids.add(key)
2✔
1705
                other_publisher_share += share
2✔
1706
                controlled_shares[key] += share
2✔
1707
        return (
2✔
1708
            controlled_relative_share,
1709
            other_publisher_share,
1710
            controlled_shares,
1711
            controlled_writer_ids,
1712
            copublished_writer_ids,
1713
        )
1714

1715
    def yield_controlled_writer_lines(
2✔
1716
        self,
1717
        work,
1718
        publisher,
1719
        controlled_shares,
1720
        copublished_writer_ids,
1721
        other_publisher_share,
1722
    ):
1723
        for wiw in work['writers']:
2✔
1724
            if not wiw['controlled']:
2✔
1725
                continue  # goes to OWR
2✔
1726
            w = wiw['writer']
2✔
1727
            agr = wiw['original_publishers'][0]['agreement']
2✔
1728
            saan = agr['recipient_agreement_number'] if agr else None
2✔
1729
            affiliations = w.get('affiliations', [])
2✔
1730
            for aff in affiliations:
2✔
1731
                if aff['affiliation_type']['code'] == 'PR':
2✔
1732
                    w['pr_society'] = aff['organization']['code']
2✔
1733
                elif aff['affiliation_type']['code'] == 'MR':
2✔
1734
                    w['mr_society'] = aff['organization']['code']
2✔
1735
                elif aff['affiliation_type']['code'] == 'SR':
2✔
1736
                    w['sr_society'] = aff['organization']['code']
2✔
1737
            share = controlled_shares[w['code']]
2✔
1738
            pr_share = share * (1 - self.agreement_pr)
2✔
1739
            mr_share = share * (1 - self.agreement_mr)
2✔
1740
            sr_share = share * (1 - self.agreement_sr)
2✔
1741
            w.update(
2✔
1742
                {
1743
                    'writer_role': wiw['writer_role']['code'],
1744
                    'share': share,
1745
                    'pr_share': pr_share,
1746
                    'mr_share': mr_share,
1747
                    'sr_share': sr_share,
1748
                    'saan': saan,
1749
                    'original_publishers': wiw['original_publishers'],
1750
                }
1751
            )
1752
            yield self.get_transaction_record('SWR', w)
2✔
1753
            if share:
2✔
1754
                yield self.get_transaction_record('SWT', w)
2✔
1755
            if share:
2✔
1756
                yield self.get_transaction_record('MAN', w)
2✔
1757
            w['publisher_sequence'] = 1
2✔
1758
            w['publisher_code'] = publisher['code']
2✔
1759
            w['publisher_name'] = publisher['name']
2✔
1760
            yield self.get_transaction_record('PWR', w)
2✔
1761
            if (
2✔
1762
                self.version in ['30', '31']
1763
                and other_publisher_share
1764
                and w
1765
                and w['code'] in copublished_writer_ids
1766
            ):
1767
                w['publisher_sequence'] = 2
2✔
1768
                yield self.get_transaction_record(
2✔
1769
                    'PWR', {'code': w['code'], 'publisher_sequence': 2}
1770
                )
1771

1772
    def yield_other_writer_lines(
2✔
1773
        self, work, controlled_writer_ids, other_publisher_share
1774
    ):
1775
        for wiw in work['writers']:
2✔
1776
            if wiw['controlled']:
2✔
1777
                continue  # done in SWR
2✔
1778
            writer = wiw['writer']
2✔
1779
            if writer and writer['code'] in controlled_writer_ids:
2✔
1780
                continue  # co-publishing, already solved
2✔
1781
            if writer:
2✔
1782
                w = wiw['writer']
2✔
1783
                affiliations = w.get('affiliations', [])
2✔
1784
                for aff in affiliations:
2✔
1785
                    if aff['affiliation_type']['code'] == 'PR':
2✔
1786
                        w['pr_society'] = aff['organization']['code']
2✔
1787
                    elif aff['affiliation_type']['code'] == 'MR':
2✔
1788
                        w['mr_society'] = aff['organization']['code']
2✔
1789
                    elif aff['affiliation_type']['code'] == 'SR':
2✔
1790
                        w['sr_society'] = aff['organization']['code']
2✔
1791
            else:
1792
                w = {'writer_unknown_indicator': 'Y'}
2✔
1793
            share = Decimal(wiw['relative_share'])
2✔
1794
            w.update(
2✔
1795
                {
1796
                    'writer_role': (
1797
                        wiw['writer_role']['code']
1798
                        if wiw['writer_role']
1799
                        else None
1800
                    ),
1801
                    'share': share,
1802
                    'pr_share': share,
1803
                    'mr_share': share,
1804
                    'sr_share': share,
1805
                }
1806
            )
1807
            yield self.get_transaction_record('OWR', w)
2✔
1808
            if w['share']:
2✔
1809
                yield self.get_transaction_record('OWT', w)
2✔
1810
            if w['share']:
2✔
1811
                yield self.get_transaction_record('MAN', w)
2✔
1812
            if self.version in ['30', '31'] and other_publisher_share:
2✔
1813
                w['publisher_sequence'] = 2
2✔
1814
                yield self.get_transaction_record('PWR', w)
2✔
1815

1816
    def get_party_lines(self, work):
2✔
1817
        """Yield SPU, SPT, OPU, SWR, SWT, OPT and PWR lines
1818

1819
        Args:
1820
            work: musical work
1821

1822
        Yields:
1823
            str: CWR record (row/line)
1824
        """
1825

1826
        # SPU, SPT
1827
        (
2✔
1828
            controlled_relative_share,
1829
            other_publisher_share,
1830
            controlled_shares,
1831
            controlled_writer_ids,
1832
            copublished_writer_ids,
1833
        ) = self.calculate_publisher_shares(work)
1834
        publisher = work['writers'][0]['original_publishers'][0]['publisher']
2✔
1835
        yield from self.yield_publisher_lines(
2✔
1836
            publisher, controlled_relative_share
1837
        )
1838
        yield from self.yield_other_publisher_lines(other_publisher_share)
2✔
1839

1840
        # SWR, SWT, PWR
1841

1842
        yield from self.yield_controlled_writer_lines(
2✔
1843
            work,
1844
            publisher,
1845
            controlled_shares,
1846
            copublished_writer_ids,
1847
            other_publisher_share,
1848
        )
1849

1850
        # OWR
1851

1852
        yield from self.yield_other_writer_lines(
2✔
1853
            work, controlled_writer_ids, other_publisher_share
1854
        )
1855

1856
    def get_alt_lines(self, work):
2✔
1857
        alt_titles = set()
2✔
1858
        for at in work['other_titles']:
2✔
1859
            alt_titles.add(at['title'])
2✔
1860
        for rec in work['recordings']:
2✔
1861
            if rec['recording_title']:
2✔
1862
                alt_titles.add(rec['recording_title'])
2✔
1863
            if rec['version_title']:
2✔
1864
                alt_titles.add(rec['version_title'])
2✔
1865
        for alt_title in sorted(alt_titles):
2✔
1866
            if alt_title == work['work_title']:
2✔
1867
                continue
2✔
1868
            yield self.get_transaction_record(
2✔
1869
                'ALT', {'alternate_title': alt_title}
1870
            )
1871

1872
    def get_per_lines(self, work):
2✔
1873
        artists = {}
2✔
1874
        for aiw in work['performing_artists']:
2✔
1875
            artists.update({aiw['artist']['code']: aiw['artist']})
2✔
1876
        for rec in work['recordings']:
2✔
1877
            if not rec['recording_artist']:
2✔
1878
                continue
2✔
1879
            artists.update(
2✔
1880
                {rec['recording_artist']['code']: rec['recording_artist']}
1881
            )
1882
        for artist in artists.values():
2✔
1883
            yield self.get_transaction_record('PER', artist)
2✔
1884

1885
    def get_rec_lines(self, work):
2✔
1886
        for rec in work['recordings']:
2✔
1887
            if rec['recording_artist']:
2✔
1888
                rec['display_artist'] = '{} {}'.format(
2✔
1889
                    rec['recording_artist']['first_name'] or '',
1890
                    rec['recording_artist']['last_name'],
1891
                ).strip()[:60]
1892
            if rec['isrc']:
2✔
1893
                rec['isrc_validity'] = 'Y'
2✔
1894
            if rec['duration']:
2✔
1895
                rec['duration'] = rec['duration'].replace(':', '')[0:6]
×
1896
            if self.version in ['21', '22'] and not any(
2✔
1897
                [
1898
                    rec['release_date'],
1899
                    rec['duration'],
1900
                    rec['isrc'],
1901
                ]
1902
            ):
1903
                continue
2✔
1904
            yield self.get_transaction_record('REC', rec)
2✔
1905

1906
    def get_other_lines(self, work):
2✔
1907
        """Yield ALT and subsequent lines
1908

1909
        Args:
1910
            work: musical work
1911

1912
        Yields:
1913
            str: CWR record (row/line)
1914
        """
1915

1916
        # ALT
1917
        yield from self.get_alt_lines(work)
2✔
1918

1919
        # VER
1920
        if work['version_type']['code'] == 'MOD':
2✔
1921
            yield self.get_transaction_record('OWK', work['original_works'][0])
2✔
1922

1923
        # PER
1924

1925
        yield from self.get_per_lines(work)
2✔
1926

1927
        # REC
1928

1929
        yield from self.get_rec_lines(work)
2✔
1930

1931
        # ORN
1932
        if work['origin']:
2✔
1933
            yield self.get_transaction_record(
2✔
1934
                'ORN',
1935
                {
1936
                    'library': work['origin']['library']['name'],
1937
                    'cd_identifier': work['origin']['cd_identifier'],
1938
                },
1939
            )
1940

1941
        # XRF
1942
        for xrf in work['cross_references']:
2✔
1943
            yield self.get_transaction_record('XRF', xrf)
2✔
1944

1945
    def get_header(self):
2✔
1946
        """Construct CWR HDR record."""
1947
        return self.get_record(
2✔
1948
            'HDR',
1949
            {
1950
                'creation_date': datetime.now(),
1951
                'filename': self.filename,
1952
                'ipi_name_number': settings.PUBLISHER_IPI_NAME,
1953
                'name': settings.PUBLISHER_NAME,
1954
                'code': settings.PUBLISHER_CODE,
1955
            },
1956
        )
1957

1958
    def yield_lines(self, works):
2✔
1959
        """Yield CWR transaction records (rows/lines) for works
1960

1961
        Args:
1962
            works (query): :class:`.models.Work` query
1963

1964
        Yields:
1965
            str: CWR record (row/line)
1966
        """
1967

1968
        self.record_count = self.record_sequence = self.transaction_count = 0
2✔
1969

1970
        yield self.get_header()
2✔
1971

1972
        if self.nwr_rev == 'NW2':
2✔
1973
            yield self.get_record('GRH', {'transaction_type': 'NWR'})
2✔
1974
        elif self.nwr_rev == 'RE2':
2✔
1975
            yield self.get_record('GRH', {'transaction_type': 'REV'})
2✔
1976
        else:
1977
            yield self.get_record('GRH', {'transaction_type': self.nwr_rev})
2✔
1978

1979
        if self.nwr_rev == 'ISR':
2✔
1980
            lines = self.yield_iswc_request_lines(works)
2✔
1981
        else:
1982
            lines = self.yield_registration_lines(works)
2✔
1983

1984
        for line in lines:
2✔
1985
            yield line
2✔
1986

1987
        yield self.get_record(
2✔
1988
            'GRT',
1989
            {
1990
                'transaction_count': self.transaction_count,
1991
                'record_count': self.record_count + 2,
1992
            },
1993
        )
1994
        yield self.get_record(
2✔
1995
            'TRL',
1996
            {
1997
                'transaction_count': self.transaction_count,
1998
                'record_count': self.record_count + 4,
1999
            },
2000
        )
2001

2002
    def create_cwr(self, publisher_code=None):
2✔
2003
        """Create CWR and save."""
2004
        now = timezone.now()
2✔
2005
        if publisher_code is None:
2✔
2006
            publisher_code = settings.PUBLISHER_CODE
2✔
2007
        self.publisher_code = publisher_code
2✔
2008
        if self.cwr:
2✔
2009
            return
2✔
2010
        self.created_on = now
2✔
2011
        self.year = now.strftime('%y')
2✔
2012
        nr = type(self).objects.filter(year=self.year)
2✔
2013
        nr = nr.order_by('-num_in_year').first()
2✔
2014
        if nr:
2✔
2015
            self.num_in_year = nr.num_in_year + 1
2✔
2016
        else:
2017
            self.num_in_year = 1
2✔
2018
        qs = self.works.order_by(
2✔
2019
            'id',
2020
        )
2021
        works = Work.objects.get_dict(qs)['works']
2✔
2022
        self.cwr = ''.join(self.yield_lines(works))
2✔
2023
        self.save()
2✔
2024
        Work.persist_work_ids(self.works)
2✔
2025

2026

2027
class WorkAcknowledgement(models.Model):
2✔
2028
    """Acknowledgement of work registration.
2029

2030
    Attributes:
2031
        date (django.db.models.DateField): Acknowledgement date
2032
        remote_work_id (django.db.models.CharField): Remote work ID
2033
        society_code (django.db.models.CharField): 3-digit society code
2034
        status (django.db.models.CharField): 2-letter status code
2035
        TRANSACTION_STATUS_CHOICES (tuple): choices for status
2036
        work (django.db.models.ForeignKey): FK to Work
2037
    """
2038

2039
    class Meta:
2✔
2040
        verbose_name = 'Registration Acknowledgement'
2✔
2041
        ordering = ('-date', '-id')
2✔
2042
        index_together = (('society_code', 'remote_work_id'),)
2✔
2043

2044
    TRANSACTION_STATUS_CHOICES = (
2✔
2045
        ('CO', 'Conflict'),
2046
        ('DU', 'Duplicate'),
2047
        ('RA', 'Transaction Accepted'),
2048
        ('AS', 'Registration Accepted'),
2049
        ('AC', 'Registration Accepted with Changes'),
2050
        ('SR', 'Registration Accepted - Ready for Payment'),
2051
        ('CR', 'Registration Accepted with Changes - Ready for Payment'),
2052
        ('RJ', 'Rejected'),
2053
        ('NP', 'No Participation'),
2054
        ('RC', 'Claim rejected'),
2055
        ('NA', 'Rejected - No Society Agreement Number'),
2056
        ('WA', 'Rejected - Wrong Society Agreement Number'),
2057
    )
2058

2059
    work = models.ForeignKey(Work, on_delete=models.PROTECT)
2✔
2060
    society_code = models.CharField('Society', max_length=3, choices=SOCIETIES)
2✔
2061
    date = models.DateField()
2✔
2062
    status = models.CharField(max_length=2, choices=TRANSACTION_STATUS_CHOICES)
2✔
2063
    remote_work_id = models.CharField(
2✔
2064
        'Remote work ID', max_length=20, blank=True, db_index=True
2065
    )
2066

2067
    def get_dict(self):
2✔
2068
        """
2069
        Return dictionary with external work IDs.
2070

2071
        Returns:
2072
            dict: JSON-serializable data structure
2073
        """
2074
        # if not self.remote_work_id:
2075
        #     return None
2076
        j = {
2✔
2077
            'organization': {
2078
                'code': self.society_code,
2079
                'name': self.get_society_code_display().split(',')[0],
2080
            },
2081
            'identifier': self.remote_work_id,
2082
        }
2083
        return j
2✔
2084

2085

2086
class ACKImport(models.Model):
2✔
2087
    """CWR acknowledgement file import.
2088

2089
    Attributes:
2090
        filename (django.db.models.CharField): Description
2091
        society_code (models.CharField): 3-digit society code,
2092
            please note that ``choices`` is not set.
2093
        society_name (models.CharField): Society name,
2094
            used if society code is missing.
2095
        date (django.db.models.DateField): Acknowledgement date
2096
        report (django.db.models.CharField): Basically a log
2097
        cwr (django.db.models.TextField): contents of CWR file
2098
    """
2099

2100
    class Meta:
2✔
2101
        verbose_name = 'CWR ACK Import'
2✔
2102
        ordering = ('-date', '-id')
2✔
2103

2104
    objects = DeferCwrManager()
2✔
2105

2106
    filename = models.CharField(max_length=60, editable=False)
2✔
2107
    society_code = models.CharField(max_length=3, editable=False)
2✔
2108
    society_name = models.CharField(max_length=45, editable=False)
2✔
2109
    date = models.DateField(editable=False)
2✔
2110
    report = models.TextField(editable=False)
2✔
2111
    cwr = models.TextField(blank=True, editable=False)
2✔
2112

2113
    def __str__(self):
2✔
2114
        return self.filename
2✔
2115

2116

2117
class DataImport(models.Model):
2✔
2118
    """Importing basic work data from a CSV file.
2119

2120
    This class just acts as log, the actual logic is in :mod:`.data_import`.
2121
    """
2122

2123
    class Meta:
2✔
2124
        verbose_name = 'Data Import'
2✔
2125
        ordering = ('-date', '-id')
2✔
2126

2127
    filename = models.CharField(max_length=60, editable=False)
2✔
2128
    report = models.TextField(editable=False)
2✔
2129
    date = models.DateTimeField(auto_now_add=True, editable=False)
2✔
2130

2131
    def __str__(self):
2✔
2132
        return self.filename
2✔
2133

2134

2135
def smart_str_conversion(value):
2✔
2136
    """Convert to Title Case only if UPPER CASE."""
2137
    if value.isupper():
2✔
2138
        return value.title()
2✔
2139
    return value
2✔
2140

2141

2142
FORCE_CASE_CHOICES = {
2✔
2143
    'upper': str.upper,
2144
    'title': str.title,
2145
    'smart': smart_str_conversion,
2146
}
2147

2148

2149
@receiver(pre_save)
2✔
2150
def change_case(sender, instance, **kwargs):
2✔
2151
    """Change case of CharFields from :mod:`music_publisher`."""
2152
    force_case = FORCE_CASE_CHOICES.get(settings.OPTION_FORCE_CASE)
2✔
2153
    if not force_case:
2✔
2154
        return
2✔
2155
    if sender._meta.app_label != 'music_publisher':
2✔
2156
        return
2✔
2157
    for field in instance._meta.get_fields():
2✔
2158
        if isinstance(field, models.CharField):
2✔
2159
            value = getattr(instance, field.name)
2✔
2160
            if (
2✔
2161
                isinstance(value, str)
2162
                and field.editable
2163
                and field.choices is None
2164
                and ('name' in field.name or 'title' in field.name)
2165
            ):
2166
                value = force_case(value)
2✔
2167
                setattr(instance, field.name, value)
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc