diff --git a/savepointradio/core/utils.py b/savepointradio/core/utils.py index 0794689..f97e4ef 100644 --- a/savepointradio/core/utils.py +++ b/savepointradio/core/utils.py @@ -1,5 +1,7 @@ import random +import re import string +from unicodedata import normalize from django.core.exceptions import ObjectDoesNotExist from django.db import connection @@ -52,3 +54,23 @@ def set_setting(name, value, setting_type=None): error_msg = 'New settings need type (Integer, Float, String, Bool)' raise TypeError(error_msg) return + + +def naturalize(string): + """ + Return a normalized unicode string, with removed starting articles, for use + in natural sorting. + + Code was inspired by 'django-naturalsortfield' from Nathan Reynolds: + https://github.com/nathforge/django-naturalsortfield + """ + def naturalize_int_match(match): + return '%08d' % (int(match.group(0)),) + + string = normalize('NFKD', string).encode('ascii', 'ignore').decode('ascii') + string = string.lower() + string = string.strip() + string = re.sub(r'^(a|an|the)\s+', '', string) + string = re.sub(r'\d+', naturalize_int_match, string) + + return string diff --git a/savepointradio/radio/admin.py b/savepointradio/radio/admin.py index 022538e..080e4f0 100644 --- a/savepointradio/radio/admin.py +++ b/savepointradio/radio/admin.py @@ -15,6 +15,8 @@ class ArtistInline(admin.TabularInline): @admin.register(Album) class AlbumAdmin(admin.ModelAdmin): + ordering = ("sorted_title",) + # Detail List display list_display = ('title', '_is_enabled', '_is_published') search_fields = ['title'] @@ -48,6 +50,8 @@ class AlbumAdmin(admin.ModelAdmin): @admin.register(Artist) class ArtistAdmin(admin.ModelAdmin): + ordering = ("sorted_full_name",) + # Detail List display list_display = ('first_name', 'alias', @@ -85,6 +89,8 @@ class ArtistAdmin(admin.ModelAdmin): @admin.register(Game) class GameAdmin(admin.ModelAdmin): + ordering = ("sorted_title",) + # Detail List display list_display = ('title', '_is_enabled', '_is_published') search_fields = ['title'] @@ -118,6 +124,8 @@ class GameAdmin(admin.ModelAdmin): @admin.register(Song) class SongAdmin(admin.ModelAdmin): + ordering = ("sorted_title",) + formfield_overrides = { models.TextField: {'widget': TextInput(attrs={'size': 160, })}, } diff --git a/savepointradio/radio/apps.py b/savepointradio/radio/apps.py index bd336cf..39319b6 100644 --- a/savepointradio/radio/apps.py +++ b/savepointradio/radio/apps.py @@ -3,3 +3,6 @@ from django.apps import AppConfig class RadioConfig(AppConfig): name = 'radio' + + def ready(self): + from .signals import update_sorted_fields diff --git a/savepointradio/radio/migrations/0001_initial.py b/savepointradio/radio/migrations/0001_initial.py index b5f2d50..73fed91 100644 --- a/savepointradio/radio/migrations/0001_initial.py +++ b/savepointradio/radio/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.0 on 2017-12-29 14:36 +# Generated by Django 2.0 on 2018-01-05 19:40 from django.db import migrations, models import django.db.models.deletion @@ -23,6 +23,7 @@ class Migration(migrations.Migration): ('disabled_reason', models.TextField(blank=True, verbose_name='reason for disabling')), ('published_date', models.DateTimeField(blank=True, default=None, null=True, verbose_name='published for listening')), ('title', models.CharField(max_length=255, unique=True, verbose_name='title')), + ('sorted_title', models.CharField(db_index=True, editable=False, max_length=255, verbose_name='naturalized title')), ], options={ 'abstract': False, @@ -41,6 +42,7 @@ class Migration(migrations.Migration): ('alias', models.CharField(blank=True, max_length=127, verbose_name='alias')), ('first_name', models.CharField(blank=True, max_length=127, verbose_name='first name')), ('last_name', models.CharField(blank=True, max_length=127, verbose_name='last name')), + ('sorted_full_name', models.CharField(db_index=True, editable=False, max_length=255, verbose_name='naturalized full name')), ], options={ 'abstract': False, @@ -57,6 +59,7 @@ class Migration(migrations.Migration): ('disabled_reason', models.TextField(blank=True, verbose_name='reason for disabling')), ('published_date', models.DateTimeField(blank=True, default=None, null=True, verbose_name='published for listening')), ('title', models.CharField(max_length=255, unique=True, verbose_name='title')), + ('sorted_title', models.CharField(db_index=True, editable=False, max_length=255, verbose_name='naturalized title')), ], options={ 'abstract': False, @@ -78,6 +81,7 @@ class Migration(migrations.Migration): ('last_played', models.DateTimeField(blank=True, editable=False, null=True, verbose_name='was last played')), ('length', models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True, verbose_name='song length (in seconds)')), ('path', models.TextField(verbose_name='absolute path to song file')), + ('sorted_title', models.CharField(db_index=True, editable=False, max_length=255, verbose_name='naturalized title')), ('album', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='radio.Album')), ('artists', models.ManyToManyField(to='radio.Artist')), ('game', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='radio.Game')), diff --git a/savepointradio/radio/models.py b/savepointradio/radio/models.py index e1523f4..77d27fc 100644 --- a/savepointradio/radio/models.py +++ b/savepointradio/radio/models.py @@ -12,6 +12,11 @@ class Album(Disableable, Publishable, Timestampable, models.Model): """ title = models.CharField(_('title'), max_length=255, unique=True) + sorted_title = models.CharField(_('naturalized title'), + db_index=True, + editable=False, + max_length=255) + def __str__(self): return self.title @@ -24,8 +29,10 @@ class Artist(Disableable, Publishable, Timestampable, models.Model): first_name = models.CharField(_('first name'), max_length=127, blank=True) last_name = models.CharField(_('last name'), max_length=127, blank=True) - class Meta: - ordering = ('first_name', 'alias',) + sorted_full_name = models.CharField(_('naturalized full name'), + db_index=True, + editable=False, + max_length=255) @property def full_name(self): @@ -49,6 +56,11 @@ class Game(Disableable, Publishable, Timestampable, models.Model): """ title = models.CharField(_('title'), max_length=255, unique=True) + sorted_title = models.CharField(_('naturalized title'), + db_index=True, + editable=False, + max_length=255) + def __str__(self): return self.title @@ -90,6 +102,11 @@ class Song(Disableable, Publishable, Timestampable, models.Model): blank=True) path = models.TextField(_('absolute path to song file')) + sorted_title = models.CharField(_('naturalized title'), + db_index=True, + editable=False, + max_length=255) + objects = models.Manager() music = SongManager() diff --git a/savepointradio/radio/signals.py b/savepointradio/radio/signals.py new file mode 100644 index 0000000..4c21359 --- /dev/null +++ b/savepointradio/radio/signals.py @@ -0,0 +1,16 @@ +from django.db.models.signals import pre_save +from django.dispatch import receiver + +from core.utils import naturalize +from .models import Album, Artist, Game, Song + + +@receiver(pre_save, sender=Album) +@receiver(pre_save, sender=Artist) +@receiver(pre_save, sender=Game) +@receiver(pre_save, sender=Song) +def update_sorted_fields(sender, instance, **kwargs): + if sender == Artist: + instance.sorted_full_name = naturalize(getattr(instance, 'full_name')) + else: + instance.sorted_title = naturalize(getattr(instance, 'title'))