use numeric primary keys, but still use unique names as lookups in the API

This commit is contained in:
Laura Klünder 2016-10-11 16:33:12 +02:00
parent fcd0f8072b
commit 9658de72a2
14 changed files with 46 additions and 95 deletions

View file

@ -4,13 +4,13 @@ from django.db.models.manager import BaseManager
from rest_framework import serializers from rest_framework import serializers
class PkField(serializers.DictField): class RelatedNameField(serializers.DictField):
""" """
give primary key give primary key
""" """
def to_representation(self, obj): def to_representation(self, obj):
if hasattr(obj, 'pk'): if hasattr(obj, 'name'):
return obj.pk return obj.name
elif isinstance(obj, Iterable): elif isinstance(obj, Iterable):
return tuple(self.to_representation(elem) for elem in obj) return tuple(self.to_representation(elem) for elem in obj)
elif isinstance(obj, BaseManager): elif isinstance(obj, BaseManager):
@ -30,7 +30,7 @@ class RecursiveSerializerMixin(serializers.Serializer):
for name in getattr(self.Meta, 'sparse_exclude', ()): for name in getattr(self.Meta, 'sparse_exclude', ()):
value = self.fields.get(name) value = self.fields.get(name)
if value is not None and isinstance(value, serializers.Serializer): if value is not None and isinstance(value, serializers.Serializer):
self.fields[name] = PkField() self.fields[name] = RelatedNameField()
if request_sparse: if request_sparse:
for name in tuple(self.fields): for name in tuple(self.fields):
@ -42,5 +42,5 @@ class RecursiveSerializerMixin(serializers.Serializer):
def recursive_value(self, serializer, obj, *args, **kwargs): def recursive_value(self, serializer, obj, *args, **kwargs):
if self.context.get('sparse'): if self.context.get('sparse'):
return PkField().to_representation(obj) return RelatedNameField().to_representation(obj)
return serializer(obj, context=self.sparse_context(), *args, **kwargs).data return serializer(obj, context=self.sparse_context(), *args, **kwargs).data

View file

@ -18,6 +18,8 @@ class HosterViewSet(ViewSet):
""" """
Retrieve and interact with package hosters Retrieve and interact with package hosters
""" """
lookup_field = 'name'
def retrieve(self, request, pk=None): def retrieve(self, request, pk=None):
if pk not in hosters: if pk not in hosters:
raise Http404 raise Http404
@ -92,6 +94,8 @@ class SubmitTaskViewSet(ViewSet):
""" """
Get hoster submit tasks Get hoster submit tasks
""" """
lookup_field = 'id'
def retrieve(self, request, pk=None): def retrieve(self, request, pk=None):
task = submit_edit_task.AsyncResult(task_id=pk) task = submit_edit_task.AsyncResult(task_id=pk)
try: try:

View file

@ -15,10 +15,6 @@ class Hoster(ABC):
self.name = name self.name = name
self.base_url = base_url self.base_url = base_url
@property
def pk(self):
return self.name
def get_packages(self): def get_packages(self):
""" """
Get a Queryset of all packages that can be handled by this hoster Get a Queryset of all packages that can be handled by this hoster

View file

@ -4,7 +4,7 @@ from rest_framework.reverse import reverse
class HosterSerializer(serializers.Serializer): class HosterSerializer(serializers.Serializer):
name = serializers.CharField() name = serializers.CharField()
url = serializers.HyperlinkedIdentityField(view_name='api:hoster-detail') url = serializers.HyperlinkedIdentityField(view_name='api:hoster-detail', lookup_field='name')
state_url = serializers.SerializerMethodField() state_url = serializers.SerializerMethodField()
auth_uri_url = serializers.SerializerMethodField() auth_uri_url = serializers.SerializerMethodField()
submit_url = serializers.SerializerMethodField() submit_url = serializers.SerializerMethodField()
@ -22,7 +22,7 @@ class HosterSerializer(serializers.Serializer):
class TaskSerializer(serializers.Serializer): class TaskSerializer(serializers.Serializer):
id = serializers.CharField() id = serializers.CharField()
url = serializers.HyperlinkedIdentityField(view_name='api:hoster-detail', lookup_field='id', lookup_url_kwarg='pk') url = serializers.HyperlinkedIdentityField(view_name='api:hoster-detail', lookup_field='id')
started = serializers.SerializerMethodField() started = serializers.SerializerMethodField()
done = serializers.SerializerMethodField() done = serializers.SerializerMethodField()
success = serializers.SerializerMethodField() success = serializers.SerializerMethodField()

View file

@ -20,6 +20,7 @@ class LevelViewSet(ReadOnlyModelViewSet):
""" """
queryset = Level.objects.all() queryset = Level.objects.all()
serializer_class = LevelSerializer serializer_class = LevelSerializer
lookup_field = 'name'
lookup_value_regex = '[^/]+' lookup_value_regex = '[^/]+'
filter_fields = ('altitude', 'package') filter_fields = ('altitude', 'package')
ordering_fields = ('altitude', 'package') ordering_fields = ('altitude', 'package')
@ -33,6 +34,7 @@ class PackageViewSet(ReadOnlyModelViewSet):
""" """
queryset = Package.objects.all() queryset = Package.objects.all()
serializer_class = PackageSerializer serializer_class = PackageSerializer
lookup_field = 'name'
lookup_value_regex = '[^/]+' lookup_value_regex = '[^/]+'
filter_fields = ('name', 'depends') filter_fields = ('name', 'depends')
ordering_fields = ('name',) ordering_fields = ('name',)
@ -46,6 +48,7 @@ class SourceViewSet(ReadOnlyModelViewSet):
""" """
queryset = Source.objects.all() queryset = Source.objects.all()
serializer_class = SourceSerializer serializer_class = SourceSerializer
lookup_field = 'name'
lookup_value_regex = '[^/]+' lookup_value_regex = '[^/]+'
filter_fields = ('package',) filter_fields = ('package',)
ordering_fields = ('name', 'package') ordering_fields = ('name', 'package')
@ -69,6 +72,8 @@ class FeatureTypeViewSet(ViewSet):
""" """
List and retrieve feature types List and retrieve feature types
""" """
lookup_field = 'name'
def list(self, request): def list(self, request):
serializer = FeatureTypeSerializer(FEATURE_TYPES.values(), many=True, context={'request': request}) serializer = FeatureTypeSerializer(FEATURE_TYPES.values(), many=True, context={'request': request})
return Response(serializer.data) return Response(serializer.data)
@ -86,4 +91,5 @@ class FeatureViewSet(ReadOnlyModelViewSet):
""" """
queryset = Feature.objects.all() queryset = Feature.objects.all()
serializer_class = FeatureSerializer serializer_class = FeatureSerializer
lookup_field = 'name'
lookup_value_regex = '[^/]+' lookup_value_regex = '[^/]+'

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-23 15:15 # Generated by Django 1.10.1 on 2016-10-11 14:00
from __future__ import unicode_literals from __future__ import unicode_literals
import c3nav.mapdata.fields import c3nav.mapdata.fields
@ -17,32 +17,27 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Feature', name='Feature',
fields=[
('name', models.SlugField(help_text='e.g. noc', primary_key=True, serialize=False, verbose_name='feature identifier')),
('feature_type', models.CharField(choices=[('building', 'Building'), ('room', 'Room'), ('outside', 'Outside Area'), ('obstacle', 'Obstacle')], max_length=50)),
('geometry', c3nav.mapdata.fields.GeometryField()),
],
),
migrations.CreateModel(
name='FeatureTitle',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('language', models.CharField(max_length=50)), ('name', models.SlugField(unique=True, verbose_name='feature identifier')),
('title', models.CharField(max_length=50)), ('feature_type', models.CharField(choices=[('building', 'Building'), ('room', 'Room'), ('outside', 'Outside Area'), ('obstacle', 'Obstacle')], max_length=50)),
('feature', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='featuretitles', to='mapdata.Feature', verbose_name='map package')), ('titles', c3nav.mapdata.fields.JSONField()),
('geometry', c3nav.mapdata.fields.GeometryField()),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name='Level', name='Level',
fields=[ fields=[
('name', models.SlugField(help_text='Usually just an integer (e.g. -1, 0, 1, 2)', primary_key=True, serialize=False, verbose_name='level name')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.SlugField(help_text='Usually just an integer (e.g. -1, 0, 1, 2)', unique=True, verbose_name='level name')),
('altitude', models.DecimalField(decimal_places=2, max_digits=6, null=True, verbose_name='level altitude')), ('altitude', models.DecimalField(decimal_places=2, max_digits=6, null=True, verbose_name='level altitude')),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name='Package', name='Package',
fields=[ fields=[
('name', models.SlugField(help_text='e.g. de.c3nav.33c3.base', primary_key=True, serialize=False, verbose_name='package identifier')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.SlugField(help_text='e.g. de.c3nav.33c3.base', unique=True, verbose_name='package identifier')),
('home_repo', models.URLField(null=True, verbose_name='URL to the home git repository')), ('home_repo', models.URLField(null=True, verbose_name='URL to the home git repository')),
('commit_id', models.CharField(max_length=40, null=True, verbose_name='current commit id')), ('commit_id', models.CharField(max_length=40, null=True, verbose_name='current commit id')),
('bottom', models.DecimalField(decimal_places=2, max_digits=6, null=True, verbose_name='bottom coordinate')), ('bottom', models.DecimalField(decimal_places=2, max_digits=6, null=True, verbose_name='bottom coordinate')),
@ -56,7 +51,8 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='Source', name='Source',
fields=[ fields=[
('name', models.SlugField(primary_key=True, serialize=False, verbose_name='source name')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.SlugField(unique=True, verbose_name='source name')),
('bottom', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='bottom coordinate')), ('bottom', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='bottom coordinate')),
('left', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='left coordinate')), ('left', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='left coordinate')),
('top', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='top coordinate')), ('top', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='top coordinate')),
@ -79,8 +75,4 @@ class Migration(migrations.Migration):
name='package', name='package',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='features', to='mapdata.Package', verbose_name='map package'), field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='features', to='mapdata.Package', verbose_name='map package'),
), ),
migrations.AlterUniqueTogether(
name='featuretitle',
unique_together=set([('feature', 'language')]),
),
] ]

View file

@ -1,33 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-26 08:58
from __future__ import unicode_literals
import c3nav.mapdata.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('mapdata', '0001_initial'),
]
operations = [
migrations.AlterUniqueTogether(
name='featuretitle',
unique_together=set([]),
),
migrations.RemoveField(
model_name='featuretitle',
name='feature',
),
migrations.AddField(
model_name='feature',
name='titles',
field=c3nav.mapdata.fields.JSONField(default={}),
preserve_default=False,
),
migrations.DeleteModel(
name='FeatureTitle',
),
]

View file

@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-29 07:36
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mapdata', '0002_auto_20160926_0858'),
]
operations = [
migrations.AlterField(
model_name='feature',
name='name',
field=models.SlugField(primary_key=True, serialize=False, verbose_name='feature identifier'),
),
]

View file

@ -16,10 +16,6 @@ class FeatureType(namedtuple('FeatureType', ('name', 'title', 'title_plural', 'g
super().__init__() super().__init__()
FEATURE_TYPES[self.name] = self FEATURE_TYPES[self.name] = self
@property
def pk(self):
return self.name
@property @property
def title_en(self): def title_en(self):
language = get_language() language = get_language()
@ -47,7 +43,7 @@ class Feature(models.Model):
""" """
TYPES = tuple((name, t.title) for name, t in FEATURE_TYPES.items()) TYPES = tuple((name, t.title) for name, t in FEATURE_TYPES.items())
name = models.SlugField(_('feature identifier'), primary_key=True, max_length=50) name = models.SlugField(_('feature identifier'), unique=True, max_length=50)
package = models.ForeignKey('mapdata.Package', on_delete=models.CASCADE, related_name='features', package = models.ForeignKey('mapdata.Package', on_delete=models.CASCADE, related_name='features',
verbose_name=_('map package')) verbose_name=_('map package'))
feature_type = models.CharField(max_length=50, choices=TYPES) feature_type = models.CharField(max_length=50, choices=TYPES)

View file

@ -6,7 +6,7 @@ class Level(models.Model):
""" """
A map level (-1, 0, 1, 2) A map level (-1, 0, 1, 2)
""" """
name = models.SlugField(_('level name'), primary_key=True, max_length=50, name = models.SlugField(_('level name'), unique=True, max_length=50,
help_text=_('Usually just an integer (e.g. -1, 0, 1, 2)')) help_text=_('Usually just an integer (e.g. -1, 0, 1, 2)'))
altitude = models.DecimalField(_('level altitude'), null=True, max_digits=6, decimal_places=2) altitude = models.DecimalField(_('level altitude'), null=True, max_digits=6, decimal_places=2)
package = models.ForeignKey('mapdata.Package', on_delete=models.CASCADE, related_name='levels', package = models.ForeignKey('mapdata.Package', on_delete=models.CASCADE, related_name='levels',

View file

@ -9,7 +9,7 @@ class Package(models.Model):
""" """
A c3nav map package A c3nav map package
""" """
name = models.SlugField(_('package identifier'), primary_key=True, max_length=50, name = models.SlugField(_('package identifier'), unique=True, max_length=50,
help_text=_('e.g. de.c3nav.33c3.base')) help_text=_('e.g. de.c3nav.33c3.base'))
depends = models.ManyToManyField('Package') depends = models.ManyToManyField('Package')
home_repo = models.URLField(_('URL to the home git repository'), null=True) home_repo = models.URLField(_('URL to the home git repository'), null=True)

View file

@ -6,7 +6,7 @@ class Source(models.Model):
""" """
A map source, images of levels that can be useful as backgrounds for the map editor A map source, images of levels that can be useful as backgrounds for the map editor
""" """
name = models.SlugField(_('source name'), primary_key=True, max_length=50) name = models.SlugField(_('source name'), unique=True, max_length=50)
package = models.ForeignKey('mapdata.Package', on_delete=models.CASCADE, related_name='sources', package = models.ForeignKey('mapdata.Package', on_delete=models.CASCADE, related_name='sources',
verbose_name=_('map package')) verbose_name=_('map package'))

View file

@ -146,9 +146,13 @@ class ReaderItem:
} }
def save(self): def save(self):
depends = []
if self.model != Package: if self.model != Package:
package_name = self.reader.package_names_by_dir[self.package_dir] package_name = self.reader.package_names_by_dir[self.package_dir]
self.data['package'] = self.reader.saved_items[Package][package_name].obj self.data['package'] = self.reader.saved_items[Package][package_name].obj
else:
depends = [self.reader.saved_items[Package][name].obj.pk for name in self.data['depends']]
self.data.pop('depends')
# Change name references to the referenced object # Change name references to the referenced object
for name, model in self.relations.items(): for name, model in self.relations.items():
@ -161,3 +165,8 @@ class ReaderItem:
self.obj = obj self.obj = obj
self.reader.saved_items[self.model][obj.name] = self self.reader.saved_items[self.model][obj.name] = self
if depends:
self.obj.depends.clear()
for dependency in depends:
self.obj.depends.add(dependency)

View file

@ -40,7 +40,7 @@ class PackageSerializer(RecursiveSerializerMixin, serializers.ModelSerializer):
fields = ('name', 'url', 'home_repo', 'commit_id', 'depends', 'bounds', 'public', 'hoster') fields = ('name', 'url', 'home_repo', 'commit_id', 'depends', 'bounds', 'public', 'hoster')
sparse_exclude = ('depends', 'hoster') sparse_exclude = ('depends', 'hoster')
extra_kwargs = { extra_kwargs = {
'url': {'view_name': 'api:package-detail'} 'url': {'view_name': 'api:package-detail', 'lookup_field': 'name'}
} }
def get_depends(self, obj): def get_depends(self, obj):
@ -59,7 +59,7 @@ class LevelSerializer(RecursiveSerializerMixin, serializers.ModelSerializer):
fields = ('name', 'url', 'altitude', 'package') fields = ('name', 'url', 'altitude', 'package')
sparse_exclude = ('package',) sparse_exclude = ('package',)
extra_kwargs = { extra_kwargs = {
'url': {'view_name': 'api:level-detail'} 'url': {'view_name': 'api:level-detail', 'lookup_field': 'name'}
} }
@ -72,7 +72,7 @@ class SourceSerializer(RecursiveSerializerMixin, serializers.ModelSerializer):
fields = ('name', 'url', 'image_url', 'package', 'bounds') fields = ('name', 'url', 'image_url', 'package', 'bounds')
sparse_exclude = ('package', ) sparse_exclude = ('package', )
extra_kwargs = { extra_kwargs = {
'url': {'view_name': 'api:source-detail'} 'url': {'view_name': 'api:source-detail', 'lookup_field': 'name'}
} }
def get_image_url(self, obj): def get_image_url(self, obj):
@ -100,7 +100,8 @@ class FeatureSerializer(RecursiveSerializerMixin, serializers.ModelSerializer):
fields = ('name', 'url', 'title', 'feature_type', 'level', 'titles', 'package', 'geometry') fields = ('name', 'url', 'title', 'feature_type', 'level', 'titles', 'package', 'geometry')
sparse_exclude = ('feature_type', 'level', 'package') sparse_exclude = ('feature_type', 'level', 'package')
extra_kwargs = { extra_kwargs = {
'url': {'view_name': 'api:feature-detail'} 'lookup_field': 'name',
'url': {'view_name': 'api:feature-detail', 'lookup_field': 'name'}
} }
def get_feature_type(self, obj): def get_feature_type(self, obj):