Implement natural sorting. (Yay!)

This commit is contained in:
Josh Washburne 2018-01-05 15:18:12 -05:00
parent 89d859e371
commit 6ca8b848d2
6 changed files with 73 additions and 3 deletions

View file

@ -1,5 +1,7 @@
import random import random
import re
import string import string
from unicodedata import normalize
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db import connection 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)' error_msg = 'New settings need type (Integer, Float, String, Bool)'
raise TypeError(error_msg) raise TypeError(error_msg)
return 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

View file

@ -15,6 +15,8 @@ class ArtistInline(admin.TabularInline):
@admin.register(Album) @admin.register(Album)
class AlbumAdmin(admin.ModelAdmin): class AlbumAdmin(admin.ModelAdmin):
ordering = ("sorted_title",)
# Detail List display # Detail List display
list_display = ('title', '_is_enabled', '_is_published') list_display = ('title', '_is_enabled', '_is_published')
search_fields = ['title'] search_fields = ['title']
@ -48,6 +50,8 @@ class AlbumAdmin(admin.ModelAdmin):
@admin.register(Artist) @admin.register(Artist)
class ArtistAdmin(admin.ModelAdmin): class ArtistAdmin(admin.ModelAdmin):
ordering = ("sorted_full_name",)
# Detail List display # Detail List display
list_display = ('first_name', list_display = ('first_name',
'alias', 'alias',
@ -85,6 +89,8 @@ class ArtistAdmin(admin.ModelAdmin):
@admin.register(Game) @admin.register(Game)
class GameAdmin(admin.ModelAdmin): class GameAdmin(admin.ModelAdmin):
ordering = ("sorted_title",)
# Detail List display # Detail List display
list_display = ('title', '_is_enabled', '_is_published') list_display = ('title', '_is_enabled', '_is_published')
search_fields = ['title'] search_fields = ['title']
@ -118,6 +124,8 @@ class GameAdmin(admin.ModelAdmin):
@admin.register(Song) @admin.register(Song)
class SongAdmin(admin.ModelAdmin): class SongAdmin(admin.ModelAdmin):
ordering = ("sorted_title",)
formfield_overrides = { formfield_overrides = {
models.TextField: {'widget': TextInput(attrs={'size': 160, })}, models.TextField: {'widget': TextInput(attrs={'size': 160, })},
} }

View file

@ -3,3 +3,6 @@ from django.apps import AppConfig
class RadioConfig(AppConfig): class RadioConfig(AppConfig):
name = 'radio' name = 'radio'
def ready(self):
from .signals import update_sorted_fields

View file

@ -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 from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
@ -23,6 +23,7 @@ class Migration(migrations.Migration):
('disabled_reason', models.TextField(blank=True, verbose_name='reason for disabling')), ('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')), ('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')), ('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={ options={
'abstract': False, 'abstract': False,
@ -41,6 +42,7 @@ class Migration(migrations.Migration):
('alias', models.CharField(blank=True, max_length=127, verbose_name='alias')), ('alias', models.CharField(blank=True, max_length=127, verbose_name='alias')),
('first_name', models.CharField(blank=True, max_length=127, verbose_name='first name')), ('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')), ('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={ options={
'abstract': False, 'abstract': False,
@ -57,6 +59,7 @@ class Migration(migrations.Migration):
('disabled_reason', models.TextField(blank=True, verbose_name='reason for disabling')), ('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')), ('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')), ('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={ options={
'abstract': False, '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')), ('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)')), ('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')), ('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')), ('album', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='radio.Album')),
('artists', models.ManyToManyField(to='radio.Artist')), ('artists', models.ManyToManyField(to='radio.Artist')),
('game', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='radio.Game')), ('game', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='radio.Game')),

View file

@ -12,6 +12,11 @@ class Album(Disableable, Publishable, Timestampable, models.Model):
""" """
title = models.CharField(_('title'), max_length=255, unique=True) 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): def __str__(self):
return self.title 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) first_name = models.CharField(_('first name'), max_length=127, blank=True)
last_name = models.CharField(_('last name'), max_length=127, blank=True) last_name = models.CharField(_('last name'), max_length=127, blank=True)
class Meta: sorted_full_name = models.CharField(_('naturalized full name'),
ordering = ('first_name', 'alias',) db_index=True,
editable=False,
max_length=255)
@property @property
def full_name(self): def full_name(self):
@ -49,6 +56,11 @@ class Game(Disableable, Publishable, Timestampable, models.Model):
""" """
title = models.CharField(_('title'), max_length=255, unique=True) 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): def __str__(self):
return self.title return self.title
@ -90,6 +102,11 @@ class Song(Disableable, Publishable, Timestampable, models.Model):
blank=True) blank=True)
path = models.TextField(_('absolute path to song file')) 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() objects = models.Manager()
music = SongManager() music = SongManager()

View file

@ -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'))