Skip to content

Commit

Permalink
django admin integration (fixes #7)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheppard committed Mar 6, 2019
1 parent 99906b3 commit 293c9e0
Show file tree
Hide file tree
Showing 43 changed files with 765 additions and 93 deletions.
85 changes: 85 additions & 0 deletions data_wizard/admin.py
@@ -0,0 +1,85 @@
from django.contrib import admin
from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
from .models import Run, RunLog, Identifier, Range, Record
from django.urls import reverse
from django.http import HttpResponseRedirect


class FixedTabularInline(admin.TabularInline):
can_delete = False
extra = 0

def has_add_permission(self, request, obj):
return False


class RangeInline(FixedTabularInline):
model = Range
readonly_fields = [
'type',
'header_col',
'start_col',
'end_col',
'header_row',
'start_row',
'end_row',
'count'
]


class RecordInline(FixedTabularInline):
model = Record
fields = readonly_fields = [
'row',
'success',
'content_type',
'content_object',
'fail_reason'
]


class RunLogInline(FixedTabularInline):
model = RunLog
readonly_fields = ['event', 'date']


@admin.register(Run)
class RunAdmin(admin.ModelAdmin):
list_display = [
'__str__', 'serializer_label', 'record_count', 'last_update'
]
inlines = [RangeInline, RecordInline, RunLogInline]


@admin.register(Identifier)
class IdentifierAdmin(admin.ModelAdmin):
list_display = ['__str__', 'type', 'resolved']


def start_data_wizard(modeladmin, request, queryset):
if queryset.count() != 1:
modeladmin.message_user(
request,
'Select a single row to start data wizard.',
level=messages.ERROR,
)
return
instance = queryset.first()
if isinstance(instance, Run):
run = instance
else:
ct = ContentType.objects.get_for_model(queryset.model)
run = Run.objects.create(
user=request.user,
content_type=ct,
object_id=instance.pk,
)
return HttpResponseRedirect(
reverse('data_wizard:run-serializers', kwargs={'pk': run.pk})
)


start_data_wizard.short_description = "Import via data wizard"

admin.site.add_action(start_data_wizard, 'data_wizard')
1 change: 1 addition & 0 deletions data_wizard/apps.py
Expand Up @@ -3,6 +3,7 @@

class WizardConfig(AppConfig):
name = 'data_wizard'
verbose_name = 'Data Wizard'

def ready(self):
self.module.autodiscover()
Expand Down
46 changes: 40 additions & 6 deletions data_wizard/backends/base.py
@@ -1,18 +1,22 @@
from data_wizard import tasks


ERROR_RETURN = 1
ERROR_RAISE = 2
ERROR_UPDATE_ASYNC = 3


class DataWizardBackend(object):
on_async_error = ERROR_UPDATE_ASYNC

def run(self, task_name, run_id, user_id, use_async=False, post=None):
if use_async:
task_id = self.run_async(task_name, run_id, user_id, post)
return {'task_id': task_id}
else:
try:
result = self.run_sync(task_name, run_id, user_id, post)
except Exception as e:
return {'error': str(e)}
else:
return {'result': result}
return self.try_run_sync(
task_name, run_id, user_id, post, ERROR_RETURN
)

def get_task_fn(self, task_name):
return getattr(tasks, task_name)
Expand All @@ -25,6 +29,36 @@ def run_sync(self, task_name, run_id, user_id, post=None):
result = fn(run_id, user_id)
return result

def try_run_sync(self, task_name, run_id, user_id, post, on_error=None):
if not on_error:
on_error = self.on_async_error
if not isinstance(on_error, tuple):
on_error = (on_error,)
try:
result = self.run_sync(task_name, run_id, user_id, post)
except Exception as e:
error = self.format_exception(e)
if ERROR_UPDATE_ASYNC in on_error:
self.update_async_status('FAILURE', error)
if ERROR_RAISE in on_error:
raise
if ERROR_RETURN in on_error:
return error
else:
if ERROR_RETURN in on_error:
return {'result': result}
else:
return result

def format_exception(self, exception):
error_string = '{name}: {message}'.format(
name=type(exception).__name__,
message=exception,
)
return {
'error': error_string
}

def run_async(self, task_name, run_id, user_id, post):
raise NotImplementedError("This backend does not support async")

Expand Down
8 changes: 6 additions & 2 deletions data_wizard/backends/celery.py
@@ -1,10 +1,12 @@
from __future__ import absolute_import
from celery import task, current_task
from celery.result import AsyncResult
from .base import DataWizardBackend
from .base import DataWizardBackend, ERROR_RAISE


class Backend(DataWizardBackend):
on_async_error = ERROR_RAISE

def run_async(self, task_name, run_id, user_id, post):
task = run_async.delay(task_name, run_id, user_id, post)
return task.task_id
Expand All @@ -22,10 +24,12 @@ def get_async_status(self, task_id):
}
if result.state in ('PROGRESS', 'SUCCESS'):
response.update(result.result)
elif result.state in ('FAILURE',):
response.update(self.format_exception(result.result))
return response


@task
def run_async(task_name, run_id, user_id, post):
from data_wizard import backend
return backend.run_sync(task_name, run_id, user_id, post)
return backend.try_run_sync(task_name, run_id, user_id, post)
2 changes: 1 addition & 1 deletion data_wizard/backends/immediate.py
Expand Up @@ -6,7 +6,7 @@ class Backend(DataWizardBackend):
current_result = None

def run_async(self, task_name, run_id, user_id, post):
self.run_sync(task_name, run_id, user_id, post)
self.try_run_sync(task_name, run_id, user_id, post)
return 'current'

def update_async_status(self, state, meta):
Expand Down
2 changes: 1 addition & 1 deletion data_wizard/backends/threading.py
Expand Up @@ -16,7 +16,7 @@ def run_async(self, task_name, run_id, user_id, post):
task_id = uuid.uuid4()
thread = threading.Thread(
name=task_id,
target=self.run_sync,
target=self.try_run_sync,
args=(task_name, run_id, user_id, post)
)
thread.start()
Expand Down
7 changes: 5 additions & 2 deletions data_wizard/migrations/0001_initial.py
@@ -1,4 +1,4 @@
# Generated by Django 2.1.7 on 2019-02-28 23:53
# Generated by Django 2.1.7 on 2019-03-05 21:54

from django.conf import settings
from django.db import migrations, models
Expand All @@ -10,8 +10,8 @@ class Migration(migrations.Migration):
initial = True

dependencies = [
('contenttypes', '0002_remove_content_type_name'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('contenttypes', '0002_remove_content_type_name'),
]

operations = [
Expand Down Expand Up @@ -71,6 +71,9 @@ class Migration(migrations.Migration):
('template', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='data_wizard.Run')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ('-pk',),
},
),
migrations.CreateModel(
name='RunLog',
Expand Down
32 changes: 24 additions & 8 deletions data_wizard/models.py
Expand Up @@ -4,6 +4,7 @@
from django.conf import settings
from rest_framework.settings import import_from_string
from data_wizard import registry
from django.urls import reverse


LOADER_PATH = getattr(
Expand Down Expand Up @@ -32,6 +33,9 @@ class Run(models.Model):
def __str__(self):
return "Run for %s" % self.content_object

def get_absolute_url(self):
return reverse('data_wizard:run-detail', kwargs={'pk': self.pk})

def save(self, *args, **kwargs):
is_new = not self.id
super(Run, self).save(*args, **kwargs)
Expand All @@ -42,6 +46,13 @@ def load_io(self):
loader = Loader(self)
return loader.load_io()

@property
def serializer_label(self):
if self.serializer:
return registry.get_serializer_name(self.serializer)

serializer_label.fget.short_description = 'serializer'

def get_serializer(self):
if self.serializer:
return registry.get_serializer(self.serializer)
Expand All @@ -56,14 +67,23 @@ def add_event(self, name):
event=name
)

@property
def last_update(self):
last = self.log.last()
if last:
return last.date

class Meta:
ordering = ('-pk',)


class RunLog(models.Model):
run = models.ForeignKey(Run, related_name='log', on_delete=models.CASCADE)
event = models.CharField(max_length=100)
date = models.DateTimeField(auto_now_add=True)

def __str__(self):
return "%s: %s at %s" % (self.run, self.event, self.date)
return self.event

class Meta:
ordering = ('date',)
Expand Down Expand Up @@ -150,8 +170,7 @@ def __str__(self):
elif self.type == "value" and self.header_col != self.start_col - 1:
header = " (header starts in Column %s)" % self.header_col

return "{run} contains {type} '{ident}' at {row}, {col}{head}".format(
run=self.run,
return "{type} '{ident}' at {row}, {col}{head}".format(
type=self.get_type_display(),
ident=self.identifier,
row=row,
Expand All @@ -177,16 +196,13 @@ class Record(models.Model):

def __str__(self):
if self.success:
return "{run} imported '{obj}' at row {row}".format(
run=self.run,
return "Imported '{obj}' at row {row}".format(
obj=self.content_object,
row=self.row,
)
else:
return "{run} failed at row {row}: {fail_reason}".format(
run=self.run,
return "Failed at row {row}".format(
row=self.row,
fail_reason=self.fail_reason,
)

class Meta:
Expand Down
2 changes: 0 additions & 2 deletions data_wizard/rest.py
Expand Up @@ -21,8 +21,6 @@ class Meta:


class RecordSerializer(wizard.RecordSerializer):
object_url = serializers.SerializerMethodField()

def get_object_url(self, instance):
obj = instance.content_object
conf = rest.router.get_model_config(type(obj))
Expand Down
28 changes: 24 additions & 4 deletions data_wizard/serializers.py
@@ -1,5 +1,6 @@
from rest_framework import serializers
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse, NoReverseMatch
from .models import Run, Record
from data_wizard import registry

Expand Down Expand Up @@ -34,10 +35,12 @@ class RunSerializer(serializers.ModelSerializer):
source="content_type",
queryset=ContentType.objects.all()
)
label = serializers.ReadOnlyField(source='__str__')
object_label = serializers.StringRelatedField(
source='content_object', read_only=True
)
serializer_label = serializers.SerializerMethodField()
serializer_label = serializers.ReadOnlyField()
last_update = serializers.ReadOnlyField()

def get_fields(self):
fields = super(RunSerializer, self).get_fields()
Expand All @@ -47,9 +50,6 @@ def get_fields(self):
)
return fields

def get_serializer_label(self, instance):
return registry.get_serializer_name(instance.serializer)

class Meta:
model = Run
exclude = ['content_type']
Expand All @@ -60,13 +60,33 @@ class RecordSerializer(serializers.ModelSerializer):
success = serializers.ReadOnlyField()
fail_reason = serializers.ReadOnlyField()
object_label = serializers.SerializerMethodField()
object_url = serializers.SerializerMethodField()

def get_row(self, instance):
return instance.row + 1

def get_object_label(self, instance):
return str(instance.content_object)

def get_object_url(self, instance):
if not instance.content_object:
return None
obj = instance.content_object
if hasattr(obj, 'get_absolute_url'):
object_url = obj.get_absolute_url()
else:
try:
object_url = reverse(
'admin:{app}_{model}_change'.format(
app=obj._meta.app_label,
model=obj._meta.model_name,
),
args=[obj.pk],
)
except NoReverseMatch:
object_url = None
return object_url

class Meta:
model = Record
fields = "__all__"

0 comments on commit 293c9e0

Please sign in to comment.