|
20 | 20 | from django.conf import settings |
21 | 21 | from django.contrib.gis.geos import Point |
22 | 22 | from django.contrib.gis.geos import Polygon |
| 23 | +from django.core.exceptions import ValidationError |
23 | 24 | from django.core.management import call_command |
24 | 25 | from django.core.management.base import BaseCommand |
25 | 26 | from django.core.management.base import CommandParser |
@@ -629,3 +630,125 @@ def parse_cache_control_header(cache_control_header): |
629 | 630 | parts = [i.strip() for i in cache_control_header.split(',')] |
630 | 631 | args = {i.split('=')[0].strip(): i.split('=')[-1].strip() for i in parts if i} |
631 | 632 | return {k: True if v == k else v for k, v in args.items()} |
| 633 | + |
| 634 | + |
| 635 | +# Maps sortby parameter values to Django model field |
| 636 | +SORTABLE_FIELDS = { |
| 637 | + 'id': 'name', |
| 638 | + 'collection': 'collection__name', |
| 639 | + 'datetime': 'properties_datetime', |
| 640 | + 'title': 'properties_title', |
| 641 | + 'created': 'created', |
| 642 | + 'updated': 'updated', |
| 643 | +} |
| 644 | + |
| 645 | + |
| 646 | +def parse_sortby_get(sortby_param, sortable_fields): |
| 647 | + '''Parse and validate the GET (string) format of the sortby parameter. |
| 648 | +
|
| 649 | + The sortby parameter is a comma-separated string of fields prefixed with '+' |
| 650 | + (ascending, default) or '-' (descending). |
| 651 | +
|
| 652 | + Example: "-created,title". |
| 653 | +
|
| 654 | + Args: |
| 655 | + sortby_param: string |
| 656 | + Comma-separated list of fields prefixed with '+' or '-' |
| 657 | + sortable_fields: dict |
| 658 | + Mapping of allowed sortby field names to Django model fields |
| 659 | +
|
| 660 | + Returns: |
| 661 | + list: List of tuples (field, direction) where `direction` is True for ascending, |
| 662 | + False for descending and `field` is the Django model field corresponding to |
| 663 | + the given input field. Returns empty list if sortby_param is None or empty. |
| 664 | +
|
| 665 | + Raises: |
| 666 | + ValidationError: If an invalid field is specified |
| 667 | + ''' |
| 668 | + if not sortby_param: |
| 669 | + return [] |
| 670 | + |
| 671 | + sort_fields = [] |
| 672 | + for sort_field in sortby_param.split(','): |
| 673 | + sort_field = sort_field.strip() |
| 674 | + if not sort_field: |
| 675 | + continue |
| 676 | + |
| 677 | + if sort_field.startswith('-'): |
| 678 | + is_ascending = False |
| 679 | + field_name = sort_field[1:] |
| 680 | + elif sort_field.startswith('+'): |
| 681 | + is_ascending = True |
| 682 | + field_name = sort_field[1:] |
| 683 | + else: |
| 684 | + is_ascending = True |
| 685 | + field_name = sort_field |
| 686 | + |
| 687 | + internal_field = _resolve_sort_field(field_name, sortable_fields) |
| 688 | + sort_fields.append((internal_field, is_ascending)) |
| 689 | + return sort_fields |
| 690 | + |
| 691 | + |
| 692 | +def parse_sortby_post(sortby_param, sortable_fields): |
| 693 | + '''Parse the POST (list of objects) format of the sortby parameter. |
| 694 | +
|
| 695 | + The sortby parameter in the request body is a list of objects with a 'field' |
| 696 | + and a 'direction' ('asc' or 'desc') property. |
| 697 | +
|
| 698 | + Example: [{"field": "created", "direction": "desc"}]. |
| 699 | +
|
| 700 | + Args: |
| 701 | + sortby_param: list |
| 702 | + List of {"field": ..., "direction": ...} objects |
| 703 | + sortable_fields: dict |
| 704 | + Mapping of allowed sortby field names to Django model fields |
| 705 | +
|
| 706 | + Returns: |
| 707 | + list: List of tuples (field, direction) where `direction` is True for ascending, |
| 708 | + False for descending and `field` is the Django model field corresponding to |
| 709 | + the given input field. Returns empty list if sortby_param is None or empty. |
| 710 | +
|
| 711 | + Raises: |
| 712 | + ValidationError: If an invalid field or direction is specified |
| 713 | + ''' |
| 714 | + sort_fields = [] |
| 715 | + for sort_item in sortby_param: |
| 716 | + if not isinstance(sort_item, dict) or 'field' not in sort_item: |
| 717 | + raise ValidationError("Each sortby entry must be an object with a 'field' property") |
| 718 | + field_name = sort_item['field'] |
| 719 | + direction = str(sort_item.get('direction', 'asc')).lower() |
| 720 | + if direction == 'asc': |
| 721 | + is_ascending = True |
| 722 | + elif direction == 'desc': |
| 723 | + is_ascending = False |
| 724 | + else: |
| 725 | + raise ValidationError( |
| 726 | + f"Invalid sort direction '{direction}'. " |
| 727 | + f"Allowed values are: 'asc', 'desc'" |
| 728 | + ) |
| 729 | + internal_field = _resolve_sort_field(field_name, sortable_fields) |
| 730 | + sort_fields.append((internal_field, is_ascending)) |
| 731 | + return sort_fields |
| 732 | + |
| 733 | + |
| 734 | +def _resolve_sort_field(field_name, sortable_fields): |
| 735 | + '''Resolve a sortby field name to its Django model field, validating it. |
| 736 | +
|
| 737 | + Args: |
| 738 | + field_name: string |
| 739 | + The field name provided in the sortby parameter |
| 740 | + sortable_fields: dict |
| 741 | + Mapping of allowed sortby field names to Django model fields |
| 742 | +
|
| 743 | + Returns: |
| 744 | + string: The Django model field corresponding to the given field name |
| 745 | +
|
| 746 | + Raises: |
| 747 | + ValidationError: If the field name is not allowed for sorting |
| 748 | + ''' |
| 749 | + if field_name not in sortable_fields: |
| 750 | + raise ValidationError( |
| 751 | + f"Invalid sort field '{field_name}'. " |
| 752 | + f"Allowed fields are: {', '.join(sortable_fields.keys())}" |
| 753 | + ) |
| 754 | + return sortable_fields[field_name] |
0 commit comments