diff --git a/ckan/lib/helpers.py b/ckan/lib/helpers.py index 4fb2228d1db..d0c1ab4234f 100644 --- a/ckan/lib/helpers.py +++ b/ckan/lib/helpers.py @@ -38,7 +38,7 @@ get_locales_dict = i18n.get_locales_dict try: - from collections import OrderedDict # from python 2.7 + from collections import OrderedDict # from python 2.7 except ImportError: from sqlalchemy.util import OrderedDict @@ -49,6 +49,7 @@ _log = logging.getLogger(__name__) + def redirect_to(*args, **kw): '''A routes.redirect_to wrapper to retain the i18n settings''' kw['__ckan_no_root'] = True @@ -56,6 +57,7 @@ def redirect_to(*args, **kw): kw['__no_cache__'] = True return _redirect_to(url_for(*args, **kw)) + def url(*args, **kw): """Create url adding i18n information if selected wrapper for pylons.url""" @@ -63,6 +65,7 @@ def url(*args, **kw): my_url = _pylons_default_url(*args, **kw) return _add_i18n_to_url(my_url, locale=locale, **kw) + def url_for(*args, **kw): """Create url adding i18n information if selected wrapper for routes.url_for""" @@ -80,6 +83,7 @@ def url_for(*args, **kw): kw['__ckan_no_root'] = no_root return _add_i18n_to_url(my_url, locale=locale, **kw) + def url_for_static(*args, **kw): """Create url for static content that does not get translated eg css, js @@ -87,6 +91,7 @@ def url_for_static(*args, **kw): my_url = _routes_default_url_for(*args, **kw) return my_url + def _add_i18n_to_url(url_to_amend, **kw): # If the locale keyword param is provided then the url is rewritten # using that locale .If return_to is provided this is used as the url @@ -149,14 +154,17 @@ def _add_i18n_to_url(url_to_amend, **kw): url = '/%s%s' % (locale, url) if url == '/packages': - raise ckan.exceptions.CkanUrlException('There is a broken url being created %s' % kw) + error = 'There is a broken url being created %s' % kw + raise ckan.exceptions.CkanUrlException(error) return url + def lang(): ''' Return the language code for the current locale eg `en` ''' return request.environ.get('CKAN_LANG') + def lang_native_name(lang=None): ''' Return the langage name currently used in it's localised form either from parameter or current environ setting''' @@ -166,6 +174,7 @@ def lang_native_name(lang=None): return locale.display_name or locale.english_name return lang + class Message(object): """A message returned by ``Flash.pop_messages()``. @@ -177,9 +186,9 @@ class Message(object): """ def __init__(self, category, message, allow_html): - self.category=category - self.message=message - self.allow_html=allow_html + self.category = category + self.message = message + self.allow_html = allow_html def __str__(self): return self.message @@ -192,6 +201,7 @@ def __html__(self): else: return escape(self.message) + class _Flash(object): # List of allowed categories. If None, allow any category. @@ -200,16 +210,19 @@ class _Flash(object): # Default category if none is specified. default_category = "" - def __init__(self, session_key="flash", categories=None, default_category=None): + def __init__(self, session_key="flash", categories=None, + default_category=None): self.session_key = session_key if categories is not None: self.categories = categories if default_category is not None: self.default_category = default_category if self.categories and self.default_category not in self.categories: - raise ValueError("unrecognized default category %r" % (self.default_category,)) + raise ValueError("unrecognized default category %r" + % (self.default_category,)) - def __call__(self, message, category=None, ignore_duplicate=False, allow_html=False): + def __call__(self, message, category=None, ignore_duplicate=False, + allow_html=False): if not category: category = self.default_category elif self.categories and category not in self.categories: @@ -244,24 +257,27 @@ def are_there_messages(self): # this is here for backwards compatability _flash = flash + def flash_notice(message, allow_html=False): ''' Show a flash message of type notice ''' flash(message, category='alert-info', allow_html=allow_html) + def flash_error(message, allow_html=False): ''' Show a flash message of type error ''' flash(message, category='alert-error', allow_html=allow_html) + def flash_success(message, allow_html=False): ''' Show a flash message of type success ''' flash(message, category='alert-success', allow_html=allow_html) + def are_there_flash_messages(): ''' Returns True if there are flash messages for the current user ''' return flash.are_there_messages() - def nav_link(*args, **kwargs): # nav_link() used to need c passing as the first arg # this is deprecated as pointless @@ -273,6 +289,7 @@ def nav_link(*args, **kwargs): raise Exception('nav_link() calling has been changed. remove c in template %s or included one' % c.__template_name) return _nav_link(*args, **kwargs) + def _nav_link(text, controller, **kwargs): highlight_actions = kwargs.pop("highlight_actions", @@ -285,6 +302,7 @@ def _nav_link(text, controller, **kwargs): else '') ) + def nav_named_link(*args, **kwargs): # subnav_link() used to need c passing as the first arg # this is deprecated as pointless @@ -297,6 +315,7 @@ def nav_named_link(*args, **kwargs): raise Exception('nav_named_link() calling has been changed. remove c in template %s or included one' % c.__template_name) return _nav_named_link(*args, **kwargs) + def _nav_named_link(text, name, **kwargs): return link_to( text, @@ -306,6 +325,7 @@ def _nav_named_link(text, name, **kwargs): # else '') ) + def subnav_link(*args, **kwargs): # subnav_link() used to need c passing as the first arg # this is deprecated as pointless @@ -317,6 +337,7 @@ def subnav_link(*args, **kwargs): raise Exception('subnav_link() calling has been changed. remove c in template %s or included one' % c.__template_name) return _subnav_link(*args, **kwargs) + def _subnav_link(text, action, **kwargs): return link_to( text, @@ -324,6 +345,7 @@ def _subnav_link(text, action, **kwargs): class_=('active' if c.action == action else '') ) + def subnav_named_route(*args, **kwargs): # subnav_link() used to need c passing as the first arg # this is deprecated as pointless @@ -336,6 +358,7 @@ def subnav_named_route(*args, **kwargs): raise Exception('subnav_named_route() calling has been changed. remove c in template %s or included one' % c.__template_name) return _subnav_named_route(*args, **kwargs) + def _subnav_named_route(text, routename, **kwargs): """ Generate a subnav element based on a named route """ return link_to( @@ -344,8 +367,10 @@ def _subnav_named_route(text, routename, **kwargs): class_=('active' if c.action == kwargs['action'] else '') ) + def default_group_type(): - return str( config.get('ckan.default.group_type', 'group') ) + return str(config.get('ckan.default.group_type', 'group')) + def unselected_facet_items(facet, limit=10): '''Return the list of unselected facet items for the given facet, sorted @@ -377,9 +402,11 @@ def unselected_facet_items(facet, limit=10): facets.append(facet_item) return sorted(facets, key=lambda item: item['count'], reverse=True)[:limit] + def facet_title(name): return config.get('search.facets.%s.title' % name, name.capitalize()) + @deprecated('Please use check_access instead.') def am_authorized(c, action, domain_object=None): ''' Deprecated. Please use check_access instead''' @@ -388,20 +415,22 @@ def am_authorized(c, action, domain_object=None): domain_object = model.System() return Authorizer.am_authorized(c, action, domain_object) + def check_access(action, data_dict=None): - from ckan.logic import check_access as check_access_logic,NotAuthorized + from ckan.logic import check_access as check_access_logic, NotAuthorized context = {'model': model, 'user': c.user or c.author} try: - check_access_logic(action,context,data_dict) + check_access_logic(action, context, data_dict) authorized = True except NotAuthorized: authorized = False return authorized + def linked_user(user, maxlength=0): if user in [model.PSEUDO_USER__LOGGED_IN, model.PSEUDO_USER__VISITOR]: return user @@ -411,13 +440,14 @@ def linked_user(user, maxlength=0): if not user: return user_name if user: - _name = user.name if model.User.VALID_NAME.match(user.name) else user.id - _icon = gravatar(user.email_hash, 20) + name = user.name if model.User.VALID_NAME.match(user.name) else user.id + icon = gravatar(user.email_hash, 20) displayname = user.display_name if maxlength and len(user.display_name) > maxlength: displayname = displayname[:maxlength] + '...' - return _icon + link_to(displayname, - url_for(controller='user', action='read', id=_name)) + return icon + link_to(displayname, + url_for(controller='user', action='read', id=name)) + def linked_authorization_group(authgroup, maxlength=0): if not isinstance(authgroup, model.AuthorizationGroup): @@ -430,7 +460,9 @@ def linked_authorization_group(authgroup, maxlength=0): if maxlength and len(display_name) > maxlength: displayname = displayname[:maxlength] + '...' return link_to(displayname, - url_for(controller='authorization_group', action='read', id=displayname)) + url_for(controller='authorization_group', + action='read', id=displayname)) + def group_name_to_title(name): group = model.Group.by_name(name) @@ -438,22 +470,30 @@ def group_name_to_title(name): return group.display_name return name + def markdown_extract(text, extract_length=190): if (text is None) or (text.strip() == ''): return '' plain = re.sub(r'<.*?>', '', markdown(text)) - return unicode(truncate(plain, length=extract_length, indicator='...', whole_word=True)) + return unicode(truncate(plain, length=extract_length, + indicator='...', whole_word=True)) + def icon_url(name): return url_for_static('/images/icons/%s.png' % name) + def icon_html(url, alt=None, inline=True): classes = '' - if inline: classes += 'inline-icon ' - return literal('%s ' % (url, alt, classes)) + if inline: + classes += 'inline-icon ' + return literal(('%s ') % (url, alt, classes)) + def icon(name, alt=None, inline=True): - return icon_html(icon_url(name),alt,inline) + return icon_html(icon_url(name), alt, inline) + def resource_icon(res): if False: @@ -463,7 +503,8 @@ def resource_icon(res): # also: 'page_white_link' return icon(icon_name) else: - return icon(format_icon(res.get('format',''))) + return icon(format_icon(res.get('format', ''))) + def format_icon(_format): _format = _format.lower() @@ -476,14 +517,18 @@ def format_icon(_format): if ('xml' in _format): return 'page_white_code' return 'page_white' + def linked_gravatar(email_hash, size=100, default=None): return literal( '' % _('Update your avatar at gravatar.com') + - '%s' % gravatar(email_hash,size,default) + '%s' % gravatar(email_hash, size, default) ) -_VALID_GRAVATAR_DEFAULTS = ['404', 'mm', 'identicon', 'monsterid', 'wavatar', 'retro'] +_VALID_GRAVATAR_DEFAULTS = ['404', 'mm', 'identicon', 'monsterid', + 'wavatar', 'retro'] + + def gravatar(email_hash, size=100, default=None): if default is None: default = config.get('ckan.gravatar_default', 'identicon') @@ -492,10 +537,9 @@ def gravatar(email_hash, size=100, default=None): # treat the default as a url default = urllib.quote(default, safe='') - return literal('''''' - % (email_hash, size, default) - ) + return literal(('') % (email_hash, size, default)) + def pager_url(page, partial=None, **kwargs): routes_dict = _pylons_default_url.environ['pylons.routes_dict'] @@ -506,14 +550,17 @@ def pager_url(page, partial=None, **kwargs): kwargs['page'] = page return url(**kwargs) + class Page(paginate.Page): # Curry the pager method of the webhelpers.paginate.Page class, so we have # our custom layout set as default. + def pager(self, *args, **kwargs): kwargs.update( - format=u"", + format=u'', symbol_previous=u'« Prev', symbol_next=u'Next »', - curpage_attr={'class':'active'}, link_attr={} + curpage_attr={'class': 'active'}, link_attr={} ) return super(Page, self).pager(*args, **kwargs) @@ -535,9 +582,11 @@ def _range(self, regexp_match): # Convert current page text = '%s' % self.page current_page_span = str(HTML.span(c=text, **self.curpage_attr)) - current_page_link = self._pagerlink(self.page, text, extra_attributes=self.curpage_attr) + current_page_link = self._pagerlink(self.page, text, + extra_attributes=self.curpage_attr) return re.sub(current_page_span, current_page_link, html) + def render_datetime(datetime_, date_format=None, with_hours=False): '''Render a datetime object or timestamp string as a pretty string (Y-m-d H:m). @@ -560,6 +609,7 @@ def render_datetime(datetime_, date_format=None, with_hours=False): else: return '' + @deprecated() def datetime_to_date_str(datetime_): '''DEPRECATED: Takes a datetime.datetime object and returns a string of it @@ -567,6 +617,7 @@ def datetime_to_date_str(datetime_): ''' return datetime_.isoformat() + def date_str_to_datetime(date_str): '''Convert ISO-like formatted datestring to datetime object. @@ -598,21 +649,24 @@ def date_str_to_datetime(date_str): return datetime.datetime(*map(int, time_tuple)) + def parse_rfc_2822_date(date_str, assume_utc=True): """ - Parse a date string of the form specified in RFC 2822, and return a datetime. + Parse a date string of the form specified in RFC 2822, and return a + datetime. - RFC 2822 is the date format used in HTTP headers. It should contain timezone - information, but that cannot be relied upon. + RFC 2822 is the date format used in HTTP headers. It should contain + timezone information, but that cannot be relied upon. - If date_str doesn't contain timezone information, then the 'assume_utc' flag - determines whether we assume this string is local (with respect to the + If date_str doesn't contain timezone information, then the 'assume_utc' + flag determines whether we assume this string is local (with respect to the server running this code), or UTC. In practice, what this means is that if - assume_utc is True, then the returned datetime is 'aware', with an associated - tzinfo of offset zero. Otherwise, the returned datetime is 'naive'. + assume_utc is True, then the returned datetime is 'aware', with an + associated tzinfo of offset zero. Otherwise, the returned datetime is + 'naive'. - If timezone information is available in date_str, then the returned datetime - is 'aware', ie - it has an associated tz_info object. + If timezone information is available in date_str, then the returned + datetime is 'aware', ie - it has an associated tz_info object. Returns None if the string cannot be parsed as a valid datetime. """ @@ -624,12 +678,14 @@ def parse_rfc_2822_date(date_str, assume_utc=True): # No timezone information available in the string if time_tuple[-1] is None and not assume_utc: - return datetime.datetime.fromtimestamp(email.utils.mktime_tz(time_tuple)) + return datetime.datetime.fromtimestamp( + email.utils.mktime_tz(time_tuple)) else: offset = 0 if time_tuple[-1] is None else time_tuple[-1] tz_info = _RFC2282TzInfo(offset) return datetime.datetime(*time_tuple[:6], microsecond=0, tzinfo=tz_info) + class _RFC2282TzInfo(datetime.tzinfo): """ A datetime.tzinfo implementation used by parse_rfc_2822_date() function. @@ -664,21 +720,26 @@ def tzname(self, dt): def time_ago_in_words_from_str(date_str, granularity='month'): if date_str: - return date.time_ago_in_words(date_str_to_datetime(date_str), granularity=granularity) + return date.time_ago_in_words(date_str_to_datetime(date_str), + granularity=granularity) else: return _('Unknown') + def button_attr(enable, type='primary'): if enable: return 'class="btn %s"' % type return 'disabled class="btn disabled"' + def dataset_display_name(package_or_package_dict): if isinstance(package_or_package_dict, dict): - return package_or_package_dict.get('title', '') or package_or_package_dict.get('name', '') + return package_or_package_dict.get('title', '') or \ + package_or_package_dict.get('name', '') else: return package_or_package_dict.title or package_or_package_dict.name + def dataset_link(package_or_package_dict): if isinstance(package_or_package_dict, dict): name = package_or_package_dict['name'] @@ -690,6 +751,7 @@ def dataset_link(package_or_package_dict): url_for(controller='package', action='read', id=name) ) + # TODO: (?) support resource objects as well def resource_display_name(resource_dict): name = resource_dict.get('name', None) @@ -698,13 +760,15 @@ def resource_display_name(resource_dict): return name elif description: description = description.split('.')[0] - max_len = 60; - if len(description)>max_len: description = description[:max_len]+'...' + max_len = 60 + if len(description) > max_len: + description = description[:max_len] + '...' return description else: noname_string = _('no name') return '[%s] %s' % (noname_string, resource_dict['id']) + def resource_link(resource_dict, package_id): text = resource_display_name(resource_dict) url = url_for(controller='package', @@ -713,6 +777,7 @@ def resource_link(resource_dict, package_id): resource_id=resource_dict['id']) return link_to(text, url) + def related_item_link(related_item_dict): text = related_item_dict.get('title', '') url = url_for(controller='related', @@ -720,46 +785,58 @@ def related_item_link(related_item_dict): id=related_item_dict['id']) return link_to(text, url) + def tag_link(tag): url = url_for(controller='tag', action='read', id=tag['name']) return link_to(tag['name'], url) + def group_link(group): url = url_for(controller='group', action='read', id=group['name']) return link_to(group['name'], url) + def dump_json(obj, **kw): return json.dumps(obj, **kw) + def auto_log_message(*args): # auto_log_message() used to need c passing as the first arg # this is deprecated as pointless # throws error if ckan.restrict_template_vars is True # When we move to strict helpers then this should be removed as a wrapper - if len(args) and asbool(config.get('ckan.restrict_template_vars', 'false')): - raise Exception('auto_log_message() calling has been changed. remove c in template %s or included one' % c.__template_name) + if len(args) and asbool(config.get('ckan.restrict_template_vars', + 'false')): + raise Exception(('auto_log_message() calling has been changed. ' + + 'remove c in template %s or included one') % + c.__template_name) return _auto_log_message() + def _auto_log_message(): - if (c.action=='new') : + if (c.action == 'new'): return _('Created new dataset.') - elif (c.action=='editresources'): + elif (c.action == 'editresources'): return _('Edited resources.') - elif (c.action=='edit'): + elif (c.action == 'edit'): return _('Edited settings.') return '' + def activity_div(template, activity, actor, object=None, target=None): actor = '%s' % actor if object: object = '%s' % object if target: target = '%s' % target - date = '%s' % render_datetime(activity['timestamp']) - template = template.format(actor=actor, date=date, object=object, target=target) + rendered_datetime = render_datetime(activity['timestamp']) + date = '%s' % rendered_datetime + template = template.format(actor=actor, date=date, + object=object, target=target) template = '
%s %s
' % (template, date) return literal(template) + def snippet(template_name, **kw): ''' This function is used to load html snippets into pages. keywords can be used to pass parameters into the snippet rendering ''' @@ -779,20 +856,20 @@ def process_names(items): array.append(item.name) return array - rev = {'id' : revision.id, - 'state' : revision.state, - 'timestamp' : revision.timestamp, - 'author' : revision.author, - 'packages' : process_names(revision.packages), - 'groups' : process_names(revision.groups), - 'message' : revision.message,} + rev = {'id': revision.id, + 'state': revision.state, + 'timestamp': revision.timestamp, + 'author': revision.author, + 'packages': process_names(revision.packages), + 'groups': process_names(revision.groups), + 'message': revision.message, } return rev import lib.dictization.model_dictize as md - converters = {'package' : md.package_dictize, - 'revisions' : dictize_revision_list} + converters = {'package': md.package_dictize, + 'revisions': dictize_revision_list} converter = converters[object_type] items = [] - context = {'model' : model} + context = {'model': model} for obj in objs: item = converter(obj, context) items.append(item) @@ -801,6 +878,7 @@ def process_names(items): # these are the types of objects that can be followed _follow_objects = ['dataset', 'user'] + def follow_button(obj_type, obj_id): '''Return a follow button for the given object type and id. @@ -822,7 +900,7 @@ def follow_button(obj_type, obj_id): assert obj_type in _follow_objects # If the user is logged in show the follow/unfollow button if c.user: - context = {'model' : model, 'session':model.Session, 'user':c.user} + context = {'model': model, 'session': model.Session, 'user': c.user} action = 'am_following_%s' % obj_type following = logic.get_action(action)(context, {'id': obj_id}) return snippet('snippets/follow_button.html', @@ -831,6 +909,7 @@ def follow_button(obj_type, obj_id): obj_type=obj_type) return '' + def follow_count(obj_type, obj_id): '''Return the number of followers of an object. @@ -847,9 +926,10 @@ def follow_count(obj_type, obj_id): obj_type = obj_type.lower() assert obj_type in _follow_objects action = '%s_follower_count' % obj_type - context = {'model' : model, 'session':model.Session, 'user':c.user} + context = {'model': model, 'session': model.Session, 'user': c.user} return logic.get_action(action)(context, {'id': obj_id}) + def dashboard_activity_stream(user_id): '''Return the dashboard activity stream of the given user. @@ -861,8 +941,10 @@ def dashboard_activity_stream(user_id): ''' import ckan.logic as logic - context = {'model' : model, 'session':model.Session, 'user':c.user} - return logic.get_action('dashboard_activity_list_html')(context, {'id': user_id}) + context = {'model': model, 'session': model.Session, 'user': c.user} + return logic.get_action('dashboard_activity_list_html')(context, + {'id': user_id}) + def get_request_param(parameter_name, default=None): ''' This function allows templates to access query string parameters @@ -870,6 +952,7 @@ def get_request_param(parameter_name, default=None): searches. ''' return request.params.get(parameter_name, default) + def render_markdown(data): ''' returns the data as rendered markdown ''' # cope with data == None