Skip to content

Commit

Permalink
Merge pull request #701 from basedosdados/feat/observation_level_order
Browse files Browse the repository at this point in the history
feat: observation_level.order
  • Loading branch information
rdahis authored Nov 6, 2024
2 parents de843a8 + fc58d72 commit cf0b995
Show file tree
Hide file tree
Showing 10 changed files with 288 additions and 28 deletions.
126 changes: 118 additions & 8 deletions backend/apps/api/v1/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
ObservationLevelInlineForm,
PollInlineForm,
ReorderColumnsForm,
ReorderObservationLevelsForm,
ReorderTablesForm,
TableForm,
TableInlineForm,
Expand Down Expand Up @@ -160,18 +161,41 @@ def has_change_permission(self, request, obj=None):
return False


class ObservationLevelInline(admin.StackedInline):
class ObservationLevelInline(OrderedStackedInline):
model = ObservationLevel
form = ObservationLevelInlineForm
extra = 0
fields = [
"id",
"entity",
]
autocomplete_fields = [
show_change_link = True
readonly_fields = [
"entity",
"order",
"move_up_down_links",
]
fields = readonly_fields
template = 'admin/observation_level_inline.html'
ordering = ["order"]

def get_formset(self, request, obj=None, **kwargs):
self.parent_obj = obj
return super().get_formset(request, obj, **kwargs)

def get_ordering_prefix(self):
"""Return the appropriate ordering prefix based on parent model"""
if isinstance(self.parent_obj, Table):
return 'table'
elif isinstance(self.parent_obj, RawDataSource):
return 'rawdatasource'
elif isinstance(self.parent_obj, InformationRequest):
return 'informationrequest'
elif isinstance(self.parent_obj, Analysis):
return 'analysis'
return super().get_ordering_prefix()

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

def has_change_permission(self, request, obj=None):
return False

class TableInline(OrderedTranslatedInline):
model = Table
Expand Down Expand Up @@ -459,6 +483,48 @@ def reset_column_order(modeladmin, request, queryset):
reset_column_order.short_description = "Reiniciar ordem das colunas"


def reorder_observation_levels(modeladmin, request, queryset):
"""Reorder observation levels in respect to parent"""
if "do_action" in request.POST:
form = ReorderObservationLevelsForm(request.POST)
if form.is_valid():
if queryset.count() != 1:
messages.error(
request,
"To pass the names manually you must select only one parent.",
)
return

parent = queryset.first()
ordered_entities = form.cleaned_data["ordered_entities"].split()

# Get observation levels for this parent
if hasattr(parent, 'observation_levels'):
obs_levels = parent.observation_levels.all()

# Create a mapping of entity names to observation levels
obs_by_entity = {ol.entity.name: ol for ol in obs_levels}

# Update order based on provided entity names
for i, entity_name in enumerate(ordered_entities):
if entity_name in obs_by_entity:
obs_by_entity[entity_name].order = i
obs_by_entity[entity_name].save()

messages.success(request, "Observation levels reordered successfully")
else:
messages.error(request, "Selected object has no observation levels")
else:
form = ReorderObservationLevelsForm()
return render(
request,
"admin/reorder_observation_levels.html",
{"title": "Reorder observation levels", "parents": queryset, "form": form},
)

reorder_observation_levels.short_description = "Alterar ordem dos níveis de observação"


################################################################################
# Model Admins
################################################################################
Expand Down Expand Up @@ -601,6 +667,7 @@ class TableAdmin(OrderedInlineModelAdminMixin, TabbedTranslationAdmin):
actions = [
reorder_columns,
reset_column_order,
reorder_observation_levels,
update_table_metadata,
update_table_neighbors,
update_page_views,
Expand Down Expand Up @@ -778,7 +845,37 @@ class ColumnOriginalNameAdmin(TabbedTranslationAdmin):
inlines = [CoverageInline]


def reset_observation_level_order(modeladmin, request, queryset):
"""Reset observation level order in respect to parent"""
# Group observation levels by their parent
by_table = {}
by_raw_data_source = {}
by_information_request = {}
by_analysis = {}

for obs in queryset:
if obs.table_id:
by_table.setdefault(obs.table_id, []).append(obs)
elif obs.raw_data_source_id:
by_raw_data_source.setdefault(obs.raw_data_source_id, []).append(obs)
elif obs.information_request_id:
by_information_request.setdefault(obs.information_request_id, []).append(obs)
elif obs.analysis_id:
by_analysis.setdefault(obs.analysis_id, []).append(obs)

# Reset order within each parent group
for parent_levels in [by_table, by_raw_data_source, by_information_request, by_analysis]:
for levels in parent_levels.values():
sorted_levels = sorted(levels, key=lambda x: x.entity.name)
for i, obs_level in enumerate(sorted_levels):
obs_level.order = i
obs_level.save()

reset_observation_level_order.short_description = "Reiniciar ordem dos níveis de observação"


class ObservationLevelAdmin(admin.ModelAdmin):
actions = [reset_observation_level_order]
readonly_fields = [
"id",
]
Expand All @@ -796,6 +893,9 @@ class ObservationLevelAdmin(admin.ModelAdmin):
]
list_filter = [
"entity__category__name",
"table",
"raw_data_source",
"information_request",
]
list_display = [
"__str__",
Expand All @@ -805,7 +905,10 @@ class ObservationLevelAdmin(admin.ModelAdmin):
]


class RawDataSourceAdmin(TabbedTranslationAdmin):
class RawDataSourceAdmin(OrderedInlineModelAdminMixin, TabbedTranslationAdmin):
actions = [
reorder_observation_levels,
]
list_display = ["name", "dataset", "created_at", "updated_at"]
search_fields = ["name", "dataset__name"]
readonly_fields = ["id", "created_at", "updated_at"]
Expand All @@ -819,11 +922,15 @@ class RawDataSourceAdmin(TabbedTranslationAdmin):
]
inlines = [
CoverageInline,
ObservationLevelInline,
PollInline,
]


class InformationRequestAdmin(TabbedTranslationAdmin):
class InformationRequestAdmin(OrderedInlineModelAdminMixin, TabbedTranslationAdmin):
actions = [
reorder_observation_levels,
]
list_display = ["__str__", "dataset", "created_at", "updated_at"]
search_fields = ["__str__", "dataset__name"]
readonly_fields = ["id", "created_at", "updated_at"]
Expand Down Expand Up @@ -1111,6 +1218,9 @@ class AnalysisTypeAdmin(TabbedTranslationAdmin):


class AnalysisAdmin(TabbedTranslationAdmin):
actions = [
reorder_observation_levels,
]
readonly_fields = [
"id",
]
Expand Down
1 change: 1 addition & 0 deletions backend/apps/api/v1/forms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
)
from backend.apps.api.v1.forms.reorder_columns_form import ReorderColumnsForm # noqa: F401
from backend.apps.api.v1.forms.reorder_tables_form import ReorderTablesForm # noqa: F401
from backend.apps.api.v1.forms.reorder_observation_levels_form import ReorderObservationLevelsForm # noqa: F401
13 changes: 12 additions & 1 deletion backend/apps/api/v1/forms/admin_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,18 @@ class Meta(UUIDHiddenIdForm.Meta):
class ObservationLevelInlineForm(UUIDHiddenIdForm):
class Meta(UUIDHiddenIdForm.Meta):
model = ObservationLevel
fields = "__all__"
fields = [
"id",
"entity",
"table",
"raw_data_source",
"information_request",
"analysis",
]
readonly_fields = [
"order",
"move_up_down_links",
]


class CoverageInlineForm(UUIDHiddenIdForm):
Expand Down
8 changes: 8 additions & 0 deletions backend/apps/api/v1/forms/reorder_observation_levels_form.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django import forms

class ReorderObservationLevelsForm(forms.Form):
ordered_entities = forms.CharField(
required=False,
widget=forms.Textarea(attrs={"rows": 10, "cols": 40}),
help_text="Enter entity names one per line in desired order",
)
18 changes: 18 additions & 0 deletions backend/apps/api/v1/migrations/0046_observationlevel_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
from django.db import migrations, models

class Migration(migrations.Migration):
dependencies = [
("v1", "0045_add_measurement_categories_and_units"),
]

operations = [
migrations.AddField(
model_name="observationlevel",
name="order",
field=models.PositiveIntegerField(
db_index=True, default=0, editable=False, verbose_name="order"
),
preserve_default=False,
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from django.db import migrations

def initialize_observation_level_order(apps, schema_editor):
ObservationLevel = apps.get_model('v1', 'ObservationLevel')

# Group by each possible parent type and set order
for table_id in ObservationLevel.objects.values_list('table_id', flat=True).distinct():
if table_id:
for i, ol in enumerate(ObservationLevel.objects.filter(table_id=table_id)):
ol.order = i
ol.save()

for rds_id in ObservationLevel.objects.values_list('raw_data_source_id', flat=True).distinct():
if rds_id:
for i, ol in enumerate(ObservationLevel.objects.filter(raw_data_source_id=rds_id)):
ol.order = i
ol.save()

for ir_id in ObservationLevel.objects.values_list('information_request_id', flat=True).distinct():
if ir_id:
for i, ol in enumerate(ObservationLevel.objects.filter(information_request_id=ir_id)):
ol.order = i
ol.save()

for analysis_id in ObservationLevel.objects.values_list('analysis_id', flat=True).distinct():
if analysis_id:
for i, ol in enumerate(ObservationLevel.objects.filter(analysis_id=analysis_id)):
ol.order = i
ol.save()

def reverse_migration(apps, schema_editor):
ObservationLevel = apps.get_model('v1', 'ObservationLevel')
ObservationLevel.objects.all().update(order=0)

class Migration(migrations.Migration):
dependencies = [
('v1', '0046_observationlevel_order'),
]

operations = [
migrations.RunPython(initialize_observation_level_order, reverse_migration),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 4.2.16 on 2024-11-04 03:54

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('v1', '0047_initialize_observation_level_order'),
]

operations = [
migrations.AlterModelOptions(
name='observationlevel',
options={'ordering': ['order'], 'verbose_name': 'Observation Level', 'verbose_name_plural': 'Observation Levels'},
),
]
41 changes: 22 additions & 19 deletions backend/apps/api/v1/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
from backend.custom.storage import OverwriteStorage, upload_to, validate_image
from backend.custom.utils import check_kebab_case, check_snake_case

import logging

logger = logging.getLogger('django.request')

class Area(BaseModel):
"""Area model"""
Expand Down Expand Up @@ -1728,7 +1731,7 @@ class Meta:
ordering = ["slug"]


class ObservationLevel(BaseModel):
class ObservationLevel(BaseModel, OrderedModel):
"""Model definition for ObservationLevel."""

id = models.UUIDField(primary_key=True, default=uuid4)
Expand Down Expand Up @@ -1764,6 +1767,8 @@ class ObservationLevel(BaseModel):
related_name="observation_levels",
)

order_with_respect_to = ('table', 'raw_data_source', 'information_request', 'analysis')

graphql_nested_filter_fields_whitelist = ["id"]

def __str__(self):
Expand All @@ -1775,25 +1780,23 @@ class Meta:
db_table = "observation_level"
verbose_name = "Observation Level"
verbose_name_plural = "Observation Levels"
ordering = ["id"]
ordering = ["order"]

def clean(self) -> None:
"""Assert that only one of "table", "raw_data_source", "information_request" is set"""
count = 0
if self.table:
count += 1
if self.raw_data_source:
count += 1
if self.information_request:
count += 1
if self.analysis:
count += 1
if count != 1:
raise ValidationError(
"One and only one of 'table', 'raw_data_source', "
"'information_request', 'analysis' must be set."
)
return super().clean()
def get_ordering_queryset(self):
"""Get queryset for ordering within the appropriate parent"""
qs = super().get_ordering_queryset()

# Filter by the appropriate parent field
if self.table_id:
return qs.filter(table_id=self.table_id)
elif self.raw_data_source_id:
return qs.filter(raw_data_source_id=self.raw_data_source_id)
elif self.information_request_id:
return qs.filter(information_request_id=self.information_request_id)
elif self.analysis_id:
return qs.filter(analysis_id=self.analysis_id)

return qs


class DateTimeRange(BaseModel):
Expand Down
Loading

0 comments on commit cf0b995

Please sign in to comment.