mirror of
				https://github.com/graphql-python/graphene-django.git
				synced 2025-11-04 01:47:57 +03:00 
			
		
		
		
	Add offset pagination (#1013)
* Add offset filtering * Formatting Co-authored-by: Thomas Leonard <thomas@loftorbital.com>
This commit is contained in:
		
							parent
							
								
									8408c51bf9
								
							
						
					
					
						commit
						2140be5e6a
					
				| 
						 | 
				
			
			@ -4,11 +4,13 @@ import six
 | 
			
		|||
from django.db.models.query import QuerySet
 | 
			
		||||
from graphql_relay.connection.arrayconnection import (
 | 
			
		||||
    connection_from_list_slice,
 | 
			
		||||
    cursor_to_offset,
 | 
			
		||||
    get_offset_with_default,
 | 
			
		||||
    offset_to_cursor,
 | 
			
		||||
)
 | 
			
		||||
from promise import Promise
 | 
			
		||||
 | 
			
		||||
from graphene import NonNull
 | 
			
		||||
from graphene import Int, NonNull
 | 
			
		||||
from graphene.relay import ConnectionField, PageInfo
 | 
			
		||||
from graphene.types import Field, List
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -81,6 +83,7 @@ class DjangoConnectionField(ConnectionField):
 | 
			
		|||
            "enforce_first_or_last",
 | 
			
		||||
            graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST,
 | 
			
		||||
        )
 | 
			
		||||
        kwargs.setdefault("offset", Int())
 | 
			
		||||
        super(DjangoConnectionField, self).__init__(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
| 
						 | 
				
			
			@ -131,6 +134,15 @@ class DjangoConnectionField(ConnectionField):
 | 
			
		|||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def resolve_connection(cls, connection, args, iterable, max_limit=None):
 | 
			
		||||
        # Remove the offset parameter and convert it to an after cursor.
 | 
			
		||||
        offset = args.pop("offset", None)
 | 
			
		||||
        after = args.get("after")
 | 
			
		||||
        if offset:
 | 
			
		||||
            if after:
 | 
			
		||||
                offset += cursor_to_offset(after) + 1
 | 
			
		||||
            # input offset starts at 1 while the graphene offset starts at 0
 | 
			
		||||
            args["after"] = offset_to_cursor(offset - 1)
 | 
			
		||||
 | 
			
		||||
        iterable = maybe_queryset(iterable)
 | 
			
		||||
 | 
			
		||||
        if isinstance(iterable, QuerySet):
 | 
			
		||||
| 
						 | 
				
			
			@ -181,6 +193,8 @@ class DjangoConnectionField(ConnectionField):
 | 
			
		|||
    ):
 | 
			
		||||
        first = args.get("first")
 | 
			
		||||
        last = args.get("last")
 | 
			
		||||
        offset = args.get("offset")
 | 
			
		||||
        before = args.get("before")
 | 
			
		||||
 | 
			
		||||
        if enforce_first_or_last:
 | 
			
		||||
            assert first or last, (
 | 
			
		||||
| 
						 | 
				
			
			@ -200,6 +214,11 @@ class DjangoConnectionField(ConnectionField):
 | 
			
		|||
                ).format(last, info.field_name, max_limit)
 | 
			
		||||
                args["last"] = min(last, max_limit)
 | 
			
		||||
 | 
			
		||||
        if offset is not None:
 | 
			
		||||
            assert before is None, (
 | 
			
		||||
                "You can't provide a `before` value at the same time as an `offset` value to properly paginate the `{}` connection."
 | 
			
		||||
            ).format(info.field_name)
 | 
			
		||||
 | 
			
		||||
        # eventually leads to DjangoObjectType's get_queryset (accepts queryset)
 | 
			
		||||
        # or a resolve_foo (does not accept queryset)
 | 
			
		||||
        iterable = resolver(root, info, **args)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -59,7 +59,7 @@ def get_args(field):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
def assert_arguments(field, *arguments):
 | 
			
		||||
    ignore = ("after", "before", "first", "last", "order_by")
 | 
			
		||||
    ignore = ("offset", "after", "before", "first", "last", "order_by")
 | 
			
		||||
    args = get_args(field)
 | 
			
		||||
    actual = [name for name in args if name not in ignore and not name.startswith("_")]
 | 
			
		||||
    assert set(arguments) == set(
 | 
			
		||||
| 
						 | 
				
			
			@ -945,7 +945,7 @@ def test_integer_field_filter_type():
 | 
			
		|||
        }
 | 
			
		||||
 | 
			
		||||
        type Query {
 | 
			
		||||
          pets(before: String, after: String, first: Int, last: Int, age: Int): PetTypeConnection
 | 
			
		||||
          pets(offset: Int, before: String, after: String, first: Int, last: Int, age: Int): PetTypeConnection
 | 
			
		||||
        }
 | 
			
		||||
    """
 | 
			
		||||
    )
 | 
			
		||||
| 
						 | 
				
			
			@ -997,7 +997,7 @@ def test_other_filter_types():
 | 
			
		|||
        }
 | 
			
		||||
 | 
			
		||||
        type Query {
 | 
			
		||||
          pets(before: String, after: String, first: Int, last: Int, age: Int, age_Isnull: Boolean, age_Lt: Int): PetTypeConnection
 | 
			
		||||
          pets(offset: Int, before: String, after: String, first: Int, last: Int, age: Int, age_Isnull: Boolean, age_Lt: Int): PetTypeConnection
 | 
			
		||||
        }
 | 
			
		||||
    """
 | 
			
		||||
    )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1314,3 +1314,144 @@ def test_should_preserve_annotations():
 | 
			
		|||
        }
 | 
			
		||||
    }
 | 
			
		||||
    assert result.data == expected, str(result.data)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_connection_should_enable_offset_filtering():
 | 
			
		||||
    Reporter.objects.create(first_name="John", last_name="Doe")
 | 
			
		||||
    Reporter.objects.create(first_name="Some", last_name="Guy")
 | 
			
		||||
 | 
			
		||||
    class ReporterType(DjangoObjectType):
 | 
			
		||||
        class Meta:
 | 
			
		||||
            model = Reporter
 | 
			
		||||
            interfaces = (Node,)
 | 
			
		||||
 | 
			
		||||
    class Query(graphene.ObjectType):
 | 
			
		||||
        all_reporters = DjangoConnectionField(ReporterType)
 | 
			
		||||
 | 
			
		||||
    schema = graphene.Schema(query=Query)
 | 
			
		||||
    query = """
 | 
			
		||||
        query {
 | 
			
		||||
            allReporters(first: 1, offset: 1) {
 | 
			
		||||
                edges {
 | 
			
		||||
                    node {
 | 
			
		||||
                        firstName
 | 
			
		||||
                        lastName
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    result = schema.execute(query)
 | 
			
		||||
    assert not result.errors
 | 
			
		||||
    expected = {
 | 
			
		||||
        "allReporters": {"edges": [{"node": {"firstName": "Some", "lastName": "Guy"}},]}
 | 
			
		||||
    }
 | 
			
		||||
    assert result.data == expected
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_connection_should_enable_offset_filtering_higher_than_max_limit(
 | 
			
		||||
    graphene_settings,
 | 
			
		||||
):
 | 
			
		||||
    graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 2
 | 
			
		||||
    Reporter.objects.create(first_name="John", last_name="Doe")
 | 
			
		||||
    Reporter.objects.create(first_name="Some", last_name="Guy")
 | 
			
		||||
    Reporter.objects.create(first_name="Jane", last_name="Roe")
 | 
			
		||||
    Reporter.objects.create(first_name="Some", last_name="Lady")
 | 
			
		||||
 | 
			
		||||
    class ReporterType(DjangoObjectType):
 | 
			
		||||
        class Meta:
 | 
			
		||||
            model = Reporter
 | 
			
		||||
            interfaces = (Node,)
 | 
			
		||||
 | 
			
		||||
    class Query(graphene.ObjectType):
 | 
			
		||||
        all_reporters = DjangoConnectionField(ReporterType)
 | 
			
		||||
 | 
			
		||||
    schema = graphene.Schema(query=Query)
 | 
			
		||||
    query = """
 | 
			
		||||
        query {
 | 
			
		||||
            allReporters(first: 1, offset: 3) {
 | 
			
		||||
                edges {
 | 
			
		||||
                    node {
 | 
			
		||||
                        firstName
 | 
			
		||||
                        lastName
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    result = schema.execute(query)
 | 
			
		||||
    assert not result.errors
 | 
			
		||||
    expected = {
 | 
			
		||||
        "allReporters": {
 | 
			
		||||
            "edges": [{"node": {"firstName": "Some", "lastName": "Lady"}},]
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    assert result.data == expected
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_connection_should_forbid_offset_filtering_with_before():
 | 
			
		||||
    class ReporterType(DjangoObjectType):
 | 
			
		||||
        class Meta:
 | 
			
		||||
            model = Reporter
 | 
			
		||||
            interfaces = (Node,)
 | 
			
		||||
 | 
			
		||||
    class Query(graphene.ObjectType):
 | 
			
		||||
        all_reporters = DjangoConnectionField(ReporterType)
 | 
			
		||||
 | 
			
		||||
    schema = graphene.Schema(query=Query)
 | 
			
		||||
    query = """
 | 
			
		||||
        query ReporterPromiseConnectionQuery ($before: String) {
 | 
			
		||||
            allReporters(first: 1, before: $before, offset: 1) {
 | 
			
		||||
                edges {
 | 
			
		||||
                    node {
 | 
			
		||||
                        firstName
 | 
			
		||||
                        lastName
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    """
 | 
			
		||||
    before = base64.b64encode(b"arrayconnection:2").decode()
 | 
			
		||||
    result = schema.execute(query, variable_values=dict(before=before))
 | 
			
		||||
    expected_error = "You can't provide a `before` value at the same time as an `offset` value to properly paginate the `allReporters` connection."
 | 
			
		||||
    assert len(result.errors) == 1
 | 
			
		||||
    assert result.errors[0].message == expected_error
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_connection_should_allow_offset_filtering_with_after():
 | 
			
		||||
    Reporter.objects.create(first_name="John", last_name="Doe")
 | 
			
		||||
    Reporter.objects.create(first_name="Some", last_name="Guy")
 | 
			
		||||
    Reporter.objects.create(first_name="Jane", last_name="Roe")
 | 
			
		||||
    Reporter.objects.create(first_name="Some", last_name="Lady")
 | 
			
		||||
 | 
			
		||||
    class ReporterType(DjangoObjectType):
 | 
			
		||||
        class Meta:
 | 
			
		||||
            model = Reporter
 | 
			
		||||
            interfaces = (Node,)
 | 
			
		||||
 | 
			
		||||
    class Query(graphene.ObjectType):
 | 
			
		||||
        all_reporters = DjangoConnectionField(ReporterType)
 | 
			
		||||
 | 
			
		||||
    schema = graphene.Schema(query=Query)
 | 
			
		||||
    query = """
 | 
			
		||||
        query ReporterPromiseConnectionQuery ($after: String) {
 | 
			
		||||
            allReporters(first: 1, after: $after, offset: 1) {
 | 
			
		||||
                edges {
 | 
			
		||||
                    node {
 | 
			
		||||
                        firstName
 | 
			
		||||
                        lastName
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    after = base64.b64encode(b"arrayconnection:0").decode()
 | 
			
		||||
    result = schema.execute(query, variable_values=dict(after=after))
 | 
			
		||||
    assert not result.errors
 | 
			
		||||
    expected = {
 | 
			
		||||
        "allReporters": {"edges": [{"node": {"firstName": "Jane", "lastName": "Roe"}},]}
 | 
			
		||||
    }
 | 
			
		||||
    assert result.data == expected
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -172,7 +172,7 @@ type Reporter {
 | 
			
		|||
  pets: [Reporter!]!
 | 
			
		||||
  aChoice: ReporterAChoice
 | 
			
		||||
  reporterType: ReporterReporterType
 | 
			
		||||
  articles(before: String, after: String, first: Int, last: Int): ArticleConnection!
 | 
			
		||||
  articles(offset: Int, before: String, after: String, first: Int, last: Int): ArticleConnection!
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum ReporterAChoice {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue
	
	Block a user