Skip to content

Commit 164dcb0

Browse files
Merge pull request #108 from datosgobar/102-editable-synchronizers
Hago Synchronizers editables desde el admin
2 parents 8dee8e5 + 162b1ce commit 164dcb0

File tree

10 files changed

+258
-74
lines changed

10 files changed

+258
-74
lines changed

django_datajsonar/admin.py

Lines changed: 82 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
from __future__ import unicode_literals
33

44
from django import forms
5+
from django.conf import settings
56
from django.contrib.contenttypes.models import ContentType
7+
from django.core.exceptions import ValidationError
68
from django.forms.models import formset_factory
79
from django.contrib import admin, messages
810
from django.contrib.admin import helpers, SimpleListFilter
@@ -14,13 +16,14 @@
1416
from scheduler.models import RepeatableJob
1517
from scheduler.admin import RepeatableJobAdmin
1618

19+
from django_datajsonar.synchronizer import create_or_update_synchro
1720
from .views import config_csv
18-
from .actions import process_node_register_file_action, confirm_delete
21+
from .actions import process_node_register_file_action
1922
from .utils import download_config_csv, generate_stages
2023
from .tasks import bulk_whitelist, read_datajson
2124
from .models import DatasetIndexingFile, NodeRegisterFile, Node, NodeMetadata, ReadDataJsonTask, Metadata, Synchronizer, Stage
2225
from .models import Catalog, Dataset, Distribution, Field, Jurisdiction
23-
from .forms import ScheduleJobForm, SynchroForm, StageFormset, StageForm
26+
from .forms import ScheduleJobForm, SynchroForm, StageForm
2427

2528

2629
class EnhancedMetaAdmin(GenericTabularInline):
@@ -346,7 +349,6 @@ def process_register_file(self, _, queryset):
346349

347350

348351
class CustomRepeatableJobAdmin(RepeatableJobAdmin):
349-
350352
actions = ['delete_and_unschedule']
351353

352354
def delete_model(self, request, obj):
@@ -357,6 +359,7 @@ def delete_and_unschedule(self, _, queryset):
357359
for job in queryset:
358360
job.unschedule()
359361
queryset.delete()
362+
360363
delete_and_unschedule.short_description = 'Delete and unschedule job'
361364

362365
def get_actions(self, request):
@@ -366,34 +369,89 @@ def get_actions(self, request):
366369

367370

368371
class SynchronizerAdmin(admin.ModelAdmin):
372+
StageFormset = formset_factory(StageForm, extra=0)
369373

370374
def add_view(self, request, form_url='', extra_context=None):
371-
synchro_form = SynchroForm()
375+
return self._synchro_view(request)
376+
377+
def change_view(self, request, object_id, form_url='', extra_context=None):
378+
synchro = Synchronizer.objects.get(id=object_id)
379+
return self._synchro_view(request, synchro)
380+
381+
def _synchro_view(self, request, synchro=None):
382+
if request.method == 'POST':
383+
return self.post_synchro_edit(request, synchro.id if synchro else None)
384+
385+
if synchro is not None:
386+
synchro_form = SynchroForm({
387+
'name': synchro.name,
388+
'frequency': synchro.frequency,
389+
'scheduled_time': synchro.scheduled_time
390+
})
391+
else:
392+
synchro_form = SynchroForm()
372393

394+
context = self.add_synchro_context(request, synchro_form, synchro)
395+
396+
return render(request, 'synchronizer.html', context)
397+
398+
def add_synchro_context(self, request, synchro_form, synchro=None):
373399
context = {
374-
'title': 'Define new process',
375-
'app_label': self.model._meta.app_label,
376400
'opts': self.model._meta,
377-
'has_change_permission': self.has_change_permission(request)
401+
'has_change_permission': self.has_change_permission(request),
402+
'synchro_form': self.admin_synchro_form(request, synchro_form),
403+
'stages_form': self.get_stages_formset(synchro),
404+
'object': synchro,
405+
}
406+
return context
407+
408+
def get_stages_formset(self, model=None):
409+
stages_data = []
410+
next_stage = model.start_stage if model else None
411+
while next_stage is not None:
412+
stages_data.append({
413+
'task': self.get_stage_choice(next_stage.name)
414+
})
415+
next_stage = next_stage.next_stage
416+
417+
if not stages_data:
418+
stages_data.append({})
419+
420+
return self.StageFormset(initial=stages_data)
421+
422+
def admin_synchro_form(self, request, synchro_form):
423+
return helpers.AdminForm(synchro_form,
424+
list([(None, {'fields': synchro_form.base_fields})]),
425+
self.get_prepopulated_fields(request))
426+
427+
def post_synchro_edit(self, request, object_id=None):
428+
synchro = Synchronizer.objects.get(id=object_id) if object_id else None
429+
synchro_form = SynchroForm(request.POST)
430+
stages_formset = self.StageFormset(request.POST)
431+
432+
if not stages_formset.is_valid() or not synchro_form.is_valid():
433+
synchro = Synchronizer.objects.get(id=object_id) if object_id else None
434+
return render(request, 'synchronizer.html', self.add_synchro_context(request, synchro_form, synchro))
435+
436+
synchro_name = synchro_form.cleaned_data['name']
437+
stages = generate_stages(stages_formset.forms, synchro_name)
438+
if not stages:
439+
messages.error(request, "No hay stages definidos")
440+
return render(request, 'synchronizer.html', self.add_synchro_context(request, synchro_form, synchro))
441+
442+
data = {
443+
'name': synchro_name,
444+
'frequency': synchro_form.cleaned_data['frequency'],
445+
'scheduled_time': synchro_form.cleaned_data['scheduled_time'],
378446
}
447+
create_or_update_synchro(object_id, stages, data)
379448

380-
if request.method == 'POST':
381-
synchro_form = SynchroForm(request.POST)
382-
stages_formset = formset_factory(form=StageForm,
383-
formset=StageFormset)(request.POST)
384-
385-
if stages_formset.is_valid() and synchro_form.is_valid():
386-
synchro_name = synchro_form.cleaned_data['name']
387-
stages = generate_stages(stages_formset.forms, synchro_name)
388-
synchro_form.create_synchronizer(start_stage=stages[0])
389-
return redirect('admin:django_datajsonar_synchronizer_changelist')
390-
391-
context['synchro_form'] = helpers.AdminForm(synchro_form, list([(None, {'fields': synchro_form.base_fields})]),
392-
self.get_prepopulated_fields(request))
393-
stages_formset = formset_factory(form=StageForm,
394-
formset=StageFormset)()
395-
context['stages_form'] = stages_formset
396-
return render(request, 'synchronizer.html', context)
449+
return redirect('admin:django_datajsonar_synchronizer_changelist')
450+
451+
def get_stage_choice(self, stage_name):
452+
for name in settings.DATAJSONAR_STAGES.keys():
453+
if name in stage_name:
454+
return name
397455

398456

399457
class EnhancedMetaFilter(SimpleListFilter):

django_datajsonar/forms.py

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,6 @@ def clean(self):
5959

6060

6161
class SynchroForm(forms.Form):
62-
class Meta:
63-
model = Synchronizer
64-
exclude = ['start_stage', 'actual_stage', 'status']
65-
6662
name = forms.CharField(max_length=50)
6763
WEEK_DAYS = 'week days'
6864
DAILY = 'every day'
@@ -81,13 +77,6 @@ def clean(self):
8177
if cleaned_data['frequency'] == self.WEEK_DAYS:
8278
self.add_error('frequency', 'week days not yet implemented')
8379

84-
def create_synchronizer(self, start_stage):
85-
scheduled_time = self.cleaned_data['scheduled_time']
86-
cron_string = "{} {} * * *".format(scheduled_time.minute, scheduled_time.hour)
87-
return Synchronizer.objects.create(name=self.cleaned_data['name'],
88-
start_stage=start_stage,
89-
cron_string=cron_string)
90-
9180

9281
class StageForm(forms.Form):
9382
task = forms.ChoiceField(choices=[(x, x) for x in settings.DATAJSONAR_STAGES.keys()],
@@ -102,9 +91,3 @@ def get_stage(self, name):
10291

10392
data = settings.DATAJSONAR_STAGES[task]
10493
return Stage.objects.update_or_create(name=name, defaults=data)[0]
105-
106-
107-
class StageFormset(forms.BaseFormSet):
108-
def __init__(self, *args, **kwargs):
109-
super(StageFormset, self).__init__(*args, **kwargs)
110-
self.queryset = Stage.objects.none()

django_datajsonar/frequency.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from datetime import datetime
2+
3+
from croniter import croniter
4+
5+
from django_datajsonar.strings import SYNCHRO_DAILY_FREQUENCY
6+
7+
8+
def get_next_run_date(start_time, scheduled_time, frequency):
9+
if frequency != SYNCHRO_DAILY_FREQUENCY:
10+
raise NotImplementedError
11+
12+
cron_string = "{} {} * * *".format(scheduled_time.minute, scheduled_time.hour)
13+
14+
return croniter(cron_string, start_time=start_time).get_next(datetime)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# -*- coding: utf-8 -*-
2+
# Generated by Django 1.11.15 on 2019-02-01 16:44
3+
from __future__ import unicode_literals
4+
5+
from django.db import migrations, models
6+
import django.utils.timezone
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
dependencies = [
12+
('django_datajsonar', 'ensure_upkeep_job_exists'),
13+
]
14+
15+
operations = [
16+
migrations.RemoveField(
17+
model_name='synchronizer',
18+
name='cron_string',
19+
),
20+
migrations.AddField(
21+
model_name='synchronizer',
22+
name='frequency',
23+
field=models.CharField(choices=[('every day', 'every day'), ('week days', 'week days')], default='every day', max_length=16),
24+
),
25+
migrations.AddField(
26+
model_name='synchronizer',
27+
name='scheduled_time',
28+
field=models.TimeField(auto_now_add=True, default=django.utils.timezone.now),
29+
preserve_default=False,
30+
),
31+
]

django_datajsonar/migrations/ensure_upkeep_job_exists.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def create_upkeep_repeatable_job(apps, schema_editor):
1111
db_alias = schema_editor.connection.alias
1212

1313
upkeep_jobs_count = RepeatableJob.objects.using(db_alias).\
14-
filter(callable='django_datajsonar.synchronizer_tasks.upkeep').count()
14+
filter(callable='django_datajsonar.synchronizer.upkeep').count()
1515

1616
if not upkeep_jobs_count:
1717
RepeatableJob.objects.using(db_alias).create(name='upkeep',

django_datajsonar/models.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
#! coding: utf-8
22
from __future__ import unicode_literals
33

4-
from datetime import datetime
54
from importlib import import_module
65

7-
from croniter import croniter, CroniterBadCronError
86
from django.contrib.auth.models import User
97
from django.core.exceptions import ImproperlyConfigured, ValidationError
108
from django.conf import settings
@@ -14,6 +12,8 @@
1412
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
1513
from django.contrib.contenttypes.models import ContentType
1614

15+
from django_datajsonar.frequency import get_next_run_date
16+
from django_datajsonar.strings import SYNCHRO_DAILY_FREQUENCY, SYNCHRO_WEEK_DAYS_FREQUENCY
1717
from django_datajsonar.utils import pending_or_running_jobs, import_string
1818

1919

@@ -458,14 +458,16 @@ class Synchronizer(models.Model):
458458
actual_stage = models.ForeignKey(to=Stage, related_name='running_synchronizer', null=True, blank=True,
459459
on_delete=models.PROTECT)
460460

461-
cron_string = models.CharField(max_length=64)
462-
last_time_ran = models.DateTimeField(auto_now_add=True)
461+
WEEK_DAYS = SYNCHRO_WEEK_DAYS_FREQUENCY
462+
DAILY = SYNCHRO_DAILY_FREQUENCY
463+
FREQUENCY_CHOICES = (
464+
(DAILY, DAILY),
465+
(WEEK_DAYS, WEEK_DAYS),
466+
)
467+
frequency = models.CharField(choices=FREQUENCY_CHOICES, max_length=16, default=DAILY)
468+
scheduled_time = models.TimeField(auto_now_add=True)
463469

464-
def clean(self):
465-
try:
466-
self.next_start_date()
467-
except CroniterBadCronError:
468-
raise ValidationError({'cron_string': "Invalid cron string: {}".format(self.cron_string)})
470+
last_time_ran = models.DateTimeField(auto_now_add=True)
469471

470472
def begin_stage(self, stage=None):
471473
if self.status == self.RUNNING and stage is None:
@@ -495,10 +497,18 @@ def next_stage(self):
495497

496498
def next_start_date(self):
497499
localtime = self.last_time_ran.astimezone(timezone.get_current_timezone())
498-
return croniter(self.cron_string, start_time=localtime).get_next(datetime)
500+
return get_next_run_date(localtime, self.scheduled_time, self.frequency)
499501

500502
def __unicode__(self):
501503
return self.name
502504

503505
def __str__(self):
504506
return self.__unicode__()
507+
508+
def get_stages(self):
509+
stages = []
510+
current_stage = self.start_stage
511+
while current_stage is not None:
512+
stages.append(current_stage)
513+
current_stage = current_stage.next_stage
514+
return stages

django_datajsonar/strings.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@
22
CATALOG_STATUS = u"Catalogo {}, status: {}"
33
DATASET_STATUS = u"Dataset ({}, {}) status: {}"
44
FILE_READ_ERROR = u"Error en la lectura del archivo de entrada"
5+
6+
SYNCHRO_DAILY_FREQUENCY = 'every day'
7+
SYNCHRO_WEEK_DAYS_FREQUENCY = 'week days'

django_datajsonar/synchronizer_tasks.py renamed to django_datajsonar/synchronizer.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from django.utils import timezone
55

6-
from .models import Synchronizer
6+
from .models import Synchronizer, Stage
77

88

99
def upkeep():
@@ -25,3 +25,25 @@ def start_synchros():
2525
for synchro in synchronizers:
2626
if now > synchro.next_start_date():
2727
synchro.begin_stage()
28+
29+
30+
def create_or_update_synchro(synchro_id, stages, data=None):
31+
if data is None:
32+
data = {}
33+
34+
if synchro_id is None:
35+
return Synchronizer.objects.create(start_stage=stages[0], **data)
36+
37+
synchro_queryset = Synchronizer.objects.filter(id=synchro_id)
38+
synchro_queryset.update(start_stage=stages[0], **data)
39+
40+
synchro = synchro_queryset.first()
41+
old_stages = synchro.get_stages()
42+
43+
Stage.objects \
44+
.filter(id__in=[stage.id for stage in old_stages]) \
45+
.exclude(id__in=[stage.id for stage in stages]) \
46+
.delete()
47+
48+
synchro.refresh_from_db()
49+
return synchro

0 commit comments

Comments
 (0)