From 7e3de5fb9ac074acc892f029f211d0a31bea4df9 Mon Sep 17 00:00:00 2001 From: tobes Date: Mon, 4 Mar 2013 15:02:05 +0000 Subject: [PATCH 01/10] [#515] Fix issue with orgs appearing in group facet --- ckan/controllers/group.py | 5 ++++- ckan/lib/dictization/model_dictize.py | 12 +++++++++--- ckan/lib/search/index.py | 4 +++- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/ckan/controllers/group.py b/ckan/controllers/group.py index c75d7fcae0a..9b099d15733 100644 --- a/ckan/controllers/group.py +++ b/ckan/controllers/group.py @@ -183,7 +183,10 @@ def _read(self, id, limit): q = c.q = request.params.get('q', '') # Search within group - q += ' groups: "%s"' % c.group_dict.get('name') + if c.group_dict.get('is_organization'): + q += ' owner_org: "%s"' % c.group_dict.get('id') + else: + q += ' groups: "%s"' % c.group_dict.get('name') try: description_formatted = ckan.misc.MarkdownFormat().to_html( diff --git a/ckan/lib/dictization/model_dictize.py b/ckan/lib/dictization/model_dictize.py index ae1ed848f2c..70f9023f478 100644 --- a/ckan/lib/dictization/model_dictize.py +++ b/ckan/lib/dictization/model_dictize.py @@ -23,7 +23,7 @@ def group_list_dictize(obj_list, context, query = search.PackageSearchQuery() q = {'q': '+capacity:public' if not with_private else '*:*', - 'fl': 'groups', 'facet.field': ['groups'], + 'fl': 'groups', 'facet.field': ['groups', 'owner_org'], 'facet.limit': -1, 'rows': 1} query.run(q) @@ -42,7 +42,10 @@ def group_list_dictize(obj_list, context, group_dict['display_name'] = obj.display_name - group_dict['packages'] = query.facets['groups'].get(obj.name, 0) + if obj.is_organization: + group_dict['packages'] = query.facets['owner_org'].get(obj.id, 0) + else: + group_dict['packages'] = query.facets['groups'].get(obj.name, 0) if context.get('for_view'): if group_dict['is_organization']: @@ -336,7 +339,10 @@ def group_dictize(group, context): context) query = search.PackageSearchQuery() - q = {'q': 'groups:"%s" +capacity:public' % group.name, 'rows': 1} + if group.is_organization: + q = {'q': 'owner_org:"%s" +capacity:public' % group.id, 'rows': 1} + else: + q = {'q': 'groups:"%s" +capacity:public' % group.name, 'rows': 1} result_dict['package_count'] = query.run(q)['count'] result_dict['tags'] = tag_list_dictize( diff --git a/ckan/lib/search/index.py b/ckan/lib/search/index.py index fd89ffd4bb6..8741b0d4eca 100644 --- a/ckan/lib/search/index.py +++ b/ckan/lib/search/index.py @@ -157,7 +157,9 @@ def index_package(self, pkg_dict, defer_commit=False): # if there is an owner_org we want to add this to groups for index # purposes if pkg_dict['owner_org']: - pkg_dict['groups'].append(pkg_dict['organization']['name']) + pkg_dict['organization'] = pkg_dict['organization']['name'] + else: + pkg_dict['organization'] = None # tracking From 106c9024e6a6d2c27e336808baa675e8799027e6 Mon Sep 17 00:00:00 2001 From: tobes Date: Wed, 13 Mar 2013 12:13:00 +0000 Subject: [PATCH 02/10] [#515] Add a check to stop errors --- ckan/lib/search/index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/lib/search/index.py b/ckan/lib/search/index.py index 8741b0d4eca..f31271df1d3 100644 --- a/ckan/lib/search/index.py +++ b/ckan/lib/search/index.py @@ -156,7 +156,7 @@ def index_package(self, pkg_dict, defer_commit=False): # if there is an owner_org we want to add this to groups for index # purposes - if pkg_dict['owner_org']: + if pkg_dict['owner_org'] and pkg_dict.get('organization'): pkg_dict['organization'] = pkg_dict['organization']['name'] else: pkg_dict['organization'] = None From 417999c9249776817c9167057498bddc3c53472d Mon Sep 17 00:00:00 2001 From: John Martin Date: Thu, 14 Mar 2013 14:54:17 +0000 Subject: [PATCH 03/10] [#630] Members table now has header and behaves like the other tables within admin --- ckan/templates/organization/members.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/templates/organization/members.html b/ckan/templates/organization/members.html index c4be4fbb029..c9453710d92 100644 --- a/ckan/templates/organization/members.html +++ b/ckan/templates/organization/members.html @@ -5,7 +5,7 @@ {% block primary_content_inner %}

{{ _('Members') }}

- +
From f35bbdef8e71bba575062a0eb72af7d0677e9024 Mon Sep 17 00:00:00 2001 From: John Martin Date: Thu, 14 Mar 2013 14:56:41 +0000 Subject: [PATCH 04/10] [#630] Moves 'Add member' button into the correct position --- ckan/templates/organization/members.html | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ckan/templates/organization/members.html b/ckan/templates/organization/members.html index c9453710d92..3124532349c 100644 --- a/ckan/templates/organization/members.html +++ b/ckan/templates/organization/members.html @@ -4,7 +4,8 @@ {% block primary_content_inner %}
-

{{ _('Members') }}

+ {% link_for _('Add member'), controller='organization', action='member_new', id=c.group_dict.id, class_='btn pull-right', icon='plus-sign-alt' %} +

{{ _('{0} members'.format(c.members|length)) }}

@@ -36,8 +37,5 @@

{{ _('Members') }}

{% endfor %}
-
- {% link_for _('Add member'), controller='organization', action='member_new', id=c.group_dict.id, class_='btn btn-primary' %} -
{% endblock %} From f609bf1ab848c65849ed3041fc61c6c51a1495d3 Mon Sep 17 00:00:00 2001 From: John Martin Date: Thu, 14 Mar 2013 14:58:23 +0000 Subject: [PATCH 05/10] [#630] Adds 'Add Dataset' button into bulk_process page --- ckan/templates/organization/bulk_process.html | 3 ++- ckan/templates/organization/members.html | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ckan/templates/organization/bulk_process.html b/ckan/templates/organization/bulk_process.html index 445aaf81099..7e05b0b4261 100644 --- a/ckan/templates/organization/bulk_process.html +++ b/ckan/templates/organization/bulk_process.html @@ -6,7 +6,8 @@

{{ _('Edit datasets') }}

-

+ {% link_for _('Add Dataset'), controller='package', action='new', group=c.group_dict.id, class_='btn pull-right', icon='plus-sign-alt' %} +

{%- if c.page.item_count -%} {{ c.page.item_count }} datasets{{ _(" found for \"{query}\"").format(query=c.q) if c.q }} {%- elif request.params -%} diff --git a/ckan/templates/organization/members.html b/ckan/templates/organization/members.html index 3124532349c..7bb5b648349 100644 --- a/ckan/templates/organization/members.html +++ b/ckan/templates/organization/members.html @@ -4,7 +4,7 @@ {% block primary_content_inner %}
- {% link_for _('Add member'), controller='organization', action='member_new', id=c.group_dict.id, class_='btn pull-right', icon='plus-sign-alt' %} + {% link_for _('Add Member'), controller='organization', action='member_new', id=c.group_dict.id, class_='btn pull-right', icon='plus-sign-alt' %}

{{ _('{0} members'.format(c.members|length)) }}

From c790b48762adba05094132781d52517e04bcc85c Mon Sep 17 00:00:00 2001 From: tobes Date: Fri, 15 Mar 2013 10:08:59 +0000 Subject: [PATCH 06/10] [#288] Fix for unicode/latin1 .ini values --- ckan/lib/app_globals.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ckan/lib/app_globals.py b/ckan/lib/app_globals.py index 6b108d42a04..e98f975ca7d 100644 --- a/ckan/lib/app_globals.py +++ b/ckan/lib/app_globals.py @@ -109,9 +109,15 @@ def get_config_value(key, default=''): value = model.get_system_info(key) else: value = None + config_value = config.get(key) + # sort encodeings if needed + if isinstance(config_value, str): + try: + config_value = config_value.decode('utf-8') + except UnicodeDecodeError: + config_value = config_value.decode('latin-1') # we want to store the config the first time we get here so we can # reset them if needed - config_value = config.get(key) if key not in _CONFIG_CACHE: _CONFIG_CACHE[key] = config_value if value is not None: From be28fe1c260821cb8027ce1ab4caab970cc79fc7 Mon Sep 17 00:00:00 2001 From: tobes Date: Fri, 15 Mar 2013 14:12:23 +0000 Subject: [PATCH 07/10] [#639] Clean logic.auth.update --- ckan/logic/auth/create.py | 2 ++ ckan/logic/auth/update.py | 41 ++++++++++++++++++--------------------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/ckan/logic/auth/create.py b/ckan/logic/auth/create.py index f791b078a17..64d5cc3e150 100644 --- a/ckan/logic/auth/create.py +++ b/ckan/logic/auth/create.py @@ -106,6 +106,8 @@ def user_create(context, data_dict=None): def _check_group_auth(context, data_dict): + # FIXME This code is shared amoung other logic.auth files and should be + # somewhere better if not data_dict: return True diff --git a/ckan/logic/auth/update.py b/ckan/logic/auth/update.py index 896c6e33991..17dbfbbf525 100644 --- a/ckan/logic/auth/update.py +++ b/ckan/logic/auth/update.py @@ -1,18 +1,17 @@ import ckan.logic as logic import ckan.new_authz as new_authz -from ckan.logic.auth import (get_package_object, get_resource_object, - get_group_object, get_user_object, - get_resource_object, get_related_object) -from ckan.logic.auth.create import _check_group_auth, package_relationship_create -from ckan.lib.base import _ -import ckan.new_authz +import ckan.logic.auth as logic_auth +from ckan.common import _ + +# FIXME this import is evil and should be refactored +from ckan.logic.auth.create import _check_group_auth def make_latest_pending_package_active(context, data_dict): - return package_update(context, data_dict) + return new_authz.is_authorized('package_update', context, data_dict) def package_update(context, data_dict): user = context.get('user') - package = get_package_object(context, data_dict) + package = logic_auth.get_package_object(context, data_dict) if package.owner_org: # if there is an owner org then we must have update_dataset @@ -37,7 +36,7 @@ def package_update(context, data_dict): def resource_update(context, data_dict): model = context['model'] user = context.get('user') - resource = get_resource_object(context, data_dict) + resource = logic_auth.get_resource_object(context, data_dict) # check authentication against package query = model.Session.query(model.Package)\ @@ -49,7 +48,7 @@ def resource_update(context, data_dict): raise logic.NotFound(_('No package found for this resource, cannot check auth.')) pkg_dict = {'id': pkg.id} - authorized = package_update(context, pkg_dict).get('success') + authorized = new_authz.is_authorized('package_update', context, pkg_dict).get('success') if not authorized: return {'success': False, 'msg': _('User %s not authorized to edit resource %s') % (str(user), resource.id)} @@ -57,11 +56,11 @@ def resource_update(context, data_dict): return {'success': True} def package_relationship_update(context, data_dict): - return package_relationship_create(context, data_dict) + return new_authz.is_authorized('package_relationship_create', context, data_dict) def package_change_state(context, data_dict): user = context['user'] - package = get_package_object(context, data_dict) + package = logic_auth.get_package_object(context, data_dict) # use the logic for package_update authorized = new_authz.is_authorized_boolean('package_update', context, data_dict) @@ -71,7 +70,7 @@ def package_change_state(context, data_dict): return {'success': True} def group_update(context, data_dict): - group = get_group_object(context, data_dict) + group = logic_auth.get_group_object(context, data_dict) user = context['user'] authorized = new_authz.has_user_permission_for_group_or_org( group.id, user, 'update') @@ -81,7 +80,7 @@ def group_update(context, data_dict): return {'success': True} def organization_update(context, data_dict): - group = get_group_object(context, data_dict) + group = logic_auth.get_group_object(context, data_dict) user = context['user'] authorized = new_authz.has_user_permission_for_group_or_org( group.id, user, 'update') @@ -96,7 +95,7 @@ def related_update(context, data_dict): if not user: return {'success': False, 'msg': _('Only the owner can update a related item')} - related = get_related_object(context, data_dict) + related = logic_auth.get_related_object(context, data_dict) userobj = model.User.get( user ) if not userobj or userobj.id != related.owner_id: return {'success': False, 'msg': _('Only the owner can update a related item')} @@ -112,7 +111,7 @@ def related_update(context, data_dict): def group_change_state(context, data_dict): user = context['user'] - group = get_group_object(context, data_dict) + group = logic_auth.get_group_object(context, data_dict) # use logic for group_update authorized = new_authz.is_authorized_boolean('group_update', context, data_dict) @@ -123,7 +122,7 @@ def group_change_state(context, data_dict): def group_edit_permissions(context, data_dict): user = context['user'] - group = get_group_object(context, data_dict) + group = logic_auth.get_group_object(context, data_dict) if not new_authz.has_user_permission_for_group_or_org(group.id, user, 'update'): return {'success': False, 'msg': _('User %s not authorized to edit permissions of group %s') % (str(user),group.id)} @@ -134,7 +133,7 @@ def group_edit_permissions(context, data_dict): def user_update(context, data_dict): user = context['user'] - user_obj = get_user_object(context, data_dict) + user_obj = logic_auth.get_user_object(context, data_dict) if not (user == user_obj.name) and \ not ('reset_key' in data_dict and data_dict['reset_key'] == user_obj.reset_key): @@ -164,9 +163,7 @@ def term_translation_update(context, data_dict): def dashboard_mark_activities_old(context, data_dict): - # FIXME: This should go through check_access() not call is_authorized() - # directly, but wait until 2939-orgs is merged before fixing this. - return ckan.new_authz.is_authorized('dashboard_activity_list', + return new_authz.is_authorized('dashboard_activity_list', context, data_dict) @@ -183,7 +180,7 @@ def package_update_rest(context, data_dict): if user in (model.PSEUDO_USER__VISITOR, ''): return {'success': False, 'msg': _('Valid API key needed to edit a package')} - return package_update(context, data_dict) + return new_authz.is_authorized('package_update', context, data_dict) def group_update_rest(context, data_dict): model = context['model'] From 2e2e9572b70c6a8d6485269c4b740c5742cfd636 Mon Sep 17 00:00:00 2001 From: John Glover Date: Mon, 18 Mar 2013 11:52:46 +0100 Subject: [PATCH 08/10] [#639] PEP8 --- ckan/logic/auth/update.py | 125 +++++++++++++++++++++++++++++--------- 1 file changed, 95 insertions(+), 30 deletions(-) diff --git a/ckan/logic/auth/update.py b/ckan/logic/auth/update.py index 17dbfbbf525..0a17ad43822 100644 --- a/ckan/logic/auth/update.py +++ b/ckan/logic/auth/update.py @@ -6,9 +6,11 @@ # FIXME this import is evil and should be refactored from ckan.logic.auth.create import _check_group_auth + def make_latest_pending_package_active(context, data_dict): return new_authz.is_authorized('package_update', context, data_dict) + def package_update(context, data_dict): user = context.get('user') package = logic_auth.get_package_object(context, data_dict) @@ -16,7 +18,9 @@ def package_update(context, data_dict): if package.owner_org: # if there is an owner org then we must have update_dataset # premission for that organization - check1 = new_authz.has_user_permission_for_group_or_org(package.owner_org, user, 'update_dataset') + check1 = new_authz.has_user_permission_for_group_or_org( + package.owner_org, user, 'update_dataset' + ) else: # If dataset is not owned then we can edit if config permissions allow if new_authz.auth_is_registered_user(): @@ -25,14 +29,19 @@ def package_update(context, data_dict): else: check1 = new_authz.check_config_permission('anon_create_dataset') if not check1: - return {'success': False, 'msg': _('User %s not authorized to edit package %s') % (str(user), package.id)} + return {'success': False, + 'msg': _('User %s not authorized to edit package %s') % + (str(user), package.id)} else: - check2 = _check_group_auth(context,data_dict) + check2 = _check_group_auth(context, data_dict) if not check2: - return {'success': False, 'msg': _('User %s not authorized to edit these groups') % str(user)} + return {'success': False, + 'msg': _('User %s not authorized to edit these groups') % + (str(user))} return {'success': True} + def resource_update(context, data_dict): model = context['model'] user = context.get('user') @@ -45,60 +54,84 @@ def resource_update(context, data_dict): .filter(model.ResourceGroup.id == resource.resource_group_id) pkg = query.first() if not pkg: - raise logic.NotFound(_('No package found for this resource, cannot check auth.')) + raise logic.NotFound( + _('No package found for this resource, cannot check auth.') + ) pkg_dict = {'id': pkg.id} authorized = new_authz.is_authorized('package_update', context, pkg_dict).get('success') if not authorized: - return {'success': False, 'msg': _('User %s not authorized to edit resource %s') % (str(user), resource.id)} + return {'success': False, + 'msg': _('User %s not authorized to edit resource %s') % + (str(user), resource.id)} else: return {'success': True} + def package_relationship_update(context, data_dict): - return new_authz.is_authorized('package_relationship_create', context, data_dict) + return new_authz.is_authorized('package_relationship_create', + context, + data_dict) + def package_change_state(context, data_dict): user = context['user'] package = logic_auth.get_package_object(context, data_dict) # use the logic for package_update - authorized = new_authz.is_authorized_boolean('package_update', context, data_dict) + authorized = new_authz.is_authorized_boolean('package_update', + context, + data_dict) if not authorized: - return {'success': False, 'msg': _('User %s not authorized to change state of package %s') % (str(user),package.id)} + return { + 'success': False, + 'msg': _('User %s not authorized to change state of package %s') % + (str(user), package.id) + } else: return {'success': True} + def group_update(context, data_dict): group = logic_auth.get_group_object(context, data_dict) user = context['user'] - authorized = new_authz.has_user_permission_for_group_or_org( - group.id, user, 'update') + authorized = new_authz.has_user_permission_for_group_or_org(group.id, + user, + 'update') if not authorized: - return {'success': False, 'msg': _('User %s not authorized to edit group %s') % (str(user),group.id)} + return {'success': False, + 'msg': _('User %s not authorized to edit group %s') % + (str(user), group.id)} else: return {'success': True} + def organization_update(context, data_dict): group = logic_auth.get_group_object(context, data_dict) user = context['user'] authorized = new_authz.has_user_permission_for_group_or_org( group.id, user, 'update') if not authorized: - return {'success': False, 'msg': _('User %s not authorized to edit organization %s') % (user, group.id)} + return {'success': False, + 'msg': _('User %s not authorized to edit organization %s') % + (user, group.id)} else: return {'success': True} + def related_update(context, data_dict): model = context['model'] user = context['user'] if not user: - return {'success': False, 'msg': _('Only the owner can update a related item')} + return {'success': False, + 'msg': _('Only the owner can update a related item')} related = logic_auth.get_related_object(context, data_dict) - userobj = model.User.get( user ) + userobj = model.User.get(user) if not userobj or userobj.id != related.owner_id: - return {'success': False, 'msg': _('Only the owner can update a related item')} + return {'success': False, + 'msg': _('Only the owner can update a related item')} # Only sysadmins can change the featured field. if ('featured' in data_dict and data_dict['featured'] != related.featured): @@ -114,57 +147,85 @@ def group_change_state(context, data_dict): group = logic_auth.get_group_object(context, data_dict) # use logic for group_update - authorized = new_authz.is_authorized_boolean('group_update', context, data_dict) + authorized = new_authz.is_authorized_boolean('group_update', + context, + data_dict) if not authorized: - return {'success': False, 'msg': _('User %s not authorized to change state of group %s') % (str(user),group.id)} + return { + 'success': False, + 'msg': _('User %s not authorized to change state of group %s') % + (str(user), group.id) + } else: return {'success': True} + def group_edit_permissions(context, data_dict): user = context['user'] group = logic_auth.get_group_object(context, data_dict) - if not new_authz.has_user_permission_for_group_or_org(group.id, user, 'update'): - return {'success': False, 'msg': _('User %s not authorized to edit permissions of group %s') % (str(user),group.id)} + authorized = new_authz.has_user_permission_for_group_or_org(group.id, + user, + 'update') + + if not authorized: + return {'success': False, + 'msg': _('User %s not authorized to edit permissions of group %s') % + (str(user), group.id)} else: return {'success': True} - def user_update(context, data_dict): user = context['user'] user_obj = logic_auth.get_user_object(context, data_dict) + user_reset = ('reset_key' in data_dict and + data_dict['reset_key'] == user_obj.reset_key) - if not (user == user_obj.name) and \ - not ('reset_key' in data_dict and data_dict['reset_key'] == user_obj.reset_key): - return {'success': False, 'msg': _('User %s not authorized to edit user %s') % (str(user), user_obj.id)} + if not (user == user_obj.name) and not user_reset: + return {'success': False, + 'msg': _('User %s not authorized to edit user %s') % + (str(user), user_obj.id)} return {'success': True} + def revision_change_state(context, data_dict): # FIXME currently only sysadmins can change state user = context['user'] + return { + 'success': False, + 'msg': _('User %s not authorized to change state of revision') % user + } - return {'success': False, 'msg': _('User %s not authorized to change state of revision' ) % user} def task_status_update(context, data_dict): # sysadmins only user = context['user'] - return {'success': False, 'msg': _('User %s not authorized to update task_status table') % user} + return { + 'success': False, + 'msg': _('User %s not authorized to update task_status table') % user + } + def vocabulary_update(context, data_dict): # sysadmins only return {'success': False} + def term_translation_update(context, data_dict): # sysadmins only user = context['user'] - return {'success': False, 'msg': _('User %s not authorized to update term_translation table') % user} + return { + 'success': False, + 'msg': _('User %s not authorized to update term_translation table') % user + } def dashboard_mark_activities_old(context, data_dict): return new_authz.is_authorized('dashboard_activity_list', - context, data_dict) + context, + data_dict) def send_email_notifications(context, data_dict): @@ -178,18 +239,22 @@ def package_update_rest(context, data_dict): model = context['model'] user = context['user'] if user in (model.PSEUDO_USER__VISITOR, ''): - return {'success': False, 'msg': _('Valid API key needed to edit a package')} + return {'success': False, + 'msg': _('Valid API key needed to edit a package')} return new_authz.is_authorized('package_update', context, data_dict) + def group_update_rest(context, data_dict): model = context['model'] user = context['user'] if user in (model.PSEUDO_USER__VISITOR, ''): - return {'success': False, 'msg': _('Valid API key needed to edit a group')} + return {'success': False, + 'msg': _('Valid API key needed to edit a group')} return group_update(context, data_dict) + def package_owner_org_update(context, data_dict): # sysadmins only return {'success': False} From 34911b9921756469a40ceb5320ba9451ecd75fe7 Mon Sep 17 00:00:00 2001 From: kindly Date: Mon, 18 Mar 2013 16:40:16 +0000 Subject: [PATCH 09/10] #515 make it so organization titles are in facet listing --- ckan/logic/action/get.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index 46ff233ee9c..f70f7b520ef 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -1323,7 +1323,7 @@ def package_search(context, data_dict): for key_, value_ in value.items(): new_facet_dict = {} new_facet_dict['name'] = key_ - if key == 'groups': + if key in ('groups', 'organization'): group = model.Group.get(key_) if group: new_facet_dict['display_name'] = group.display_name From 4204dee347e40f76661dc2d781e237d79236a113 Mon Sep 17 00:00:00 2001 From: John Martin Date: Tue, 19 Mar 2013 10:29:42 +0000 Subject: [PATCH 10/10] [#629] Adds .clearfix to .module-heading to make the 'Clear all' a little nicer --- ckan/public/base/less/module.less | 1 + 1 file changed, 1 insertion(+) diff --git a/ckan/public/base/less/module.less b/ckan/public/base/less/module.less index fe8b5cf7598..1f2f791da4c 100644 --- a/ckan/public/base/less/module.less +++ b/ckan/public/base/less/module.less @@ -3,6 +3,7 @@ } .module-heading { + .clearfix; margin: 0; padding: 7px @gutterX; font-size: 14px;