diff --git a/ckan/config/middleware.py b/ckan/config/middleware.py index f28a896f378..026cf02722f 100644 --- a/ckan/config/middleware.py +++ b/ckan/config/middleware.py @@ -133,6 +133,17 @@ def __init__(self, app, config): self.default_locale = config.get('ckan.locale_default', 'en') self.local_list = get_locales() + def get_cookie_lang(self, environ): + # get the lang from cookie if present + cookie = environ.get('HTTP_COOKIE') + if cookie: + cookies = [c.strip() for c in cookie.split(';')] + lang = [c.split('=')[1] for c in cookies \ + if c.startswith('ckan_lang')] + if lang and lang[0] in self.local_list: + return lang[0] + return None + def __call__(self, environ, start_response): # strip the language selector from the requested url # and set environ variables for the language selected @@ -153,9 +164,16 @@ def __call__(self, environ, start_response): else: environ['PATH_INFO'] = '/' else: - # use default language from config - environ['CKAN_LANG'] = self.default_locale - environ['CKAN_LANG_IS_DEFAULT'] = True + # use cookie lang or default language from config + cookie_lang = self.get_cookie_lang(environ) + if cookie_lang: + environ['CKAN_LANG'] = cookie_lang + default = (cookie_lang == self.default_locale) + environ['CKAN_LANG_IS_DEFAULT'] = default + else: + environ['CKAN_LANG'] = self.default_locale + environ['CKAN_LANG_IS_DEFAULT'] = True + # Current application url path_info = environ['PATH_INFO'] diff --git a/ckan/config/routing.py b/ckan/config/routing.py index 64ab7d73eab..b99178a2e67 100644 --- a/ckan/config/routing.py +++ b/ckan/config/routing.py @@ -164,7 +164,6 @@ def make_map(): ])) ) - m.connect('/dataset', action='index') m.connect('/dataset/{action}/{id}/{revision}', action='read_ajax', requirements=dict(action='|'.join([ 'read', diff --git a/ckan/lib/helpers.py b/ckan/lib/helpers.py index 0a19353590e..ad544419194 100644 --- a/ckan/lib/helpers.py +++ b/ckan/lib/helpers.py @@ -96,7 +96,8 @@ def _add_i18n_to_url(url_to_amend, **kw): except TypeError: root = '' if default_locale: - url = '%s%s' % (root, url_to_amend) + url = url_to_amend[len(root):] + url = '%s%s' % (root, url) else: # we need to strip the root from the url and the add it before # the language specification. diff --git a/ckan/lib/i18n.py b/ckan/lib/i18n.py index 743ef880aad..f53863a8412 100644 --- a/ckan/lib/i18n.py +++ b/ckan/lib/i18n.py @@ -3,6 +3,7 @@ from babel import Locale, localedata from babel.core import LOCALE_ALIASES from pylons import config +from pylons import response from pylons import i18n import ckan.i18n @@ -89,11 +90,20 @@ def get_available_locales(): def handle_request(request, tmpl_context): ''' Set the language for the request ''' - lang = request.environ.get('CKAN_LANG', - config.get('ckan.locale_default', 'en')) + lang = request.environ.get('CKAN_LANG') or \ + config.get('ckan.locale_default', 'en') if lang != 'en': i18n.set_lang(lang) tmpl_context.language = lang + + # set ckan_lang cookie if we have changed the language. We need to + # remember this because repoze.who does it's own redirect. + try: + if request.cookies.get('ckan_lang') != lang: + response.set_cookie('ckan_lang', lang, max_age=3600) + except AttributeError: + # when testing FakeRequest does not have cookies + pass return lang def get_lang(): diff --git a/ckan/public/css/bootstrap.css b/ckan/public/css/bootstrap.css new file mode 100644 index 00000000000..708a79e9043 --- /dev/null +++ b/ckan/public/css/bootstrap.css @@ -0,0 +1,662 @@ +/* ================== */ +/* :: Bootstrap - Extracted CSS */ +/* :: If we include full bootstrap this could be deprecated */ +/* ================== */ + +/* ================== */ +/* :: Bootstrap v1.0 :: */ +/* ================== */ + +/* ================================== */ +/* = Twitter.Bootstrap Form Buttons = */ +/* ================================== */ + +.pretty-button { + cursor: pointer; + display: inline-block; + background-color: #e6e6e6; + background-repeat: no-repeat; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), color-stop(0.25, #ffffff), to(#e6e6e6)); + background-image: -webkit-linear-gradient(#ffffff, #ffffff 0.25, #e6e6e6); + background-image: -moz-linear-gradient(#ffffff, #ffffff 0.25, #e6e6e6); + background-image: -ms-linear-gradient(#ffffff, #ffffff 0.25, #e6e6e6); + background-image: -o-linear-gradient(#ffffff, #ffffff 0.25, #e6e6e6); + background-image: linear-gradient(#ffffff, #ffffff 0.25, #e6e6e6); + padding: 4px 14px; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); + color: #333; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: normal; + font-size: 13px; + line-height: 18px; + border: 1px solid #ccc; + border-bottom-color: #bbb; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -webkit-transition: 0.1s linear all; + -moz-transition: 0.1s linear all; + transition: 0.1s linear all; +} +.pretty-button.depressed, +.pretty-button:hover { + background-position: 0 -15px; + color: #333; + text-decoration: none; +} +.pretty-button.primary, .pretty-button.danger { + color: #fff; +} +.pretty-button.primary:hover, .pretty-button.danger:hover { + color: #fff; +} +.pretty-button.primary { + background-color: #0064cd; + background-repeat: repeat-x; + background-image: -khtml-gradient(linear, left top, left bottom, from(#049cdb), to(#0064cd)); + background-image: -moz-linear-gradient(#049cdb, #0064cd); + background-image: -ms-linear-gradient(#049cdb, #0064cd); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #049cdb), color-stop(100%, #0064cd)); + background-image: -webkit-linear-gradient(#049cdb, #0064cd); + background-image: -o-linear-gradient(#049cdb, #0064cd); + background-image: linear-gradient(#049cdb, #0064cd); + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + border-color: #0064cd #0064cd #003f81; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); +} +.pretty-button.danger { + background-color: #9d261d; + background-repeat: repeat-x; + background-image: -khtml-gradient(linear, left top, left bottom, from(#d83a2e), to(#9d261d)); + background-image: -moz-linear-gradient(#d83a2e, #9d261d); + background-image: -ms-linear-gradient(#d83a2e, #9d261d); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #d83a2e), color-stop(100%, #9d261d)); + background-image: -webkit-linear-gradient(#d83a2e, #9d261d); + background-image: -o-linear-gradient(#d83a2e, #9d261d); + background-image: linear-gradient(#d83a2e, #9d261d); + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + border-color: #9d261d #9d261d #5c1611; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); +} +.pretty-button.large { + font-size: 16px; + line-height: 28px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} +.pretty-button.small-button { + padding-right: 9px; + padding-left: 9px; + font-size: 11px; +} +.pretty-button.disabled { + background-image: none; + filter: alpha(opacity=65); + -khtml-opacity: 0.65; + -moz-opacity: 0.65; + opacity: 0.65; + cursor: default; +} +.pretty-button:disabled { + background-image: none; + filter: alpha(opacity=65); + -khtml-opacity: 0.65; + -moz-opacity: 0.65; + opacity: 0.65; + cursor: default; +} +.pretty-button.depressed, +.pretty-button:active { + -webkit-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.05); +} +button.pretty-button::-moz-focus-inner, input.pretty-button::-moz-focus-inner { + padding: 0; + border: 0; +} + +/* ================== */ +/* :: Bootstrap v2.0 :: */ +/* ================== */ + +/* ================================== */ +/* = Twitter.Bootstrap: Buttons etc = */ +/* ================================== */ + + +.fade { + -webkit-transition: opacity 0.15s linear; + -moz-transition: opacity 0.15s linear; + -ms-transition: opacity 0.15s linear; + -o-transition: opacity 0.15s linear; + transition: opacity 0.15s linear; + opacity: 0; +} +.fade.in { + opacity: 1; +} +.collapse { + -webkit-transition: height 0.35s ease; + -moz-transition: height 0.35s ease; + -ms-transition: height 0.35s ease; + -o-transition: height 0.35s ease; + transition: height 0.35s ease; + position: relative; + overflow: hidden; + height: 0; +} +.collapse.in { + height: auto; +} +.close { + float: right; + font-size: 20px; + font-weight: bold; + line-height: 18px; + color: #000000; + text-shadow: 0 1px 0 #ffffff; + opacity: 0.2; + filter: alpha(opacity=20); +} +.close:hover { + color: #000000; + text-decoration: none; + opacity: 0.4; + filter: alpha(opacity=40); + cursor: pointer; +} +.btn { + display: inline-block; + padding: 4px 10px 4px; + font-size: 13px; + line-height: 18px; + color: #333333; + text-align: center; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); + background-color: #fafafa; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), color-stop(25%, #ffffff), to(#e6e6e6)); + background-image: -webkit-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); + background-image: -moz-linear-gradient(top, #ffffff, #ffffff 25%, #e6e6e6); + background-image: -ms-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); + background-image: -o-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); + background-image: linear-gradient(#ffffff, #ffffff 25%, #e6e6e6); + background-repeat: no-repeat; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0); + border: 1px solid #ccc; + border-bottom-color: #bbb; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + cursor: pointer; + *margin-left: .3em; +} +.btn:first-child { + *margin-left: 0; +} +.btn:hover { + color: #333333; + text-decoration: none; + background-color: #e6e6e6; + background-position: 0 -15px; + -webkit-transition: background-position 0.1s linear; + -moz-transition: background-position 0.1s linear; + -ms-transition: background-position 0.1s linear; + -o-transition: background-position 0.1s linear; + transition: background-position 0.1s linear; +} +.btn:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +.btn.active, .btn:active { + background-image: none; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + background-color: #e6e6e6; + background-color: #d9d9d9 \9; + color: rgba(0, 0, 0, 0.5); + outline: 0; +} +.btn.disabled, .btn[disabled] { + cursor: default; + background-image: none; + background-color: #e6e6e6; + opacity: 0.65; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} +.btn-large { + padding: 9px 14px; + font-size: 15px; + line-height: normal; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} +.btn-large .icon { + margin-top: 1px; +} +.btn-small { + padding: 5px 9px; + font-size: 11px; + line-height: 16px; +} +.btn-small .icon { + margin-top: -1px; +} +.btn-primary, +.btn-primary:hover, +.btn-warning, +.btn-warning:hover, +.btn-danger, +.btn-danger:hover, +.btn-success, +.btn-success:hover, +.btn-info, +.btn-info:hover { + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + color: #ffffff; +} +.btn-primary.active, +.btn-warning.active, +.btn-danger.active, +.btn-success.active, +.btn-info.active { + color: rgba(255, 255, 255, 0.75); +} +.btn-primary { + background-color: #006dcc; + background-image: -moz-linear-gradient(top, #0088cc, #0044cc); + background-image: -ms-linear-gradient(top, #0088cc, #0044cc); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); + background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); + background-image: -o-linear-gradient(top, #0088cc, #0044cc); + background-image: linear-gradient(top, #0088cc, #0044cc); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0); + border-color: #0044cc #0044cc #002a80; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); +} +.btn-primary:hover, +.btn-primary:active, +.btn-primary.active, +.btn-primary.disabled, +.btn-primary[disabled] { + background-color: #0044cc; +} +.btn-primary:active, .btn-primary.active { + background-color: #003399 \9; +} +.btn-warning { + background-color: #faa732; + background-image: -moz-linear-gradient(top, #fbb450, #f89406); + background-image: -ms-linear-gradient(top, #fbb450, #f89406); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); + background-image: -webkit-linear-gradient(top, #fbb450, #f89406); + background-image: -o-linear-gradient(top, #fbb450, #f89406); + background-image: linear-gradient(top, #fbb450, #f89406); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0); + border-color: #f89406 #f89406 #ad6704; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); +} +.btn-warning:hover, +.btn-warning:active, +.btn-warning.active, +.btn-warning.disabled, +.btn-warning[disabled] { + background-color: #f89406; +} +.btn-warning:active, .btn-warning.active { + background-color: #c67605 \9; +} +.btn-danger { + background-color: #da4f49; + background-image: -moz-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -ms-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -o-linear-gradient(top, #ee5f5b, #bd362f); + background-image: linear-gradient(top, #ee5f5b, #bd362f); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#bd362f', GradientType=0); + border-color: #bd362f #bd362f #802420; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); +} +.btn-danger:hover, +.btn-danger:active, +.btn-danger.active, +.btn-danger.disabled, +.btn-danger[disabled] { + background-color: #bd362f; +} +.btn-danger:active, .btn-danger.active { + background-color: #942a25 \9; +} +.btn-success { + background-color: #5bb75b; + background-image: -moz-linear-gradient(top, #62c462, #51a351); + background-image: -ms-linear-gradient(top, #62c462, #51a351); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351)); + background-image: -webkit-linear-gradient(top, #62c462, #51a351); + background-image: -o-linear-gradient(top, #62c462, #51a351); + background-image: linear-gradient(top, #62c462, #51a351); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#51a351', GradientType=0); + border-color: #51a351 #51a351 #387038; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); +} +.btn-success:hover, +.btn-success:active, +.btn-success.active, +.btn-success.disabled, +.btn-success[disabled] { + background-color: #51a351; +} +.btn-success:active, .btn-success.active { + background-color: #408140 \9; +} +.btn-info { + background-color: #49afcd; + background-image: -moz-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -ms-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4)); + background-image: -webkit-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -o-linear-gradient(top, #5bc0de, #2f96b4); + background-image: linear-gradient(top, #5bc0de, #2f96b4); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#2f96b4', GradientType=0); + border-color: #2f96b4 #2f96b4 #1f6377; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); +} +.btn-info:hover, +.btn-info:active, +.btn-info.active, +.btn-info.disabled, +.btn-info[disabled] { + background-color: #2f96b4; +} +.btn-info:active, .btn-info.active { + background-color: #24748c \9; +} +button.btn, input[type="submit"].btn { + *padding-top: 2px; + *padding-bottom: 2px; +} +button.btn::-moz-focus-inner, input[type="submit"].btn::-moz-focus-inner { + padding: 0; + border: 0; +} +button.btn.large, input[type="submit"].btn.large { + *padding-top: 7px; + *padding-bottom: 7px; +} +button.btn.small, input[type="submit"].btn.small { + *padding-top: 3px; + *padding-bottom: 3px; +} +.btn-group { + position: relative; + *zoom: 1; + *margin-left: .3em; +} +.btn-group:before, .btn-group:after { + display: table; + content: ""; +} +.btn-group:after { + clear: both; +} +.btn-group:first-child { + *margin-left: 0; +} +.btn-group + .btn-group { + margin-left: 5px; +} +.btn-toolbar { + margin-top: 9px; + margin-bottom: 9px; +} +.btn-toolbar .btn-group { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + + *zoom: 1; +} +.btn-group .btn { + position: relative; + float: left; + margin-left: -1px; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} +.btn-group .btn:first-child { + margin-left: 0; + -webkit-border-top-left-radius: 4px; + -moz-border-radius-topleft: 4px; + border-top-left-radius: 4px; + -webkit-border-bottom-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; + border-bottom-left-radius: 4px; +} +.btn-group .btn:last-child, .btn-group .dropdown-toggle { + -webkit-border-top-right-radius: 4px; + -moz-border-radius-topright: 4px; + border-top-right-radius: 4px; + -webkit-border-bottom-right-radius: 4px; + -moz-border-radius-bottomright: 4px; + border-bottom-right-radius: 4px; +} +.btn-group .btn.large:first-child { + margin-left: 0; + -webkit-border-top-left-radius: 6px; + -moz-border-radius-topleft: 6px; + border-top-left-radius: 6px; + -webkit-border-bottom-left-radius: 6px; + -moz-border-radius-bottomleft: 6px; + border-bottom-left-radius: 6px; +} +.btn-group .btn.large:last-child, .btn-group .large.dropdown-toggle { + -webkit-border-top-right-radius: 6px; + -moz-border-radius-topright: 6px; + border-top-right-radius: 6px; + -webkit-border-bottom-right-radius: 6px; + -moz-border-radius-bottomright: 6px; + border-bottom-right-radius: 6px; +} +.btn-group .btn:hover, +.btn-group .btn:focus, +.btn-group .btn:active, +.btn-group .btn.active { + z-index: 2; +} +.btn-group .dropdown-toggle:active, .btn-group.open .dropdown-toggle { + outline: 0; +} +.btn-group .dropdown-toggle { + padding-left: 8px; + padding-right: 8px; + -webkit-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + *padding-top: 5px; + *padding-bottom: 5px; +} +.btn-group.open { + *z-index: 1000; +} +.btn-group.open .dropdown-menu { + display: block; + margin-top: 1px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} +.btn-group.open .dropdown-toggle { + background-image: none; + -webkit-box-shadow: inset 0 1px 6px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 6px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 6px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} +.btn .caret { + margin-top: 7px; + margin-left: 0; +} +.btn:hover .caret, .open.btn-group .caret { + opacity: 1; + filter: alpha(opacity=100); +} +.btn-primary .caret, +.btn-danger .caret, +.btn-info .caret, +.btn-success .caret { + border-top-color: #ffffff; + opacity: 0.75; + filter: alpha(opacity=75); +} +.btn-small .caret { + margin-top: 4px; +} + +/* ================================== */ +/* = Twitter.Bootstrap Alerts = */ +/* ================================== */ + +.alert { + padding: 8px 35px 8px 14px; + margin-bottom: 18px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + background-color: #fcf8e3; + border: 1px solid #fbeed5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.alert, .alert-heading { + color: #c09853; +} +.alert .close { + position: relative; + top: -2px; + right: -21px; + line-height: 18px; +} +.alert-success { + background-color: #dff0d8; + border-color: #d6e9c6; +} +.alert-success, .alert-success .alert-heading { + color: #468847; +} +.alert-danger, .alert-error { + background-color: #f2dede; + border-color: #eed3d7; +} +.alert-danger, +.alert-error, +.alert-danger .alert-heading, +.alert-error .alert-heading { + color: #b94a48; +} +.alert-info { + background-color: #d9edf7; + border-color: #bce8f1; +} +.alert-info, .alert-info .alert-heading { + color: #3a87ad; +} +.alert-block { + padding-top: 14px; + padding-bottom: 14px; +} +.alert-block > p, .alert-block > ul { + margin-bottom: 0; +} +.alert-block p + p { + margin-top: 5px; +} + +/* ================================== */ +/* = Twitter.Bootstrap Pagination = */ +/* ================================== */ + +.pagination { + height: 36px; + margin: 18px 0; +} +.pagination ul { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + + *zoom: 1; + margin-left: 0; + margin-bottom: 0; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} +.pagination li { + display: inline; +} +.pagination a { + float: left; + padding: 0 14px; + line-height: 34px; + text-decoration: none; + border: 1px solid #ddd; + border-left-width: 0; +} +.pagination a:hover, .pagination .active a { + background-color: #f5f5f5; +} +.pagination .active a { + color: #999999; + cursor: default; +} +.pagination .disabled a, .pagination .disabled a:hover { + color: #999999; + background-color: transparent; + cursor: default; +} +.pagination li:first-child a { + border-left-width: 1px; + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} +.pagination li:last-child a { + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} +.pagination-centered { + text-align: center; +} +.pagination-right { + text-align: right; +} + + diff --git a/ckan/public/css/pretty_buttons.css b/ckan/public/css/pretty_buttons.css deleted file mode 100644 index 9a529b66299..00000000000 --- a/ckan/public/css/pretty_buttons.css +++ /dev/null @@ -1,111 +0,0 @@ -/* ================================== */ -/* = Twitter.Bootstrap Form Buttons = */ -/* ================================== */ -.pretty-button { - cursor: pointer; - display: inline-block; - background-color: #e6e6e6; - background-repeat: no-repeat; - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), color-stop(0.25, #ffffff), to(#e6e6e6)); - background-image: -webkit-linear-gradient(#ffffff, #ffffff 0.25, #e6e6e6); - background-image: -moz-linear-gradient(#ffffff, #ffffff 0.25, #e6e6e6); - background-image: -ms-linear-gradient(#ffffff, #ffffff 0.25, #e6e6e6); - background-image: -o-linear-gradient(#ffffff, #ffffff 0.25, #e6e6e6); - background-image: linear-gradient(#ffffff, #ffffff 0.25, #e6e6e6); - padding: 4px 14px; - text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); - color: #333; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-weight: normal; - font-size: 13px; - line-height: 18px; - border: 1px solid #ccc; - border-bottom-color: #bbb; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - -webkit-transition: 0.1s linear all; - -moz-transition: 0.1s linear all; - transition: 0.1s linear all; -} -.pretty-button.depressed, -.pretty-button:hover { - background-position: 0 -15px; - color: #333; - text-decoration: none; -} -.pretty-button.primary, .pretty-button.danger { - color: #fff; -} -.pretty-button.primary:hover, .pretty-button.danger:hover { - color: #fff; -} -.pretty-button.primary { - background-color: #0064cd; - background-repeat: repeat-x; - background-image: -khtml-gradient(linear, left top, left bottom, from(#049cdb), to(#0064cd)); - background-image: -moz-linear-gradient(#049cdb, #0064cd); - background-image: -ms-linear-gradient(#049cdb, #0064cd); - background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #049cdb), color-stop(100%, #0064cd)); - background-image: -webkit-linear-gradient(#049cdb, #0064cd); - background-image: -o-linear-gradient(#049cdb, #0064cd); - background-image: linear-gradient(#049cdb, #0064cd); - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - border-color: #0064cd #0064cd #003f81; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); -} -.pretty-button.danger { - background-color: #9d261d; - background-repeat: repeat-x; - background-image: -khtml-gradient(linear, left top, left bottom, from(#d83a2e), to(#9d261d)); - background-image: -moz-linear-gradient(#d83a2e, #9d261d); - background-image: -ms-linear-gradient(#d83a2e, #9d261d); - background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #d83a2e), color-stop(100%, #9d261d)); - background-image: -webkit-linear-gradient(#d83a2e, #9d261d); - background-image: -o-linear-gradient(#d83a2e, #9d261d); - background-image: linear-gradient(#d83a2e, #9d261d); - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - border-color: #9d261d #9d261d #5c1611; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); -} -.pretty-button.large { - font-size: 16px; - line-height: 28px; - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; -} -.pretty-button.small-button { - padding-right: 9px; - padding-left: 9px; - font-size: 11px; -} -.pretty-button.disabled { - background-image: none; - filter: alpha(opacity=65); - -khtml-opacity: 0.65; - -moz-opacity: 0.65; - opacity: 0.65; - cursor: default; -} -.pretty-button:disabled { - background-image: none; - filter: alpha(opacity=65); - -khtml-opacity: 0.65; - -moz-opacity: 0.65; - opacity: 0.65; - cursor: default; -} -.pretty-button.depressed, -.pretty-button:active { - -webkit-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.05); -} -button.pretty-button::-moz-focus-inner, input.pretty-button::-moz-focus-inner { - padding: 0; - border: 0; -} diff --git a/ckan/public/css/style.css b/ckan/public/css/style.css index 10ec32d5858..0143eb36e1a 100644 --- a/ckan/public/css/style.css +++ b/ckan/public/css/style.css @@ -1,5 +1,5 @@ @import url('forms.css'); -@import url('pretty_buttons.css'); +@import url('bootstrap.css'); body.no-sidebar #sidebar { display: none; } body.no-sidebar #content { @@ -1417,114 +1417,6 @@ body.authz form button { color:#fff; } - -/* ================== */ -/* :: Bootstrap :: */ -/* ================== */ - -.alert-message { - position: relative; - padding: 7px 15px; - margin-bottom: 18px; - color: #404040; - background-color: #eedc94; - background-repeat: repeat-x; - background-image: -khtml-gradient(linear, left top, left bottom, from(#fceec1), to(#eedc94)); - background-image: -moz-linear-gradient(top, #fceec1, #eedc94); - background-image: -ms-linear-gradient(top, #fceec1, #eedc94); - background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fceec1), color-stop(100%, #eedc94)); - background-image: -webkit-linear-gradient(top, #fceec1, #eedc94); - background-image: -o-linear-gradient(top, #fceec1, #eedc94); - background-image: linear-gradient(top, #fceec1, #eedc94); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fceec1', endColorstr='#eedc94', GradientType=0); - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - border-color: #eedc94 #eedc94 #e4c652; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); - border-width: 1px; - border-style: solid; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); - -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); -} -.alert-message .close { - margin-top: 1px; - *margin-top: 0; -} -.alert-message a { - font-weight: bold; - color: #404040; -} -.alert-message.danger p a, -.alert-message.error p a, -.alert-message.success p a, -.alert-message.info p a { - color: #ffffff; -} -.alert-message h5 { - line-height: 18px; -} -.alert-message p { - margin-bottom: 0; -} -.alert-message div { - margin-top: 5px; - margin-bottom: 2px; - line-height: 28px; -} -.alert-message .btn { - -webkit-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.25); - -moz-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.25); - box-shadow: 0 1px 0 rgba(255, 255, 255, 0.25); -} -.alert-message.block-message { - background-image: none; - background-color: #fdf5d9; - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - padding: 14px; - border-color: #fceec1; - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; -} -.alert-message.block-message ul, .alert-message.block-message p { - margin-right: 30px; -} -.alert-message.block-message ul { - margin-bottom: 0; -} -.alert-message.block-message li { - color: #404040; -} -.alert-message.block-message .alert-actions { - margin-top: 5px; -} -.alert-message.block-message.error, .alert-message.block-message.success, .alert-message.block-message.info { - color: #404040; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); -} -.alert-message.block-message.error { - background-color: #fddfde; - border-color: #fbc7c6; -} -.alert-message.block-message.success { - background-color: #d1eed1; - border-color: #bfe7bf; -} -.alert-message.block-message.info { - background-color: #ddf4fb; - border-color: #c6edf9; -} -.alert-message.block-message.danger p a, -.alert-message.block-message.error p a, -.alert-message.block-message.success p a, -.alert-message.block-message.info p a { - color: #404040; -} - /* ==================== */ /* = Activity Streams = */ /* ==================== */ diff --git a/ckan/public/scripts/application.js b/ckan/public/scripts/application.js index 3a359aee443..cc9491c0069 100644 --- a/ckan/public/scripts/application.js +++ b/ckan/public/scripts/application.js @@ -849,9 +849,26 @@ CKAN.View.ResourceAddLink = Backbone.View.extend({ my.$dialog.html('

Loading ...

'); function initializeDataExplorer(dataset) { + var views = [ + { + id: 'grid', + label: 'Grid', + view: new recline.View.DataGrid({ + model: dataset + }) + }, + { + id: 'graph', + label: 'Graph', + view: new recline.View.FlotGraph({ + model: dataset + }) + } + ]; var dataExplorer = new recline.View.DataExplorer({ el: my.$dialog , model: dataset + , views: views , config: { readOnly: true } @@ -882,21 +899,13 @@ CKAN.View.ResourceAddLink = Backbone.View.extend({ } if (resourceData.webstore_url) { - var backend = new recline.Model.BackendWebstore({ - url: resourceData.webstore_url - }); - recline.Model.setBackend(backend); - var dataset = backend.getDataset(); + var dataset = new recline.Model.Dataset(resourceData, 'webstore'); initializeDataExplorer(dataset); } else if (resourceData.formatNormalized in {'csv': '', 'xls': ''}) { - var backend = new recline.Model.BackendDataProxy({ - url: resourceData.url - , type: resourceData.formatNormalized - }); - recline.Model.setBackend(backend); - var dataset = backend.getDataset(); + var dataset = new recline.Model.Dataset(resourceData, 'dataproxy'); initializeDataExplorer(dataset); + $('.recline-query-editor .text-query').hide(); } else if (resourceData.formatNormalized in { 'rdf+xml': '', @@ -1010,7 +1019,7 @@ CKAN.View.ResourceAddLink = Backbone.View.extend({ my.showError = function (error) { var _html = _.template( - '
<%= title %>
<%= message %>
' + '
<%= title %>
<%= message %>
' , error ); my.$dialog.html(_html); diff --git a/ckan/public/scripts/templates.js b/ckan/public/scripts/templates.js index fe82805fadb..a4c33ce93a5 100644 --- a/ckan/public/scripts/templates.js +++ b/ckan/public/scripts/templates.js @@ -57,7 +57,7 @@ CKAN.Templates.resourceUpload = ' \ \ \ \ - \ + \ \ \ '; diff --git a/ckan/public/scripts/vendor/ckanjs/1.0.0/ckanjs.js b/ckan/public/scripts/vendor/ckanjs/1.0.0/ckanjs.js index 1d9386c5df9..08f478f57ac 100755 --- a/ckan/public/scripts/vendor/ckanjs/1.0.0/ckanjs.js +++ b/ckan/public/scripts/vendor/ckanjs/1.0.0/ckanjs.js @@ -1614,7 +1614,7 @@ this.CKAN.View || (this.CKAN.View = {}); } var tmpl = $.tmpl(CKAN.Templates.resourceUpload, tmplData); this.el.html(tmpl); - this.$messages = this.el.find('.alert-message'); + this.$messages = this.el.find('.alert'); this.setupFileUpload(); return this; }, @@ -1743,8 +1743,8 @@ this.CKAN.View || (this.CKAN.View = {}); }, setMessage: function(msg, category) { - var category = category || 'info'; - this.$messages.removeClass('info success error'); + var category = category || 'alert-info'; + this.$messages.removeClass('alert-info alert-success alert-error'); this.$messages.addClass(category); this.$messages.show(); this.$messages.html(msg); diff --git a/ckan/public/scripts/vendor/recline/css/data-explorer.css b/ckan/public/scripts/vendor/recline/css/data-explorer.css index c66839545bd..72eff40041d 100644 --- a/ckan/public/scripts/vendor/recline/css/data-explorer.css +++ b/ckan/public/scripts/vendor/recline/css/data-explorer.css @@ -12,17 +12,50 @@ padding-left: 0; } -.header .pagination { +.header .recline-results-info { + line-height: 28px; + margin-left: 20px; + display: inline; +} + +.header .recline-query-editor { float: right; - margin: 4px; + height: 30px; +} + +.header .recline-query-editor form { + height: 30px; + margin-bottom: 0; } -.header .pagination label { +.header .recline-query-editor label { float: none; } -.header .pagination input { +.header .recline-query-editor label { + float: none; +} + +.header .recline-query-editor input.text-query { + float: left; + margin-top: 1px; + margin-right: 5px; + width: 200px; +} + +.header .recline-query-editor .pagination input { width: 30px; + height: 18px; + padding: 2px 4px; + margin-top: -4px; +} + +.header .recline-query-editor .pagination a { + line-height: 28px; +} + +.header .recline-query-editor form .btn { + margin-top: -23px; } .data-view-container { @@ -90,36 +123,11 @@ * Notifications *********************************************************/ -.notification-container { - width: 400px; - left: 520px; - display: none; - position: fixed; - top: 0; - z-index: 100; - text-align: center; -} - -.notification { - display: inline-block; - margin: 0 auto; - padding: 5px 8px 4px; - font-size: 1.3em; - text-align: left; - font-weight: bold; - background: #fe8; - -moz-border-radius: 2px; - -webkit-border-radius: 2px; - border-radius: 2px; -} - -.notification-action { - padding-left: 10px; -} - .notification-loader { - padding: 0 3px 0 0; - opacity: 0.3; + width: 18px; + margin-left: 5px; + background: url(images/small-spinner.gif) no-repeat; + display: inline-block; } @@ -135,11 +143,13 @@ .data-table { border: 1px solid #ccc; font-size: 12px; + width: 100%; } .data-table td, .data-table th { border-left: 1px solid #ccc; padding: 3px 4px; + text-align: left; } .data-table tr td:first-child, .data-table tr th:first-child { @@ -150,7 +160,7 @@ * Data Table Menus *********************************************************/ -a.column-header-menu { +a.column-header-menu, a.root-header-menu { float: right; display: block; margin: 0 4px 0 0; @@ -160,7 +170,7 @@ a.column-header-menu { background-repeat: no-repeat; } -a.row-header-menu:hover { +a.row-header-menu:hover, a.root-header-menu:hover { background-position: -17px 0px; text-decoration: none; } @@ -175,6 +185,10 @@ a.row-header-menu { background-repeat: no-repeat; } +.read-only a.row-header-menu { + display: none; +} + a.column-header-menu:hover { background-position: -17px 0px; text-decoration: none; @@ -511,14 +525,14 @@ td.expression-preview-value { * Read-only mode *********************************************************/ -.read-only .data-table tr td:first-child, -.read-only .data-table tr th:first-child +.read-only .no-hidden .data-table tr td:first-child, +.read-only .no-hidden .data-table tr th:first-child { display: none; } -.read-only .column-header-menu, -.read-only .row-header-menu, + +.read-only .write-op, .read-only a.data-table-cell-edit { display: none; diff --git a/ckan/public/scripts/vendor/recline/css/images/small-spinner.gif b/ckan/public/scripts/vendor/recline/css/images/small-spinner.gif new file mode 100755 index 00000000000..5b33f7e54f4 Binary files /dev/null and b/ckan/public/scripts/vendor/recline/css/images/small-spinner.gif differ diff --git a/ckan/public/scripts/vendor/recline/recline.js b/ckan/public/scripts/vendor/recline/recline.js index 177236b4120..4b29f5d6246 100644 --- a/ckan/public/scripts/vendor/recline/recline.js +++ b/ckan/public/scripts/vendor/recline/recline.js @@ -117,152 +117,71 @@ var costco = function() { }; } - function updateDocs(editFunc) { - var dfd = $.Deferred(); - util.notify("Download entire database into Recline. This could take a while...", {persist: true, loader: true}); - couch.request({url: app.baseURL + "api/json"}).then(function(docs) { - util.notify("Updating " + docs.docs.length + " documents. This could take a while...", {persist: true, loader: true}); - var toUpdate = costco.mapDocs(docs.docs, editFunc).edited; - costco.uploadDocs(toUpdate).then( - function(updatedDocs) { - util.notify(updatedDocs.length + " documents updated successfully"); - recline.initializeTable(app.offset); - dfd.resolve(updatedDocs); - }, - function(err) { - util.notify("Errorz! " + err); - dfd.reject(err); - } - ); - }); - return dfd.promise(); - } - - function updateDoc(doc) { - return couch.request({type: "PUT", url: app.baseURL + "api/" + doc._id, data: JSON.stringify(doc)}) - } - - function uploadDocs(docs) { - var dfd = $.Deferred(); - if(!docs.length) dfd.resolve("Failed: No docs specified"); - couch.request({url: app.baseURL + "api/_bulk_docs", type: "POST", data: JSON.stringify({docs: docs})}) - .then( - function(resp) {ensureCommit().then(function() { - var error = couch.responseError(resp); - if (error) { - dfd.reject(error); - } else { - dfd.resolve(resp); - } - })}, - function(err) { dfd.reject(err.responseText) } - ); - return dfd.promise(); - } - - function ensureCommit() { - return couch.request({url: app.baseURL + "api/_ensure_full_commit", type:'POST', data: "''"}); - } - - function deleteColumn(name) { - var deleteFunc = function(doc) { - delete doc[name]; - return doc; - } - return updateDocs(deleteFunc); - } - - function uploadCSV() { - var file = $('#file')[0].files[0]; - if (file) { - var reader = new FileReader(); - reader.readAsText(file); - reader.onload = function(event) { - var payload = { - url: window.location.href + "/api/_bulk_docs", // todo more robust url composition - data: event.target.result - }; - var worker = new Worker('script/costco-csv-worker.js'); - worker.onmessage = function(event) { - var message = event.data; - if (message.done) { - var error = couch.responseError(JSON.parse(message.response)) - console.log('e',error) - if (error) { - app.emitter.emit(error, 'error'); - } else { - util.notify("Data uploaded successfully!"); - recline.initializeTable(app.offset); - } - util.hide('dialog'); - } else if (message.percent) { - if (message.percent === 100) { - util.notify("Waiting for CouchDB...", {persist: true, loader: true}) - } else { - util.notify("Uploading... " + message.percent + "%"); - } - } else { - util.notify(JSON.stringify(message)); - } - }; - worker.postMessage(payload); - }; - } else { - util.notify('File not selected. Please try again'); - } - }; - return { evalFunction: evalFunction, previewTransform: previewTransform, - mapDocs: mapDocs, - updateDocs: updateDocs, - updateDoc: updateDoc, - uploadDocs: uploadDocs, - deleteColumn: deleteColumn, - ensureCommit: ensureCommit, - uploadCSV: uploadCSV + mapDocs: mapDocs }; }(); +// # Recline Backbone Models this.recline = this.recline || {}; +this.recline.Model = this.recline.Model || {}; -// Models module following classic module pattern -recline.Model = function($) { - -var my = {}; +(function($, my) { -// A Dataset model. +// ## A Dataset model // -// Other than standard list of Backbone attributes it has two important attributes: +// A model must have the following (Backbone) attributes: // +// * fields: (aka columns) is a FieldList listing all the fields on this +// Dataset (this can be set explicitly, or, on fetch() of Dataset +// information from the backend, or as is perhaps most common on the first +// query) // * currentDocuments: a DocumentList containing the Documents we have currently loaded for viewing (you update currentDocuments by calling getRows) // * docCount: total number of documents in this dataset (obtained on a fetch for this Dataset) my.Dataset = Backbone.Model.extend({ __type__: 'Dataset', - initialize: function() { + initialize: function(model, backend) { + _.bindAll(this, 'query'); + this.backend = backend; + if (backend && backend.constructor == String) { + this.backend = my.backends[backend]; + } + this.fields = new my.FieldList(); this.currentDocuments = new my.DocumentList(); this.docCount = null; + this.queryState = new my.Query(); + this.queryState.bind('change', this.query); }, - // AJAX method with promise API to get rows (documents) from the backend. + // ### query // - // Resulting DocumentList are used to reset this.currentDocuments and are - // also returned. + // AJAX method with promise API to get documents from the backend. // - // :param numRows: passed onto backend getDocuments. - // :param start: passed onto backend getDocuments. + // It will query based on current query state (given by this.queryState) + // updated by queryObj (if provided). // - // this does not fit very well with Backbone setup. Backbone really expects you to know the ids of objects your are fetching (which you do in classic RESTful ajax-y world). But this paradigm does not fill well with data set up we have here. - // This also illustrates the limitations of separating the Dataset and the Backend - getDocuments: function(numRows, start) { + // Resulting DocumentList are used to reset this.currentDocuments and are + // also returned. + query: function(queryObj) { + this.trigger('query:start'); var self = this; + this.queryState.set(queryObj, {silent: true}); var dfd = $.Deferred(); - this.backend.getDocuments(this.id, numRows, start).then(function(rows) { + this.backend.query(this, this.queryState.toJSON()).done(function(rows) { var docs = _.map(rows, function(row) { - return new my.Document(row); + var _doc = new my.Document(row); + _doc.backend = self.backend; + _doc.dataset = self; + return _doc; }); self.currentDocuments.reset(docs); + self.trigger('query:done'); dfd.resolve(self.currentDocuments); + }) + .fail(function(arguments) { + self.trigger('query:fail', arguments); + dfd.reject(arguments); }); return dfd.promise(); }, @@ -270,287 +189,84 @@ my.Dataset = Backbone.Model.extend({ toTemplateJSON: function() { var data = this.toJSON(); data.docCount = this.docCount; + data.fields = this.fields.toJSON(); return data; } }); +// ## A Document (aka Row) +// +// A single entry or row in the dataset my.Document = Backbone.Model.extend({ __type__: 'Document' }); +// ## A Backbone collection of Documents my.DocumentList = Backbone.Collection.extend({ __type__: 'DocumentList', - // webStore: new WebStore(this.url), model: my.Document }); -// Backends section -// ================ - -my.setBackend = function(backend) { - Backbone.sync = backend.sync; -}; - -// Backend which just caches in memory +// ## A Field (aka Column) on a Dataset // -// Does not need to be a backbone model but provides some conveniences -my.BackendMemory = Backbone.Model.extend({ - // Initialize a Backend with a local in-memory dataset. - // - // NB: We can handle one and only one dataset at a time. - // - // :param dataset: the data for a dataset on which operations will be - // performed. Its form should be a hash with metadata and data - // attributes. - // - // - metadata: hash of key/value attributes of any kind (but usually with title attribute) - // - data: hash with 2 keys: - // - headers: list of header names/labels - // - rows: list of hashes, each hash being one row. A row *must* have an id attribute which is unique. - // - // Example of data: - // - // { - // headers: ['x', 'y', 'z'] - // , rows: [ - // {id: 0, x: 1, y: 2, z: 3} - // , {id: 1, x: 2, y: 4, z: 6} - // ] - // }; - initialize: function(dataset) { - // deep copy - this._datasetAsData = $.extend(true, {}, dataset); - _.bindAll(this, 'sync'); - }, - getDataset: function() { - var dataset = new my.Dataset({ - id: this._datasetAsData.metadata.id - }); - // this is a bit weird but problem is in sync this is set to parent model object so need to give dataset a reference to backend explicitly - dataset.backend = this; - return dataset; - }, - sync: function(method, model, options) { - var self = this; - if (method === "read") { - var dfd = $.Deferred(); - // this switching on object type is rather horrible - // think may make more sense to do work in individual objects rather than in central Backbone.sync - if (model.__type__ == 'Dataset') { - var dataset = model; - var rawDataset = this._datasetAsData; - dataset.set(rawDataset.metadata); - dataset.set({ - headers: rawDataset.data.headers - }); - dataset.docCount = rawDataset.data.rows.length; - dfd.resolve(dataset); - } - return dfd.promise(); - } else if (method === 'update') { - var dfd = $.Deferred(); - if (model.__type__ == 'Document') { - _.each(this._datasetAsData.data.rows, function(row, idx) { - if(row.id === model.id) { - self._datasetAsData.data.rows[idx] = model.toJSON(); - } - }); - dfd.resolve(model); - } - return dfd.promise(); - } else if (method === 'delete') { - var dfd = $.Deferred(); - if (model.__type__ == 'Document') { - this._datasetAsData.data.rows = _.reject(this._datasetAsData.data.rows, function(row) { - return (row.id === model.id); - }); - dfd.resolve(model); - } - return dfd.promise(); - } else { - alert('Not supported: sync on BackendMemory with method ' + method + ' and model ' + model); - } +// Following attributes as standard: +// +// * id: a unique identifer for this field- usually this should match the key in the documents hash +// * label: the visible label used for this field +// * type: the type of the data +my.Field = Backbone.Model.extend({ + defaults: { + id: null, + label: null, + type: 'String' }, - getDocuments: function(datasetId, numRows, start) { - if (start === undefined) { - start = 0; + // In addition to normal backbone initialization via a Hash you can also + // just pass a single argument representing id to the ctor + initialize: function(data) { + // if a hash not passed in the first argument is set as value for key 0 + if ('0' in data) { + throw new Error('Looks like you did not pass a proper hash with id to Field constructor'); } - if (numRows === undefined) { - numRows = 10; + if (this.attributes.label == null) { + this.set({label: this.id}); } - var dfd = $.Deferred(); - rows = this._datasetAsData.data.rows; - var results = rows.slice(start, start+numRows); - dfd.resolve(results); - return dfd.promise(); - } + } }); -// Webstore Backend for connecting to the Webstore -// -// Initializing model argument must contain a url attribute pointing to -// relevant Webstore table. -// -// Designed to only attach to one dataset and one dataset only ... -// Could generalize to support attaching to different datasets -my.BackendWebstore = Backbone.Model.extend({ - getDataset: function(id) { - var dataset = new my.Dataset({ - id: id - }); - dataset.backend = this; - return dataset; - }, - sync: function(method, model, options) { - if (method === "read") { - // this switching on object type is rather horrible - // think may make more sense to do work in individual objects rather than in central Backbone.sync - if (this.__type__ == 'Dataset') { - var dataset = this; - // get the schema and return - var base = this.backend.get('url'); - var schemaUrl = base + '/schema.json'; - var jqxhr = $.ajax({ - url: schemaUrl, - dataType: 'jsonp', - jsonp: '_callback' - }); - var dfd = $.Deferred(); - jqxhr.then(function(schema) { - headers = _.map(schema.data, function(item) { - return item.name; - }); - dataset.set({ - headers: headers - }); - dataset.docCount = schema.count; - dfd.resolve(dataset, jqxhr); - }); - return dfd.promise(); - } - } - }, - getDocuments: function(datasetId, numRows, start) { - if (start === undefined) { - start = 0; - } - if (numRows === undefined) { - numRows = 10; - } - var base = this.get('url'); - var jqxhr = $.ajax({ - url: base + '.json?_limit=' + numRows, - dataType: 'jsonp', - jsonp: '_callback', - cache: true - }); - var dfd = $.Deferred(); - jqxhr.then(function(results) { - dfd.resolve(results.data); - }); - return dfd.promise(); - } +my.FieldList = Backbone.Collection.extend({ + model: my.Field }); -// DataProxy Backend for connecting to the DataProxy -// -// Example initialization: -// -// BackendDataProxy({ -// model: { -// url: {url-of-data-to-proxy}, -// type: xls || csv, -// format: json || jsonp # return format (defaults to jsonp) -// dataproxy: {url-to-proxy} # defaults to http://jsonpdataproxy.appspot.com -// } -// }) -my.BackendDataProxy = Backbone.Model.extend({ +// ## A Query object storing Dataset Query state +my.Query = Backbone.Model.extend({ defaults: { - dataproxy: 'http://jsonpdataproxy.appspot.com' - , type: 'csv' - , format: 'jsonp' - }, - getDataset: function(id) { - var dataset = new my.Dataset({ - id: id - }); - dataset.backend = this; - return dataset; - }, - sync: function(method, model, options) { - if (method === "read") { - // this switching on object type is rather horrible - // think may make more sense to do work in individual objects rather than in central Backbone.sync - if (this.__type__ == 'Dataset') { - var dataset = this; - // get the schema and return - var base = this.backend.get('dataproxy'); - var data = this.backend.toJSON(); - delete data['dataproxy']; - // TODO: should we cache for extra efficiency - data['max-results'] = 1; - var jqxhr = $.ajax({ - url: base - , data: data - , dataType: 'jsonp' - }); - var dfd = $.Deferred(); - jqxhr.then(function(results) { - dataset.set({ - headers: results.fields - }); - dfd.resolve(dataset, jqxhr); - }); - return dfd.promise(); - } - } else { - alert('This backend only supports read operations'); - } - }, - getDocuments: function(datasetId, numRows, start) { - if (start === undefined) { - start = 0; - } - if (numRows === undefined) { - numRows = 10; - } - var base = this.get('dataproxy'); - var data = this.toJSON(); - delete data['dataproxy']; - data['max-results'] = numRows; - var jqxhr = $.ajax({ - url: base - , data: data - , dataType: 'jsonp' - // , cache: true - }); - var dfd = $.Deferred(); - jqxhr.then(function(results) { - var _out = _.map(results.data, function(row) { - var tmp = {}; - _.each(results.fields, function(key, idx) { - tmp[key] = row[idx]; - }); - return tmp; - }); - dfd.resolve(_out); - }); - return dfd.promise(); - } + size: 100 + , from: 0 + } }); -return my; +// ## Backend registry +// +// Backends will register themselves by id into this registry +my.backends = {}; -}(jQuery); +}(jQuery, this.recline.Model)); var util = function() { var templates = { transformActions: '
  • Global transform...
  • ' , columnActions: ' \ -
  • Transform...
  • \ -
  • Delete this column
  • \ +
  • Transform...
  • \ +
  • Delete this column
  • \ +
  • Sort ascending
  • \ +
  • Sort descending
  • \ +
  • Hide this column
  • \ ' - , rowActions: '
  • Delete this row
  • ' + , rowActions: '
  • Delete this row
  • ' + , rootActions: ' \ + {{#columns}} \ +
  • Show column: {{.}}
  • \ + {{/columns}}' , cellEditor: ' \