Repository: saleor/saleor

Issue:
Mutations for staff management
There are no mutations available for staff and permission management.

Blocks #2593


Discussion:
Queries and mutations that we have now:

- query to fetch all staff users:
```
users(isStaff: true) {
    edges {
      node {
        id
      }
    }
  }
```

- query to fetch all available permissions:
```
shop {
    permissions {
      code
      name
    }
  }
```

- mutation to create a staff user:
```
mutation {
  staffCreate(input: {email: "", permissions: [], sendPasswordEmail: true}) {
    errors {
      field
      message
    }
    user {
      id
    }
  }
}
```

- mutation to update the staff user:
```
staffUpdate(id: "", input: {permissions: ["order.manage_orders"]}) {
    errors {
      field
      message
    }
    user {
      id
    }
  }
```

What else would we need @patrys? These above should allow to implement most of the logic.
Sorry, I've missed `staffCreate` and `staffUpdate`. As for the API I'd consider providing explicit endpoints for customers and staff. Currently it's quite confusing as we're exposing the database structure rather than logical units it represents.
~@patrys Should we split it between `customers` and `staff` as it was on the old dashboard?
At this moment we have a generic `users` which returns every User registered in the store.~
Nevermind, I've missed it in  your post

Resolving patch:
diff --git a/saleor/graphql/account/resolvers.py b/saleor/graphql/account/resolvers.py
--- a/saleor/graphql/account/resolvers.py
+++ b/saleor/graphql/account/resolvers.py
@@ -1,3 +1,4 @@
+from django.db.models import Q
 from i18naddress import get_validation_rules
 
 from ...account import models
@@ -11,8 +12,16 @@
     'default_shipping_address__country')
 
 
-def resolve_users(info, query):
-    qs = models.User.objects.all().prefetch_related('addresses')
+def resolve_customers(info, query):
+    qs = models.User.objects.filter(
+        Q(is_staff=False) | (Q(is_staff=True) & Q(orders__isnull=False))
+    ).prefetch_related('addresses')
+    return filter_by_query_param(
+        queryset=qs, query=query, search_fields=USER_SEARCH_FIELDS)
+
+
+def resolve_staff_users(info, query):
+    qs = models.User.objects.filter(is_staff=True)
     return filter_by_query_param(
         queryset=qs, query=query, search_fields=USER_SEARCH_FIELDS)
 
diff --git a/saleor/graphql/api.py b/saleor/graphql/api.py
--- a/saleor/graphql/api.py
+++ b/saleor/graphql/api.py
@@ -6,8 +6,9 @@
 from .account.mutations import (
     CustomerCreate, CustomerUpdate, PasswordReset, SetPassword, StaffCreate,
     StaffUpdate, AddressCreate, AddressUpdate, AddressDelete)
-from .account.resolvers import resolve_users, resolve_address_validator
-from .account.types import User, AddressValidationData, AddressValidationInput
+from .account.types import AddressValidationData, AddressValidationInput, User
+from .account.resolvers import (
+    resolve_address_validator, resolve_customers, resolve_staff_users) 
 from .menu.resolvers import resolve_menu, resolve_menus, resolve_menu_items
 from .menu.types import Menu, MenuItem
 # FIXME: sorting import by putting below line at the beginning breaks app
@@ -155,10 +156,13 @@ class Query(graphene.ObjectType):
     user = graphene.Field(
         User, id=graphene.Argument(graphene.ID),
         description='Lookup an user by ID.')
-    users = DjangoFilterConnectionField(
+    customers = DjangoFilterConnectionField(
         User, description='List of the shop\'s users.',
         query=graphene.String(
             description=DESCRIPTIONS['user']))
+    staff_users = DjangoFilterConnectionField(
+        User, description='List of the shop\'s staff users.',
+        query=graphene.String(description=DESCRIPTIONS['user']))
     node = graphene.Node.Field()
 
     def resolve_attributes(self, info, in_category=None, query=None, **kwargs):
@@ -181,8 +185,12 @@ def resolve_user(self, info, id):
         return graphene.Node.get_node_from_global_id(info, id, User)
 
     @permission_required('account.manage_users')
-    def resolve_users(self, info, query=None, **kwargs):
-        return resolve_users(info, query=query)
+    def resolve_customers(self, info, query=None, **kwargs):
+        return resolve_customers(info, query=query)
+
+    @permission_required('account.manage_staff')
+    def resolve_staff_users(self, info, query=None, **kwargs):
+        return resolve_staff_users(info, query=query)
 
     def resolve_menu(self, info, id=None, name=None):
         return resolve_menu(info, id, name)
diff --git a/saleor/graphql/core/mutations.py b/saleor/graphql/core/mutations.py
--- a/saleor/graphql/core/mutations.py
+++ b/saleor/graphql/core/mutations.py
@@ -272,6 +272,8 @@ def mutate(cls, root, info, **data):
                 info, id, errors, 'id', model_type)
         else:
             instance = cls._meta.model()
+        if errors:
+            return cls(errors=errors)
 
         cleaned_input = cls.clean_input(info, instance, input, errors)
         instance = cls.construct_instance(instance, cleaned_input)


Test patch:
diff --git a/tests/api/test_account.py b/tests/api/test_account.py
--- a/tests/api/test_account.py
+++ b/tests/api/test_account.py
@@ -143,10 +143,10 @@ def test_query_user(admin_api_client, customer_user):
     assert address['phone'] == user_address.phone.as_e164
 
 
-def test_query_users(admin_api_client, user_api_client):
+def test_query_customers(admin_api_client, user_api_client):
     query = """
-    query Users($isStaff: Boolean) {
-        users(isStaff: $isStaff) {
+    query Users {
+        customers {
             totalCount
             edges {
                 node {
@@ -156,21 +156,44 @@ def test_query_users(admin_api_client, user_api_client):
         }
     }
     """
-    variables = json.dumps({'isStaff': True})
+    variables = json.dumps({})
     response = admin_api_client.post(
         reverse('api'), {'query': query, 'variables': variables})
     content = get_graphql_content(response)
-    users = content['data']['users']['edges']
+    users = content['data']['customers']['edges']
     assert users
-    assert all([user['node']['isStaff'] for user in users])
+    assert all([not user['node']['isStaff'] for user in users])
+
+    # check permissions
+    response = user_api_client.post(
+        reverse('api'), {'query': query, 'variables': variables})
+    assert_no_permission(response)
+
 
-    variables = json.dumps({'isStaff': False})
+def test_query_staff(
+        admin_api_client, user_api_client, staff_user, customer_user,
+        admin_user):
+    query = """
+    {
+        staffUsers {
+            edges {
+                node {
+                    email
+                    isStaff
+                }
+            }
+        }
+    }
+    """
+    variables = json.dumps({})
     response = admin_api_client.post(
         reverse('api'), {'query': query, 'variables': variables})
     content = get_graphql_content(response)
-    users = content['data']['users']['edges']
-    assert users
-    assert all([not user['node']['isStaff'] for user in users])
+    data = content['data']['staffUsers']['edges']
+    assert len(data) == 2
+    staff_emails = [user['node']['email'] for user in data]
+    assert sorted(staff_emails) == [admin_user.email, staff_user.email]
+    assert all([user['node']['isStaff'] for user in data])
 
     # check permissions
     response = user_api_client.post(
@@ -191,7 +214,7 @@ def test_who_can_see_user(
 
     query_2 = """
     query Users {
-        users {
+        customers {
             totalCount
         }
     }
@@ -219,7 +242,7 @@ def test_who_can_see_user(
     response = staff_api_client.post(reverse('api'), {'query': query_2})
     content = get_graphql_content(response)
     model = get_user_model()
-    assert content['data']['users']['totalCount'] == model.objects.count()
+    assert content['data']['customers']['totalCount'] == 1
 
 
 @patch('saleor.account.emails.send_password_reset_email.delay')