Repository: saleor/saleor

Issue:
Dashboard GraphQL endpoint for site settings
We need to be able to fetch (and change where currently possible) things like:
- [x] the name and description of the current site
- [x] the list of supported countries
- [x] the list of currencies
- [x] the list of available tax rates
- [x] authorization keys
- [x] mutation to assign `top_menu` and `bottom_menu`
- [x] default currency


Discussion:
Shopify exposes this by wrapping their entire API in a `Shop` object. The object contains endpoints for articles, collections, and products but it also lists the store name, the currency that all prices use, payment configuration etc.

Resolving patch:
diff --git a/saleor/graphql/api.py b/saleor/graphql/api.py
--- a/saleor/graphql/api.py
+++ b/saleor/graphql/api.py
@@ -12,8 +12,8 @@
 from .menu.types import Menu, MenuItem
 # FIXME: sorting import by putting below line at the beginning breaks app
 from .menu.mutations import (
-    MenuCreate, MenuDelete, MenuUpdate, MenuItemCreate, MenuItemDelete,
-    MenuItemUpdate)
+    AssignNavigation, MenuCreate, MenuDelete, MenuUpdate, MenuItemCreate,
+    MenuItemDelete, MenuItemUpdate)
 from .descriptions import DESCRIPTIONS
 from .discount.resolvers import resolve_sales, resolve_vouchers
 from .discount.types import Sale, Voucher
@@ -245,6 +245,8 @@ def resolve_shipping_zones(self, info, **kwargs):
 
 
 class Mutations(graphene.ObjectType):
+    assign_navigation = AssignNavigation.Field()
+
     token_create = CreateToken.Field()
     token_refresh = graphql_jwt.Refresh.Field()
     token_verify = VerifyToken.Field()
diff --git a/saleor/graphql/menu/mutations.py b/saleor/graphql/menu/mutations.py
--- a/saleor/graphql/menu/mutations.py
+++ b/saleor/graphql/menu/mutations.py
@@ -1,7 +1,9 @@
 import graphene
+from graphql_jwt.decorators import permission_required
 
 from ...menu import models
-from ..core.mutations import ModelDeleteMutation, ModelMutation
+from ..core.mutations import BaseMutation, ModelDeleteMutation, ModelMutation
+from .types import Menu
 
 
 class MenuInput(graphene.InputObjectType):
@@ -142,3 +144,48 @@ class Meta:
     @classmethod
     def user_is_allowed(cls, user, input):
         return user.has_perm('menu.manage_menus')
+
+
+class NavigationType(graphene.Enum):
+    MAIN = 'main'
+    SECONDARY = 'secondary'
+
+    @property
+    def description(self):
+        if self == NavigationType.MAIN:
+            return 'Main storefront\'s navigation.'
+        return 'Secondary storefront\'s navigation.'
+
+
+class AssignNavigation(BaseMutation):
+    menu = graphene.Field(Menu, description='Assigned navigation menu.')
+
+    class Arguments:
+        menu = graphene.ID(
+            description='ID of the menu.')
+        navigation_type = NavigationType(
+            description='Type of the navigation bar to assign the menu to.',
+            required=True)
+
+    class Meta:
+        description = 'Assigns storefront\'s navigation menus.'
+
+    @classmethod
+    @permission_required(['menu.manage_menus', 'site.manage_settings'])
+    def mutate(cls, root, info, navigation_type, menu=None):
+        errors = []
+        site_settings = info.context.site.settings
+        if menu is not None:
+            menu = cls.get_node_or_error(
+                info, menu, errors=errors, field='menu')
+        if not errors:
+            if navigation_type == NavigationType.MAIN:
+                site_settings.top_menu = menu
+                site_settings.save(update_fields=['top_menu'])
+            elif navigation_type == NavigationType.SECONDARY:
+                site_settings.bottom_menu = menu
+                site_settings.save(update_fields=['bottom_menu'])
+            else:
+                raise AssertionError(
+                    'Unknown navigation type: %s' % navigation_type)
+        return AssignNavigation(menu=menu, errors=errors)


Test patch:
diff --git a/tests/api/test_menu.py b/tests/api/test_menu.py
--- a/tests/api/test_menu.py
+++ b/tests/api/test_menu.py
@@ -5,6 +5,10 @@
 from django.shortcuts import reverse
 from tests.utils import get_graphql_content
 
+from saleor.graphql.menu.mutations import NavigationType
+
+from .utils import assert_no_permission
+
 
 def test_menu_query(user_api_client, menu):
     query = """
@@ -268,3 +272,63 @@ def test_add_more_than_one_item(admin_api_client, menu, menu_item, page):
     data = content['data']['menuItemUpdate']['errors'][0]
     assert data['field'] == 'items'
     assert data['message'] == 'More than one item provided.'
+
+
+def test_assign_menu(
+        staff_api_client, menu, permission_manage_menus,
+        permission_manage_settings, site_settings):
+    query = """
+    mutation AssignMenu($menu: ID, $navigationType: NavigationType!) {
+        assignNavigation(menu: $menu, navigationType: $navigationType) {
+            errors {
+                field
+                message
+            }
+            menu {
+                name
+            }
+        }
+    }
+    """
+
+    # test mutations fails without proper permissions
+    menu_id = graphene.Node.to_global_id('Menu', menu.pk)
+    variables = json.dumps({
+        'menu': menu_id, 'navigationType': NavigationType.MAIN.name})
+    response = staff_api_client.post(
+        reverse('api'), {'query': query, 'variables': variables})
+    assert_no_permission(response)
+
+    staff_api_client.user.user_permissions.add(permission_manage_menus)
+    staff_api_client.user.user_permissions.add(permission_manage_settings)
+
+    # test assigning main menu
+    response = staff_api_client.post(
+        reverse('api'), {'query': query, 'variables': variables})
+    content = get_graphql_content(response)
+    assert 'errors' not in content
+    assert content['data']['assignNavigation']['menu']['name'] == menu.name
+    site_settings.refresh_from_db()
+    assert site_settings.top_menu.name == menu.name
+
+    # test assigning secondary menu
+    variables = json.dumps({
+        'menu': menu_id, 'navigationType': NavigationType.SECONDARY.name})
+    response = staff_api_client.post(
+        reverse('api'), {'query': query, 'variables': variables})
+    content = get_graphql_content(response)
+    assert 'errors' not in content
+    assert content['data']['assignNavigation']['menu']['name'] == menu.name
+    site_settings.refresh_from_db()
+    assert site_settings.bottom_menu.name == menu.name
+
+    # test unasigning menu
+    variables = json.dumps({
+        'id': None, 'navigationType': NavigationType.MAIN.name})
+    response = staff_api_client.post(
+        reverse('api'), {'query': query, 'variables': variables})
+    content = get_graphql_content(response)
+    assert 'errors' not in content
+    assert not content['data']['assignNavigation']['menu']
+    site_settings.refresh_from_db()
+    assert site_settings.top_menu is None