From de2d91b465d57705e3d7d1933dea4608e7f2b42f Mon Sep 17 00:00:00 2001 From: bnpysse Date: Thu, 9 Jun 2016 01:27:22 +0800 Subject: [PATCH 1/5] Merge remote-tracking branch 'airbnb/master' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 请输入一个提交信息以解释此合并的必要性,尤其是将一个更新后的上游分支 # 合并到主题分支。 # # 以 '#' 开头的行将被忽略,而且空提交说明将会终止提交。 --- .idea/other.xml | 6 + CONTRIBUTING.md | 2 +- babel/messages.pot | 384 +++++++- caravel/__init__.py | 12 +- caravel/assets/images/create_role.png | Bin 0 -> 51474 bytes caravel/assets/images/druid_agg.png | Bin 0 -> 104052 bytes .../{dashboard.js => dashboard.jsx} | 237 ++++- caravel/assets/javascripts/explore.js | 3 +- caravel/assets/package.json | 2 + caravel/assets/stylesheets/caravel.css | 12 +- caravel/assets/stylesheets/dashboard.css | 11 +- caravel/assets/visualizations/heatmap.js | 1 + caravel/assets/visualizations/nvd3_vis.js | 426 ++++----- caravel/assets/visualizations/pivot_table.css | 2 +- caravel/assets/visualizations/table.css | 2 +- caravel/assets/visualizations/treemap.js | 10 +- caravel/assets/visualizations/world_map.js | 1 + caravel/assets/webpack.config.js | 2 +- caravel/bin/caravel | 2 + caravel/config.py | 10 +- caravel/data/__init__.py | 13 +- caravel/forms.py | 856 ++++++++++-------- ...3_fix_wrong_constraint_on_table_columns.py | 38 + caravel/models.py | 43 +- caravel/templates/caravel/dashboard.html | 69 +- caravel/templates/caravel/explore.html | 4 +- .../translations/es/LC_MESSAGES/messages.mo | Bin 0 -> 5877 bytes .../translations/es/LC_MESSAGES/messages.po | 388 +++++++- .../translations/fr/LC_MESSAGES/messages.mo | Bin 2457 -> 5950 bytes .../translations/fr/LC_MESSAGES/messages.po | 396 +++++++- .../translations/it/LC_MESSAGES/messages.mo | Bin 0 -> 5975 bytes .../translations/it/LC_MESSAGES/messages.po | 470 ++++++++++ .../translations/zh/LC_MESSAGES/messages.mo | Bin 2392 -> 5674 bytes .../translations/zh/LC_MESSAGES/messages.po | 401 +++++++- caravel/utils.py | 32 + caravel/views.py | 258 ++++-- caravel/viz.py | 68 +- docs/druid.rst | 48 + docs/index.rst | 2 + docs/installation.rst | 16 +- docs/security.rst | 70 ++ setup.py | 7 +- tests/core_tests.py | 132 ++- 43 files changed, 3460 insertions(+), 976 deletions(-) create mode 100644 .idea/other.xml create mode 100644 caravel/assets/images/create_role.png create mode 100644 caravel/assets/images/druid_agg.png rename caravel/assets/javascripts/{dashboard.js => dashboard.jsx} (56%) create mode 100644 caravel/migrations/versions/1226819ee0e3_fix_wrong_constraint_on_table_columns.py create mode 100644 caravel/translations/es/LC_MESSAGES/messages.mo create mode 100644 caravel/translations/it/LC_MESSAGES/messages.mo create mode 100644 caravel/translations/it/LC_MESSAGES/messages.po create mode 100644 docs/druid.rst create mode 100644 docs/security.rst diff --git a/.idea/other.xml b/.idea/other.xml new file mode 100644 index 000000000000..c120077f5d12 --- /dev/null +++ b/.idea/other.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 49ec182ceee0..899385f8b5ae 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -185,7 +185,7 @@ meets these guidelines: 5. Please rebase and resolve all conflicts before submitting. -## Tranlations +## Translations We use [Babel](http://babel.pocoo.org/en/latest/) to translate Caravel. The key is to instrument the strings that need translation using diff --git a/babel/messages.pot b/babel/messages.pot index 66280dee0a4c..28a096d93cc2 100644 --- a/babel/messages.pot +++ b/babel/messages.pot @@ -1,14 +1,14 @@ -# Translations template for Caravel. +# Translations template for PROJECT. # Copyright (C) 2016 ORGANIZATION -# This file is distributed under the same license as the Caravel project. -# Maxime Beauchemin , 2016. +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2016. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2016-05-02 00:21-0700\n" +"POT-Creation-Date: 2016-05-25 12:39+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,87 +17,415 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.3.4\n" -#: caravel/models.py:564 +#: caravel/models.py:607 msgid "" "Datetime column not provided as part table configuration and is required " "by this type of chart" msgstr "" -#: caravel/models.py:1153 +#: caravel/models.py:1247 msgid "No data was returned." msgstr "" -#: caravel/views.py:116 +#: caravel/views.py:123 msgid "" "Whether to make this column available as a [Time Granularity] option, " "column has to be DATETIME or DATETIME-like" msgstr "" -#: caravel/views.py:215 +#: caravel/views.py:132 caravel/views.py:160 +msgid "Column" +msgstr "" + +#: caravel/views.py:133 caravel/views.py:193 caravel/views.py:222 +msgid "Verbose Name" +msgstr "" + +#: caravel/views.py:134 caravel/views.py:192 caravel/views.py:221 +#: caravel/views.py:399 caravel/views.py:534 +msgid "Description" +msgstr "" + +#: caravel/views.py:135 caravel/views.py:163 +msgid "Groupable" +msgstr "" + +#: caravel/views.py:136 caravel/views.py:164 +msgid "Filterable" +msgstr "" + +#: caravel/views.py:137 caravel/views.py:196 caravel/views.py:307 +#: caravel/views.py:405 +msgid "Table" +msgstr "" + +#: caravel/views.py:138 caravel/views.py:165 +msgid "Count Distinct" +msgstr "" + +#: caravel/views.py:139 caravel/views.py:166 +msgid "Sum" +msgstr "" + +#: caravel/views.py:140 caravel/views.py:167 +msgid "Min" +msgstr "" + +#: caravel/views.py:141 caravel/views.py:168 +msgid "Max" +msgstr "" + +#: caravel/views.py:142 +msgid "Expression" +msgstr "" + +#: caravel/views.py:143 +msgid "Is temporal" +msgstr "" + +#: caravel/views.py:161 caravel/views.py:194 caravel/views.py:223 +#: caravel/views.py:423 +msgid "Type" +msgstr "" + +#: caravel/views.py:162 caravel/views.py:398 +msgid "Datasource" +msgstr "" + +#: caravel/views.py:191 caravel/views.py:220 +msgid "Metric" +msgstr "" + +#: caravel/views.py:195 +msgid "SQL Expression" +msgstr "" + +#: caravel/views.py:224 caravel/views.py:502 +msgid "JSON" +msgstr "" + +#: caravel/views.py:225 +msgid "Druid Datasource" +msgstr "" + +#: caravel/views.py:256 caravel/views.py:309 +msgid "Database" +msgstr "" + +#: caravel/views.py:257 +msgid "SQL link" +msgstr "" + +#: caravel/views.py:258 caravel/views.py:396 caravel/views.py:458 +msgid "Creator" +msgstr "" + +#: caravel/views.py:259 caravel/views.py:310 +msgid "Last Changed" +msgstr "" + +#: caravel/views.py:260 +msgid "SQLAlchemy URI" +msgstr "" + +#: caravel/views.py:261 caravel/views.py:316 caravel/views.py:395 +#: caravel/views.py:540 +msgid "Cache Timeout" +msgstr "" + +#: caravel/views.py:262 +msgid "Extra" +msgstr "" + +#: caravel/views.py:278 msgid "Databases" msgstr "" -#: caravel/views.py:217 caravel/views.py:261 caravel/views.py:284 +#: caravel/views.py:280 caravel/views.py:336 caravel/views.py:368 msgid "Sources" msgstr "" -#: caravel/views.py:260 -msgid "Tables" +#: caravel/views.py:308 +msgid "Changed By" +msgstr "" + +#: caravel/views.py:311 +msgid "SQL Editor" msgstr "" -#: caravel/views.py:282 -msgid "Druid Clusters" +#: caravel/views.py:312 caravel/views.py:536 +msgid "Is Featured" msgstr "" #: caravel/views.py:313 -msgid "Slices" +msgid "Schema" +msgstr "" + +#: caravel/views.py:314 caravel/views.py:538 +msgid "Default Endpoint" +msgstr "" + +#: caravel/views.py:315 +msgid "Offset" +msgstr "" + +#: caravel/views.py:353 caravel/views.py:533 +msgid "Cluster" +msgstr "" + +#: caravel/views.py:354 +msgid "Coordinator Host" +msgstr "" + +#: caravel/views.py:355 +msgid "Coordinator Port" +msgstr "" + +#: caravel/views.py:356 +msgid "Coordinator Endpoint" +msgstr "" + +#: caravel/views.py:357 +msgid "Broker Host" +msgstr "" + +#: caravel/views.py:358 +msgid "Broker Port" +msgstr "" + +#: caravel/views.py:359 +msgid "Broker Endpoint" +msgstr "" + +#: caravel/views.py:397 caravel/views.py:478 +msgid "Dashboards" +msgstr "" + +#: caravel/views.py:400 +msgid "Last Modified" +msgstr "" + +#: caravel/views.py:401 caravel/views.py:457 +msgid "Owners" +msgstr "" + +#: caravel/views.py:402 +msgid "Parameters" +msgstr "" + +#: caravel/views.py:403 caravel/views.py:424 +msgid "Slice" +msgstr "" + +#: caravel/views.py:404 +msgid "Name" +msgstr "" + +#: caravel/views.py:406 caravel/views.py:425 +msgid "Visualization Type" msgstr "" -#: caravel/views.py:341 +#: caravel/views.py:440 msgid "" "This json object describes the positioning of the widgets in the " "dashboard. It is dynamically generated when adjusting the widgets size " "and positions by using drag & drop in the dashboard view" msgstr "" -#: caravel/views.py:346 +#: caravel/views.py:445 msgid "" "The css for individual dashboards can be altered here, or in the " "dashboard view where changes are immediately visible" msgstr "" -#: caravel/views.py:367 -msgid "Dashboards" +#: caravel/views.py:449 +msgid "To get a readable URL for your dashboard" +msgstr "" + +#: caravel/views.py:453 +msgid "Dashboard" +msgstr "" + +#: caravel/views.py:454 +msgid "Title" +msgstr "" + +#: caravel/views.py:455 +msgid "Slug" +msgstr "" + +#: caravel/views.py:456 +msgid "Slices" +msgstr "" + +#: caravel/views.py:459 +msgid "Modified" +msgstr "" + +#: caravel/views.py:460 +msgid "Position JSON" msgstr "" -#: caravel/views.py:392 +#: caravel/views.py:461 +msgid "CSS" +msgstr "" + +#: caravel/views.py:462 +msgid "JSON Metadata" +msgstr "" + +#: caravel/views.py:499 +msgid "User" +msgstr "" + +#: caravel/views.py:500 +msgid "Action" +msgstr "" + +#: caravel/views.py:501 +msgid "dttm" +msgstr "" + +#: caravel/views.py:508 msgid "Action Log" msgstr "" -#: caravel/views.py:393 +#: caravel/views.py:509 msgid "Security" msgstr "" -#: caravel/views.py:430 -msgid "Druid Datasources" +#: caravel/views.py:526 +msgid "Timezone offset (in hours) for this datasource" msgstr "" -#: caravel/views.py:514 -msgid "The datasource seems to have been deleted" +#: caravel/views.py:532 +msgid "Data Source" msgstr "" -#: caravel/views.py:522 -msgid "You don't seem to have access to this datasource" +#: caravel/views.py:535 +msgid "Owner" msgstr "" -#: caravel/views.py:843 +#: caravel/views.py:537 +msgid "Is Hidden" +msgstr "" + +#: caravel/views.py:539 +msgid "Time Offset" +msgstr "" + +#: caravel/views.py:554 +msgid "Druid Datasources" +msgstr "" + +#: caravel/views.py:969 msgid "This view requires the `all_datasource_access` permission" msgstr "" -#: caravel/views.py:954 +#: caravel/views.py:1064 msgid "CSS Templates" msgstr "" +#: caravel/viz.py:324 +msgid "Table View" +msgstr "" + +#: caravel/viz.py:385 +msgid "Pivot Table" +msgstr "" + +#: caravel/viz.py:447 +msgid "Markup" +msgstr "" + +#: caravel/viz.py:475 +msgid "Word Cloud" +msgstr "" + +#: caravel/viz.py:507 +msgid "Treemap" +msgstr "" + +#: caravel/viz.py:551 +msgid "Calender Heatmap" +msgstr "" + +#: caravel/viz.py:622 +msgid "Box Plot" +msgstr "" + +#: caravel/viz.py:729 +msgid "Bubble Chart" +msgstr "" + +#: caravel/viz.py:797 +msgid "Big Number with Trendline" +msgstr "" + +#: caravel/viz.py:847 +msgid "Big Number" +msgstr "" + +#: caravel/viz.py:893 +msgid "Time Series - Line Chart" +msgstr "" + +#: caravel/viz.py:1045 +msgid "Time Series - Bar Chart" +msgstr "" + +#: caravel/viz.py:1063 +msgid "Time Series - Percent Change" +msgstr "" + +#: caravel/viz.py:1071 +msgid "Time Series - Stacked" +msgstr "" + +#: caravel/viz.py:1090 +msgid "Distribution - NVD3 - Pie Chart" +msgstr "" + +#: caravel/viz.py:1126 +msgid "Distribution - Bar Chart" +msgstr "" + +#: caravel/viz.py:1206 +msgid "Sunburst" +msgstr "" + +#: caravel/viz.py:1272 +msgid "Sankey" +msgstr "" + +#: caravel/viz.py:1336 +msgid "Directed Force Layout" +msgstr "" + +#: caravel/viz.py:1378 +msgid "World Map" +msgstr "" + +#: caravel/viz.py:1452 +msgid "Filters" +msgstr "" + +#: caravel/viz.py:1500 +msgid "iFrame" +msgstr "" + +#: caravel/viz.py:1518 +msgid "Parallel Coordinates" +msgstr "" + +#: caravel/viz.py:1554 +msgid "Heatmap" +msgstr "" + +#: caravel/viz.py:1622 +msgid "Horizon Charts" +msgstr "" + #: caravel/templates/appbuilder/navbar_right.html:34 msgid "Profile" msgstr "" diff --git a/caravel/__init__.py b/caravel/__init__.py index 72999097b8ea..45fa4b71e9c4 100644 --- a/caravel/__init__.py +++ b/caravel/__init__.py @@ -9,10 +9,10 @@ from logging.handlers import TimedRotatingFileHandler from flask import Flask, redirect -from flask.ext.appbuilder import SQLA, AppBuilder, IndexView -from flask.ext.appbuilder.baseviews import expose -from flask.ext.cache import Cache -from flask.ext.migrate import Migrate +from flask_appbuilder import SQLA, AppBuilder, IndexView +from flask_appbuilder.baseviews import expose +from flask_cache import Cache +from flask_migrate import Migrate from caravel import version @@ -46,6 +46,10 @@ backupCount=app.config.get('BACKUP_COUNT')) logging.getLogger().addHandler(handler) +if app.config.get('ENABLE_CORS'): + from flask_cors import CORS + CORS(app, **app.config.get('CORS_OPTIONS')) + class MyIndexView(IndexView): @expose('/') diff --git a/caravel/assets/images/create_role.png b/caravel/assets/images/create_role.png new file mode 100644 index 0000000000000000000000000000000000000000..0914a5829bc30a1e9faf22710949b77035061c90 GIT binary patch literal 51474 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 U?r@xIx|^Tc#> zzp&ev;x&lh_Z>MP)XZYL1t1KQflab-3k>BhUm9P_##GB8waL)s3RwhBs17f5J>M&G zu~bbYuC=sw4%H@On_-WJRjE#iuLa39u*lm&9?vaE9>skesCmbi@(ZZ;U5&!Q zAfywgLr@fxXMIkkq%$DWE_hMbhr%iv$Msn1hG4hFP47G84cBfFy~t>fxuY&9Gb+G+ z$xPg4^`OB|)7JF{uE9nqJt4jm#kG#jsJAu^`d$S>7GjU9dJC3AG=l6irc@M zBO>)!{j7(rgDY)xu0tk%>^d&!oyC~(N(h!tkFPyT?hYlbeG)x1d*%IbglDGK?0N;H zw7(b}QbH6_x1Gja9WX+JqgFp5EQZEJ4c*q-nixrDsZ_IdV;3=m zekY`LJ3CI)loSr&=#*&Ze*1fcTl zxX-B=)$ze!e*S9onPbfJdkyEgvp^k{oN^+-cD~xM_wryp>?Nh~t`!}F)76L{@+hwC z3nk%@im^}J^`o&pmY14^F&P5zgo$V!aP9${&0} zOzB0IxtlT$f8bMOtqakyVc!*H8Y5$!3Yr}-BwC+-8CgKO(_%i*dYAM;N7*+)hVgT) zi)~u_Aw}xWlsV=ai^E+LtsdVW9En=5zSkKt+W-7R2=(JSuR_nzKJms0_`muOsGUY> z-MA%k2L6O7gbJ$%b!5nk26E_S1~5xtaO84DiMCaVOuFt{b}6+VZ3t z*EH3D!*2|-ix|9Tet|p$;+hep#0u~*GOKs?RmBS3>MaEWqUNsOW*;4S-ha=Iln;4} z3E&zbR0wyZ>icqbF2Fl^>5}McvPg8&KgnV8>p!$ZBp%bdg3V#n z3QUe`AthS%r^h!4eG^S`IWZ?i{EsZsQ+6aDIZ-sr|0Z%6t0>mc8drwbF8@zN!w@d9A+Q1o^%+vzzo0hWwAt%5$Dq+p^ce%NT|V#Y$c*Anc4)Kt z(W|>Ue1?KIdu~<(7P=pF4P0rb8^338O0uB?((pPlCH8suA@i9c-Q3;Npo`mciB$rf)T!qK~p(#;1XaX_VIc9anF%5!sp!sCSDYG-A+9VioXrNzq zm6aGGyl7j9#Y!8q^-)Kzz)=e+*Fuq_+YIG^YmSaup8FT2*)T)I;qW=!<4EZQ{yrrVaxg2U%?vy*M3H9J+E4!D|8G2_(-~X+cZdgI~1I1uUrbWTjGfT z-Z2IFgq!SJwR3=P^EcZX1Lj2H=_L8D+NZg@r}8UU+h6X$+c)&+4@_F z9{|t!&Y@LQL)6SlxRuv%6V4ReL~kFw%->UgR@>r3@Q_M8Kus8Y41M zA|M>&2uOo;zuMQMT>iEI?H1eYZyZwxYg^BJ#B6ae?y6cRZ8slW5iOhqxY#9RqJbz# znsC-7zlwM|2~CrW_@JRyn`8)>*waNIp}PlUZawJhv%{h)%geujxCin0YOaY3XwN6X zGi-;QZs6JS0*ag1KJecP>*)o_@v6eEd9z_>+aonVK4o`n4TgQFUF-(BXiln%zSq!U zIjO4^^$PMOYh2n-r#a2ux!Ndb#54{Tu?+oJWd5D6Sgl?&-n+XgG-KDaunQ(0cUMCM z?_70^5$jbSm9=Jsu}_?G(;*N4=x0#g{k6%KDgnURZ#d~L0Gvq`Jx8;)TbzJ7F3D2B zo2(W|c$>E`R;&of{&8R&f;c<^Cfp&+MowH8gXFk33`D@-(rpXu+DeS0s@zD-L_sQ zcV-#QBO!Y2MAi*FC5j51pPu8S9&EvjOqagn#YR;?<50oLc3+Ht7@}gw0VDiQ;&rQ3 z>zMp_Xe&@G+}9OKLdQ9XU6h)-)r-Fy6l=O2*W=0<;U_iu196*%`ERgZqOzILil90c zIJ)h@3B-_4cH*jieryuorGQ}MMvaL*;Cq}^^+5*1of9=Pq&wMtLpiLHz)EwfiueTu z{;-sQ`Y8Y$w{?sg3|^k@rw?I&6|HF$hmtkdt&hwIiVRi?u|$JQ0Is1A`1^VE&$Zki z2O)nzKb=I;S z6Gj{0eiJkIU`|}uxCR;{XK)pJx*sS4L?2nEXdRIwKJ319=Uxs_VkhTGg~~n)XpSdS zFdRB7JvV-!j$k}7VRa^?0>%&22J@nwj40f{n2Fi1OnztJ3(8KHaYQ7}n2-{p*~$w< zn-2aw)pIyDoe9A`Ov$+7vnO01as_#E9dDq%gGhh?kCxTBp14o48>L{8T^O<76n02w zP^<#0fPtzzC78CI1js^sgT9KpWy1$0pKWEuFGK9T0-_(o@_jgB^Tl~(%6(0*yBu!;H=wh5ijQ<4B`qIp$X${e6@95 z8SQ?l2EV*V&uF9c)mI{672amxwUHe{>5CN>+n%z6lLeaO;c z&`&G67ObB8`HcY4SwrXzavnukUaZh={YBhw`vfbe~jesgn4~i0@24sL}f{>H$|4@HxIA%LTb^wJnv=%No`kH zR5P0y|ByBLgn+%S`Wd+C)rtn=&@e!R5=1i<5d}T9^W{UT3xkB$?c6LN-Vyl(w$Gk| zsALulgBYs?$I^o|y|W|zx}mKkn0EFq5znpm2(ktgg?h*mqU{kJ$3=9_qvO%UE93iA z0G^q%`mnsOb)+?fqBkzI(mwgHbiU?94O7Wra_elsc4ccj`kWeZV^a4dG0w=>trIIf z6G+ZrJsKFd#ojwVN}uJR@yJ^U>mli2y=?AV7M7HLQVE75;BMP0qqe{S>oSt>ntYI0 zz4$=uJxKwSNBy%=dR_&Hj4VC6V~F_ zbzgsj02%M424ZQI>9o5ZFj2=R{y0NHQr}xzU!+F$wB^hV*Kp>)$Vds&6!Iy{?rEVS z)O3G{WlXO54DpeiYF+=7a;5BGrFrF}*|9A)%y{$0;Gls=bPM-vIJt>5U+`yDhOo$R z@2xon^pJJqOJqD4hK$;qd_QRGA762t9z3|uKC0|NIdG&XD5$8>o7&q;RO1>_%_^eg z(>QwdR-kmF{HFGPzuCR>+4PMkEv-~7*)$JK<8*E6rbV-!;l8jF^l(aP;9oNC483 z3KePshyLXn_2nYNkE9KTMamT{X1>G~B*_SsQt*fNKnaSSk$q9!v~^IaHY~JaxWGAy z=WY0fXW1J;;$LFB9!z*NzT0Xwj?#}#=Bf{A{N|fY6hXu8IOF&w3#Q-EGrj)vssc@d z0Hori$baPqp__8IJ+IsQlKH|#v+8Yly_w4izj6`2S4m&%RFv1jm=^_oji~i()ym_o@z4rD zHxBPRP(}cNc@;3qonB}iqZ*8Iw_9}Pm@mDXIAEWgq{~7^;FI}SSi2vq5=E1MeJ{sf;GLzz7(i1d<#B$z`fnM zk@M=!PrU``03LzPtZMG=2pnv~`o3tfsm&JGqkD+!*a=-XGL9^*TpkS+J_Q1*4!Vnv zuY?r_WLQJl5?Usyesb9XQMl5M4J%oYvThMfI%i##`M&cq!u6e6wo{|;m5=j;C0++~ zq>nqH)HY!dU%sUS#&*9SFLwv_?5>KMEM0s$l$Mcz_(YW@#f$JtoKUYhZ!~`1){j6^ zvQW>0b4SCm@e0$mB{rKxgL0yhXi|@iO{B9${w!eEH!WmhTX1FbC--aj=PsBQZUmj; z2u^uYYPGzedjgFm?FWom%n#2%jrIPm#v&~@3NkAVzOA-}Bw$)g42jc^3bzBy+q`F3 zFOQKQJ`qHvErOjUcPAQU6oz4n7CJzl+yhIdc0ZWbev+g#@TZCTP)}KK|KfR1p0BVq z+Ve1)XdzYQ>n_?>)jxdbwcnzr=kLH~Ag_B>1nN4m^gJ~Q*+tS< z3!8B6=?Gu_GXGG7dg1bDWOCsiTpWZsHv>UIS0g&sDp!+6Qc0Hf98`K?ndC9E2Ay;=lP6@#F69)kO)OlGy2$ z{zw?LG*njMkOX|zP#Xg#8{uA?ds1YA^%^;^_ySOPDWkS87^BDpU1^5BoYKY~Pm z`wDBwUq|dUe}z<>uV;_}LZPmEK{Yp+dCDE2)a}k%bN~&+G6}g*krDpYaV(+X*TeO9 zpz1Dar|0|UY)5@K;y;9xCOP%Fnk8D!;_#n;Em;ReNb42U%>IjLPKy@EwNRhH|6?@b z&y8f~1GppVbe$0YBAP3O6kzCWNk8IU!CCtY!h(JR;?WoP$@+ZF!@Kj?IM3E=>GnRDd^xW zIVAUper=4u3LmgV)&bem?sAmFzo{KD!D_GNxx(`I_xkxAz1#snxHQDJUinMX^XEHy zU4;Qf`HeLGIV28IO< zpF8f?Pyc)3{wyQEe)@lV;=Cq^Vu51O*-k$k0vNP(A$y^0JG^Qko;$@SCCYY1q~+=;3Oh3KnHcybbg(j#l1*wg1{6myC!wD@@M z@d9Gi1$DMHtivas*HmQ&GxP`fno^B+P<;&KrJaf4b^X=mXuk$hui=pnP>q+yfCWNw z;R-wWte`1VFt~FOK=XH@$lICogzD$%+xbyq(00|14i!W@P~;bg5zYiq2j`~4?}^m$ z3}D@%d>8?~i5q<(aKdFlVNODx^uEPEwAIgJE;JB&#Qd&voxm}7mF%{GW4?^qX$vN~ zMD_Kx>OJY;cZ_@~vnZDorXTkmTljJ(2YCq$%_x<&ozwHmBU=R2zw;uE_>-+?y zYCkn!f&I&C0dTzXFae7>=zU< zKy}kUzTQ_zF3#@^hlGfKUnHUXvKH*L>w1Rrzq|cEe^3fiLUJs3l70tL{W)ph-N0F% zsA~=Xx0&E?!CU_C!u`5tKhF67^}^|^Rp4#{Dq>}iZrB4XA9yI|;){`(j|KjI8B?+# zi<)Xof5V>(^!um8>%C<>$kc^Pfof9b<^s-Ygu1C@X`NE-qut1t&Hrr*Y_K)@OuP%(5@cZ&c zj!M0<>7xjp?Qqij+-*5ebWI=0Kd;FpY_WOs>07~nosEt6l9T>${AU{)uH9Mm?)!u{ zUAp#k!LHzfXi#~Q}ohtsNP zcSQ+qZVKAXcb>GZZxY^WGglNhm!b7$l*&4I{D|8ZW4V&r27AUq*UI4oe>jx$e%XYZ z`bY~a{p+|@&p)q#Bol1s!2y~e$}KcXH29pvrj{>#qV>zFBHTGI>|6IvWblY*F}7#Q z0xef-aeTsjYPlJ;(3H6U@EyM|+mr;V8=<&tAbx5qpy3k^Qt`RKSeJPT`ZGHjbS`(0 ze*u}2`m&YNw{X4gp()vj(bp^@mzBiBA@g|A#zzBf-d0JwJyoL(C<^C4YLh2kwO5i! zEzy)Z3+WVTuLaiE_ek5J(Ob{hfD)HQiUO7SxTst820=8`M;=eeM&juE4{K86p zAETt#5ZV#FYjAInywt@;rX<9xoFPHiL3S@#Vl0a6uuQD9Qq$fuj0=mL62PX^qZaCQ zwoQma#7Qc}xBU7Wgth&7D-`sqds7n2zEycc*gFEK*|21+%jA-j^N&EFSkexNQ=3l) z5AL@)M_;A!LDI{o5!`r2t%^Vl_}fnK`^_BhrC2pG`727+<0`Br?CyLGNO{ncy&!Wy z)PP+QP{r*UY@^f>+(RI-Ibs%-@i*V=&*K3&bCuQCJBh{Ivn)pMNC}Wv1f{AGFK8X< z4`%nD;avED4(Gl9qu&_WueAj_i;S+pbn-Cby5nIjuj~jJ$ENLPmfYcvyw8^g*-Y#Hai-!q zz@1Boa_fYbVsQ51RpxHIrFk+FZX7wXWR{EB?L%XEe?KO0?N2#+xA~G;btc?#uQ0-s z5->d;76(}(*~me{M<59r=Ow;zN^xSum-XbVgU-KXX@9<$$@A06QZT+6S?ZA|QbwQa1QaKIz`qs;8Tv|rYJA{pS=F9-i{R2yq{u&) zvR*IH6=3VW0b0HcFu+C-n#i#TEavwhJK>M62a;{JhEqM-%!abwg+X#$AH?34gNZXJ z$JV(}KF>Wcx`*TAw^tW+L-e|Vwr2-QYGH^Fy@`)SWz&qmsnUV~;-6>baSDV?_dDIh zfGepFOhrn9#w(vp0QWl+WY0Ooc^;$dY?@X8WGwDbFdm_U{V13N%9@lu5r#j!F`Qf7 zU?UbTwl};3L?Ux`(9~zs#214(6(4-o^e7Q2B1dWeaU+0}?slX1Az!lGk`~CzpIwZU z#<>H<|2PA8;u}cO>-rrs3c0P>B`frZZY#E^Le$P8C{z`JFY(Z#fU{!E_OJ{ZKTt zE^DPA+~*yx$3fHL=;WDw+WL4eEJ^(QuxDW^=!}5i0ne)c=CJ%fHw1v*#=)_*8z4=< zBDm%bM=Qn>ZwIyQy87^ebtZgg4!Df)rHZukhM}JOS=C~u=yKqQg2!QpULWg+T$QhW zvv@~^?9YH2gxxZNsV0W}&YZ#&B!1up<8y{Bn`sy{7(5}%XurthVK=@6;x-kRcNP>9 zo*^Ah#*;~e6U~1F!LI+gMrbi^d_~4YCl(*WcfO}b%#X;MgE79I4Y`0aH3e(peOzER z@mxKTa+JBSTNL5O4tYrzA(4ouTdW@>9?Sp{WUQyV1s+rzv)*oqWGYLx@>8TCgG)1( zxt5}&QP~U0zFsCnnzD@|dl6D(-{p7Sx+#Od-p}Wm_cimp=Q+Dt`zfwDeu4Ls3V=Aty*rDf{G=^9y!L} zm$wNjR}BtoW{)+^ch|rUs5r;Yq0QBI;YFZEmz?rL?^6Pq~Pm=4pw@N-nd)*YtjIu{;t9DGie{` zfZE?wPPgb0?NiWZoe={vO=_a4!s;U;tPB9M#W-jo4SyY9REYy_gl4AJ=JZ9+YSy?T zW14r!O{vj!-)yyUxwG?2g`G7Xq)a(IhbOnqcUiYVe+8WT5};toq~eYu-iP{hmlM?g zW}Gj=a?|6v;SHRuBm~R08PO;~^oDd2b*Dj^C?=T~duYV&r8lr~gGMdic8uQ2tLV)J z_F^JJcAVnIM7RB!LHKW+$J^cv00D@nuM;CHXs+4=AE9I}l&{IN0briSgL z;QmyVOrsbs%8{1!vc+}JTk(hp^>X8C1h)L2Ql>LQh^K7@DgX!6j^WcSMBNc2MkT zljb7yp&c8QWJ{wG{C7Egs;F$90@v#X7^(yiDM?7*v+0YmX_ILCcS`%xtidKbz%PeS z4ceK2@VsMz{$&rnAC7+UY${q?9MgFn;9&7Uipk54)#^roN;!9riCTG4gC78{BObt9 zdrE*oBm3GNC+?ew=uaTTuB&F^6H%s#0HiiM+;~g!bLtx&jrrkmXcGUZh3?cDy0M%A zh&c5vJ`bel3F|P?gLKvE@S;)Qfe&-jz!_nhx8Pt{l(C_Wr#*+u*9p*bfdN~>dLM1g z|E>|BUw6nVPz%yNGzz+c8eZ_WDT99?$8m^S$W4%WNoQr{)B7kLy_r5*Nfy^=y{2=4 zJu5NU`b64^oexesEnabyLd9=nmmbiRXAe>BKr8+#B=lbZaS$x)a-l$_oXIz*s{%DD z20S??KdTLPrto~db_nGf0BaRNKDHn=u*0Y`#dvGvY>j(q#Co!jt$3z3xy7IjO-?xf zVc%m?l_y|KN9TfC-z!&riRwx)W}$87l+mrZJRlU9KluaIyR==`$sl%Z!8v4DwKpG( zOU@D&(?ooKVY~bKJ^5!+|FXK_Y2*TpN5=S7LH5f*Df4ddFtN$( z!O1lF^`D^Qb@KFHY|WgETZ*72FbS?76ZBmK3W90<^AP4~m#(t_z<*kliB&>x@1$*6 zJxIVHg!Hzz!Dd*5rOrq^u18a?;Px9sQ!uB>%G%&zT~b3^=5Vi6tv`(@9f z(HA?%9;Y3dCC(_7j)WZPxRbH9={-~@=>*|@h0)kt7*}#DF7MDryR#|R=s*lGctj0f z2%G%q{=Py19@oK?L0Ue@{-on=so=2AK5?GC!)zqn>(wTk&m!dW8C8Vh++N?Q6Fbv5 zeEw^#D5v3rN`@To>+n1v<@FMAZT=n;3cisbgO+$NFWadT*-ez2RX|z`+3La((Y)LV z+KR(QNG1aeye2uf?ArRz$BD|PSns7CKR@5qX@YX=HP6}~`VYU(X&oKzFTr2lywZ(I z*V#ZSr)b4uz?^@;lgAOA1z9W0??uw(E+yLx{O>EFG6r)f1|KWg1z9EiWFC8!VwKqc zdQQ{MuDg<%xCgt~otdaq5WzfyUPw9|C{)sZc<5$X;>pVTp<2J6Mdt6@cE1x6IBwu> znYv%8?%jJg>zanQe1L}|CE1qw6f7=lP2p7Qb@p_$;IN$onS~&rVd3Y@L&1qj&lX&$ zwXM!#IP`=i|Mmi#*2tksCvYQCBh4lUkMt`Lp71VuWMA+_1 z`}yo=zs@iGKzCu(DP_anlB&Ifk2W73}s#aM%V%>3z zBUec}hm&nmu30RPN&pt!KQl~}+v1(Ci{>u|DmYR6lmiFqHqx0wObvX64f<8I{KNgR z!i224&guxMBqT-Kk|wNqX?ptcZ(1QNxq-{W9Kn80g3&>FlE^xAxd4Gx1G*hmOHd)Y zji?O%&7|9?Ee*p;6|vz$H=lXQ3k}3v@-d+Q@FL?9H$=;=nN9Z_4y3LzoS4oBO7C*- zShA;1>7B_-U5Vj1ecp1`@~ZKV3FV!SKT)O z73*B4JsZvxj>K0D4 zTtLI$cwSh&Wb#l-pP77i`6dT%)I2VikH9@={YSG5Rm5TVS{-h@XgZ}t=j0Rf$1Q*Mx ztD6Hvbo}kcqe`nh9?_>s%yY3IgUp3LCkIABi#>Nl^&M-L3$7qG>t4WOpafMI|Lg!; zV)G_jVjR(-!M@4*$)!lL)MUt|nU#n688&hY0el33?$9Sbd=hMYeo0|-+Ws#7$JGL^ z7gsv|p4;7#(j+l!uFf-iO04#I^;HDVu#~SNSDrd?8B{4>Hfktp!+T}2zj(F>h$won z9bllG-iJA5##UXlq3DLC{XQa-m#0{me}Qxz zjSZq1+or~+LwAN#gwN65f2-TG z?V|RT2BHG|XI8Fl|0#mFHsFTD4Ygips=Un}38EnS)VGOWJ(!w=fnKHfX7E=ua3jB(szo&+y*X1%2420cGc%Q-h>oPCmJZ z*e=0VNe@!fO#8B;=y#KF%hK$@1m73lOB;h65Ys1?v39@Z%|>l^y7-|`r`Qe7ohyE+ zCc%?K(x0T$AzIs=d9ORivornnjG)|yWLhUTMXP#6evK5dag*q?soA?vf6A^(kC|z- z_nBPfO&Z1(mO12+AC;0bnAgy^oxScK*z6BGmNKV;h)u+8f*`>>vTfDJO2Ak^Gk&Ed zLOE+U1Rgok1uHWW7(0vvmzguaudq4rKju)ayv-jEUgJ5aqO`=;y~(4%Dk( zKj=|kZHya%WmWXXtekS-2BIMm!B%^%HYNf|%7q)RlvWWh<7$+I#40F!Zk4aXrLYac z8on48q^i&S++;-FY18(ppF(***PWd%j7cbVA}vptpKBOYw#cq^o?a+(yp+KLe@C(U K+UZ(neE$!@vp(Pe literal 0 HcmV?d00001 diff --git a/caravel/assets/images/druid_agg.png b/caravel/assets/images/druid_agg.png new file mode 100644 index 0000000000000000000000000000000000000000..2d14e1e580a499c8b954dfe7b1a8746abca0b4fb GIT binary patch literal 104052 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

xyYkjyC<`)Fbb7x<^$$U0o)2hwZsU^z}bRP znFM6P=FBzFp%Ea!FgaE`dB2*Pv_II3RL#8a%V8T)LpoBuI$hzg2XC(pwK-;vB93&2 zVxDL5MynQ0m?3wyWZ?kHBIO1E!7aOx81NZ%~y1MCRd&r@`F#fUXuuE-|Qj zennpgTI)=GI$YsYqVLc@c;66dG$F}cmGG5;RsYleq-FVDf7*b@2;+^wr-OHCL*Jr0 zBYvVa9ZQCoHL%Z1cWJB!h0P1(+}+r{Q;}_dmw5HD@O^FX9Qzyii)H-7f(vMr$CIU4%IKi)Eva$9Y#MeH0bkU2Rlgdgc5X z_nKzE0R%OKdLmL`2DRo>(+h-%iTl`Q37nbk!!LQBL^;H!aj`+8Pd;auc7to4mx;t? z<|n<4b=n!AwPI40-_qM(|T10DnGtIXR*GfX8#Ht zDh;E5QLwiRF(uJ;Bq{iQSBP}dl~ZGb&c|QP$#K$$obYMCv@btb390H-DvJsFS2y=9 z$>U$gRr@)7J)H0FE69zQ_V^2o5?hwuROKQKjS##0DSB2Fcl`gzp-x>)(ERanMO<5Xx`#li?K%H=!jx+nA_2xA@WftEg8I4oBf_wCT~|wLBB(=D_${N= zcpYj3F9}hwVuP63=2miLhUA)d!KN&60ezQzoywd90(A$op~Igu{6-;DWZ2yqqjlbh zIROTUF4N7^W%_cpefIYnR<7uNkkA~s66eoe9s$?x-!m4}|%VsaD5 z^6#|?GwMQ~Hp(W??w02!ZEp83CutxIOZFU1e)e&ddB-sxuP_$Dtvw(%#bil*pe%{g z$}D-Pt}UY@P58eylLdYN3R8xsI;ETbU{k4rMXrHRUQAfLT+miMWhfu3_oc#lihWqt zy@q>aY9`xv0cpkxCW5(fIqTtcy7)KR%G(0xyK91d+Q&J}`FeTn2i4o{b54q#(1@FJ z%<`XO>ncn@BPAB{;6wU@*#w3iafOTYf!={-GM@NWkZ|cb*|s7y<-5sdh#7)(V&$W< zfZgSxY(lz6S$lgi7trhQ<4G>uc+r?)DTTV=`t2GXBc5__A{K)`XW| zvS9=RNKyE2&}EKRY_cr0b8kiGnm6Pn;k#S<)HP9s>W2!v`m4%4)>(&Fn+XOj{%*BT zvAP9y6(XL>fs*b0eYE`QnkfUF_QCe&SzVe|mY_!|U8GUgptCcb8N*pp_sB0@YFqqc z1|0C#njYPd4Alr=qKgk+qVcEKdgvmm!DI7#SN}d$2K+nHC6k{UWeoSYtqc0vQ=+QIu|Q@XW2wLOnZYS zU7y$RW_5RmklX}YYQ3<`07cM&l0!pl|2g8ft}ueNtKlJBtA`WJXOi1x5d7y#eS2E# zy2lqL8y}bbgF)Eeley{r=Z6flly+t!1s_Ej$ps%~kV!E;A|cnAWt;BEK&SYkP~`4% z^>w;txIg}>)jWF^lTW|g0HqbdeNFYxC#AtCKL8s>5N5sc2g}Ok#9>ygXesHSfg6Jt ztN)(Xb?~sLa;p1N@nD0$PLqE*S{xj9A5|M&U+^?!f;=`2kJ=>lKS)Bo^= z@4^7SEnt`bD@OaDg9xX|0eyd+diKv@{p-d4^1IKp>xOX>|I=kJfX6uN`n&Cs|L`2S z;JTUV-=zO?iNE{m$FIO+z!Q(f|IwiJC%f(6|LDKN_wQo@-sFFW@9(Gce`nv{Ev0w= z|1I7py2e+z28@C9p`>`p7fBwGChdtpor4DtM^qh15otcaR|0*+r$l$MlG%Zm0uQKD`?k|6cg0`%nZ3!Jo8$k3Q52$)xH9WsJ zh5Y@(`^S?0c$3Cec5h+fk+tQLbSMM_s*kA|Gg`mhO0A@qbQ-aB#!|~kfcQ}9)wlur zBm>{Fosn|=+#V;!XFaUi@NchYVvGI!vP^BKh$@Y@%8K~%>bHx2os&TEh9>Sut%U*2 zdOT2wSmdPFGP!oz^> zGG;n|USVhU;=VUfq&e%;);O9&lj@k-p9p5zMbcC}d}rcpbjBRf9oj8S-&5JveD|4o z5KYohu^{W}zink&rvOp^e!cO7A7_B*jAPUljp8)rQ2fi|r&mezmDWwn z6u`rv$u^dIuRM}*pVdOj0j$4|CS4fgj4y4z@*vTc;Rw61Su8@B5nv%Q^y|hez*fv8 z^M3RKu%aqr7c%*I?k-J#QNF`CHiV_eUwv-Og8jpcxRg12H_3KtrPIyRNQvimq{7b4 z=!QrnuYY1oc|@p3`jN>M;FD_$h`7aoVj=3cv*PkW>If}k{_CPBR!?>KR2DPC9{got zs6<2VBJ0l(9gw_M@600#fC!t_dzOtopxC!{>$**vuS?H8z9C@d^oYifHI{w+{bYiy zPw^;lx{yElikS>~_~igi+sSo)6yCHnI5U@SW#YNj}yj1rORXNWFi`&G_@XHogL7~UPjY0!9Q8w z%7}E+Z?g%k^*c4dWO@5|=bAC8((mftR)%CQk4vBE&dq+(lT~Q5-<{_3dsI_|fzbZ+ zQTl+Lo~YVgR_gD!ukeaF{+s&3pLBYo&ld+PoGm-9Thb(o_Mf8RvA#eRZ2{;{f9^7G*J2O})Y131|XYiS<6=^3X#eX4hWI$aiquiBzyl zY_U?sMLI#E9iQw-g8`T(??r0>HAV+OUxt0Rpu?&w443|IiSNKE)fX4!B2(PrdfwY6 z^D+@~$D|mhfp(!YP~BOVTYezBa=X_hQ6Mk-f|RDzG;8&bFu`37J*KJbi6vm6w*gsd zIGNlH_uH%)+fX0;MkWIQM6<`4;djo1|NM@G&zb7^CiVw)`>SW2t8#oLWrs*TihO!Q|^G`-&& zKp@6fPQvGC)MccW3MPn4!tQ6}-IN=E@4XQj%EG&-zz6zrexwi?={4oZ9|Q8OU}Q7P zYhCZ^fg_!lpU9*AAMXSDh%dDkjM|%O&4;v<{s05ITP!MYm^3 z0ejn0lt?GFNESdyrwK}?SnEM%`a8h>M)3ebOTX#Lp{yCA$AEC#c!p}zZxN6h_I=a3 zH?R5EKe* z-I~Du%25*55ePK{g&RUrRYw{W%EBet#@h;n@DfU#mIt1oAaS zT+2Q0lVh(!m;iRHtzxXb<9FLzvo3IYIF*X{$7KRRqpAf9*!Gj&@tfuut_GUk=27!( zsIfwt6ypAH&V)K{958%p()QR}r-3$ZbBWCluqbzZx}`p0_r7@osYse7t4q#aMTBk> zD^kS~f=5`8x(m@jOJLO!|JqopF6wbdH;_+tMH>5GxV7Y8RseSHs<)+#{_t6>j5uH6 zJW(!{ng|%~V^?d>qIo>bJwYK8w~~jvb9u1F2Y6k=zi7z4mdJY%VY$TJE|H-7y&sJZo#<|qmfRo&yaCIA8tTg zA;syIJ?!$RR&RKlOOz2rMZdmXZ~y0ag~^!`I7%SrfD>d^H0tRK{d!88S;MYb2FHFI)4M2)YoQg zFCo7EWQbV2+Bz`4*{CygH{ChWGm;54S7}(js;+e__0~M_4lAUoALf6+-N+59{~Rg5 z^Bo81hy~=OaRnR9Z5B|QvfI(t{<8A@V&qr7;c71w&A_OZ3RRp84B`RC>D||xciu*@ zmr;S@^+J z0H$(d!ui=>?kIjxl3}Aq;Xgq!g5rG=G>>bMD_uRSV1eySK!MEB*cCKy!Nm+<81w~& zBF)G5OHOBa^fA+ej=gtFV6)!5@ImRh`I;zBBLDj8QwVR?Mn?Lk>N*+<@cWE}Voe2V zA1?~YMeY&FVRet_gVVHSo$oZI^I}A7+F~b_+kxv>=TU@}0(3vO7OBlv`@fem0;Za^&yN@slNOB zDHxeRoQJ^r(b}6&f7d+~ef9A?{7Thp!3wkZS1l4FpVM{j1*54^;#HjP8H#$G>hFi! z)#2(PdLPo>y*#3Pswu5u+<9(jb9BjPQ+`R_X31@AvRXdHe{AmTzz)(2n?Tq^mc={9 zA1bX4T{{~sT0K7g)Ux7#8?Y@$u2 z*6dhkW8=rSgSH}7=Rftpo)`PeL>@nR=3L-m&a0>6uDEo{ZC=}cRv&m+muE|2^41-LvLgqFAQ>+$_EuwXA_9-y4TVPtYA23!*8+nZTxaEgfANNG z$NTrU&T7QIm)XA7x^}#{xKr^cx47tPcCKaiu-~04C%(tmmNFHkKYjcunJse zT?|QcS$j5#&+n9t(d>8*M4ev-oVBYJqn@VCwoUoR6MlPQJT7sWnkcb5ww?uPKV{(Q zR@;ofQhRpjQ*N$UqkB-+?kJ6ZLL9GnyBubvD=K|0fKhDc+ESF?g)0{xj=d?;sEk?8 zmoRhK&?t=2hWTKx@VChqH7`9FGxbrp^0{rc=IvLtt&gyl*)LnQz^coMS-&B7{drK& z%ULN)rLu*niE9BBV(B+B=FnG%gfAG~*=l#Z$4xWh+8IBdYl=Tj02H}A69RdQ=6roI zyJfb=`={K8-(t=hbKze0`UnL&D7C)bNO=39GuY2ydX3Mq10Oes159EJJae17dC3%# zqCMJ|eYR-$tpn*P@9;kjYW_7fW(+1)P(hs}?S*k&x_z?czP|j*Yt6pjgbUczQe%3| zUbRpUdsn?n{O$7bJ=eNjYhH>GcUsTeh-Od0f>Ir)`H&)L_H@cZ%7?i%R=UG*xPpzxJCS?Ar`V+mpyT_NipcR@Q7oRAP)F zjAbw+24l<4Sl;XI`+a`Jlm2>-_jup;c>1fO?zxxiIeDSp9*DNpMe~#RTU9IUq%Lg5T^|@)1Mo7!Ue-? ztVNWG=qau4w-7X~Dxz)ll3^>uQu8}FSh3=fWgPSviz_yH_$Nj)FH5X@h+^`E*QMxI z*_>?Tk%eun@Ty`!89JgxWDG@lzcRaG-4Ggf=pL-IgcH_22ei9*xs?bpq)3IPB*j423n@*Im5S!+hMO*}!3y9g#=0*DEAvp0OUxxaM$7sH!t#u6Iqgpb`IWRbd zjz2$!vs74;tk90B5<-N38J9w(W~_vnzRFr0)%XX!&@{v=#S=lU7Mrr43zD?Dh}-^E z`E*-^^0uh-YW~>g1>erxRup*YSiMgqlc8E*K$j9LSp3ctxJ|(ZnRZeSsIqn3X(pGZQKft-hs?@^JjwfZ`@`guZG{%~skJhdy)rJ#new;q*7=WJ0ovpla;WHQ>7 zNL>ASOn$6%<>YQj^bGIesO!$9<>;~3k-Odt;-NE^R-f!J5mgU z25IW3^V-Th3NQ;61wW|Gzr%Q`(ICG5u-BsSGK?esM%%%Uria*VJwKU=9*3%JN#0Js4{0ykXC*cId61#X)f%> zMO)RvDE1bI9#mG&0CJVc%6hQ>#5_B#J;{Opq)1Qr3Q`4L4*0}5A?kL+^L+3qali%0 zA-H|YEFaIeLB0d}=m7VYu^;(3g)#D(x~Q)HIho?2D0OexVC)_PYj198WbZbXa}WpY zzKMplnh3|@_A*1A6WAHf!ylOF4y(oNU1i^{@^i@Uq?);FzY#uQ8o_7Cq ziCIEvL`qMd#9CHSkDdtQ`BCf>l`VpZrJ1FV!E7i4XZMG*OER={wC0Es3&r-p53mus zA<2c_du|!&CyQIl-~uJm&F;b4gR?&FFDKes5N9Sj-pZ>5TQ;80F+Xc65b2%LhVxN4 zE$hGKzf5NT`HOP0f5rjLbP1M_1ULVwaK~j!n}WW8te)pG`e>9US5bpmmBlGnI#4u}e6^pX4g8 zq9Q7iRi_mPOQ$nBYf%3Pt^7TPbLtb*qA+1Gw#j?Zz*bSmNFaZg#XRm|+$63&E_GZYIMVDTE)XkaJ#nJ2-l49#uDwfmQ+ zbZ5HA(nU}=y_@(~z_yzCa-jHel!bkEOEITz5f5uP-77&`PoWkrbgrAj{7-weFs z`7AAw+PRou{yr<8ML?3Fg|6#-s3O`b&Lu#3g9~xWD^Dz+6J;HVbvqzngPD~QK&`hwsz`z0qMf=Q-}&>F5$+#LH|CCTLJ}%*zKqRJ`2)ftTQCZuYE05lX8XDvN_rRwoqn3Hw8l6)CiD#AZY^O<<7?HS=! z*k?NLkhYqb>;kv$3JX#?21n&-4zv^wP;KtF$hWaL`1xE>mzp(UrQWqafuLN^EyQ@StlhJ4Ak(yFJ!=N6qf~ zPZDr?4BHJx+kDj&c8CvlL@N>Wcv&WR=N0#ohTCCPdTrFY!yk1W1vM#b&)PwXO2M57 z+(63bI9g%S>RgfSGybWsrXEnJIR$9I{mi;632mQB&~5}kmWp=U%loM_wRt*SU;YNi zj3^*PSFjIr5u{{VBvePxs^@sOW{j*%ASQg0VN4_RAuH3s;e4}$FGl`t>!}r#+eeMu zqo%gTv9i$c+w2?_!jh{Bn4tdkilV$Xv)}S}h?_CtvqN~Uy)Xak3p*I?I|DU~Rb-%u zbygA=ksj{+W&h4v*RCATkLO3ud#M%eG+JC)Sg4UP0+jJ@__mGCRX0TQC7soBSV^!Z zrNq_fVx5TgMOkr)o?jm)yS;+sY-59fpxt$9Rq@*nNWL-CB4Ul^a=#duIl8N+MOL0N zcW?Hx2y6mAzt+9?M{eo7=<@7;Ixt6c%%NhYB6Bidz!8(}GO$As0c%O3mHeV1AR>2b zQJ25AveZ_QecFVkhR}2she>d78COG2g$sGMn}$f$?J=6Xs~YDvYcLY!Mi(MJlE$QC z5qNtf^b}73TrrcNWBN3-f4~<=v)kgZnxEc&v2V}9noux>I0Ri?8So$z%wd5^I-7D8wv$Iq*wBqqwH zKXcM)On{hY;1O%^Ihm!VkUEfkf)fDQuecn~JfMshdIX=2Z3`L=mx!~=y$+Gw!|4tW zdPq^vKEqMdzq3&G&?fC~`FPJkCaFX;LS=)Q-AXg9T)C^lOMdJM(6d0{STN(2SE46P zUz(SW6=w8ZsR{!$DY>q(a(OXT0-Dr;+iRX|Tjf%=su-gVo;HD}Wch*!O@}TjAC=iX zzqU$R%dmZ!ybo5+|CN)h*!T`m@rDFlvmq6&+>tAxwS$SVnP@+Kg*`rQ6t34(Wx zjAp9E6|+Q=ZBBVCj(tEl04`S-^ht5Wp=3BEJD5l-8dK9PkRa!7P>IQK z$wN4}B!V!<<6C)rXs}3^)8$!DvIMEaO_taHJ-ggTa%3R+D>lpz036#M;m{Z6&&YtW7Z0ZRRqVAU^r7_*mz} z*w9W}x3j!;3o|N)%=B1b|(d(1#_P;i~JE=QJ|eW|YS3G(CRv0BpG z+LHs3>03=BWcf71-8Jcy*{-QjOJOIztOkuJIYl?=6es*em(+58C2`u-G}2lCWT7L% znjYa=k~P(fI+z9462eMSm6MxBN1DnKrB!c4oysDrBXAg;{nF_Qj&Q@=Tkus0?iaU3 zCX&n6rcSDDiK#4$n=?CXEWczfI05k{cc>f_>lu|!k{7ls4xe8HYy{d0T;mw(xH;hH zw)JW?+`#Gj98KGqlVmZNYLJ9=`(Qw<+pE6##U(o+$b;ETbv`aF5-2n-3m@oLX_qd^ zPncxWB<# z0>CFNEpl41(+eDHUSpb=5nfFz3vo=QdDJjFhiSgb-9*~Qy5Zb?h5`j-|L@ifcC`FY zt>uyCW70|8miGn*5)X<}GV?|rwFDA@Y19O>V0-}f_tFM=oGAyhlP5}VY=fTSN6W8w z-WJE~Gx~M_l-j&%wU92y7noh@`GkhB5&p$ii3N)#F<%{5cZpK8cAKrd&&(5TIuxg= z%TdG5n^*~w=>`gSE$r*}3AY=Usg(bNUpU$ocmS`$#ZLi`=Yhg0+n%xV`5rf^w+?Of z$JID&huKE;0k1grsJ@*#L?HiNILCI}{EG1)_SOzHFpD^y3ze%$rBp>b!vwc$g=7}Z;O!?OLzzXX^Z0db5i-d1GejecHTDZ2}} zv@eef-QBpqKO+HS9l&W^-8tQ<^7D5aujV}nxRC>fqfQtB3i8|}=Zt&lqJ-7+751CRq-5VD7*LT`>8c-uYa%kDL;W+;C1^+J3zsvJe z$bS(GK&<_*^!$6ceycS9)jj{J=HCj&u754pzlQzy&c^@Keh=Rx(+7Xg0{GRzTJL-P z|LP*{>hfD#833evC%J;|KX_fhq_oaYShongZ)jjtC~XF^e?<{sRx}Ev>b;GLot?a_ z%X&gx9Gf^B;3h*#0faY>06oSHN_9whp{Xj@#mP8&A?iQss6XA}nIKu8_98H_H8N7$ zsMvBur1i+51(kv~{eSSTm18R3JVXQ>Sy1U9Vgq2C-#tY`wtfCxcBLa1$vpT^Za*7M z6=u!!$ifQxzR$YVSpTtzQ@o{bcygd7Mnq#+ZTeTaaF=8MtTz1eLcFsy-GK8aZz*4i3v3N_~Do1G#|K>k@s4E9==+6c9*f~C(6*Nhi-miuD{SgI`MJ4a9 z{D%m@@8_p$og672+q1d^1^2g%yaqzsb}j}VLf-2t?fgf~_&=e{u`K~}ylU+tC7Roj zNm7z$)-S-XYcs?ba6bFp%j{Z|wfKERo_jE-l%>%3y}vgr(2~d=fZ4tMc+~t;5aWO@ z-kXqSEvfmD!0Qy9^4)IiWB@z^DyV|^IHgi*zDr=x*64p;>es(iYVDCEi{UxhqX6#nf_JtuEJI=aa8`qbth z4it_l2$(;;`VQ#ofA<1E1SX(|;o!n7k|q80pDyA9-(mX2GhlNi<}W+32Sn!;AuZ4T za8YG6h;;d^%QoNNQ?k$J!NbZ)3O3g^R~NFz${-w$7X1BY{r3eLso-Hf6RbjK|L{28 z6c95jGx7X#dj4_s5MpZpd}2P72^2RreWzoZAh2eC%J5Uk{PUJG7?6;er(WCl2ko&d zh!aHL;h*+wx@BOayy3FFZB)do2YCZ_NB!?01{KH_QJJ@-lIjNp({R zJ;l3=W}8Hn<5MBR5M|X`zGucqVyT2)E3wjl_ku5DV{q$*-$?wo3mjg}bV~d0-tq4Pk)i{R%D*@I-?2tVW9#lo2t%rSZ%QSo!rh#j zuH1GY0yHA*w_q)R5~bVNdF?y%b3f22MF83g%<5j9X~BoSs)0uRtS0M4EKk5nQuTi8y>h>H|5Z<>k6@vklWwiwz_g{qR zha`KefgAfEUdn?T4_yxk+EJVKED!-z6|n#aeQuOeo2bDFzTfrewV|SguTM}nZ|Np? zxfAVKSy%W!rpj$N4l_kS1y@kj#-MM-)AFA=YYu#*49!Y&rgwozPR4;$NJ?cVb_=pD zpY-|`S~55C*ZFLn@rt!o@lLhgw$hapi}kYoTpw76B8@^&aIJi{M;R?$zWc)44|I97 z$jhopi_lvi51>UZhRMq>B@3d*5Y1bTux&6S;=oRe=X!=g+tRt+q9I-vU6+Ol8pnqG zc>NW&XZZ1;Ia}%KIQ-nI&Hc`ec4&+y1pkq+npyMX&5ZX31nuR!Pd+A^$PPY*W!J)q ziF>LVSJxkI-b=uO(!RI*sV|{y4usc`=~z{ z-og7oIZn%YNhg?wUOmbQ?n+L}LjycMRQcytIO%2PH62GXIBMY3z)u40O-Wtel&qxS zv8|i?M3rU0Z^HRZFM>fGMLi|ZwlDYYlsU&>1>uSgk16wtrwv2Mb-0YNXr)JK2*4tl);`9)lK8G+MP zdt)@uwCo!j4<}T7+lSZNRv`lxU_c4Jq)`m|6GlE)fJJ{*Xa~xE=fMa0l%rR&K@~0Z zC<}6-Pbdu=8^8%rS2hJf83OjJUw0n+sds7D&z)<1d23Sxp)* z-}_+*7kK%-1(7!15fss}8cG*iB?qE5UlBI2BAg~;WB0fV+i$a(bbxR{^!3Xh$ixyq zM=z}Y$gW~qe~v`Y+r{6)lty-yvyXNHJR;UodOC(_FLJ3FeNrfFY@t6S$Aknh81Q=q zK)pxhHHc@#14$lu6|TzFA;QKw2thMisMN6{xGvyR1{8bKaZ_22h>Q~+U3biZ=v+`k zjA&9Q&`u}?$EgSA_;gcjWKpNMDZAL)B>>zF3mQFiixP%ptWD)%DMk=@SQl^(*L z$U$TUw-Z6fiV_Vz^I`OxY0xQ_#YzibtkWiFvKcHlcwjUsgPXytX-)@-=~vpzrM|htv)fD$8>j>HhGUjd%6nfzUgFKw71CO{B((#deY*X1 zMz$W^?PNjLUbl8D+hQNRr8-aOE$i#R{l=?wH z`I74?AM9v8D({Q!FFM&@tA=z=5TCY`q3RX`Jg-fA^rU_NQ;}>LoR@P@W)iA53sH`t z4e#KfEa9wmy@!)25wEhSYjZ^vSqC&xgs|DwIg;NR)7Os|r2$Syd*3yI+iziNdH5a2 z?QV>}>klsJG49O%`Jx+CGHr??aMy0;N+G*wZ^B;k)8ugaE%&of^5bOI7V&YZ?7kA9 z}ZXYX*51eC7Zd=SBw2gWrW{-tFM>^axsu$G+%YP2s6O_B+4%kb?A7=oFQ-hA~UPu&gcA3=m1H?!`kdpNt5`iTTZ` zSTjp$qX8xi2GhIufzUQMgQOR>i1_Ns#>$|XTWFHPKBko`{6fyZa)AcI_+bzDEmS+# z^VI~!p7C5R9~^BxfEc{7J{^f3uUMj461T@brl#@!&x=Td7{|bMHE)gXi}d!I7bh>%F92cJ zfbf1{p_cj%#RJ!_(j_LJZM37+pK$B(S7o^!HgE}&S?)WKRET5p zVN`DqdW3z#xIMZF0UAm^rPNH7ZUq)0SL~DT11QM!+KCG-rha zOp44`X4`U3_iSe{xDhL~KhNfZNgw&*i?qmhJ6TlZQf8W;DB-pU zFo2q=jF|7Aj$4YUQVPZ9$BIZsQ;X3B8WNE$9NA;Z=^l9vwnibAg^qPVd5K^xO%t(X zzg6yh2jXb6cO1USPNH;7@3$tq^3CpJeIk`o=sguO3-1ru?3pMgCwjk^WWWi;I*<_a zSuYHmmmw=O`#l6X11QrbIMuuq%bz72^5KNs_B`uy+#IY`RkSZ~D#%hYDR@L5P#t2q zGBP!Cck~=mIH3+ansV<3C}C&h+OVBsAljgnz!-^|-3Acfqu!yS%}*@$y$`fwOrBR; zn^BvU)U~w*AbW1~>P#vH4`C9+3T7eC#2NDcpfp=BtsNJt21Wx$seaQV1MGJ0HyY{{ znu@>&JYKa*b)YlY2s3$~r$uPH%hpwpdghLi6Lh;)TXGMi=ad%fEu}U2rHm8tONM-^ z-MR8QoiR>YU{`Mi_zHiUPvvynm8!fmp435@yoh$w)#fRJnoi{r?|T7@pXmz!`_m6ke2K?o5NQqIOJ7imn7yIX zDc=Mx)=;!G)|78{GNla&jNi=hzS&`&nlNH{n}UfU2n+z_HSyVs(!rc0=2-Z|P&y|z z4F3Wg+&pBk5Pm=yU7#yda0llCIglfy2VKH!(y;9nz}yR}wGu~B-j3z7gaV!^Gt7rj z&M4E>$7;SW&pIg|2X?{NXWE@ zae^Bmy~f_qLpX<%_JK<86qYAv&9K>bHNQxvf@krohK2r(4GuOy`bW>TciQZ1m|!Qr zyecqq=EV(Ul%ab^$E}QJBu=$^lw?PIQUn{)7|dq|6o{(j&wPMEj#xIExh6lmf~{Cq zDsSwvCQb!E&7LkF6D?*_ew6u|_>J4L{oAneIeDW@AXA1e=Xww8Mn=<6ko zH#d{#o`UppHtQh29M4uAno@&&ykIor{b_)X&&(#FrU!42gO*F9%O7@69=P{ZxHGM= zAGkK0#qE*mXYf8f(>A>X<^Gs4m(hH-OXk5S@mE>J=Mu!tv&!=Y2EY*1*Rxj4!#Z$) zv88=h{0Ob?jWTrCo&DTt;OJR>Tt2G}<>Yj4pKuzhO|D^~8r9{FxVwvZmX+p#BH9!= zV^tY=?4e3nA-yNLYN{(Duusizso8c2&2!|eY~92xlWnGY2pa#^*Xz_JByyha138Zs z&k^ViED>DIug8nrTFDbaPdk{$1(&{d4%LK=xh>k~hvb>|iY5*CHhg=x=v;k8an)>4 z;5?c$hGy|9kGJ~1lOezvVGE(%|!Z`(Gyo&jaG5sXA>;E>d_PPqNP_<(h-^Jm{`&febi- zGu$+BI6?q%hj6GK#jBA+d!LgmGdFPGx5uGkWiBb2E^`&XwyZXmVS#%;8aID6ID9xt zglL!7+2gz4;$pjRI_r)c#F#CGY*ko1S+$3PI`gF`!q?>MHkJk1+0Wj&>H16j=oK-C zvN*R}3O7G*qq%(M8L8h__YC~Z=d{i_^l}jq*r=}^tMdxeq(I(%=Gq`+VS2>&x}XsV58c@o z@qq}<*whhwO#CyRyisu8S(BosCsi zrEf3fobBt7lBK|caJ~LH8lMvA5fdef#<5V_YvjxFQfx7Yr}_@~z4HZu@`arzoMcsH z`ob{vs8dsmoxXj2z>Cai7PO?bToPqliRN)O%lEolMeD2F3xf44--1u zk(n(Qw?252WBAFGhMwWtmgq*Ft+(!G63~H5oH2Jr*;t)5A>22i$>V_3%nd|*(?>M5 zi#+nVZ|Q@C+^W8p}HW%A8QLcCGgQg;Od=3EW=SKVC`b|K$MrfH(hh}3Ho%Z znbGot>D}^(KpVbx&@)Nt4_d&w#=~%(GAz@{AtyLkkLjn%XGy5`q9MFI2t*th7vn29 zWBw{A_*P7dhA}i(q-)FHwtQtF?V?YWyksTa94GB46-)&h`BWa~*C*1LYTrsxn|J9E6aXK~lwr<~1Kk{KxWJ(N_Qj|@l} zo(8iYOk2F4WKqq&y^}L;-#%3u!Rg`%km{jsqV!?pV$}-E>ha{2^)5rnBPKa|HPUH#6W0jK$Hzf~B1FV)7|Fpou7y*d*NwL77xE(t97C|Dp{+ z1sDI76ZTa+Qu$IWf2q4Lv4ABB!Sz0#gV-?cDW=*?W{iwRt0t%WrM5{b7)R6cKaoSK z@!u7$U?@hDL38c8tugil$?$68ydKE01tcD^l7D6XS8bq9*1&|{n$IFQ=-RpatrX?6 z2JU*umD|Ok1C@2RO@degX?WZbm>g@V4=1|u-|5dTHjn(t7deSZ%*aVdBh( zpwdMJm@Q~9gfMc?+Uh(ia~kCgl3#5#xK4x@_j?Wc!wvCb0%}hq2-8859Ul`c_LXFY zw;_X|?wzFIXKezJ<}{UaLb5z1Lod~dh0<2cULv8U2A~1J9JFudXg(}F=@_me%&Dtc zz}IekZ{KT5dTOGc0#l)bPwP$X))o4_!tXDoaISzXncn3Ff<5Ga5+3&VsJ)3cH2^uV zq{33rpip<6BSDKUYS1@kMDWr81bK$Q>FPua#g-rqWzu;Nf^t72;WNErNF8&W_ZdNn z_h*_EWI3uic*iPh>Xsec1qQUauMJi?AUjjHQ?zPSsX|1zJik19o}NJ(E5(YUBnJD| z$CYpP)DM%DXksHGZ|SA)un8CH1l_rWr%LA{gwpxF9h$B(WL?s|n;i5STTDkY@cjUC z@}4-89q5hEXqII^RtmC3b*UF;q2fn)RQ{j_5M}Oo?iNqdZetTQOL1^eYc9D#R zlT3%PpdT(DN^;G%L)kPN4Pj7MUW zZ}y@%Pvngo5#p)9hc+q%P?DvG0w(RC<#2{cld|O)jdHRXcv>?SkZYzO8DB&{=MHEGzB>GJz9h>3Klv_4cb_0AR65D= z;&vx``s4XEzR+PlC>97^8@k5q+(7&4yCP^Xlb!V6w*T0XlV*Q40=!LIFJGT&f9x24 zC-+?5o@Kf!3)gQzO;M+k00-vks|1)hbJm>Fr*q@fKW|92krW7^~9GVlSJoy zhmYZG4cCv}7!uEJi!1WrjF@Ih&q#tsCpE~YrgKg{8&p@M)7fSkS9f$}%+W7{b(uem zO;g65i*XpG?%yHY9AJ^(trZ0&1cbEg2y~;a*~=<*OWupbs9~u%p>aAUi$IgRncp!h z;ijrH#}*GfS~lZqqJ4}LlW65JF|VhS6mIr(@8tGF3(%cQI#DwPx-*tco^iV)bUW^6BVs-gGVdbR(2Z9CMpLde+5|9SSUZ>?0)iu0Jg zJL~eGYsKT;+=5Na{}sc*0$G}u7O}N&8xpqg2WWw25=Ir2`u)7~F?0eibZo8tx|Z}@ zWc)@YS~>dkM5ts@dMSCD+>ZO!Dl2rm3v>9|%Lv|aYyP@!tvB`2u}M^GkD{7dF@b*; zy`bD)mj? zp z4qzxtB~-1=v(B=>0ReV-b$=srgTekqnFnBw0$hySd^(W8_1pXNBA~x_Ek9Uce!jtR z`FRDf2?#LIg+dwE^xE_`TY#^lqbR7Wb|} z&DQ5hzJ>FaN-eP)GX7SyM{UO6{io{L54!*c)v=k7KzoqB14y;!eo;Ih2@00XU7j{Z zpol8#2wMin`#jKC3Q!Tc4is2!PIdevLA?S&SJtFkXu#*z3j{#5DUd*|NPhvpEAt~I zx}uEAKUh=z(^r0IxI8ntVYkm1;5Q!X2@yfRKlvy3!EtA_0Te zu|7dox>|r=qS=$FL&PI$4ir>>{kC#;@D2qh)mgx$GXb*Phf8(3b}}db_3H-wy`y{Y zSKhtcr1*4Q(7JxFPVqhm625gz2lodA+_C-OG&#-W-TFg4c5fO`#t7wr zfdP5&6DygKzkAvL%HcLHfJNF281kQc2lO2T!mA)-%eLWLet8@Kn1Dv9d{dz(zb@YT z=NgMb-hLBa4|w~V-Ra-u`FDAKFVX*RO3#}{|H2GtywbL&{7cp~vaS#04<6&)issoy zcoBQ~H|LrY8!4ZuV4JZJ@WZqwRj6dp5b~xd^bjQzf+BI;9)(Z z3rWTC zvH@H1S1p8N+H_C60f@9MZsp~zKV4J;ChWLS0i%!98x-w4R@ z%-gTIabm){i?V>8ytQwBy*t1D!80-7S$J_}w?AE!zxzVVQsv6q?NHOhTlh@f3D|`< zl{7B-aQBhq)#y0)dOm;V&2O*<97H$I^RBzY#iiOzwAFCQvZ<4q;fPf7;8aU^iBGD# z@(Aoi(E1nqMMq8^0BV}3{{j}~O!StC6AB@^e|OqX8!MYRD?D%7C5fsQ9xS#mCoOfJ zh(~^Lw;t*ACZ;ooyUXp|+(6`meOTT4g^69LPs4!)!g*eGA=u6ec!^kylm9k({(X@3fq@_qlx36|F|b8rI_ zI8|*rw$X9C=fT)%T{+AkM`6|rxM_C}gO_9lH7U;h%4^ulpen9sQ(+ExyZwRmq^(>s zyX+E3H5*NZhuyFQiAH99{CSrP2m8UW&UX-)@j`y1oH>{-m0dLaFc7I?U4@Yx|G0WA zgV_t7Y{&^)(a`Yw<4Rm+S=gQrTcP7-tGv$J94HvRZ`?Cv&)L1U$Z-((sW*G1$b3al z@*u*fU=>u3I}@TNw7rYz67JI&FAC<3Z<_hLaY$c|A>6uc;RfgImNhRnXeLlZLB|il&p!mqKkxCv< za3L*H{x)t*-{vco2cY!^KmzZ$(C%#5@=;y0(8nxJ(qm2etZsO_Vd9N)f7~6>$4o9AcanjEe zJVfBA7svR%DgqHI6*Ky!vF`bp zR4@ZHWn+r}=h=+j9q{77GE)n3Uv%u!Jk?&{Wlph*&k~=%{RpXR5Rv@z%8f1$Xp-xl zk9LrY8MNej%j{zpU~$Eu(CRK`(0#Prdm@tHE;s>Db`!JVOq)1o0hj1_@y1GyyDjv1 z2fud{oHBaNUv2oX|Naw%Mvb zpQlwG?(jkI!);vwoEF6>U=gJMaQji6=a~S#X8z(n`@~ptpc!uL-I1X_hPWk>A}NEL zH~%q!>+i+bam9FwE^g~q#z6}M(9<{Yrg}f+FXP8-QRmjav#hBDv3rtTIFbBx6`-4= z^6=87nE-zap_C?)U#IWc)DJVR0_q1Ajzd0#^8FCc>i`)1%i{;y)#{+MbZ&zk)I1fS&O5hXds6Kw45Tm%e?$%?^t=dwKhCQ)u9vIhcda`2(w5PLNyxsdz1b!-|@E ze*pRBY!J0_r*vak%lA2O+!Df{lNo)!9r0PNtx=32TnB92=&?L$M2ydH2 z;a8jhc>0TY-$4Nd;o3WN`Q~*vFuQP;6))nEJHZ+v$&a0h1vqE#j{gkUS}BAH&@V~# z!PL${xEJ=76X9-^?4#6`2|pR<&tQ)B__USF6ue@!%OnZ%ndo#B~tYaXDvvXVxH zyPT@kZ~8)&WB?l|a0ZMFA>Psl=-Sz4e6adG8c{~x!_EN6^0bpZX>4^7y;i&oN+jH% z*i)U>HgzfqCQ#z|nx+V3*$A?HGx)Og5|IS3AQ9s-Q=+`MV{ z(-QuDfd2j$dXS=3iMCs2=ASiB`J()&O^>dV+4=KnH^o{4*+ph6oG^%OiQ@r116a_A zatn}wNZaHwln-5V zy~A9=2%8{8&`ElAvRsG%z71#_W$8e@aRdmUp^sb{^qZeYIhvH2_=9buA`KC70DP$; zHu|Qz4w_Fz*J7^NKoI(_7ZN74M%O+D5U8`mLEj`+W}0n{-?iS-e`>eXk}6%W<1TEQ z2bdTb+As9}HvO*P(aH9NAUi6Ujn}!PRbBy|XJ~q96j>iF3JxBnBp?evfF!K?d=f}( zT6I;ER&Aee3PHMRwWI#BSZadGIkTzdUtuaxF^JA_bNk(R0H~7d0;+G{Q-NTC8f0JaApTg6bCXh5JKbOrkv69MI4B?maw(vwyJ+`la9#)K`Ls%O~m z2ox7yb9Ui;<{P=1CYv_6A7b$vhm3BK?Ft#vA!Axm>6R&{$H-xaBC}Yjv4v)WUp-mr z1f)hl)v?=dd~;xZV>5V5&Y?tQuVPTnjQ|j+q-B7#&3@7Km?KdK02WyyA@HpE$0ORA zmI#1js)I0|xjiHRi4f5U51Nu#UFgT!7j@sdrE4A;N)`cNObB}0($6+o3Yst|vHlQ_ z)7DBfw@+r=4irb3?`0%M>AglY>z-T#M<~a3N5hmHmY$Ig&UuobUwK@U9|UQfc1`%p85F+LPk%iQ5E{-vwYRm2tB_+D zjv9IP>B=7tK4B;9!JkY9lZtuAZ^Z!YtHs>y>qJmK#XEyB`zQ$SBX_g@aOUsJJJ3;k z4hP|Vc)KRRv7zhK6_{^A7AhB5jHlwxC;dQ=%SAAUGDkj925P^6pt)^>$GrgmH=~%x zMEQIa(4lY2#@%v`11K@^Y!?**_=1)fcnjy{yhNjvEeWGxO?Pz9^KR|e82}Z@b3k#k zeDe0f$gpGtA{2nlNNaQiHT;WU2^vyUAL3Vi{*&R*KO!jD25s6fWEiRK~ z{IqVMw(tb>h9}Ov^8N(wlw$ypssr13JTG}si6m*Iiwyv6IW@rJS8M>PX-RTL%FT;` z9Fitxb$9?jwT(h>P#$OYql92JT#S8f6t+HFMcW~i+Y0}eDS%LbU$6YpHulq_{8cL~ z>4HT2<8suN3#?PYl2f0jAUpS?n;a@U{J?>5gT0&rJWG*48VeKoD2wI^WtnYl>1TII zE)69q$!{$T&c)84b+HpO&1yNC?u8*|?W71$3rUF|#Ra!=!cV6vTlEDAVk3seW_GGo ze{Gy8*WXO98^3|JvR!BXl1 zN}CV4pswOBICb{d1^HWl|2PKES!j=X+C9xvAZuTB8CkB67t50bqlDdPyC7Tv(Ah?} zz<67}451!mMrLkZG_?TJhztTQSWCJneU-{B5W6YV0TO53C_S6>uq`=lSWu@nRN!d1 zF4e!LYWp$N2W(K*vk1PPh?g$Txzl)=iU-e!*CHTTg2uLrHBwLc zfc(>|>RYlT1H^c@S_FJJ7JX{kWUue{;{neOe(NY#DrfT$zpl-8^{c*7RDD)^TZ0s3 z;mJj9X}{I7aS!iA?=SPq37^8l!*AM%i;C(@gy@#quKV6!BJ=>%p@pScig@+sq8*8e zi4Mzat8k33SBo3**6_Xx`;Udu=$wU7(kSt3F7PV*svouziyXCKhD9B8x1|m@I=JM> zNX@TM=P4Cg3!re-TIw-zi$w;x++6WxWfP|#NT{C{@VphgYD&Cl>H>|an1S=^dBn|L zaVrh=l+(KdSJT#)_19lssR1&(N@Haun?ICx4otl-VzAeu-Bb|#|M%Wzxqd_%Y(P$0 ze|=QZcjuDT;Nx(sGn+oWHx#H1g{oq^c^WER*q9P?>BJj}O)G4E&gwv7b9T-6q(}k7QnF3{ zxGv8ozdfc|c^tVmbFojkDi({y@=1tIzV=&PE*2LT*Q0vaIl14t!>4TS(b{*h!lcvk z#ZLpXZl{cO0zU3U?ptEwU0Gf6X}1V2nFQ2hS-~M8Z9X&=qbpvokjj*y=@#`#k&$NW z&8C*susso|LtjRXb)H18tdHT?&(`y9UUDbGc&Gk&d#|#%}+4H9!RA0 z6&XDR8!Md-OD-A&f!6PU%z=hY7nZnZ;W*MSPAJ;?#x#&@x5*iBKw2}ja|*THKCbOP zdkTB}4vcy`zT8B&s$%Zj;Gl&RPt9O^$itD((=Fnw3?Bd+lw;yF$n8jt# z3V3B)c+osNJ3G>r1J3{_CUYt>%&VWrk2osR1rzy&!e^l-@ZNWUp)=24j zegQEQC0(D)64w$+PIKgRN80jj`W3Y9oNtCIc`hjz27!eV7OJPb4McR%%bGJRNR1z| zn`|xWxt$=@*Dg3}d*dj@NMNi&?9F~L zat&&%R4j~*NHI?= z#ouozW@#4k$SMjc7cJcW`H^aakOPWGJa{!y(naW+-@14 z7^f+d0)5~~kv|9ubdSq3*;~lL*88Q@jWx+CWT`u1AsF9rijS)a_oiTh2Oz0x-_HJ` z;lT~Opo&Z)tTSiR6ru%>lPL9w!&}+YjYoA-Hh-C2c4v9#_u2rqrb=d!k-x5Qwt$d8 z6&}2Hv=#9Ln`k~V832#?>->yQl;tO$P2XYH1;yUUNv8w_T)|M^whHTZ2D{^%N)V_! z8SYk|P7}lU3%knQYNSPUgaik>cSy`{o)$QG>P^G*ScUsZjI9P%4-PEfJ>C+Cps6U9 zyhYeJ7x(9mwzCBE_xJnVN4o4;4pw-@{S}A9iH}lz?D zCKh*y2kR6GZJt=#HEQ&{qj`oc@0%(I2S-M8bMtI81Kt@aUWE3jcsVvMYS!CE_e6Z( zvUhe(c216V$MG4rQAyU#7oswEJ5!gr-KPluoEM~-eHDY6E_BC*C-+!Xl~$2m-aP4I z|MsrZQ8gsK;O9qeJWGRY&wM%cT~Y&)`&Ls-;K4#qMuc3_%-Sn+h8XV=vz~s|_S#K- zeS1?;+jY?M+VibOc_|OT4VGNUQm#3GnSmhy-pI6MUVj|nRm~GXa`kgCs0i6q)yNv3 z2nh5lnm1G1w-x05wOShk;HS0`vrfZ9ghnZg%XXWqy@RKm^q((k1gqUEM<$-EK#VD^ zN&5C`cX68Ct2jwNbF=CXbAl$H$=<67jv?ZiSxQ#f8pf{nirnY^df1>>`^cXb{W48+ zQ2O+^IKjo`*gX5>`9Z^84`zz*AAYhWAawhF1*5WpKgh;Goe~i24XgPt{lQOq0CF1p tx}~4|Vc^-nxA|=n|HmQv|8#9C;fn<>ga&7J_buSh^{c8^vgM5L{x45gzsLXp literal 0 HcmV?d00001 diff --git a/caravel/assets/javascripts/dashboard.js b/caravel/assets/javascripts/dashboard.jsx similarity index 56% rename from caravel/assets/javascripts/dashboard.js rename to caravel/assets/javascripts/dashboard.jsx index 89b96f324d1d..3d050d8877a1 100644 --- a/caravel/assets/javascripts/dashboard.js +++ b/caravel/assets/javascripts/dashboard.jsx @@ -4,18 +4,193 @@ var px = require('./modules/caravel.js'); var d3 = require('d3'); var showModal = require('./modules/utils.js').showModal; require('bootstrap'); +import React from 'react'; +import { render } from 'react-dom'; var ace = require('brace'); require('brace/mode/css'); require('brace/theme/crimson_editor'); require('./caravel-select2.js'); -require('../node_modules/gridster/dist/jquery.gridster.min.css'); -require('../node_modules/gridster/dist/jquery.gridster.min.js'); +require('../node_modules/react-grid-layout/css/styles.css'); +require('../node_modules/react-resizable/css/styles.css'); require('../stylesheets/dashboard.css'); +import { Responsive, WidthProvider } from "react-grid-layout"; +const ResponsiveReactGridLayout = WidthProvider(Responsive); + +class SliceCell extends React.Component { + render() { + const slice = this.props.slice, + createMarkup = function () { + return { __html: slice.description_markeddown }; + }; + + return ( +

+
+
+
+ {slice.slice_name} +
+
+ +
+ {slice.description ? + + + + : ""} + + + + + + + + + +
+
+ +
+
+
+
+
+ +
+ loading +
+
+
+
+ ); + } +} + +class GridLayout extends React.Component { + removeSlice(sliceId) { + $('[data-toggle="tooltip"]').tooltip("hide"); + this.setState({ + layout: this.state.layout.filter(function (reactPos) { + return reactPos.i !== String(sliceId); + }), + slices: this.state.slices.filter(function (slice) { + return slice.slice_id !== sliceId; + }), + sliceElements: this.state.sliceElements.filter(function (sliceElement) { + return sliceElement.key !== String(sliceId); + }) + }); + } + + onResizeStop(layout, oldItem, newItem) { + if (oldItem.w != newItem.w || oldItem.h != newItem.h) { + this.setState({ + layout: layout + }, function () { + this.props.dashboard.getSlice(newItem.i).resize(); + }); + } + } + + onDragStop(layout) { + this.setState({ + layout: layout + }); + } + + serialize() { + return this.state.layout.map(function (reactPos) { + return { + slice_id: reactPos.i, + col: reactPos.x + 1, + row: reactPos.y, + size_x: reactPos.w, + size_y: reactPos.h + }; + }); + } + + componentWillMount() { + var layout = [], + sliceElements = []; + + this.props.slices.forEach(function (slice, index) { + var pos = this.props.posDict[slice.slice_id]; + if (!pos) { + pos = { + col: (index * 4 + 1) % 12, + row: Math.floor((index) / 3) * 4, + size_x: 4, + size_y: 4 + }; + } + + sliceElements.push( +
+ +
+ ); + + layout.push({ + i: String(slice.slice_id), + x: pos.col - 1, + y: pos.row, + w: pos.size_x, + minW: 2, + h: pos.size_y + }); + }, this); + + this.setState({ + layout: layout, + sliceElements: sliceElements, + slices: this.props.slices + }); + } + + render() { + return ( + + {this.state.sliceElements} + + ); + } +} + var Dashboard = function (dashboardData) { + var reactGridLayout; + var dashboard = $.extend(dashboardData, { filters: {}, init: function () { @@ -39,6 +214,7 @@ var Dashboard = function (dashboardData) { this.slices = sliceObjects; this.refreshTimer = null; this.startPeriodicRender(0); + this.bindResizeToWindowResize(); }, setFilter: function (slice_id, col, vals) { this.addFilter(slice_id, col, vals, false); @@ -61,6 +237,18 @@ var Dashboard = function (dashboardData) { // Returns a list of human readable active filters return JSON.stringify(this.filters, null, 4); }, + bindResizeToWindowResize: function () { + var resizeTimer; + var dash = this; + $(window).on('resize', function (e) { + clearTimeout(resizeTimer); + resizeTimer = setTimeout(function () { + dash.slices.forEach(function (slice) { + slice.resize(); + }); + }, 500); + }); + }, stopPeriodicRender: function () { if (this.refreshTimer) { clearTimeout(this.refreshTimer); @@ -128,30 +316,17 @@ var Dashboard = function (dashboardData) { } }, initDashboardView: function () { + var posDict = {} + this.position_json.forEach(function (position) { + posDict[position.slice_id] = position; + }); + + reactGridLayout = render( + , + document.getElementById("grid-container") + ); + dashboard = this; - var gridster = $(".gridster ul").gridster({ - autogrow_cols: true, - widget_margins: [10, 10], - widget_base_dimensions: [95, 95], - draggable: { - handle: '.drag' - }, - resize: { - enabled: true, - stop: function (e, ui, element) { - dashboard.getSlice($(element).attr('slice_id')).resize(); - } - }, - serialize_params: function (_w, wgd) { - return { - slice_id: $(_w).attr('slice_id'), - col: wgd.col, - row: wgd.row, - size_x: wgd.size_x, - size_y: wgd.size_y - }; - } - }).data('gridster'); // Displaying widget controls on hover $('.chart-header').hover( @@ -162,18 +337,18 @@ var Dashboard = function (dashboardData) { $(this).find('.chart-controls').fadeOut(300); } ); - $("div.gridster").css('visibility', 'visible'); + $("div.grid-container").css('visibility', 'visible'); $("#savedash").click(function () { var expanded_slices = {}; $.each($(".slice_info"), function (i, d) { var widget = $(this).parents('.widget'); var slice_description = widget.find('.slice_description'); if (slice_description.is(":visible")) { - expanded_slices[$(d).attr('slice_id')] = true; + expanded_slices[$(widget).attr('data-slice-id')] = true; } }); var data = { - positions: gridster.serialize(), + positions: reactGridLayout.serialize(), css: editor.getValue(), expanded_slices: expanded_slices }; @@ -236,12 +411,8 @@ var Dashboard = function (dashboardData) { slice.render(true); }); }); - $("a.remove-chart").click(function () { - var li = $(this).parents("li"); - gridster.remove_widget(li); - }); - $("li.widget").click(function (e) { + $("div.widget").click(function (e) { var $this = $(this); var $target = $(e.target); diff --git a/caravel/assets/javascripts/explore.js b/caravel/assets/javascripts/explore.js index 55c71b90e0c8..fe64c85e59fd 100644 --- a/caravel/assets/javascripts/explore.js +++ b/caravel/assets/javascripts/explore.js @@ -237,7 +237,8 @@ function initExploreView() { function set_filters() { for (var i = 1; i < 10; i++) { var eq = px.getParam("flt_eq_" + i); - if (eq !== '') { + var col = px.getParam("flt_col_" + i); + if (eq !== '' && col !== '') { add_filter(i); } } diff --git a/caravel/assets/package.json b/caravel/assets/package.json index e6f5e0188cfd..07a41fa15470 100644 --- a/caravel/assets/package.json +++ b/caravel/assets/package.json @@ -65,6 +65,8 @@ "react": "^0.14.7", "react-bootstrap": "^0.28.3", "react-dom": "^0.14.7", + "react-grid-layout": "^0.12.3", + "react-resizable": "^1.3.3", "select2": "3.5", "select2-bootstrap-css": "^1.4.6", "style-loader": "^0.13.0", diff --git a/caravel/assets/stylesheets/caravel.css b/caravel/assets/stylesheets/caravel.css index 05614ecd6365..b30d868f1a75 100644 --- a/caravel/assets/stylesheets/caravel.css +++ b/caravel/assets/stylesheets/caravel.css @@ -170,12 +170,12 @@ li.widget:hover { z-index: 1000; } -li.widget .chart-header { +div.widget .chart-header { padding: 5px; background-color: #f1f1f1; } -li.widget .chart-header a { +div.widget .chart-header a { margin-left: 5px; } @@ -183,16 +183,18 @@ li.widget .chart-header a { display: none; } -li.widget .chart-controls { +div.widget .chart-controls { + background-clip: content-box; background-color: #f1f1f1; position: absolute; right: 0; left: 0; - padding: 0px 5px; + top: 5px; + padding: 5px 5px; opacity: 0.75; display: none; } -li.widget .slice_container { +div.widget .slice_container { overflow: auto; } diff --git a/caravel/assets/stylesheets/dashboard.css b/caravel/assets/stylesheets/dashboard.css index e9376487fd54..3a77cf0c71c9 100644 --- a/caravel/assets/stylesheets/dashboard.css +++ b/caravel/assets/stylesheets/dashboard.css @@ -4,23 +4,22 @@ .dashboard i.drag { cursor: move !important; } -.dashboard .gridster .preview-holder { +.dashboard .slice-grid .preview-holder { z-index: 1; position: absolute; background-color: #AAA; border-color: #AAA; opacity: 0.3; } -.gridster li.widget{ - list-style-type: none; + +.slice-grid div.widget{ border-radius: 0; - margin: 5px; border: 1px solid #ccc; box-shadow: 2px 1px 5px -2px #aaa; background-color: #fff; } -.dashboard .gridster .dragging, -.dashboard .gridster .resizing { +.dashboard .slice-grid .dragging, +.dashboard .slice-grid .resizing { opacity: 0.5; } .dashboard img.loading { diff --git a/caravel/assets/visualizations/heatmap.js b/caravel/assets/visualizations/heatmap.js index 55ec7ef46efd..18aa2eea3fb9 100644 --- a/caravel/assets/visualizations/heatmap.js +++ b/caravel/assets/visualizations/heatmap.js @@ -21,6 +21,7 @@ function heatmapVis(slice) { }; d3.json(slice.jsonEndpoint(), function (error, payload) { + slice.container.html(''); var matrix = {}; if (error) { slice.error(error.responseText); diff --git a/caravel/assets/visualizations/nvd3_vis.js b/caravel/assets/visualizations/nvd3_vis.js index 71de1a0f5219..a6ffe54b2025 100644 --- a/caravel/assets/visualizations/nvd3_vis.js +++ b/caravel/assets/visualizations/nvd3_vis.js @@ -14,222 +14,222 @@ function nvd3Vis(slice) { var render = function () { $.getJSON(slice.jsonEndpoint(), function (payload) { - var fd = payload.form_data; - var viz_type = fd.viz_type; - var f = d3.format('.3s'); - - nv.addGraph(function () { - if (!chart) { - switch (viz_type) { - case 'line': - if (fd.show_brush) { - chart = nv.models.lineWithFocusChart(); - chart.lines2.xScale(d3.time.scale.utc()); - chart.x2Axis - .showMaxMin(fd.x_axis_showminmax) - .staggerLabels(false); - } else { - chart = nv.models.lineChart(); - } - // To alter the tooltip header - // chart.interactiveLayer.tooltip.headerFormatter(function(){return '';}); - chart.xScale(d3.time.scale.utc()); - chart.interpolate(fd.line_interpolation); - chart.xAxis - .showMaxMin(fd.x_axis_showminmax) - .staggerLabels(false); - break; - - case 'bar': - chart = nv.models.multiBarChart() - .showControls(true) - .groupSpacing(0.1); - - chart.xAxis - .showMaxMin(false) - .staggerLabels(true); - - chart.stacked(fd.bar_stacked); - break; - - case 'dist_bar': - chart = nv.models.multiBarChart() - .showControls(true) //Allow user to switch between 'Grouped' and 'Stacked' mode. - .reduceXTicks(false) - .rotateLabels(45) - .groupSpacing(0.1); //Distance between each group of bars. - - chart.xAxis - .showMaxMin(false); - - chart.stacked(fd.bar_stacked); - break; - - case 'pie': - chart = nv.models.pieChart(); - colorKey = 'x'; - chart.valueFormat(f); - if (fd.donut) { - chart.donut(true); - chart.labelsOutside(true); - } - chart.labelsOutside(true); - chart.cornerRadius(true); - break; - - case 'column': - chart = nv.models.multiBarChart() - .reduceXTicks(false) - .rotateLabels(45); - break; - - case 'compare': - chart = nv.models.cumulativeLineChart(); - chart.xScale(d3.time.scale.utc()); - chart.xAxis - .showMaxMin(false) - .staggerLabels(true); - break; - - case 'bubble': - var row = function (col1, col2) { - return "" + col1 + "" + col2 + ""; - }; - chart = nv.models.scatterChart(); - chart.showDistX(true); - chart.showDistY(true); - chart.tooltip.contentGenerator(function (obj) { - var p = obj.point; - var s = ""; - s += ''; - s += row(fd.x, f(p.x)); - s += row(fd.y, f(p.y)); - s += row(fd.size, f(p.size)); - s += "
' + p[fd.entity] + ' (' + p.group + ')
"; - return s; - }); - chart.pointRange([5, fd.max_bubble_size * fd.max_bubble_size]); - break; - - case 'area': - chart = nv.models.stackedAreaChart(); - chart.style(fd.stacked_style); - chart.xScale(d3.time.scale.utc()); - chart.xAxis - .showMaxMin(false) - .staggerLabels(true); - break; - - case 'box_plot': - colorKey = 'label'; - chart = nv.models.boxPlotChart(); - chart.x(function (d) { - return d.label; - }); - chart.staggerLabels(true); - chart.maxBoxWidth(75); // prevent boxes from being incredibly wide - break; - - default: - throw new Error("Unrecognized visualization for nvd3" + viz_type); + var fd = payload.form_data; + var viz_type = fd.viz_type; + var f = d3.format('.3s'); + slice.container.html(''); + + nv.addGraph(function () { + switch (viz_type) { + case 'line': + if (fd.show_brush) { + chart = nv.models.lineWithFocusChart(); + chart.lines2.xScale(d3.time.scale.utc()); + chart + .x2Axis + .showMaxMin(fd.x_axis_showminmax) + .staggerLabels(false); + } else { + chart = nv.models.lineChart(); } - } - - if ("showLegend" in chart && typeof fd.show_legend !== 'undefined') { - chart.showLegend(fd.show_legend); - } - - var height = slice.height(); - height -= 15; // accounting for the staggered xAxis - - chart.height(height); - slice.container.css('height', height + 'px'); - - if ((viz_type === "line" || viz_type === "area") && fd.rich_tooltip) { - chart.useInteractiveGuideline(true); - } - if (fd.y_axis_zero) { - chart.forceY([0, 1]); - } else if (fd.y_log_scale) { - chart.yScale(d3.scale.log()); - } - if (fd.x_log_scale) { - chart.xScale(d3.scale.log()); - } - var xAxisFormatter = null; - if (viz_type === 'bubble') { - xAxisFormatter = d3.format('.3s'); - } else if (fd.x_axis_format === 'smart_date') { - xAxisFormatter = px.formatDate; - chart.xAxis.tickFormat(xAxisFormatter); - } else if (fd.x_axis_format !== undefined) { - xAxisFormatter = px.timeFormatFactory(fd.x_axis_format); - chart.xAxis.tickFormat(xAxisFormatter); - } - - if (chart.hasOwnProperty("x2Axis")) { - chart.x2Axis.tickFormat(xAxisFormatter); - height += 30; - } - - if (viz_type === 'bubble') { - chart.xAxis.tickFormat(d3.format('.3s')); - } else if (fd.x_axis_format === 'smart_date') { - chart.xAxis.tickFormat(px.formatDate); - } else if (fd.x_axis_format !== undefined) { - chart.xAxis.tickFormat(px.timeFormatFactory(fd.x_axis_format)); - } - if (chart.yAxis !== undefined) { - chart.yAxis.tickFormat(d3.format('.3s')); - } - - if (fd.contribution || fd.num_period_compare || viz_type === 'compare') { - chart.yAxis.tickFormat(d3.format('.3p')); - if (chart.y2Axis !== undefined) { - chart.y2Axis.tickFormat(d3.format('.3p')); + // To alter the tooltip header + // chart.interactiveLayer.tooltip.headerFormatter(function(){return '';}); + chart.xScale(d3.time.scale.utc()); + chart.interpolate(fd.line_interpolation); + chart.xAxis + .showMaxMin(fd.x_axis_showminmax) + .staggerLabels(false); + break; + + case 'bar': + chart = nv.models.multiBarChart() + .showControls(true) + .groupSpacing(0.1); + + chart.xAxis + .showMaxMin(false) + .staggerLabels(true); + + chart.stacked(fd.bar_stacked); + break; + + case 'dist_bar': + chart = nv.models.multiBarChart() + .showControls(true) //Allow user to switch between 'Grouped' and 'Stacked' mode. + .reduceXTicks(false) + .rotateLabels(45) + .groupSpacing(0.1); //Distance between each group of bars. + + chart.xAxis + .showMaxMin(false); + + chart.stacked(fd.bar_stacked); + break; + + case 'pie': + chart = nv.models.pieChart(); + colorKey = 'x'; + chart.valueFormat(f); + if (fd.donut) { + chart.donut(true); + chart.labelsOutside(true); } - } else if (fd.y_axis_format) { - chart.yAxis.tickFormat(d3.format(fd.y_axis_format)); - - if (chart.y2Axis !== undefined) { - chart.y2Axis.tickFormat(d3.format(fd.y_axis_format)); - } - } - chart.color(function (d, i) { - return px.color.category21(d[colorKey]); - }); - - var svg = d3.select(slice.selector).select("svg"); - if (svg.empty()) { - svg = d3.select(slice.selector).append("svg"); - } - - svg - .datum(payload.data) - .transition().duration(500) - .attr('height', height) - .call(chart); - - return chart; - }); - - slice.done(payload); - }) - .fail(function (xhr) { - slice.error(xhr.responseText); + chart.labelsOutside(true); + chart.cornerRadius(true); + break; + + case 'column': + chart = nv.models.multiBarChart() + .reduceXTicks(false) + .rotateLabels(45); + break; + + case 'compare': + chart = nv.models.cumulativeLineChart(); + chart.xScale(d3.time.scale.utc()); + chart.xAxis + .showMaxMin(false) + .staggerLabels(true); + break; + + case 'bubble': + var row = function (col1, col2) { + return "" + col1 + "" + col2 + ""; + }; + chart = nv.models.scatterChart(); + chart.showDistX(true); + chart.showDistY(true); + chart.tooltip.contentGenerator(function (obj) { + var p = obj.point; + var s = ""; + s += ''; + s += row(fd.x, f(p.x)); + s += row(fd.y, f(p.y)); + s += row(fd.size, f(p.size)); + s += "
' + p[fd.entity] + ' (' + p.group + ')
"; + return s; + }); + chart.pointRange([5, fd.max_bubble_size * fd.max_bubble_size]); + break; + + case 'area': + chart = nv.models.stackedAreaChart(); + chart.style(fd.stacked_style); + chart.xScale(d3.time.scale.utc()); + chart.xAxis + .showMaxMin(false) + .staggerLabels(true); + break; + + case 'box_plot': + colorKey = 'label'; + chart = nv.models.boxPlotChart(); + chart.x(function (d) { + return d.label; + }); + chart.staggerLabels(true); + chart.maxBoxWidth(75); // prevent boxes from being incredibly wide + break; + + default: + throw new Error("Unrecognized visualization for nvd3" + viz_type); + } + + if ("showLegend" in chart && typeof fd.show_legend !== 'undefined') { + chart.showLegend(fd.show_legend); + } + + var height = slice.height(); + height -= 15; // accounting for the staggered xAxis + + chart.height(height); + slice.container.css('height', height + 'px'); + + if ((viz_type === "line" || viz_type === "area") && fd.rich_tooltip) { + chart.useInteractiveGuideline(true); + } + if (fd.y_axis_zero) { + chart.forceY([0]); + } else if (fd.y_log_scale) { + chart.yScale(d3.scale.log()); + } + if (fd.x_log_scale) { + chart.xScale(d3.scale.log()); + } + var xAxisFormatter = null; + if (viz_type === 'bubble') { + xAxisFormatter = d3.format('.3s'); + } else if (fd.x_axis_format === 'smart_date') { + xAxisFormatter = px.formatDate; + chart.xAxis.tickFormat(xAxisFormatter); + } else if (fd.x_axis_format !== undefined) { + xAxisFormatter = px.timeFormatFactory(fd.x_axis_format); + chart.xAxis.tickFormat(xAxisFormatter); + } + + if (chart.hasOwnProperty("x2Axis")) { + chart.x2Axis.tickFormat(xAxisFormatter); + height += 30; + } + + if (viz_type === 'bubble') { + chart.xAxis.tickFormat(d3.format('.3s')); + } else if (fd.x_axis_format === 'smart_date') { + chart.xAxis.tickFormat(px.formatDate); + } else if (fd.x_axis_format !== undefined) { + chart.xAxis.tickFormat(px.timeFormatFactory(fd.x_axis_format)); + } + if (chart.yAxis !== undefined) { + chart.yAxis.tickFormat(d3.format('.3s')); + } + + if (fd.contribution || fd.num_period_compare || viz_type === 'compare') { + chart.yAxis.tickFormat(d3.format('.3p')); + if (chart.y2Axis !== undefined) { + chart.y2Axis.tickFormat(d3.format('.3p')); + } + } else if (fd.y_axis_format) { + chart.yAxis.tickFormat(d3.format(fd.y_axis_format)); + + if (chart.y2Axis !== undefined) { + chart.y2Axis.tickFormat(d3.format(fd.y_axis_format)); + } + } + chart.color(function (d, i) { + return px.color.category21(d[colorKey]); }); - }; - - var update = function () { - if (chart && chart.update) { - chart.update(); - } - }; - - return { - render: render, - resize: update - }; + + var svg = d3.select(slice.selector).select("svg"); + if (svg.empty()) { + svg = d3.select(slice.selector).append("svg"); + } + + svg + .datum(payload.data) + .transition().duration(500) + .attr('height', height) + .call(chart); + + return chart; + }); + + slice.done(payload); + }) + .fail(function (xhr) { + slice.error(xhr.responseText); + }); +}; + +var update = function () { + if (chart && chart.update) { + chart.update(); + } +}; + +return { + render: render, + resize: update +}; } module.exports = nvd3Vis; diff --git a/caravel/assets/visualizations/pivot_table.css b/caravel/assets/visualizations/pivot_table.css index 8b8b136d3c45..7d94f85344af 100644 --- a/caravel/assets/visualizations/pivot_table.css +++ b/caravel/assets/visualizations/pivot_table.css @@ -1,4 +1,4 @@ -.gridster .widget.pivot_table .slice_container { +.slice-grid .widget.pivot_table .slice_container { overflow: auto !important; } diff --git a/caravel/assets/visualizations/table.css b/caravel/assets/visualizations/table.css index 3d11841bb4da..655a360c14c4 100644 --- a/caravel/assets/visualizations/table.css +++ b/caravel/assets/visualizations/table.css @@ -1,4 +1,4 @@ -.gridster .widget.table .slice_container { +.slice-grid .widget.table .slice_container { overflow: auto !important; } diff --git a/caravel/assets/visualizations/treemap.js b/caravel/assets/visualizations/treemap.js index 2be008f7ecfb..680d3d4f0181 100644 --- a/caravel/assets/visualizations/treemap.js +++ b/caravel/assets/visualizations/treemap.js @@ -72,13 +72,11 @@ function treemap(slice) { // We also take a snapshot of the original children (_children) to avoid // the children being overwritten when when layout is computed. var accumulate = function (d) { - var results; - if (d._children === d.children) { - results = d.children.reduce(function (p, v) { return p + accumulate(v); }, 0); - } else { - results = d.value; + d._children = d.children; + if (d._children) { + d.value = d.children.reduce(function (p, v) { return p + accumulate(v); }, 0); } - return results; + return d.value; }; // Compute the treemap layout recursively such that each group of siblings diff --git a/caravel/assets/visualizations/world_map.js b/caravel/assets/visualizations/world_map.js index 87f09b30be38..76c991d3a247 100644 --- a/caravel/assets/visualizations/world_map.js +++ b/caravel/assets/visualizations/world_map.js @@ -14,6 +14,7 @@ function worldMapChart(slice) { container.css('height', slice.height()); d3.json(slice.jsonEndpoint(), function (error, json) { + div.selectAll("*").remove(); var fd = json.form_data; if (error !== null) { diff --git a/caravel/assets/webpack.config.js b/caravel/assets/webpack.config.js index d9253f3d5c41..cabb8bc12ceb 100644 --- a/caravel/assets/webpack.config.js +++ b/caravel/assets/webpack.config.js @@ -6,7 +6,7 @@ var config = { // for now generate one compiled js file per entry point / html page entry: { 'css-theme': APP_DIR + '/javascripts/css-theme.js', - dashboard: APP_DIR + '/javascripts/dashboard.js', + dashboard: APP_DIR + '/javascripts/dashboard.jsx', explore: APP_DIR + '/javascripts/explore.js', welcome: APP_DIR + '/javascripts/welcome.js', sql: APP_DIR + '/javascripts/sql.js', diff --git a/caravel/bin/caravel b/caravel/bin/caravel index 1d6c8f93ecdf..ccec716be2eb 100755 --- a/caravel/bin/caravel +++ b/caravel/bin/caravel @@ -47,6 +47,8 @@ def runserver(debug, port, timeout, workers): "-w {workers} " "--timeout {timeout} " "-b 0.0.0.0:{port} " + "--limit-request-line 0 " + "--limit-request-field_size 0 " "caravel:app").format(**locals()) print("Starting server with command: " + cmd) Popen(cmd, shell=True).wait() diff --git a/caravel/config.py b/caravel/config.py index e20c4ef97686..87156504d8b5 100644 --- a/caravel/config.py +++ b/caravel/config.py @@ -112,8 +112,8 @@ # The allowed translation for you app LANGUAGES = { 'en': {'flag': 'us', 'name': 'English'}, - # 'fr': {'flag': 'fr', 'name': 'French'}, - # 'zh': {'flag': 'cn', 'name': 'Chinese'}, + 'fr': {'flag': 'fr', 'name': 'French'}, + 'zh': {'flag': 'cn', 'name': 'Chinese'}, } # --------------------------------------------------- # Image and file configuration @@ -132,6 +132,10 @@ CACHE_DEFAULT_TIMEOUT = None CACHE_CONFIG = {'CACHE_TYPE': 'null'} +# CORS Options +ENABLE_CORS = False +CORS_OPTIONS = {} + # --------------------------------------------------- # List of viz_types not allowed in your environment @@ -172,7 +176,7 @@ try: from caravel_config import * # noqa -except Exception: +except ImportError: pass if not CACHE_DEFAULT_TIMEOUT: diff --git a/caravel/data/__init__.py b/caravel/data/__init__.py index c6586ec8b4f4..d853a0947742 100644 --- a/caravel/data/__init__.py +++ b/caravel/data/__init__.py @@ -375,7 +375,8 @@ def load_world_bank_health_n_pop(): print("Creating a World's Health Bank dashboard") dash_name = "World's Bank Data" - dash = db.session.query(Dash).filter_by(dashboard_title=dash_name).first() + slug = "world_health" + dash = db.session.query(Dash).filter_by(slug=slug).first() if not dash: dash = Dash() @@ -459,7 +460,7 @@ def load_world_bank_health_n_pop(): dash.dashboard_title = dash_name dash.position_json = json.dumps(l, indent=4) - dash.slug = "world_health" + dash.slug = slug dash.slices = slices[:-1] db.session.merge(dash) @@ -475,14 +476,14 @@ def load_css_templates(): if not obj: obj = CSS(template_name="Flat") css = textwrap.dedent("""\ - .gridster li.widget { + .gridster div.widget { transition: background-color 0.5s ease; background-color: #FAFAFA; border: 1px solid #CCC; box-shadow: none; border-radius: 0px; } - .gridster li.widget:hover { + .gridster div.widget:hover { border: 1px solid #000; background-color: #EAEAEA; } @@ -515,7 +516,7 @@ def load_css_templates(): if not obj: obj = CSS(template_name="Courier Black") css = textwrap.dedent("""\ - .gridster li.widget { + .gridster div.widget { transition: background-color 0.5s ease; background-color: #EEE; border: 2px solid #444; @@ -529,7 +530,7 @@ def load_css_templates(): .navbar { box-shadow: none; } - .gridster li.widget:hover { + .gridster div.widget:hover { border: 2px solid #000; background-color: #EAEAEA; } diff --git a/caravel/forms.py b/caravel/forms.py index 5c4e1a70bf92..d7c77439ce69 100644 --- a/caravel/forms.py +++ b/caravel/forms.py @@ -52,7 +52,7 @@ def iter_choices(self): d[value] = (value, label, selected) if self.data: for value in self.data: - if value: + if value and value in d: yield d.pop(value) while d: yield d.popitem(last=False)[1] @@ -129,122 +129,151 @@ def __init__(self, viz): default_groupby = gb_cols[0] if gb_cols else None group_by_choices = self.choicify(gb_cols) # Pool of all the fields that can be used in Caravel - self.field_dict = { - 'viz_type': SelectField( - 'Viz', - default='table', - choices=[(k, v.verbose_name) for k, v in viz_types.items()], - description="The type of visualization to display"), - 'metrics': SelectMultipleSortableField( - 'Metrics', choices=datasource.metrics_combo, - default=[default_metric], - description="One or many metrics to display"), - 'metric': SelectField( - 'Metric', choices=datasource.metrics_combo, - default=default_metric, - description="Chose the metric"), - 'stacked_style': SelectField( - 'Chart Style', choices=self.choicify( - ['stack', 'stream', 'expand']), - default='stack', - description=""), - 'linear_color_scheme': SelectField( - 'Color Scheme', choices=self.choicify([ + field_data = { + 'viz_type': (SelectField, { + "label": "Viz", + "default": 'table', + "choices": [(k, v.verbose_name) for k, v in viz_types.items()], + "description": "The type of visualization to display" + }), + 'metrics': (SelectMultipleSortableField, { + "label": "Metrics", + "choices": datasource.metrics_combo, + "default": [default_metric], + "description": "One or many metrics to display" + }), + 'metric': (SelectField, { + "label": "Metric", + "choices": datasource.metrics_combo, + "default": default_metric, + "description": "Choose the metric" + }), + 'stacked_style': (SelectField, { + "label": "Chart Style", + "choices": self.choicify(['stack', 'stream', 'expand']), + "default": 'stack', + "description": "" + }), + 'linear_color_scheme': (SelectField, { + "label": "Color Scheme", + "choices": self.choicify([ 'fire', 'blue_white_yellow', 'white_black', 'black_white']), - default='blue_white_yellow', - description=""), - 'normalize_across': SelectField( - 'Normalize Across', choices=self.choicify([ + "default": 'blue_white_yellow', + "description": "" + }), + 'normalize_across': (SelectField, { + "label": "Normalize Across", + "choices": self.choicify([ 'heatmap', 'x', 'y']), - default='heatmap', - description=( + "default": 'heatmap', + "description": ( "Color will be rendered based on a ratio " "of the cell against the sum of across this " - "criteria")), - 'horizon_color_scale': SelectField( - 'Color Scale', choices=self.choicify([ - 'series', 'overall', 'change']), - default='series', - description="Defines how the color are attributed."), - 'canvas_image_rendering': SelectField( - 'Rendering', choices=( + "criteria") + }), + 'horizon_color_scale': (SelectField, { + "label": "Color Scale", + "choices": self.choicify(['series', 'overall', 'change']), + "default": 'series', + "description": "Defines how the color are attributed." + }), + 'canvas_image_rendering': (SelectField, { + "label": "Rendering", + "choices": ( ('pixelated', 'pixelated (Sharp)'), ('auto', 'auto (Smooth)'), ), - default='pixelated', - description=( + "default": 'pixelated', + "description": ( "image-rendering CSS attribute of the canvas object that " - "defines how the browser scales up the image")), - 'xscale_interval': SelectField( - 'XScale Interval', choices=self.choicify(range(1, 50)), - default='1', - description=( + "defines how the browser scales up the image") + }), + 'xscale_interval': (SelectField, { + "label": "XScale Interval", + "choices": self.choicify(range(1, 50)), + "default": '1', + "description": ( "Number of step to take between ticks when " - "printing the x scale")), - 'yscale_interval': SelectField( - 'YScale Interval', choices=self.choicify(range(1, 50)), - default='1', - description=( + "printing the x scale") + }), + 'yscale_interval': (SelectField, { + "label": "YScale Interval", + "choices": self.choicify(range(1, 50)), + "default": '1', + "description": ( "Number of step to take between ticks when " - "printing the y scale")), - 'bar_stacked': BetterBooleanField( - 'Stacked Bars', - default=False, - description=""), - 'include_series': BetterBooleanField( - 'Include Series', - default=False, - description="Include series name as an axis"), - 'secondary_metric': SelectField( - 'Color Metric', choices=datasource.metrics_combo, - default=default_metric, - description="A metric to use for color"), - 'country_fieldtype': SelectField( - 'Country Field Type', - default='cca2', - choices=( + "printing the y scale") + }), + 'bar_stacked': (BetterBooleanField, { + "label": "Stacked Bars", + "default": False, + "description": "" + }), + 'include_series': (BetterBooleanField, { + "label": "Include Series", + "default": False, + "description": "Include series name as an axis" + }), + 'secondary_metric': (SelectField, { + "label": "Color Metric", + "choices": datasource.metrics_combo, + "default": default_metric, + "description": "A metric to use for color" + }), + 'country_fieldtype': (SelectField, { + "label": "Country Field Type", + "default": 'cca2', + "choices": ( ('name', 'Full name'), ('cioc', 'code International Olympic Committee (cioc)'), ('cca2', 'code ISO 3166-1 alpha-2 (cca2)'), ('cca3', 'code ISO 3166-1 alpha-3 (cca3)'), ), - description=( + "description": ( "The country code standard that Caravel should expect " - "to find in the [country] column")), - 'groupby': SelectMultipleSortableField( - 'Group by', - choices=self.choicify(datasource.groupby_column_names), - description="One or many fields to group by"), - 'columns': SelectMultipleSortableField( - 'Columns', - choices=self.choicify(datasource.groupby_column_names), - description="One or many fields to pivot as columns"), - 'all_columns': SelectMultipleSortableField( - 'Columns', - choices=self.choicify(datasource.column_names), - description="Columns to display"), - 'all_columns_x': SelectField( - 'X', - choices=self.choicify(datasource.column_names), - description="Columns to display"), - 'all_columns_y': SelectField( - 'Y', - choices=self.choicify(datasource.column_names), - description="Columns to display"), - 'druid_time_origin': FreeFormSelectField( - 'Origin', - choices=( + "to find in the [country] column") + }), + 'groupby': (SelectMultipleSortableField, { + "label": "Group by", + "choices": self.choicify(datasource.groupby_column_names), + "description": "One or many fields to group by" + }), + 'columns': (SelectMultipleSortableField, { + "label": "Columns", + "choices": self.choicify(datasource.groupby_column_names), + "description": "One or many fields to pivot as columns" + }), + 'all_columns': (SelectMultipleSortableField, { + "label": "Columns", + "choices": self.choicify(datasource.column_names), + "description": "Columns to display" + }), + 'all_columns_x': (SelectField, { + "label": "X", + "choices": self.choicify(datasource.column_names), + "description": "Columns to display" + }), + 'all_columns_y': (SelectField, { + "label": "Y", + "choices": self.choicify(datasource.column_names), + "description": "Columns to display" + }), + 'druid_time_origin': (FreeFormSelectField, { + "label": "Origin", + "choices": ( ('', 'default'), ('now', 'now'), ), - default='', - description=( + "default": '', + "description": ( "Defines the origin where time buckets start, " - "accepts natural dates as in 'now', 'sunday' or '1970-01-01'")), - 'granularity': FreeFormSelectField( - 'Time Granularity', default="one day", - choices=self.choicify([ + "accepts natural dates as in 'now', 'sunday' or '1970-01-01'") + }), + 'granularity': (FreeFormSelectField, { + "label": "Time Granularity", + "default": "one day", + "choices": self.choicify([ 'all', '5 seconds', '30 seconds', @@ -255,36 +284,42 @@ def __init__(self, viz): '1 day', '7 days', ]), - description=( + "description": ( "The time granularity for the visualization. Note that you " "can type and use simple natural language as in '10 seconds', " - "'1 day' or '56 weeks'")), - 'domain_granularity': SelectField( - 'Domain', default="month", - choices=self.choicify([ + "'1 day' or '56 weeks'") + }), + 'domain_granularity': (SelectField, { + "label": "Domain", + "default": "month", + "choices": self.choicify([ 'hour', 'day', 'week', 'month', 'year', ]), - description=( - "The time unit used for the grouping of blocks")), - 'subdomain_granularity': SelectField( - 'Subdomain', default="day", - choices=self.choicify([ + "description": ( + "The time unit used for the grouping of blocks") + }), + 'subdomain_granularity': (SelectField, { + "label": "Subdomain", + "default": "day", + "choices": self.choicify([ 'min', 'hour', 'day', 'week', 'month', ]), - description=( + "description": ( "The time unit for each block. Should be a smaller unit than " - "domain_granularity. Should be larger or equal to Time Grain")), - 'link_length': FreeFormSelectField( - 'Link Length', default="200", - choices=self.choicify([ + "domain_granularity. Should be larger or equal to Time Grain") + }), + 'link_length': (FreeFormSelectField, { + "label": "Link Length", + "default": "200", + "choices": self.choicify([ '10', '25', '50', @@ -294,10 +329,12 @@ def __init__(self, viz): '200', '250', ]), - description="Link length in the force layout"), - 'charge': FreeFormSelectField( - 'Charge', default="-500", - choices=self.choicify([ + "description": "Link length in the force layout" + }), + 'charge': (FreeFormSelectField, { + "label": "Charge", + "default": "-500", + "choices": self.choicify([ '-50', '-75', '-100', @@ -309,32 +346,41 @@ def __init__(self, viz): '-2500', '-5000', ]), - description="Charge in the force layout"), - 'granularity_sqla': SelectField( - 'Time Column', - default=datasource.main_dttm_col or datasource.any_dttm_col, - choices=self.choicify(datasource.dttm_cols), - description=( + "description": "Charge in the force layout" + }), + 'granularity_sqla': (SelectField, { + "label": "Time Column", + "default": datasource.main_dttm_col or datasource.any_dttm_col, + "choices": self.choicify(datasource.dttm_cols), + "description": ( "The time column for the visualization. Note that you " "can define arbitrary expression that return a DATETIME " "column in the table editor. Also note that the " "filter bellow is applied against this column or " - "expression")), - 'resample_rule': FreeFormSelectField( - 'Resample Rule', default='', - choices=self.choicify(('1T', '1H', '1D', '7D', '1M', '1AS')), - description=("Pandas resample rule")), - 'resample_how': FreeFormSelectField( - 'Resample How', default='', - choices=self.choicify(('', 'mean', 'sum', 'median')), - description=("Pandas resample how")), - 'resample_fillmethod': FreeFormSelectField( - 'Resample Fill Method', default='', - choices=self.choicify(('', 'ffill', 'bfill')), - description=("Pandas resample fill method")), - 'since': FreeFormSelectField( - 'Since', default="7 days ago", - choices=self.choicify([ + "expression") + }), + 'resample_rule': (FreeFormSelectField, { + "label": "Resample Rule", + "default": '', + "choices": self.choicify(('1T', '1H', '1D', '7D', '1M', '1AS')), + "description": ("Pandas resample rule") + }), + 'resample_how': (FreeFormSelectField, { + "label": "Resample How", + "default": '', + "choices": self.choicify(('', 'mean', 'sum', 'median')), + "description": ("Pandas resample how") + }), + 'resample_fillmethod': (FreeFormSelectField, { + "label": "Resample Fill Method", + "default": '', + "choices": self.choicify(('', 'ffill', 'bfill')), + "description": ("Pandas resample fill method") + }), + 'since': (FreeFormSelectField, { + "label": "Since", + "default": "7 days ago", + "choices": self.choicify([ '1 hour ago', '12 hours ago', '1 day ago', @@ -343,22 +389,25 @@ def __init__(self, viz): '90 days ago', '1 year ago' ]), - description=( + "description": ( "Timestamp from filter. This supports free form typing and " - "natural language as in '1 day ago', '28 days' or '3 years'")), - 'until': FreeFormSelectField( - 'Until', default="now", - choices=self.choicify([ + "natural language as in '1 day ago', '28 days' or '3 years'") + }), + 'until': (FreeFormSelectField, { + "label": "Until", + "default": "now", + "choices": self.choicify([ 'now', '1 day ago', '7 days ago', '28 days ago', '90 days ago', '1 year ago']) - ), - 'max_bubble_size': FreeFormSelectField( - 'Max Bubble Size', default="25", - choices=self.choicify([ + }), + 'max_bubble_size': (FreeFormSelectField, { + "label": "Max Bubble Size", + "default": "25", + "choices": self.choicify([ '5', '10', '15', @@ -367,27 +416,28 @@ def __init__(self, viz): '75', '100', ]) - ), - 'whisker_options': FreeFormSelectField( - 'Whisker/outlier options', default="Tukey", - description=( + }), + 'whisker_options': (FreeFormSelectField, { + "label": "Whisker/outlier options", + "default": "Tukey", + "description": ( "Determines how whiskers and outliers are calculated."), - choices=self.choicify([ + "choices": self.choicify([ 'Tukey', 'Min/max (no outliers)', '2/98 percentiles', '9/91 percentiles', ]) - ), - 'treemap_ratio': DecimalField( - 'Ratio', - default=0.5 * (1 + math.sqrt(5)), # d3 default, golden ratio - description='Target aspect ratio for treemap tiles.', - ), - 'number_format': FreeFormSelectField( - 'Number format', - default='.3s', - choices=[ + }), + 'treemap_ratio': (DecimalField, { + "label": "Ratio", + "default": 0.5 * (1 + math.sqrt(5)), # d3 default, golden ratio + "description": 'Target aspect ratio for treemap tiles.', + }), + 'number_format': (FreeFormSelectField, { + "label": "Number format", + "default": '.3s', + "choices": [ ('.3s', '".3s" | 12.3k'), ('.3%', '".3%" | 1234543.210%'), ('.4r', '".4r" | 12350'), @@ -395,109 +445,130 @@ def __init__(self, viz): ('+,', '"+," | +12,345.4321'), ('$,.2f', '"$,.2f" | $12,345.43'), ], - description="D3 format syntax for numbers " - "https://github.com/mbostock/\n" - "d3/wiki/Formatting"), - - 'row_limit': - FreeFormSelectField( - 'Row limit', - default=config.get("ROW_LIMIT"), - choices=self.choicify( - [10, 50, 100, 250, 500, 1000, 5000, 10000, 50000])), - 'limit': - FreeFormSelectField( - 'Series limit', - choices=self.choicify(self.series_limits), - default=50, - description=( - "Limits the number of time series that get displayed")), - 'rolling_type': SelectField( - 'Rolling', - default='None', - choices=[(s, s) for s in ['None', 'mean', 'sum', 'std', 'cumsum']], - description=( + "description": "D3 format syntax for numbers " + "https: //github.com/mbostock/\n" + "d3/wiki/Formatting" + }), + 'row_limit': (FreeFormSelectField, { + "label": 'Row limit', + "default": config.get("ROW_LIMIT"), + "choices": self.choicify( + [10, 50, 100, 250, 500, 1000, 5000, 10000, 50000]) + }), + 'limit': (FreeFormSelectField, { + "label": 'Series limit', + "choices": self.choicify(self.series_limits), + "default": 50, + "description": ( + "Limits the number of time series that get displayed") + }), + 'rolling_type': (SelectField, { + "label": "Rolling", + "default": 'None', + "choices": [(s, s) for s in ['None', 'mean', 'sum', 'std', 'cumsum']], + "description": ( "Defines a rolling window function to apply, works along " - "with the [Periods] text box")), - 'rolling_periods': IntegerField( - 'Periods', - validators=[validators.optional()], - description=( + "with the [Periods] text box") + }), + 'rolling_periods': (IntegerField, { + "label": "Periods", + "validators": [validators.optional()], + "description": ( "Defines the size of the rolling window function, " - "relative to the time granularity selected")), - 'series': SelectField( - 'Series', choices=group_by_choices, - default=default_groupby, - description=( + "relative to the time granularity selected") + }), + 'series': (SelectField, { + "label": "Series", + "choices": group_by_choices, + "default": default_groupby, + "description": ( "Defines the grouping of entities. " "Each serie is shown as a specific color on the chart and " - "has a legend toggle")), - 'entity': SelectField( - 'Entity', choices=group_by_choices, - default=default_groupby, - description="This define the element to be plotted on the chart"), - 'x': SelectField( - 'X Axis', choices=datasource.metrics_combo, - default=default_metric, - description="Metric assigned to the [X] axis"), - 'y': SelectField( - 'Y Axis', choices=datasource.metrics_combo, - default=default_metric, - description="Metric assigned to the [Y] axis"), - 'size': SelectField( - 'Bubble Size', - default=default_metric, - choices=datasource.metrics_combo), - 'url': TextField( - 'URL', - description=( + "has a legend toggle") + }), + 'entity': (SelectField, { + "label": "Entity", + "choices": group_by_choices, + "default": default_groupby, + "description": "This define the element to be plotted on the chart" + }), + 'x': (SelectField, { + "label": "X Axis", + "choices": datasource.metrics_combo, + "default": default_metric, + "description": "Metric assigned to the [X] axis" + }), + 'y': (SelectField, { + "label": "Y Axis", + "choices": datasource.metrics_combo, + "default": default_metric, + "description": "Metric assigned to the [Y] axis" + }), + 'size': (SelectField, { + "label": 'Bubble Size', + "default": default_metric, + "choices": datasource.metrics_combo + }), + 'url': (TextField, { + "label": "URL", + "description": ( "The URL, this field is templated, so you can integrate " "{{ width }} and/or {{ height }} in your URL string." ), - default='https://www.youtube.com/embed/JkI5rg_VcQ4',), - 'where': TextField( - 'Custom WHERE clause', default='', - description=( + "default": 'https: //www.youtube.com/embed/JkI5rg_VcQ4', + }), + 'where': (TextField, { + "label": "Custom WHERE clause", + "default": '', + "description": ( "The text in this box gets included in your query's WHERE " "clause, as an AND to other criteria. You can include " "complex expression, parenthesis and anything else " - "supported by the backend it is directed towards.")), - 'having': TextField( - 'Custom HAVING clause', default='', - description=( + "supported by the backend it is directed towards.") + }), + 'having': (TextField, { + "label": "Custom HAVING clause", + "default": '', + "description": ( "The text in this box gets included in your query's HAVING" " clause, as an AND to other criteria. You can include " "complex expression, parenthesis and anything else " - "supported by the backend it is directed towards.")), - 'compare_lag': TextField( - 'Comparison Period Lag', - description=( + "supported by the backend it is directed towards.") + }), + 'compare_lag': (TextField, { + "label": "Comparison Period Lag", + "description": ( "Based on granularity, number of time periods to " - "compare against")), - 'compare_suffix': TextField( - 'Comparison suffix', - description="Suffix to apply after the percentage display"), - 'table_timestamp_format': FreeFormSelectField( - 'Table Timestamp Format', - default='smart_date', - choices=TIMESTAMP_CHOICES, - description="Timestamp Format"), - 'series_height': FreeFormSelectField( - 'Series Height', - default=25, - choices=self.choicify([10, 25, 40, 50, 75, 100, 150, 200]), - description="Pixel height of each series"), - 'x_axis_format': FreeFormSelectField( - 'X axis format', - default='smart_date', - choices=TIMESTAMP_CHOICES, - description="D3 format syntax for y axis " - "https://github.com/mbostock/\n" - "d3/wiki/Formatting"), - 'y_axis_format': FreeFormSelectField( - 'Y axis format', - default='.3s', - choices=[ + "compare against") + }), + 'compare_suffix': (TextField, { + "label": "Comparison suffix", + "description": "Suffix to apply after the percentage display" + }), + 'table_timestamp_format': (FreeFormSelectField, { + "label": "Table Timestamp Format", + "default": 'smart_date', + "choices": TIMESTAMP_CHOICES, + "description": "Timestamp Format" + }), + 'series_height': (FreeFormSelectField, { + "label": "Series Height", + "default": 25, + "choices": self.choicify([10, 25, 40, 50, 75, 100, 150, 200]), + "description": "Pixel height of each series" + }), + 'x_axis_format': (FreeFormSelectField, { + "label": "X axis format", + "default": 'smart_date', + "choices": TIMESTAMP_CHOICES, + "description": "D3 format syntax for y axis " + "https: //github.com/mbostock/\n" + "d3/wiki/Formatting" + }), + 'y_axis_format': (FreeFormSelectField, { + "label": "Y axis format", + "default": '.3s', + "choices": [ ('.3s', '".3s" | 12.3k'), ('.3%', '".3%" | 1234543.210%'), ('.4r', '".4r" | 12350'), @@ -505,107 +576,155 @@ def __init__(self, viz): ('+,', '"+," | +12,345.4321'), ('$,.2f', '"$,.2f" | $12,345.43'), ], - description="D3 format syntax for y axis " - "https://github.com/mbostock/\n" - "d3/wiki/Formatting"), - 'markup_type': SelectField( - "Markup Type", - choices=self.choicify(['markdown', 'html']), - default="markdown", - description="Pick your favorite markup language"), - 'rotation': SelectField( - "Rotation", - choices=[(s, s) for s in ['random', 'flat', 'square']], - default="random", - description="Rotation to apply to words in the cloud"), - 'line_interpolation': SelectField( - "Line Style", - choices=self.choicify([ + "description": "D3 format syntax for y axis " + "https: //github.com/mbostock/\n" + "d3/wiki/Formatting" + }), + 'markup_type': (SelectField, { + "label": "Markup Type", + "choices": self.choicify(['markdown', 'html']), + "default": "markdown", + "description": "Pick your favorite markup language" + }), + 'rotation': (SelectField, { + "label": "Rotation", + "choices": [(s, s) for s in ['random', 'flat', 'square']], + "default": "random", + "description": "Rotation to apply to words in the cloud" + }), + 'line_interpolation': (SelectField, { + "label": "Line Style", + "choices": self.choicify([ 'linear', 'basis', 'cardinal', 'monotone', 'step-before', 'step-after']), - default='linear', - description="Line interpolation as defined by d3.js"), - 'code': TextAreaField( - "Code", description="Put your code here", default=''), - 'pandas_aggfunc': SelectField( - "Aggregation function", - choices=self.choicify([ + "default": 'linear', + "description": "Line interpolation as defined by d3.js" + }), + 'code': (TextAreaField, { + "label": "Code", + "description": "Put your code here", + "default": '' + }), + 'pandas_aggfunc': (SelectField, { + "label": "Aggregation function", + "choices": self.choicify([ 'sum', 'mean', 'min', 'max', 'median', 'stdev', 'var']), - default='sum', - description=( + "default": 'sum', + "description": ( "Aggregate function to apply when pivoting and " - "computing the total rows and columns")), - 'size_from': TextField( - "Font Size From", - default="20", - description="Font size for the smallest value in the list"), - 'size_to': TextField( - "Font Size To", - default="150", - description="Font size for the biggest value in the list"), - 'show_brush': BetterBooleanField( - "Range Filter", default=False, - description=( - "Whether to display the time range interactive selector")), - 'show_datatable': BetterBooleanField( - "Data Table", default=False, - description="Whether to display the interactive data table"), - 'include_search': BetterBooleanField( - "Search Box", default=False, - description=( - "Whether to include a client side search box")), - 'show_bubbles': BetterBooleanField( - "Show Bubbles", default=False, - description=( - "Whether to display bubbles on top of countries")), - 'show_legend': BetterBooleanField( - "Legend", default=True, - description="Whether to display the legend (toggles)"), - 'x_axis_showminmax': BetterBooleanField( - "X bounds", default=True, - description=( - "Whether to display the min and max values of the X axis")), - 'rich_tooltip': BetterBooleanField( - "Rich Tooltip", default=True, - description=( + "computing the total rows and columns") + }), + 'size_from': (TextField, { + "label": "Font Size From", + "default": "20", + "description": "Font size for the smallest value in the list" + }), + 'size_to': (TextField, { + "label": "Font Size To", + "default": "150", + "description": "Font size for the biggest value in the list" + }), + 'show_brush': (BetterBooleanField, { + "label": "Range Filter", + "default": False, + "description": ( + "Whether to display the time range interactive selector") + }), + 'show_datatable': (BetterBooleanField, { + "label": "Data Table", + "default": False, + "description": "Whether to display the interactive data table" + }), + 'include_search': (BetterBooleanField, { + "label": "Search Box", + "default": False, + "description": ( + "Whether to include a client side search box") + }), + 'show_bubbles': (BetterBooleanField, { + "label": "Show Bubbles", + "default": False, + "description": ( + "Whether to display bubbles on top of countries") + }), + 'show_legend': (BetterBooleanField, { + "label": "Legend", + "default": True, + "description": "Whether to display the legend (toggles)" + }), + 'x_axis_showminmax': (BetterBooleanField, { + "label": "X bounds", + "default": True, + "description": ( + "Whether to display the min and max values of the X axis") + }), + 'rich_tooltip': (BetterBooleanField, { + "label": "Rich Tooltip", + "default": True, + "description": ( "The rich tooltip shows a list of all series for that" - " point in time")), - 'y_axis_zero': BetterBooleanField( - "Y Axis Zero", default=False, - description=( + " point in time") + }), + 'y_axis_zero': (BetterBooleanField, { + "label": "Y Axis Zero", + "default": False, + "description": ( "Force the Y axis to start at 0 instead of the minimum " - "value")), - 'y_log_scale': BetterBooleanField( - "Y Log", default=False, - description="Use a log scale for the Y axis"), - 'x_log_scale': BetterBooleanField( - "X Log", default=False, - description="Use a log scale for the X axis"), - 'donut': BetterBooleanField( - "Donut", default=False, - description="Do you want a donut or a pie?"), - 'contribution': BetterBooleanField( - "Contribution", default=False, - description="Compute the contribution to the total"), - 'num_period_compare': IntegerField( - "Period Ratio", default=None, - validators=[validators.optional()], - description=( + "value") + }), + 'y_log_scale': (BetterBooleanField, { + "label": "Y Log", + "default": False, + "description": "Use a log scale for the Y axis" + }), + 'x_log_scale': (BetterBooleanField, { + "label": "X Log", + "default": False, + "description": "Use a log scale for the X axis" + }), + 'donut': (BetterBooleanField, { + "label": "Donut", + "default": False, + "description": "Do you want a donut or a pie?" + }), + 'contribution': (BetterBooleanField, { + "label": "Contribution", + "default": False, + "description": "Compute the contribution to the total" + }), + 'num_period_compare': (IntegerField, { + "label": "Period Ratio", + "default": None, + "validators": [validators.optional()], + "description": ( "[integer] Number of period to compare against, " - "this is relative to the granularity selected")), - 'time_compare': TextField( - "Time Shift", - default="", - description=( + "this is relative to the granularity selected") + }), + 'time_compare': (TextField, { + "label": "Time Shift", + "default": "", + "description": ( "Overlay a timeseries from a " "relative time period. Expects relative time delta " - "in natural language (example: 24 hours, 7 days, " - "56 weeks, 365 days")), - 'subheader': TextField( - 'Subheader', - description=( + "in natural language (example: 24 hours, 7 days, " + "56 weeks, 365 days") + }), + 'subheader': (TextField, { + "label": "Subheader", + "description": ( "Description text that shows up below your Big " - "Number")), + "Number") + }), + } + + # Override default arguments with form overrides + for field_name, override_map in viz.form_overrides.items(): + if field_name in field_data: + field_data[field_name][1].update(override_map) + + self.field_dict = { + field_name: v[0](**v[1]) + for field_name, v in field_data.items() } @staticmethod @@ -642,20 +761,6 @@ class QueryForm(OmgWtForm): collapsed_fieldsets = HiddenField() viz_type = self.field_dict.get('viz_type') - filter_cols = viz.datasource.filterable_column_names or [''] - for i in range(10): - setattr(QueryForm, 'flt_col_' + str(i), SelectField( - 'Filter 1', - default=filter_cols[0], - choices=self.choicify(filter_cols))) - setattr(QueryForm, 'flt_op_' + str(i), SelectField( - 'Filter 1', - default='in', - choices=self.choicify(['in', 'not in']))) - setattr( - QueryForm, 'flt_eq_' + str(i), - TextField("Super", default='')) - for field in viz.flat_form_fields(): setattr(QueryForm, field, self.field_dict[field]) @@ -663,8 +768,11 @@ def add_to_form(attrs): for attr in attrs: setattr(QueryForm, attr, self.field_dict[attr]) + filter_choices = self.choicify(['in', 'not in']) # datasource type specific form elements - if viz.datasource.__class__.__name__ == 'SqlaTable': + datasource_classname = viz.datasource.__class__.__name__ + time_fields = None + if datasource_classname == 'SqlaTable': QueryForm.fieldsets += ({ 'label': 'SQL', 'fields': ['where', 'having'], @@ -675,8 +783,6 @@ def add_to_form(attrs): add_to_form(('where', 'having')) grains = viz.datasource.database.grains() - if not viz.datasource.any_dttm_col: - return QueryForm if grains: time_fields = ('granularity_sqla', 'time_grain_sqla') self.field_dict['time_grain_sqla'] = SelectField( @@ -695,19 +801,35 @@ def add_to_form(attrs): else: time_fields = 'granularity_sqla' add_to_form((time_fields, )) - else: + elif datasource_classname == 'DruidDatasource': time_fields = ('granularity', 'druid_time_origin') add_to_form(('granularity', 'druid_time_origin')) field_css_classes['granularity'] = ['form-control', 'select2_freeform'] field_css_classes['druid_time_origin'] = ['form-control', 'select2_freeform'] + filter_choices = self.choicify(['in', 'not in', 'regex']) add_to_form(('since', 'until')) - QueryForm.fieldsets = ({ - 'label': 'Time', - 'fields': ( - time_fields, - ('since', 'until'), - ), - 'description': "Time related form attributes", - },) + tuple(QueryForm.fieldsets) + filter_cols = viz.datasource.filterable_column_names or [''] + for i in range(10): + setattr(QueryForm, 'flt_col_' + str(i), SelectField( + 'Filter 1', + default=filter_cols[0], + choices=self.choicify(filter_cols))) + setattr(QueryForm, 'flt_op_' + str(i), SelectField( + 'Filter 1', + default='in', + choices=filter_choices)) + setattr( + QueryForm, 'flt_eq_' + str(i), + TextField("Super", default='')) + + if time_fields: + QueryForm.fieldsets = ({ + 'label': 'Time', + 'fields': ( + time_fields, + ('since', 'until'), + ), + 'description': "Time related form attributes", + },) + tuple(QueryForm.fieldsets) return QueryForm diff --git a/caravel/migrations/versions/1226819ee0e3_fix_wrong_constraint_on_table_columns.py b/caravel/migrations/versions/1226819ee0e3_fix_wrong_constraint_on_table_columns.py new file mode 100644 index 000000000000..9b1d8018c341 --- /dev/null +++ b/caravel/migrations/versions/1226819ee0e3_fix_wrong_constraint_on_table_columns.py @@ -0,0 +1,38 @@ +"""Fix wrong constraint on table columns + +Revision ID: 1226819ee0e3 +Revises: 956a063c52b3 +Create Date: 2016-05-27 15:03:32.980343 + +""" + +# revision identifiers, used by Alembic. +revision = '1226819ee0e3' +down_revision = '956a063c52b3' + +from alembic import op +import sqlalchemy as sa +from caravel.utils import generic_find_constraint_name + +naming_convention = { + "fk": + "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", +} + +def find_constraint_name(upgrade=True): + cols = {'column_name'} if upgrade else {'datasource_name'} + return generic_find_constraint_name(table='columns', columns=cols, referenced='datasources') + +def upgrade(): + constraint = find_constraint_name() or 'fk_columns_column_name_datasources' + with op.batch_alter_table("columns", + naming_convention=naming_convention) as batch_op: + batch_op.drop_constraint(constraint, type_="foreignkey") + batch_op.create_foreign_key('fk_columns_datasource_name_datasources', 'datasources', ['datasource_name'], ['datasource_name']) + +def downgrade(): + constraint = find_constraint_name(False) or 'fk_columns_datasource_name_datasources' + with op.batch_alter_table("columns", + naming_convention=naming_convention) as batch_op: + batch_op.drop_constraint(constraint, type_="foreignkey") + batch_op.create_foreign_key('fk_columns_column_name_datasources', 'datasources', ['column_name'], ['datasource_name']) diff --git a/caravel/models.py b/caravel/models.py index cb9577bf7a1e..6a73cb2223e2 100644 --- a/caravel/models.py +++ b/caravel/models.py @@ -20,10 +20,10 @@ from dateutil.parser import parse from flask import request, g -from flask.ext.appbuilder import Model -from flask.ext.appbuilder.models.mixins import AuditMixin -from flask.ext.appbuilder.models.decorators import renders -from flask.ext.babelpkg import gettext as _ +from flask_appbuilder import Model +from flask_appbuilder.models.mixins import AuditMixin +from flask_appbuilder.models.decorators import renders +from flask_babelpkg import gettext as _ from pydruid.client import PyDruid from pydruid.utils.filters import Dimension, Filter @@ -197,6 +197,7 @@ def datasource_id(self): @property def data(self): + """Data used to render slice in templates""" d = {} self.token = '' try: @@ -205,6 +206,11 @@ def data(self): except Exception as e: d['error'] = str(e) d['slice_id'] = self.id + d['slice_name'] = self.slice_name + d['description'] = self.description + d['slice_url'] = self.slice_url + d['edit_url'] = self.edit_url + d['description_markeddown'] = self.description_markeddown return d @property @@ -309,6 +315,7 @@ def json_data(self): 'dashboard_title': self.dashboard_title, 'slug': self.slug, 'slices': [slc.data for slc in self.slices], + 'position_json': json.loads(self.position_json) if self.position_json else [], } return json.dumps(d) @@ -424,6 +431,7 @@ def grains(self): ), } db_time_grains['redshift'] = db_time_grains['postgresql'] + db_time_grains['vertica'] = db_time_grains['postgresql'] for db_type, grains in db_time_grains.items(): if self.sqlalchemy_uri.startswith(db_type): return grains @@ -629,7 +637,7 @@ def query( # sqla for s in groupby: col = cols[s] outer = col.sqla_col - inner = col.sqla_col.label('__' + col.column_name) + inner = col.sqla_col.label(col.column_name + '__') groupby_exprs.append(outer) select_exprs.append(outer) @@ -715,7 +723,7 @@ def query( # sqla on_clause = [] for i, gb in enumerate(groupby): on_clause.append( - groupby_exprs[i] == column("__" + gb)) + groupby_exprs[i] == column(gb + '__')) tbl = tbl.join(subq.alias(), and_(*on_clause)) @@ -1165,16 +1173,18 @@ def recursive_get_fields(_conf): if len(splitted) > 1: for s in eq.split(','): s = s.strip() - fields.append(Filter.build_filter(Dimension(col) == s)) + fields.append(Dimension(col) == s) cond = Filter(type="or", fields=fields) else: cond = Dimension(col) == eq if op == 'not in': cond = ~cond + elif op == 'regex': + cond = Filter(type="regex", pattern=eq, dimension=col) if filters: filters = Filter(type="and", fields=[ - Filter.build_filter(cond), - Filter.build_filter(filters) + cond, + filters ]) else: filters = cond @@ -1201,7 +1211,8 @@ def recursive_get_fields(_conf): } client.groupby(**pre_qry) query_str += "// Two phase query\n// Phase 1\n" - query_str += json.dumps(client.query_dict, indent=2) + "\n" + query_str += json.dumps( + client.query_builder.last_query.query_dict, indent=2) + "\n" query_str += "//\nPhase 2 (built based on phase one's results)\n" df = client.export_pandas() if df is not None and not df.empty: @@ -1210,11 +1221,11 @@ def recursive_get_fields(_conf): for unused, row in df.iterrows(): fields = [] for dim in dims: - f = Filter.build_filter(Dimension(dim) == row[dim]) + f = Dimension(dim) == row[dim] fields.append(f) if len(fields) > 1: filt = Filter(type="and", fields=fields) - filters.append(Filter.build_filter(filt)) + filters.append(filt) elif fields: filters.append(fields[0]) @@ -1224,8 +1235,8 @@ def recursive_get_fields(_conf): qry['filter'] = ff else: qry['filter'] = Filter(type="and", fields=[ - Filter.build_filter(ff), - Filter.build_filter(orig_filters)]) + ff, + orig_filters]) qry['limit_spec'] = None if row_limit: qry['limit_spec'] = { @@ -1237,7 +1248,8 @@ def recursive_get_fields(_conf): }], } client.groupby(**qry) - query_str += json.dumps(client.query_dict, indent=2) + query_str += json.dumps( + client.query_builder.last_query.query_dict, indent=2) df = client.export_pandas() if df is None or df.size == 0: raise Exception(_("No data was returned.")) @@ -1272,7 +1284,6 @@ class Log(Model): user_id = Column(Integer, ForeignKey('ab_user.id')) dashboard_id = Column(Integer) slice_id = Column(Integer) - user_id = Column(Integer, ForeignKey('ab_user.id')) json = Column(Text) user = relationship('User', backref='logs', foreign_keys=[user_id]) dttm = Column(DateTime, default=func.now()) diff --git a/caravel/templates/caravel/dashboard.html b/caravel/templates/caravel/dashboard.html index 5c8cbed67fb5..bde25e2031ad 100644 --- a/caravel/templates/caravel/dashboard.html +++ b/caravel/templates/caravel/dashboard.html @@ -6,6 +6,7 @@ {% endblock %} {% block title %}[dashboard] {{ dashboard.dashboard_title }}{% endblock %} {% block body %} +
@@ -97,71 +98,9 @@

- {% endblock %} diff --git a/caravel/templates/caravel/explore.html b/caravel/templates/caravel/explore.html index 64b27e87a0b0..5e12efeba199 100644 --- a/caravel/templates/caravel/explore.html +++ b/caravel/templates/caravel/explore.html @@ -16,10 +16,10 @@
{% set field = form.get_field(fieldname)%}
- {{ viz.get_form_override(fieldname, 'label') or field.label }} + {{ field.label }} {% if field.description %} + title="{{ field.description }}"> {% endif %} {{ field(class_=form.field_css_classes(field.name)) }}
diff --git a/caravel/translations/es/LC_MESSAGES/messages.mo b/caravel/translations/es/LC_MESSAGES/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..96bca730b584261edae8f417b2db8c849f0506a3 GIT binary patch literal 5877 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\n" "Language: es\n" @@ -18,87 +18,423 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.3.4\n" -#: caravel/models.py:564 +#: caravel/models.py:607 msgid "" "Datetime column not provided as part table configuration and is required " "by this type of chart" msgstr "" -#: caravel/models.py:1153 +#: caravel/models.py:1243 msgid "No data was returned." msgstr "" -#: caravel/views.py:116 +#: caravel/views.py:124 msgid "" "Whether to make this column available as a [Time Granularity] option, " "column has to be DATETIME or DATETIME-like" msgstr "" -#: caravel/views.py:215 +#: caravel/views.py:133 caravel/views.py:161 +msgid "Column" +msgstr "" + +#: caravel/views.py:134 caravel/views.py:194 caravel/views.py:223 +msgid "Verbose Name" +msgstr "" + +#: caravel/views.py:135 caravel/views.py:193 caravel/views.py:222 +#: caravel/views.py:400 caravel/views.py:535 +msgid "Description" +msgstr "" + +#: caravel/views.py:136 caravel/views.py:164 +msgid "Groupable" +msgstr "" + +#: caravel/views.py:137 caravel/views.py:165 +msgid "Filterable" +msgstr "" + +#: caravel/views.py:138 caravel/views.py:197 caravel/views.py:308 +#: caravel/views.py:406 +msgid "Table" +msgstr "" + +#: caravel/views.py:139 caravel/views.py:166 +msgid "Count Distinct" +msgstr "" + +#: caravel/views.py:140 caravel/views.py:167 +msgid "Sum" +msgstr "" + +#: caravel/views.py:141 caravel/views.py:168 +msgid "Min" +msgstr "" + +#: caravel/views.py:142 caravel/views.py:169 +msgid "Max" +msgstr "" + +#: caravel/views.py:143 +msgid "Expression" +msgstr "" + +#: caravel/views.py:144 +msgid "Is temporal" +msgstr "" + +#: caravel/views.py:162 caravel/views.py:195 caravel/views.py:224 +#: caravel/views.py:424 +msgid "Type" +msgstr "" + +#: caravel/views.py:163 caravel/views.py:399 +msgid "Datasource" +msgstr "" + +#: caravel/views.py:192 caravel/views.py:221 +msgid "Metric" +msgstr "" + +#: caravel/views.py:196 +msgid "SQL Expression" +msgstr "" + +#: caravel/views.py:225 caravel/views.py:503 +msgid "JSON" +msgstr "" + +#: caravel/views.py:226 +msgid "Druid Datasource" +msgstr "" + +#: caravel/views.py:257 caravel/views.py:310 +msgid "Database" +msgstr "" + +#: caravel/views.py:258 +msgid "SQL link" +msgstr "" + +#: caravel/views.py:259 caravel/views.py:397 caravel/views.py:459 +msgid "Creator" +msgstr "" + +#: caravel/views.py:260 caravel/views.py:311 +msgid "Last Changed" +msgstr "" + +#: caravel/views.py:261 +msgid "SQLAlchemy URI" +msgstr "" + +#: caravel/views.py:262 caravel/views.py:317 caravel/views.py:396 +#: caravel/views.py:541 +msgid "Cache Timeout" +msgstr "" + +#: caravel/views.py:263 +msgid "Extra" +msgstr "" + +#: caravel/views.py:279 msgid "Databases" msgstr "" -#: caravel/views.py:217 caravel/views.py:261 caravel/views.py:284 +#: caravel/views.py:281 caravel/views.py:337 caravel/views.py:369 msgid "Sources" msgstr "" -#: caravel/views.py:260 -msgid "Tables" +#: caravel/views.py:309 +msgid "Changed By" msgstr "" -#: caravel/views.py:282 -msgid "Druid Clusters" +#: caravel/views.py:312 +msgid "SQL Editor" msgstr "" -#: caravel/views.py:313 -msgid "Slices" +#: caravel/views.py:313 caravel/views.py:537 +msgid "Is Featured" +msgstr "" + +#: caravel/views.py:314 +msgid "Schema" +msgstr "" + +#: caravel/views.py:315 caravel/views.py:539 +msgid "Default Endpoint" +msgstr "" + +#: caravel/views.py:316 +msgid "Offset" +msgstr "" + +#: caravel/views.py:354 caravel/views.py:534 +msgid "Cluster" +msgstr "" + +#: caravel/views.py:355 +msgid "Coordinator Host" +msgstr "" + +#: caravel/views.py:356 +msgid "Coordinator Port" +msgstr "" + +#: caravel/views.py:357 +msgid "Coordinator Endpoint" +msgstr "" + +#: caravel/views.py:358 +msgid "Broker Host" +msgstr "" + +#: caravel/views.py:359 +msgid "Borker Port" +msgstr "" + +#: caravel/views.py:360 +msgid "Broker Endpoint" +msgstr "" + +#: caravel/views.py:398 caravel/views.py:479 +msgid "Dashboards" +msgstr "" + +#: caravel/views.py:401 +msgid "Last Modified" +msgstr "" + +#: caravel/views.py:402 caravel/views.py:458 +msgid "Owners" +msgstr "" + +#: caravel/views.py:403 +msgid "Parameters" +msgstr "" + +#: caravel/views.py:404 caravel/views.py:425 +msgid "Slice" +msgstr "" + +#: caravel/views.py:405 +msgid "Name" +msgstr "" + +#: caravel/views.py:407 caravel/views.py:426 +msgid "Visualization Type" msgstr "" -#: caravel/views.py:341 +#: caravel/views.py:441 msgid "" "This json object describes the positioning of the widgets in the " "dashboard. It is dynamically generated when adjusting the widgets size " "and positions by using drag & drop in the dashboard view" msgstr "" -#: caravel/views.py:346 +#: caravel/views.py:446 msgid "" "The css for individual dashboards can be altered here, or in the " "dashboard view where changes are immediately visible" msgstr "" -#: caravel/views.py:367 -msgid "Dashboards" +#: caravel/views.py:450 +msgid "To get a readable URL for your dashboard" +msgstr "" + +#: caravel/views.py:454 +msgid "Dashboard" +msgstr "" + +#: caravel/views.py:455 +msgid "Title" +msgstr "" + +#: caravel/views.py:456 +msgid "Slug" +msgstr "" + +#: caravel/views.py:457 +msgid "Slices" +msgstr "" + +#: caravel/views.py:460 +msgid "Modified" +msgstr "" + +#: caravel/views.py:461 +msgid "Position JSON" +msgstr "" + +#: caravel/views.py:462 +msgid "CSS" +msgstr "" + +#: caravel/views.py:463 +msgid "JSON Metadata" +msgstr "" + +#: caravel/views.py:500 +msgid "User" +msgstr "" + +#: caravel/views.py:501 +msgid "Action" msgstr "" -#: caravel/views.py:392 +#: caravel/views.py:502 +msgid "dttm" +msgstr "" + +#: caravel/views.py:509 msgid "Action Log" msgstr "" -#: caravel/views.py:393 +#: caravel/views.py:510 msgid "Security" msgstr "" -#: caravel/views.py:430 +#: caravel/views.py:527 +msgid "Timezone offset (in hours) for this datasource" +msgstr "" + +#: caravel/views.py:533 +msgid "Data Source" +msgstr "" + +#: caravel/views.py:536 +msgid "Owner" +msgstr "" + +#: caravel/views.py:538 +msgid "Is Hidden" +msgstr "" + +#: caravel/views.py:540 +msgid "Time Offset" +msgstr "" + +#: caravel/views.py:555 msgid "Druid Datasources" msgstr "" -#: caravel/views.py:514 +#: caravel/views.py:639 msgid "The datasource seems to have been deleted" msgstr "" -#: caravel/views.py:522 +#: caravel/views.py:647 msgid "You don't seem to have access to this datasource" msgstr "" -#: caravel/views.py:843 +#: caravel/views.py:970 msgid "This view requires the `all_datasource_access` permission" msgstr "" -#: caravel/views.py:954 +#: caravel/views.py:1081 msgid "CSS Templates" msgstr "" +#: caravel/viz.py:324 +msgid "Table View" +msgstr "" + +#: caravel/viz.py:385 +msgid "Pivot Table" +msgstr "" + +#: caravel/viz.py:447 +msgid "Markup" +msgstr "" + +#: caravel/viz.py:475 +msgid "Word Cloud" +msgstr "" + +#: caravel/viz.py:507 +msgid "Treemap" +msgstr "" + +#: caravel/viz.py:551 +msgid "Calender Heatmap" +msgstr "" + +#: caravel/viz.py:622 +msgid "Box Plot" +msgstr "" + +#: caravel/viz.py:729 +msgid "Bubble Chart" +msgstr "" + +#: caravel/viz.py:797 +msgid "Big Number with Trendline" +msgstr "" + +#: caravel/viz.py:847 +msgid "Big Number" +msgstr "" + +#: caravel/viz.py:893 +msgid "Time Series - Line Chart" +msgstr "" + +#: caravel/viz.py:1045 +msgid "Time Series - Bar Chart" +msgstr "" + +#: caravel/viz.py:1063 +msgid "Time Series - Percent Change" +msgstr "" + +#: caravel/viz.py:1071 +msgid "Time Series - Stacked" +msgstr "" + +#: caravel/viz.py:1090 +msgid "Distribution - NVD3 - Pie Chart" +msgstr "" + +#: caravel/viz.py:1126 +msgid "Distribution - Bar Chart" +msgstr "" + +#: caravel/viz.py:1206 +msgid "Sunburst" +msgstr "" + +#: caravel/viz.py:1272 +msgid "Sankey" +msgstr "" + +#: caravel/viz.py:1336 +msgid "Directed Force Layout" +msgstr "" + +#: caravel/viz.py:1378 +msgid "World Map" +msgstr "" + +#: caravel/viz.py:1452 +msgid "Filters" +msgstr "" + +#: caravel/viz.py:1500 +msgid "iFrame" +msgstr "" + +#: caravel/viz.py:1518 +msgid "Parallel Coordinates" +msgstr "" + +#: caravel/viz.py:1554 +msgid "Heatmap" +msgstr "" + +#: caravel/viz.py:1622 +msgid "Horizon Charts" +msgstr "" + #: caravel/templates/appbuilder/navbar_right.html:34 msgid "Profile" msgstr "" @@ -116,3 +452,9 @@ msgstr "" msgid "Welcome!" msgstr "" +#~ msgid "Tables" +#~ msgstr "" + +#~ msgid "Druid Clusters" +#~ msgstr "" + diff --git a/caravel/translations/fr/LC_MESSAGES/messages.mo b/caravel/translations/fr/LC_MESSAGES/messages.mo index 5a7c0e9bf19d22e7318e16bbe821dac28be9ed2b..4fdcc9a62a1e4bcb7a38a759123f941551c62dfe 100644 GIT binary patch literal 5950 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 I^|{jD0Zt%Dr2qf` delta 722 zcmcJ}KS%;$7{~F)GE+%GC5>pf#Y%`!w{kKYA{vSlY7)-#%!AX@`x69~Q%yA;K}B;= zTRBD{G&Ho<+E!Cb5Hz&(d*X&@XiK>JypQkQd*7SsqgdxD>OCO*61c{2J@D}3x91@W zV%~=#c<9cjFo5|2mf#f(!_hvX2u#5Nn1eo;hr@6I4#E=j6LqNSKA^iB8}7UfZ9KRG zQ}7e^V6vZR2EITXK!TU32~#i)k0D3jLk?fV*Ew8`!Nfbb228H5J<0#;ocPswd-g}DqsH4@_yxc7l4t+` diff --git a/caravel/translations/fr/LC_MESSAGES/messages.po b/caravel/translations/fr/LC_MESSAGES/messages.po index 4fad2485ec85..f7556c9c1188 100644 --- a/caravel/translations/fr/LC_MESSAGES/messages.po +++ b/caravel/translations/fr/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2016-05-02 00:21-0700\n" +"POT-Creation-Date: 2016-05-20 20:30-0700\n" "PO-Revision-Date: 2016-05-01 23:07-0700\n" "Last-Translator: FULL NAME \n" "Language: fr\n" @@ -18,87 +18,423 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.3.4\n" -#: caravel/models.py:564 +#: caravel/models.py:607 msgid "" "Datetime column not provided as part table configuration and is required " "by this type of chart" msgstr "" -#: caravel/models.py:1153 +#: caravel/models.py:1243 msgid "No data was returned." msgstr "" -#: caravel/views.py:116 +#: caravel/views.py:124 msgid "" "Whether to make this column available as a [Time Granularity] option, " "column has to be DATETIME or DATETIME-like" msgstr "" -#: caravel/views.py:215 -msgid "Databases" +#: caravel/views.py:133 caravel/views.py:161 +msgid "Column" +msgstr "Colonne" + +#: caravel/views.py:134 caravel/views.py:194 caravel/views.py:223 +msgid "Verbose Name" +msgstr "Nom Complet" + +#: caravel/views.py:135 caravel/views.py:193 caravel/views.py:222 +#: caravel/views.py:400 caravel/views.py:535 +msgid "Description" +msgstr "" + +#: caravel/views.py:136 caravel/views.py:164 +msgid "Groupable" +msgstr "" + +#: caravel/views.py:137 caravel/views.py:165 +msgid "Filterable" +msgstr "Filtrable" + +#: caravel/views.py:138 caravel/views.py:197 caravel/views.py:308 +#: caravel/views.py:406 +msgid "Table" +msgstr "" + +#: caravel/views.py:139 caravel/views.py:166 +msgid "Count Distinct" +msgstr "" + +#: caravel/views.py:140 caravel/views.py:167 +msgid "Sum" +msgstr "Somme" + +#: caravel/views.py:141 caravel/views.py:168 +msgid "Min" +msgstr "" + +#: caravel/views.py:142 caravel/views.py:169 +msgid "Max" +msgstr "" + +#: caravel/views.py:143 +msgid "Expression" +msgstr "" + +#: caravel/views.py:144 +msgid "Is temporal" +msgstr "Est temporel" + +#: caravel/views.py:162 caravel/views.py:195 caravel/views.py:224 +#: caravel/views.py:424 +msgid "Type" +msgstr "" + +#: caravel/views.py:163 caravel/views.py:399 +msgid "Datasource" +msgstr "Source de données" + +#: caravel/views.py:192 caravel/views.py:221 +msgid "Metric" +msgstr "" + +#: caravel/views.py:196 +msgid "SQL Expression" +msgstr "" + +#: caravel/views.py:225 caravel/views.py:503 +msgid "JSON" +msgstr "" + +#: caravel/views.py:226 +msgid "Druid Datasource" +msgstr "Source de données Druid" + +#: caravel/views.py:257 caravel/views.py:310 +msgid "Database" +msgstr "Base de données" + +#: caravel/views.py:258 +msgid "SQL link" +msgstr "Lien SQL" + +#: caravel/views.py:259 caravel/views.py:397 caravel/views.py:459 +msgid "Creator" +msgstr "Createur" + +#: caravel/views.py:260 caravel/views.py:311 +msgid "Last Changed" +msgstr "Modifié" + +#: caravel/views.py:261 +msgid "SQLAlchemy URI" +msgstr "" + +#: caravel/views.py:262 caravel/views.py:317 caravel/views.py:396 +#: caravel/views.py:541 +msgid "Cache Timeout" +msgstr "" + +#: caravel/views.py:263 +msgid "Extra" msgstr "" -#: caravel/views.py:217 caravel/views.py:261 caravel/views.py:284 +#: caravel/views.py:279 +msgid "Databases" +msgstr "Base de Données" + +#: caravel/views.py:281 caravel/views.py:337 caravel/views.py:369 msgid "Sources" msgstr "" -#: caravel/views.py:260 -msgid "Tables" +#: caravel/views.py:309 +msgid "Changed By" msgstr "" -#: caravel/views.py:282 -msgid "Druid Clusters" +#: caravel/views.py:312 +msgid "SQL Editor" msgstr "" -#: caravel/views.py:313 -msgid "Slices" +#: caravel/views.py:313 caravel/views.py:537 +msgid "Is Featured" +msgstr "" + +#: caravel/views.py:314 +msgid "Schema" +msgstr "" + +#: caravel/views.py:315 caravel/views.py:539 +msgid "Default Endpoint" +msgstr "" + +#: caravel/views.py:316 +msgid "Offset" +msgstr "" + +#: caravel/views.py:354 caravel/views.py:534 +msgid "Cluster" +msgstr "" + +#: caravel/views.py:355 +msgid "Coordinator Host" +msgstr "" + +#: caravel/views.py:356 +msgid "Coordinator Port" +msgstr "" + +#: caravel/views.py:357 +msgid "Coordinator Endpoint" +msgstr "" + +#: caravel/views.py:358 +msgid "Broker Host" msgstr "" -#: caravel/views.py:341 +#: caravel/views.py:359 +msgid "Borker Port" +msgstr "" + +#: caravel/views.py:360 +msgid "Broker Endpoint" +msgstr "" + +#: caravel/views.py:398 caravel/views.py:479 +msgid "Dashboards" +msgstr "" + +#: caravel/views.py:401 +msgid "Last Modified" +msgstr "" + +#: caravel/views.py:402 caravel/views.py:458 +msgid "Owners" +msgstr "" + +#: caravel/views.py:403 +msgid "Parameters" +msgstr "" + +#: caravel/views.py:404 caravel/views.py:425 +msgid "Slice" +msgstr "Graphique" + +#: caravel/views.py:405 +msgid "Name" +msgstr "Nom" + +#: caravel/views.py:407 caravel/views.py:426 +msgid "Visualization Type" +msgstr "Type de visualization" + +#: caravel/views.py:441 msgid "" "This json object describes the positioning of the widgets in the " "dashboard. It is dynamically generated when adjusting the widgets size " "and positions by using drag & drop in the dashboard view" msgstr "" -#: caravel/views.py:346 +#: caravel/views.py:446 msgid "" "The css for individual dashboards can be altered here, or in the " "dashboard view where changes are immediately visible" msgstr "" -#: caravel/views.py:367 -msgid "Dashboards" +#: caravel/views.py:450 +msgid "To get a readable URL for your dashboard" msgstr "" -#: caravel/views.py:392 +#: caravel/views.py:454 +msgid "Dashboard" +msgstr "Tableau de Bord" + +#: caravel/views.py:455 +msgid "Title" +msgstr "Titre" + +#: caravel/views.py:456 +msgid "Slug" +msgstr "" + +#: caravel/views.py:457 +msgid "Slices" +msgstr "Graphiques" + +#: caravel/views.py:460 +msgid "Modified" +msgstr "Modifié" + +#: caravel/views.py:461 +msgid "Position JSON" +msgstr "" + +#: caravel/views.py:462 +msgid "CSS" +msgstr "" + +#: caravel/views.py:463 +msgid "JSON Metadata" +msgstr "" + +#: caravel/views.py:500 +msgid "User" +msgstr "Utilisateur" + +#: caravel/views.py:501 +msgid "Action" +msgstr "" + +#: caravel/views.py:502 +msgid "dttm" +msgstr "" + +#: caravel/views.py:509 msgid "Action Log" msgstr "" -#: caravel/views.py:393 +#: caravel/views.py:510 msgid "Security" msgstr "Securité" -#: caravel/views.py:430 -msgid "Druid Datasources" +#: caravel/views.py:527 +msgid "Timezone offset (in hours) for this datasource" msgstr "" -#: caravel/views.py:514 -msgid "The datasource seems to have been deleted" +#: caravel/views.py:533 +msgid "Data Source" +msgstr "Source de Données" + +#: caravel/views.py:536 +msgid "Owner" +msgstr "Propriétair" + +#: caravel/views.py:538 +msgid "Is Hidden" +msgstr "Caché" + +#: caravel/views.py:540 +msgid "Time Offset" msgstr "" -#: caravel/views.py:522 +#: caravel/views.py:555 +msgid "Druid Datasources" +msgstr "Source de données Druid" + +#: caravel/views.py:639 +msgid "The datasource seems to have been deleted" +msgstr "Cette source semble seche" + +#: caravel/views.py:647 msgid "You don't seem to have access to this datasource" -msgstr "" +msgstr "Vous n'avez pas acces a cette source de donnees" -#: caravel/views.py:843 +#: caravel/views.py:970 msgid "This view requires the `all_datasource_access` permission" msgstr "" -#: caravel/views.py:954 +#: caravel/views.py:1081 msgid "CSS Templates" msgstr "" +#: caravel/viz.py:324 +msgid "Table View" +msgstr "" + +#: caravel/viz.py:385 +msgid "Pivot Table" +msgstr "" + +#: caravel/viz.py:447 +msgid "Markup" +msgstr "" + +#: caravel/viz.py:475 +msgid "Word Cloud" +msgstr "Nuage de Mots" + +#: caravel/viz.py:507 +msgid "Treemap" +msgstr "" + +#: caravel/viz.py:551 +msgid "Calender Heatmap" +msgstr "" + +#: caravel/viz.py:622 +msgid "Box Plot" +msgstr "" + +#: caravel/viz.py:729 +msgid "Bubble Chart" +msgstr "Graphique en Bulles" + +#: caravel/viz.py:797 +msgid "Big Number with Trendline" +msgstr "" + +#: caravel/viz.py:847 +msgid "Big Number" +msgstr "Gros Chiffre" + +#: caravel/viz.py:893 +msgid "Time Series - Line Chart" +msgstr "" + +#: caravel/viz.py:1045 +msgid "Time Series - Bar Chart" +msgstr "" + +#: caravel/viz.py:1063 +msgid "Time Series - Percent Change" +msgstr "" + +#: caravel/viz.py:1071 +msgid "Time Series - Stacked" +msgstr "" + +#: caravel/viz.py:1090 +msgid "Distribution - NVD3 - Pie Chart" +msgstr "" + +#: caravel/viz.py:1126 +msgid "Distribution - Bar Chart" +msgstr "" + +#: caravel/viz.py:1206 +msgid "Sunburst" +msgstr "" + +#: caravel/viz.py:1272 +msgid "Sankey" +msgstr "" + +#: caravel/viz.py:1336 +msgid "Directed Force Layout" +msgstr "" + +#: caravel/viz.py:1378 +msgid "World Map" +msgstr "Carte du monde" + +#: caravel/viz.py:1452 +msgid "Filters" +msgstr "Filtres" + +#: caravel/viz.py:1500 +msgid "iFrame" +msgstr "" + +#: caravel/viz.py:1518 +msgid "Parallel Coordinates" +msgstr "" + +#: caravel/viz.py:1554 +msgid "Heatmap" +msgstr "" + +#: caravel/viz.py:1622 +msgid "Horizon Charts" +msgstr "" + #: caravel/templates/appbuilder/navbar_right.html:34 msgid "Profile" msgstr "" @@ -116,3 +452,9 @@ msgstr "" msgid "Welcome!" msgstr "Bienvenue!" +#~ msgid "Tables" +#~ msgstr "" + +#~ msgid "Druid Clusters" +#~ msgstr "" + diff --git a/caravel/translations/it/LC_MESSAGES/messages.mo b/caravel/translations/it/LC_MESSAGES/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..034d5c86e1d5b786cbce880c51da60d86a84016f GIT binary patch literal 5975 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, 2016. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2016-05-20 20:30-0700\n" +"PO-Revision-Date: 2016-05-19 16:43+0200\n" +"Last-Translator: Riccardo Magliocchetti " +"\n" +"Language: it\n" +"Language-Team: it \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.3.4\n" + +#: caravel/models.py:607 +msgid "" +"Datetime column not provided as part table configuration and is required " +"by this type of chart" +msgstr "" +"La colonna di tipo datetime non è stata fornita nella configurazione " +"della tabella ma richiesta da questo tipo di grafico" + +#: caravel/models.py:1243 +msgid "No data was returned." +msgstr "Nessun dato disponibile." + +#: caravel/views.py:124 +msgid "" +"Whether to make this column available as a [Time Granularity] option, " +"column has to be DATETIME or DATETIME-like" +msgstr "" +"Rendi questa colonna disponibile come una opzione [Time Granularity], la " +"colonna deve essere DATATIME o DATETIME-like" + +#: caravel/views.py:133 caravel/views.py:161 +msgid "Column" +msgstr "" + +#: caravel/views.py:134 caravel/views.py:194 caravel/views.py:223 +msgid "Verbose Name" +msgstr "" + +#: caravel/views.py:135 caravel/views.py:193 caravel/views.py:222 +#: caravel/views.py:400 caravel/views.py:535 +msgid "Description" +msgstr "" + +#: caravel/views.py:136 caravel/views.py:164 +msgid "Groupable" +msgstr "" + +#: caravel/views.py:137 caravel/views.py:165 +msgid "Filterable" +msgstr "" + +#: caravel/views.py:138 caravel/views.py:197 caravel/views.py:308 +#: caravel/views.py:406 +msgid "Table" +msgstr "" + +#: caravel/views.py:139 caravel/views.py:166 +msgid "Count Distinct" +msgstr "" + +#: caravel/views.py:140 caravel/views.py:167 +msgid "Sum" +msgstr "" + +#: caravel/views.py:141 caravel/views.py:168 +msgid "Min" +msgstr "" + +#: caravel/views.py:142 caravel/views.py:169 +msgid "Max" +msgstr "" + +#: caravel/views.py:143 +msgid "Expression" +msgstr "" + +#: caravel/views.py:144 +msgid "Is temporal" +msgstr "" + +#: caravel/views.py:162 caravel/views.py:195 caravel/views.py:224 +#: caravel/views.py:424 +msgid "Type" +msgstr "" + +#: caravel/views.py:163 caravel/views.py:399 +msgid "Datasource" +msgstr "" + +#: caravel/views.py:192 caravel/views.py:221 +msgid "Metric" +msgstr "" + +#: caravel/views.py:196 +msgid "SQL Expression" +msgstr "" + +#: caravel/views.py:225 caravel/views.py:503 +msgid "JSON" +msgstr "" + +#: caravel/views.py:226 +msgid "Druid Datasource" +msgstr "" + +#: caravel/views.py:257 caravel/views.py:310 +msgid "Database" +msgstr "" + +#: caravel/views.py:258 +msgid "SQL link" +msgstr "" + +#: caravel/views.py:259 caravel/views.py:397 caravel/views.py:459 +msgid "Creator" +msgstr "" + +#: caravel/views.py:260 caravel/views.py:311 +msgid "Last Changed" +msgstr "" + +#: caravel/views.py:261 +msgid "SQLAlchemy URI" +msgstr "" + +#: caravel/views.py:262 caravel/views.py:317 caravel/views.py:396 +#: caravel/views.py:541 +msgid "Cache Timeout" +msgstr "" + +#: caravel/views.py:263 +msgid "Extra" +msgstr "" + +#: caravel/views.py:279 +msgid "Databases" +msgstr "Database" + +#: caravel/views.py:281 caravel/views.py:337 caravel/views.py:369 +msgid "Sources" +msgstr "Sorgenti" + +#: caravel/views.py:309 +msgid "Changed By" +msgstr "" + +#: caravel/views.py:312 +msgid "SQL Editor" +msgstr "" + +#: caravel/views.py:313 caravel/views.py:537 +msgid "Is Featured" +msgstr "" + +#: caravel/views.py:314 +msgid "Schema" +msgstr "" + +#: caravel/views.py:315 caravel/views.py:539 +msgid "Default Endpoint" +msgstr "" + +#: caravel/views.py:316 +msgid "Offset" +msgstr "" + +#: caravel/views.py:354 caravel/views.py:534 +msgid "Cluster" +msgstr "" + +#: caravel/views.py:355 +msgid "Coordinator Host" +msgstr "" + +#: caravel/views.py:356 +msgid "Coordinator Port" +msgstr "" + +#: caravel/views.py:357 +msgid "Coordinator Endpoint" +msgstr "" + +#: caravel/views.py:358 +msgid "Broker Host" +msgstr "" + +#: caravel/views.py:359 +msgid "Borker Port" +msgstr "" + +#: caravel/views.py:360 +msgid "Broker Endpoint" +msgstr "" + +#: caravel/views.py:398 caravel/views.py:479 +msgid "Dashboards" +msgstr "Dashboard" + +#: caravel/views.py:401 +msgid "Last Modified" +msgstr "" + +#: caravel/views.py:402 caravel/views.py:458 +msgid "Owners" +msgstr "" + +#: caravel/views.py:403 +msgid "Parameters" +msgstr "" + +#: caravel/views.py:404 caravel/views.py:425 +msgid "Slice" +msgstr "" + +#: caravel/views.py:405 +msgid "Name" +msgstr "" + +#: caravel/views.py:407 caravel/views.py:426 +msgid "Visualization Type" +msgstr "" + +#: caravel/views.py:441 +msgid "" +"This json object describes the positioning of the widgets in the " +"dashboard. It is dynamically generated when adjusting the widgets size " +"and positions by using drag & drop in the dashboard view" +msgstr "" +"Questo oggetto json descrive il posizionamento dei widget nella " +"dashboard.E` generata dinamicamente quando vengono cambiate la dimensioni" +" dei widget o laposizione tramite il drag&drop nella vista dashboard." + +#: caravel/views.py:446 +msgid "" +"The css for individual dashboards can be altered here, or in the " +"dashboard view where changes are immediately visible" +msgstr "" +"Il css per ogni dashboard può essere modificato qui o nella vistdashboard" +" dove i cambiamenti sono visibili immediatamente" + +#: caravel/views.py:450 +msgid "To get a readable URL for your dashboard" +msgstr "" + +#: caravel/views.py:454 +msgid "Dashboard" +msgstr "" + +#: caravel/views.py:455 +msgid "Title" +msgstr "" + +#: caravel/views.py:456 +msgid "Slug" +msgstr "" + +#: caravel/views.py:457 +msgid "Slices" +msgstr "Slice" + +#: caravel/views.py:460 +msgid "Modified" +msgstr "" + +#: caravel/views.py:461 +msgid "Position JSON" +msgstr "" + +#: caravel/views.py:462 +msgid "CSS" +msgstr "" + +#: caravel/views.py:463 +msgid "JSON Metadata" +msgstr "" + +#: caravel/views.py:500 +msgid "User" +msgstr "" + +#: caravel/views.py:501 +msgid "Action" +msgstr "" + +#: caravel/views.py:502 +msgid "dttm" +msgstr "" + +#: caravel/views.py:509 +msgid "Action Log" +msgstr "Log delle azioni" + +#: caravel/views.py:510 +msgid "Security" +msgstr "Sicurezza" + +#: caravel/views.py:527 +msgid "Timezone offset (in hours) for this datasource" +msgstr "" + +#: caravel/views.py:533 +msgid "Data Source" +msgstr "" + +#: caravel/views.py:536 +msgid "Owner" +msgstr "" + +#: caravel/views.py:538 +msgid "Is Hidden" +msgstr "" + +#: caravel/views.py:540 +msgid "Time Offset" +msgstr "" + +#: caravel/views.py:555 +msgid "Druid Datasources" +msgstr "Datasource Druid" + +#: caravel/views.py:639 +msgid "The datasource seems to have been deleted" +msgstr "Sembra che il datasource sia stato eliminato" + +#: caravel/views.py:647 +msgid "You don't seem to have access to this datasource" +msgstr "Non hai i permessi per accedere a questo datasource" + +#: caravel/views.py:970 +msgid "This view requires the `all_datasource_access` permission" +msgstr "Questa vista richiede il permesso `all_datasource_access`" + +#: caravel/views.py:1081 +msgid "CSS Templates" +msgstr "Template CSS" + +#: caravel/viz.py:324 +msgid "Table View" +msgstr "" + +#: caravel/viz.py:385 +msgid "Pivot Table" +msgstr "" + +#: caravel/viz.py:447 +msgid "Markup" +msgstr "" + +#: caravel/viz.py:475 +msgid "Word Cloud" +msgstr "" + +#: caravel/viz.py:507 +msgid "Treemap" +msgstr "" + +#: caravel/viz.py:551 +msgid "Calender Heatmap" +msgstr "" + +#: caravel/viz.py:622 +msgid "Box Plot" +msgstr "" + +#: caravel/viz.py:729 +msgid "Bubble Chart" +msgstr "" + +#: caravel/viz.py:797 +msgid "Big Number with Trendline" +msgstr "" + +#: caravel/viz.py:847 +msgid "Big Number" +msgstr "" + +#: caravel/viz.py:893 +msgid "Time Series - Line Chart" +msgstr "" + +#: caravel/viz.py:1045 +msgid "Time Series - Bar Chart" +msgstr "" + +#: caravel/viz.py:1063 +msgid "Time Series - Percent Change" +msgstr "" + +#: caravel/viz.py:1071 +msgid "Time Series - Stacked" +msgstr "" + +#: caravel/viz.py:1090 +msgid "Distribution - NVD3 - Pie Chart" +msgstr "" + +#: caravel/viz.py:1126 +msgid "Distribution - Bar Chart" +msgstr "" + +#: caravel/viz.py:1206 +msgid "Sunburst" +msgstr "" + +#: caravel/viz.py:1272 +msgid "Sankey" +msgstr "" + +#: caravel/viz.py:1336 +msgid "Directed Force Layout" +msgstr "" + +#: caravel/viz.py:1378 +msgid "World Map" +msgstr "" + +#: caravel/viz.py:1452 +msgid "Filters" +msgstr "" + +#: caravel/viz.py:1500 +msgid "iFrame" +msgstr "" + +#: caravel/viz.py:1518 +msgid "Parallel Coordinates" +msgstr "" + +#: caravel/viz.py:1554 +msgid "Heatmap" +msgstr "" + +#: caravel/viz.py:1622 +msgid "Horizon Charts" +msgstr "" + +#: caravel/templates/appbuilder/navbar_right.html:34 +msgid "Profile" +msgstr "Profilo" + +#: caravel/templates/appbuilder/navbar_right.html:35 +msgid "Logout" +msgstr "Logout" + +#: caravel/templates/appbuilder/navbar_right.html:40 +msgid "Login" +msgstr "Login" + +#: caravel/templates/caravel/welcome.html:8 +#: caravel/templates/caravel/welcome.html:13 +msgid "Welcome!" +msgstr "Benvenuto!" + +#~ msgid "Tables" +#~ msgstr "Tabelle" + +#~ msgid "Druid Clusters" +#~ msgstr "Cluster Druid" + diff --git a/caravel/translations/zh/LC_MESSAGES/messages.mo b/caravel/translations/zh/LC_MESSAGES/messages.mo index 397a1eee9f858dd550d503e75720acf48b8e480d..9cebcf132f375aa39938767973824eeb82e580cc 100644 GIT binary patch literal 5674 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 Apa1{> diff --git a/caravel/translations/zh/LC_MESSAGES/messages.po b/caravel/translations/zh/LC_MESSAGES/messages.po index 3a9bdece3621..9961cc692f2b 100644 --- a/caravel/translations/zh/LC_MESSAGES/messages.po +++ b/caravel/translations/zh/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2016-05-02 00:21-0700\n" +"POT-Creation-Date: 2016-05-20 20:30-0700\n" "PO-Revision-Date: 2016-05-01 23:07-0700\n" "Last-Translator: FULL NAME \n" "Language: zh\n" @@ -18,87 +18,425 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.3.4\n" -#: caravel/models.py:564 +#: caravel/models.py:607 msgid "" "Datetime column not provided as part table configuration and is required " "by this type of chart" msgstr "所选表格需要日期时间但在表格配置文件中没有被提供" -#: caravel/models.py:1153 +#: caravel/models.py:1243 msgid "No data was returned." msgstr "所选数据为空" -#: caravel/views.py:116 +#: caravel/views.py:124 msgid "" "Whether to make this column available as a [Time Granularity] option, " "column has to be DATETIME or DATETIME-like" +msgstr "是否要让这列接受[Time Granularity]的选项," +"这列必须是DATETIME或DATETIME-like" + +#: caravel/views.py:133 caravel/views.py:161 +msgid "Column" +msgstr "列" + +#: caravel/views.py:134 caravel/views.py:194 caravel/views.py:223 +msgid "Verbose Name" +msgstr "详细名称" + +#: caravel/views.py:135 caravel/views.py:193 caravel/views.py:222 +#: caravel/views.py:400 caravel/views.py:535 +msgid "Description" +msgstr "描述" + +#: caravel/views.py:136 caravel/views.py:164 +msgid "Groupable" +msgstr "可分组的" + +#: caravel/views.py:137 caravel/views.py:165 +msgid "Filterable" +msgstr "" + +#: caravel/views.py:138 caravel/views.py:197 caravel/views.py:308 +#: caravel/views.py:406 +msgid "Table" +msgstr "" + +#: caravel/views.py:139 caravel/views.py:166 +msgid "Count Distinct" +msgstr "" + +#: caravel/views.py:140 caravel/views.py:167 +msgid "Sum" +msgstr "" + +#: caravel/views.py:141 caravel/views.py:168 +msgid "Min" +msgstr "" + +#: caravel/views.py:142 caravel/views.py:169 +msgid "Max" +msgstr "" + +#: caravel/views.py:143 +msgid "Expression" +msgstr "" + +#: caravel/views.py:144 +msgid "Is temporal" +msgstr "" + +#: caravel/views.py:162 caravel/views.py:195 caravel/views.py:224 +#: caravel/views.py:424 +msgid "Type" +msgstr "" + +#: caravel/views.py:163 caravel/views.py:399 +msgid "Datasource" +msgstr "" + +#: caravel/views.py:192 caravel/views.py:221 +msgid "Metric" +msgstr "" + +#: caravel/views.py:196 +msgid "SQL Expression" +msgstr "" + +#: caravel/views.py:225 caravel/views.py:503 +msgid "JSON" +msgstr "" + +#: caravel/views.py:226 +msgid "Druid Datasource" +msgstr "" + +#: caravel/views.py:257 caravel/views.py:310 +msgid "Database" msgstr "" -#: caravel/views.py:215 +#: caravel/views.py:258 +msgid "SQL link" +msgstr "" + +#: caravel/views.py:259 caravel/views.py:397 caravel/views.py:459 +msgid "Creator" +msgstr "" + +#: caravel/views.py:260 caravel/views.py:311 +msgid "Last Changed" +msgstr "" + +#: caravel/views.py:261 +msgid "SQLAlchemy URI" +msgstr "" + +#: caravel/views.py:262 caravel/views.py:317 caravel/views.py:396 +#: caravel/views.py:541 +msgid "Cache Timeout" +msgstr "" + +#: caravel/views.py:263 +msgid "Extra" +msgstr "" + +#: caravel/views.py:279 msgid "Databases" msgstr "数据库" -#: caravel/views.py:217 caravel/views.py:261 caravel/views.py:284 +#: caravel/views.py:281 caravel/views.py:337 caravel/views.py:369 msgid "Sources" msgstr "源" -#: caravel/views.py:260 -msgid "Tables" -msgstr "表格" +#: caravel/views.py:309 +msgid "Changed By" +msgstr "" -#: caravel/views.py:282 -msgid "Druid Clusters" -msgstr "Druid簇" +#: caravel/views.py:312 +msgid "SQL Editor" +msgstr "" -#: caravel/views.py:313 -msgid "Slices" -msgstr "切片" +#: caravel/views.py:313 caravel/views.py:537 +msgid "Is Featured" +msgstr "" + +#: caravel/views.py:314 +msgid "Schema" +msgstr "" + +#: caravel/views.py:315 caravel/views.py:539 +msgid "Default Endpoint" +msgstr "" + +#: caravel/views.py:316 +msgid "Offset" +msgstr "" + +#: caravel/views.py:354 caravel/views.py:534 +msgid "Cluster" +msgstr "" + +#: caravel/views.py:355 +msgid "Coordinator Host" +msgstr "" + +#: caravel/views.py:356 +msgid "Coordinator Port" +msgstr "" + +#: caravel/views.py:357 +msgid "Coordinator Endpoint" +msgstr "" + +#: caravel/views.py:358 +msgid "Broker Host" +msgstr "" -#: caravel/views.py:341 +#: caravel/views.py:359 +msgid "Borker Port" +msgstr "" + +#: caravel/views.py:360 +msgid "Broker Endpoint" +msgstr "" + +#: caravel/views.py:398 caravel/views.py:479 +msgid "Dashboards" +msgstr "仪表盘" + +#: caravel/views.py:401 +msgid "Last Modified" +msgstr "" + +#: caravel/views.py:402 caravel/views.py:458 +msgid "Owners" +msgstr "" + +#: caravel/views.py:403 +msgid "Parameters" +msgstr "" + +#: caravel/views.py:404 caravel/views.py:425 +msgid "Slice" +msgstr "" + +#: caravel/views.py:405 +msgid "Name" +msgstr "" + +#: caravel/views.py:407 caravel/views.py:426 +msgid "Visualization Type" +msgstr "" + +#: caravel/views.py:441 msgid "" "This json object describes the positioning of the widgets in the " "dashboard. It is dynamically generated when adjusting the widgets size " "and positions by using drag & drop in the dashboard view" -msgstr "" +msgstr "这个json描述了部件在面板中的位置。 当通过拖拽来" +"改变窗口大小和位子的时候,它会被自动生成。" -#: caravel/views.py:346 +#: caravel/views.py:446 msgid "" "The css for individual dashboards can be altered here, or in the " "dashboard view where changes are immediately visible" +msgstr "单独面板的css可以在此变更,或者在面板视窗(立即生效)" + +#: caravel/views.py:450 +msgid "To get a readable URL for your dashboard" msgstr "" -#: caravel/views.py:367 -msgid "Dashboards" -msgstr "仪表盘" +#: caravel/views.py:454 +msgid "Dashboard" +msgstr "" + +#: caravel/views.py:455 +msgid "Title" +msgstr "" + +#: caravel/views.py:456 +msgid "Slug" +msgstr "" + +#: caravel/views.py:457 +msgid "Slices" +msgstr "切片" + +#: caravel/views.py:460 +msgid "Modified" +msgstr "" + +#: caravel/views.py:461 +msgid "Position JSON" +msgstr "" + +#: caravel/views.py:462 +msgid "CSS" +msgstr "" + +#: caravel/views.py:463 +msgid "JSON Metadata" +msgstr "" + +#: caravel/views.py:500 +msgid "User" +msgstr "" + +#: caravel/views.py:501 +msgid "Action" +msgstr "" -#: caravel/views.py:392 +#: caravel/views.py:502 +msgid "dttm" +msgstr "" + +#: caravel/views.py:509 msgid "Action Log" msgstr "行动记录" -#: caravel/views.py:393 +#: caravel/views.py:510 msgid "Security" msgstr "权限" -#: caravel/views.py:430 +#: caravel/views.py:527 +msgid "Timezone offset (in hours) for this datasource" +msgstr "" + +#: caravel/views.py:533 +msgid "Data Source" +msgstr "" + +#: caravel/views.py:536 +msgid "Owner" +msgstr "" + +#: caravel/views.py:538 +msgid "Is Hidden" +msgstr "" + +#: caravel/views.py:540 +msgid "Time Offset" +msgstr "" + +#: caravel/views.py:555 msgid "Druid Datasources" msgstr "Druid数据源" -#: caravel/views.py:514 +#: caravel/views.py:639 msgid "The datasource seems to have been deleted" msgstr "此数据源好像已被删除" -#: caravel/views.py:522 +#: caravel/views.py:647 msgid "You don't seem to have access to this datasource" msgstr "看来您不能读取此数据源" -#: caravel/views.py:843 +#: caravel/views.py:970 msgid "This view requires the `all_datasource_access` permission" msgstr "此视图需要`all_datasource_access`权限" -#: caravel/views.py:954 +#: caravel/views.py:1081 msgid "CSS Templates" msgstr "CSS模板" +#: caravel/viz.py:324 +msgid "Table View" +msgstr "" + +#: caravel/viz.py:385 +msgid "Pivot Table" +msgstr "" + +#: caravel/viz.py:447 +msgid "Markup" +msgstr "" + +#: caravel/viz.py:475 +msgid "Word Cloud" +msgstr "" + +#: caravel/viz.py:507 +msgid "Treemap" +msgstr "" + +#: caravel/viz.py:551 +msgid "Calender Heatmap" +msgstr "" + +#: caravel/viz.py:622 +msgid "Box Plot" +msgstr "" + +#: caravel/viz.py:729 +msgid "Bubble Chart" +msgstr "" + +#: caravel/viz.py:797 +msgid "Big Number with Trendline" +msgstr "" + +#: caravel/viz.py:847 +msgid "Big Number" +msgstr "" + +#: caravel/viz.py:893 +msgid "Time Series - Line Chart" +msgstr "" + +#: caravel/viz.py:1045 +msgid "Time Series - Bar Chart" +msgstr "" + +#: caravel/viz.py:1063 +msgid "Time Series - Percent Change" +msgstr "" + +#: caravel/viz.py:1071 +msgid "Time Series - Stacked" +msgstr "" + +#: caravel/viz.py:1090 +msgid "Distribution - NVD3 - Pie Chart" +msgstr "" + +#: caravel/viz.py:1126 +msgid "Distribution - Bar Chart" +msgstr "" + +#: caravel/viz.py:1206 +msgid "Sunburst" +msgstr "" + +#: caravel/viz.py:1272 +msgid "Sankey" +msgstr "" + +#: caravel/viz.py:1336 +msgid "Directed Force Layout" +msgstr "" + +#: caravel/viz.py:1378 +msgid "World Map" +msgstr "" + +#: caravel/viz.py:1452 +msgid "Filters" +msgstr "" + +#: caravel/viz.py:1500 +msgid "iFrame" +msgstr "" + +#: caravel/viz.py:1518 +msgid "Parallel Coordinates" +msgstr "" + +#: caravel/viz.py:1554 +msgid "Heatmap" +msgstr "" + +#: caravel/viz.py:1622 +msgid "Horizon Charts" +msgstr "" + #: caravel/templates/appbuilder/navbar_right.html:34 msgid "Profile" msgstr "个人资料" @@ -115,3 +453,10 @@ msgstr "登录" #: caravel/templates/caravel/welcome.html:13 msgid "Welcome!" msgstr "欢迎" + +#~ msgid "Tables" +#~ msgstr "表格" + +#~ msgid "Druid Clusters" +#~ msgstr "Druid簇" + diff --git a/caravel/utils.py b/caravel/utils.py index 7e3d4c079f50..9a48b42a6736 100644 --- a/caravel/utils.py +++ b/caravel/utils.py @@ -7,16 +7,27 @@ import functools import json import logging +import numpy from datetime import datetime import parsedatetime +import sqlalchemy as sa from dateutil.parser import parse +from alembic import op from flask import flash, Markup from flask_appbuilder.security.sqla import models as ab_models from markdown import markdown as md from sqlalchemy.types import TypeDecorator, TEXT +class CaravelException(Exception): + pass + + +class CaravelSecurityException(CaravelException): + pass + + def flasher(msg, severity=None): """Flask's flash if available, logging call if not""" try: @@ -221,6 +232,12 @@ def json_iso_dttm_ser(obj): """ if isinstance(obj, datetime): obj = obj.isoformat() + elif isinstance(obj, numpy.int64): + obj = int(obj) + else: + raise TypeError( + "Unserializable object {} of type {}".format(obj, type(obj)) + ) return obj @@ -240,3 +257,18 @@ def readfile(filepath): with open(filepath) as f: content = f.read() return content + + +def generic_find_constraint_name(table, columns, referenced): + """ + Utility to find a constraint name in alembic migrations + """ + engine = op.get_bind().engine + m = sa.MetaData({}) + t = sa.Table(table, m, autoload=True, autoload_with=engine) + + for fk in t.foreign_key_constraints: + if fk.referred_table.name == referenced and \ + set(fk.column_keys) == columns: + return fk.name + return None diff --git a/caravel/views.py b/caravel/views.py index f451695ba3ea..d7ebacc2711f 100644 --- a/caravel/views.py +++ b/caravel/views.py @@ -16,14 +16,14 @@ from flask import ( g, request, redirect, flash, Response, render_template, Markup) -from flask.ext.appbuilder import ModelView, CompactCRUDMixin, BaseView, expose -from flask.ext.appbuilder.actions import action -from flask.ext.appbuilder.models.sqla.interface import SQLAInterface -from flask.ext.appbuilder.security.decorators import has_access -from flask.ext.babelpkg import gettext as _ +from flask_appbuilder import ModelView, CompactCRUDMixin, BaseView, expose +from flask_appbuilder.actions import action +from flask_appbuilder.models.sqla.interface import SQLAInterface +from flask_appbuilder.security.decorators import has_access +from flask_babelpkg import gettext as __ +from flask_babelpkg import lazy_gettext as _ from flask_appbuilder.models.sqla.filters import BaseFilter -from pydruid.client import doublesum from sqlalchemy import create_engine, select, text from sqlalchemy.sql.expression import TextAsFrom from werkzeug.routing import BaseConverter @@ -35,6 +35,35 @@ log_this = models.Log.log_this +def check_ownership(obj, raise_if_false=True): + """Meant to be used in `pre_update` hooks on models to enforce ownership + + Admin have all access, and other users need to be referenced on either + the created_by field that comes with the ``AuditMixin``, or in a field + named ``owners`` which is expected to be a one-to-many with the User + model. It is meant to be used in the ModelView's pre_update hook in + which raising will abort the update. + """ + roles = (r.name for r in get_user_roles()) + if 'Admin' in roles: + return True + session = db.create_scoped_session() + orig_obj = session.query(obj.__class__).filter_by(id=obj.id).first() + owner_names = (user.username for user in orig_obj.owners) + if ( + hasattr(orig_obj, 'created_by') and + orig_obj.created_by and + orig_obj.created_by.username == g.user.username): + return True + if hasattr(orig_obj, 'owners') and g.user.username in owner_names: + return True + if raise_if_false: + raise utils.CaravelSecurityException( + "You don't have the rights to alter [{}]".format(obj)) + else: + return False + + def get_user_roles(): if g.user.is_anonymous(): return [appbuilder.sm.find_role('Public')] @@ -56,7 +85,6 @@ def apply(self, query, func): # noqa if any([r.name in ('Admin', 'Alpha') for r in get_user_roles()]): return query qry = query.filter(self.model.perm.in_(self.get_perms())) - print(qry) return qry @@ -65,16 +93,23 @@ def apply(self, query, func): # noqa if any([r.name in ('Admin', 'Alpha') for r in get_user_roles()]): return query Slice = models.Slice # noqa + Dash = models.Dashboard # noqa slice_ids_qry = ( db.session .query(Slice.id) .filter(Slice.perm.in_(self.get_perms())) ) - return query.filter( - self.model.slices.any( - models.Slice.id.in_(slice_ids_qry) + print([r for r in slice_ids_qry.all()]) + query = query.filter( + Dash.id.in_( + db.session.query(Dash.id) + .distinct() + .join(Dash.slices) + .filter(Slice.id.in_(slice_ids_qry)) ) ) + print(query) + return query def validate_json(form, field): # noqa @@ -128,6 +163,20 @@ class TableColumnInlineView(CompactCRUDMixin, CaravelModelView): # noqa "a valid SQL expression as supported by the underlying backend. " "Example: `substr(name, 1, 1)`", True), } + label_columns = { + 'column_name': _("Column"), + 'verbose_name': _("Verbose Name"), + 'description': _("Description"), + 'groupby': _("Groupable"), + 'filterable': _("Filterable"), + 'table': _("Table"), + 'count_distinct': _("Count Distinct"), + 'sum': _("Sum"), + 'min': _("Min"), + 'max': _("Max"), + 'expression': _("Expression"), + 'is_dttm': _("Is temporal"), + } appbuilder.add_view_no_menu(TableColumnInlineView) @@ -142,6 +191,17 @@ class DruidColumnInlineView(CompactCRUDMixin, CaravelModelView): # noqa 'sum', 'min', 'max'] can_delete = False page_size = 500 + label_columns = { + 'column_name': _("Column"), + 'type': _("Type"), + 'datasource': _("Datasource"), + 'groupby': _("Groupable"), + 'filterable': _("Filterable"), + 'count_distinct': _("Count Distinct"), + 'sum': _("Sum"), + 'min': _("Min"), + 'max': _("Max"), + } def post_update(self, col): col.generate_metrics() @@ -162,6 +222,14 @@ class SqlMetricInlineView(CompactCRUDMixin, CaravelModelView): # noqa } add_columns = edit_columns page_size = 500 + label_columns = { + 'metric_name': _("Metric"), + 'description': _("Description"), + 'verbose_name': _("Verbose Name"), + 'metric_type': _("Type"), + 'expression': _("SQL Expression"), + 'table': _("Table"), + } appbuilder.add_view_no_menu(SqlMetricInlineView) @@ -183,6 +251,14 @@ class DruidMetricInlineView(CompactCRUDMixin, CaravelModelView): # noqa "(http://druid.io/docs/latest/querying/post-aggregations.html)", True), } + label_columns = { + 'metric_name': _("Metric"), + 'description': _("Description"), + 'verbose_name': _("Verbose Name"), + 'metric_type': _("Type"), + 'json': _("JSON"), + 'datasource': _("Druid Datasource"), + } appbuilder.add_view_no_menu(DruidMetricInlineView) @@ -211,6 +287,15 @@ class DatabaseView(CaravelModelView, DeleteMixin): # noqa "(http://docs.sqlalchemy.org/en/rel_1_0/core/metadata.html" "#sqlalchemy.schema.MetaData) call. ", True), } + label_columns = { + 'database_name': _("Database"), + 'sql_link': _("SQL link"), + 'creator': _("Creator"), + 'changed_on_': _("Last Changed"), + 'sqlalchemy_uri': _("SQLAlchemy URI"), + 'cache_timeout': _("Cache Timeout"), + 'extra': _("Extra"), + } def pre_add(self, db): conn = sqla.engine.url.make_url(db.sqlalchemy_uri) @@ -227,7 +312,8 @@ def pre_update(self, db): "Databases", label=_("Databases"), icon="fa-database", - category=_("Sources"), + category="Sources", + category_label=_("Sources"), category_icon='fa-database',) @@ -253,15 +339,28 @@ class TableModelView(CaravelModelView, DeleteMixin): # noqa "Supports " "markdown"), } + label_columns = { + 'table_link': _("Table"), + 'changed_by_': _("Changed By"), + 'database': _("Database"), + 'changed_on_': _("Last Changed"), + 'sql_link': _("SQL Editor"), + 'is_featured': _("Is Featured"), + 'schema': _("Schema"), + 'default_endpoint': _("Default Endpoint"), + 'offset': _("Offset"), + 'cache_timeout': _("Cache Timeout"), + } def post_add(self, table): + table_name = table.table_name try: table.fetch_metadata() except Exception as e: logging.exception(e) flash( "Table [{}] doesn't seem to exist, " - "couldn't fetch metadata".format(table.table_name), + "couldn't fetch metadata".format(table_name), "danger") utils.merge_perm(sm, 'datasource_access', table.perm) @@ -270,8 +369,10 @@ def post_update(self, table): appbuilder.add_view( TableModelView, - _("Tables"), - category=_("Sources"), + "Tables", + label=_("Tables"), + category="Sources", + category_label=_("Sources"), icon='fa-table',) @@ -287,14 +388,25 @@ class DruidClusterModelView(CaravelModelView, DeleteMixin): # noqa ] edit_columns = add_columns list_columns = ['cluster_name', 'metadata_last_refreshed'] + label_columns = { + 'cluster_name': _("Cluster"), + 'coordinator_host': _("Coordinator Host"), + 'coordinator_port': _("Coordinator Port"), + 'coordinator_endpoint': _("Coordinator Endpoint"), + 'broker_host': _("Broker Host"), + 'broker_port': _("Broker Port"), + 'broker_endpoint': _("Broker Endpoint"), + } if config['DRUID_IS_ACTIVE']: appbuilder.add_view( DruidClusterModelView, - _("Druid Clusters"), + name="Druid Clusters", + label=_("Druid Clusters"), icon="fa-cubes", - category=_("Sources"), + category="Sources", + category_label=_("Sources"), category_icon='fa-database',) @@ -320,10 +432,28 @@ class SliceModelView(CaravelModelView, DeleteMixin): # noqa "markdown"), } base_filters = [['id', FilterSlice, lambda: []]] + label_columns = { + 'cache_timeout': _("Cache Timeout"), + 'creator': _("Creator"), + 'dashboards': _("Dashboards"), + 'datasource_link': _("Datasource"), + 'description': _("Description"), + 'modified': _("Last Modified"), + 'owners': _("Owners"), + 'params': _("Parameters"), + 'slice_link': _("Slice"), + 'slice_name': _("Name"), + 'table': _("Table"), + 'viz_type': _("Visualization Type"), + } + + def pre_update(self, obj): + check_ownership(obj) appbuilder.add_view( SliceModelView, - _("Slices"), + "Slices", + label=_("Slices"), icon="fa-bar-chart", category="", category_icon='',) @@ -335,8 +465,9 @@ class SliceAsync(SliceModelView): # noqa 'creator', 'modified', 'icons'] label_columns = { 'icons': ' ', - 'viz_type': 'Type', - 'slice_link': 'Slice', + 'viz_type': _('Type'), + 'slice_link': _('Slice'), + 'viz_type': _('Visualization Type'), } appbuilder.add_view_no_menu(SliceAsync) @@ -360,9 +491,21 @@ class DashboardModelView(CaravelModelView, DeleteMixin): # noqa "The css for individual dashboards can be altered here, or " "in the dashboard view where changes are immediately " "visible"), - 'slug': "To get a readable URL for your dashboard", + 'slug': _("To get a readable URL for your dashboard"), } base_filters = [['slice', FilterDashboard, lambda: []]] + label_columns = { + 'dashboard_link': _("Dashboard"), + 'dashboard_title': _("Title"), + 'slug': _("Slug"), + 'slices': _("Slices"), + 'owners': _("Owners"), + 'creator': _("Creator"), + 'modified': _("Modified"), + 'position_json': _("Position JSON"), + 'css': _("CSS"), + 'json_metadata': _("JSON Metadata"), + } def pre_add(self, obj): obj.slug = obj.slug.strip() or None @@ -371,6 +514,7 @@ def pre_add(self, obj): obj.slug = re.sub(r'\W+', '', obj.slug) def pre_update(self, obj): + check_ownership(obj) self.pre_add(obj) @@ -379,7 +523,6 @@ def pre_update(self, obj): "Dashboards", label=_("Dashboards"), icon="fa-dashboard", - category="", category_icon='',) @@ -398,12 +541,19 @@ class LogModelView(CaravelModelView): list_columns = ('user', 'action', 'dttm') edit_columns = ('user', 'action', 'dttm', 'json') base_order = ('dttm', 'desc') + label_columns = { + 'user': _("User"), + 'action': _("Action"), + 'dttm': _("dttm"), + 'json': _("JSON"), + } appbuilder.add_view( LogModelView, "Action Log", label=_("Action Log"), - category=_("Security"), + category="Security", + category_label=_("Security"), icon="fa-list-ol") @@ -420,11 +570,22 @@ class DruidDatasourceModelView(CaravelModelView, DeleteMixin): # noqa page_size = 500 base_order = ('datasource_name', 'asc') description_columns = { - 'offset': "Timezone offset (in hours) for this datasource", + 'offset': _("Timezone offset (in hours) for this datasource"), 'description': Markup( "Supports markdown"), } + label_columns = { + 'datasource_name': _("Data Source"), + 'cluster': _("Cluster"), + 'description': _("Description"), + 'owner': _("Owner"), + 'is_featured': _("Is Featured"), + 'is_hidden': _("Is Hidden"), + 'default_endpoint': _("Default Endpoint"), + 'offset': _("Time Offset"), + 'cache_timeout': _("Cache Timeout"), + } def post_add(self, datasource): datasource.generate_metrics() @@ -439,6 +600,7 @@ def post_update(self, datasource): "Druid Datasources", label=_("Druid Datasources"), category="Sources", + category_label=_("Sources"), icon="fa-cube") @@ -521,7 +683,7 @@ def explore(self, datasource_type, datasource_id): .first() ) if not datasource: - flash(_("The datasource seems to have been deleted"), "alert") + flash(__("The datasource seems to have been deleted"), "alert") return redirect(error_redirect) all_datasource_access = self.appbuilder.sm.has_access( @@ -529,7 +691,7 @@ def explore(self, datasource_type, datasource_id): datasource_access = self.appbuilder.sm.has_access( 'datasource_access', datasource.perm) if not (all_datasource_access or datasource_access): - flash(_("You don't seem to have access to this datasource"), "danger") + flash(__("You don't seem to have access to this datasource"), "danger") return redirect(error_redirect) action = request.args.get('action') @@ -606,7 +768,7 @@ def save_or_overwrite_slice(self, args, slc, slice_add_perm, slice_edit_perm): d = args.to_dict(flat=False) del d['action'] del d['previous_viz_type'] - as_list = ('metrics', 'groupby', 'columns') + as_list = ('metrics', 'groupby', 'columns', 'all_columns') for k in d: v = d.get(k) if k in as_list and not isinstance(v, list): @@ -647,11 +809,15 @@ def save_slice(self, slc): flash(msg, "info") def overwrite_slice(self, slc): - session = db.session() - msg = "Slice [{}] has been overwritten".format(slc.slice_name) - session.merge(slc) - session.commit() - flash(msg, "info") + can_update = check_ownership(slc, raise_if_false=False) + if not can_update: + flash("You cannot overwrite [{}]".format(slc)) + else: + session = db.session() + session.merge(slc) + session.commit() + msg = "Slice [{}] has been overwritten".format(slc.slice_name) + flash(msg, "info") @has_access @expose("/checkbox////", methods=['GET']) @@ -695,6 +861,7 @@ def save_dash(self, dashboard_id): session = db.session() Dash = models.Dashboard # noqa dash = session.query(Dash).filter_by(id=dashboard_id).first() + check_ownership(dash, raise_if_false=True) dash.slices = [o for o in dash.slices if o.id in slice_ids] dash.position_json = json.dumps(data['positions'], indent=4) md = dash.metadata_dejson @@ -768,15 +935,9 @@ def dashboard(**kwargs): # noqa pass dashboard(dashboard_id=dash.id) - pos_dict = {} - if dash.position_json: - pos_dict = { - int(o['slice_id']): o - for o in json.loads(dash.position_json)} return self.render_template( "caravel/dashboard.html", dashboard=dash, templates=templates, - pos_dict=pos_dict, dash_save_perm=appbuilder.sm.has_access('can_save_dash', 'Caravel'), dash_edit_perm=appbuilder.sm.has_access('can_edit', 'DashboardModelView')) @@ -852,7 +1013,7 @@ def runsql(self): if ( not self.appbuilder.sm.has_access( 'all_datasource_access', 'all_datasource_access')): - raise Exception(_( + raise utils.CaravelSecurityException(_( "This view requires the `all_datasource_access` permission")) content = "" if mydb: @@ -887,12 +1048,13 @@ def refresh_datasources(self): """endpoint that refreshes druid datasources metadata""" session = db.session() for cluster in session.query(models.DruidCluster).all(): + cluster_name = cluster.cluster_name try: cluster.refresh_datasources() except Exception as e: flash( "Error while processing cluster '{}'\n{}".format( - cluster, str(e)), + cluster_name, str(e)), "danger") logging.exception(e) return redirect('/druidclustermodelview/list/') @@ -904,22 +1066,6 @@ def refresh_datasources(self): session.commit() return redirect("/druiddatasourcemodelview/list/") - @expose("/autocomplete///") - def autocomplete(self, datasource, column): - """used for filter autocomplete""" - client = utils.get_pydruid_client() - top = client.topn( - datasource=datasource, - granularity='all', - intervals='2013-10-04/2020-10-10', - aggregations={"count": doublesum("count")}, - dimension=column, - metric='count', - threshold=1000, - ) - values = sorted([d[column] for d in top[0]['result']]) - return json.dumps(values) - @app.errorhandler(500) def show_traceback(self): if config.get("SHOW_STACKTRACE"): @@ -949,6 +1095,7 @@ def welcome(self): "Refresh Druid Metadata", href='/caravel/refresh_datasources/', category='Sources', + category_label=_("Sources"), category_icon='fa-database', icon="fa-cog") @@ -966,6 +1113,7 @@ class CssTemplateModelView(CaravelModelView, DeleteMixin): label=_("CSS Templates"), icon="fa-css3", category="Sources", + category_label=_("Sources"), category_icon='') diff --git a/caravel/viz.py b/caravel/viz.py index 3e15b1163bb4..ae67f2e0271a 100644 --- a/caravel/viz.py +++ b/caravel/viz.py @@ -17,7 +17,8 @@ from datetime import datetime, timedelta import pandas as pd import numpy as np -from flask import request, Markup +from flask import request +from flask_babelpkg import lazy_gettext as _ from markdown import markdown from pandas.io.json import dumps from six import string_types @@ -89,16 +90,6 @@ def __init__(self, datasource, form_data, slice_=None): self.groupby = self.form_data.get('groupby') or [] self.reassignments() - def get_form_override(self, fieldname, attr): - if ( - fieldname in self.form_overrides and - attr in self.form_overrides[fieldname]): - s = self.form_overrides[fieldname][attr] - if attr == 'label': - s = ''.format(**locals()) - s = Markup(s) - return s - @classmethod def flat_form_fields(cls): l = set() @@ -320,7 +311,7 @@ class TableViz(BaseViz): """A basic html table that is sortable and searchable""" viz_type = "table" - verbose_name = "Table View" + verbose_name = _("Table View") credits = 'a Caravel original' fieldsets = ({ 'label': "GROUP BY", @@ -343,6 +334,11 @@ class TableViz(BaseViz): ('include_search', None), ) }) + form_overrides = ({ + 'metrics': { + 'default': [], + }, + }) is_timeseries = False def query_obj(self): @@ -381,7 +377,7 @@ class PivotTableViz(BaseViz): """A pivot table view, define your rows, columns and metrics""" viz_type = "pivot_table" - verbose_name = "Pivot Table" + verbose_name = _("Pivot Table") credits = 'a Caravel original' is_timeseries = False fieldsets = ({ @@ -443,7 +439,7 @@ class MarkupViz(BaseViz): """Use html or markdown to create a free form widget""" viz_type = "markup" - verbose_name = "Markup Widget" + verbose_name = _("Markup") fieldsets = ({ 'label': None, 'fields': ('markup_type', 'code') @@ -471,7 +467,7 @@ class WordCloudViz(BaseViz): """ viz_type = "word_cloud" - verbose_name = "Word Cloud" + verbose_name = _("Word Cloud") is_timeseries = False fieldsets = ({ 'label': None, @@ -503,7 +499,7 @@ class TreemapViz(BaseViz): """Tree map visualisation for hierarchical data.""" viz_type = "treemap" - verbose_name = "Treemap" + verbose_name = _("Treemap") credits = 'd3.js' is_timeseries = False fieldsets = ({ @@ -547,7 +543,7 @@ class CalHeatmapViz(BaseViz): """Calendar heatmap.""" viz_type = "cal_heatmap" - verbose_name = "Calender Heatmap" + verbose_name = _("Calender Heatmap") credits = ( 'cal-heatmap') is_timeseries = True @@ -618,7 +614,7 @@ class BoxPlotViz(NVD3Viz): """Box plot viz from ND3""" viz_type = "box_plot" - verbose_name = "Box Plot" + verbose_name = _("Box Plot") sort_series = False is_timeseries = True fieldsets = ({ @@ -725,7 +721,7 @@ class BubbleViz(NVD3Viz): """Based on the NVD3 bubble chart""" viz_type = "bubble" - verbose_name = "Bubble Chart" + verbose_name = _("Bubble Chart") is_timeseries = False fieldsets = ({ 'label': None, @@ -793,7 +789,7 @@ class BigNumberViz(BaseViz): """Put emphasis on a single metric with this big number viz""" viz_type = "big_number" - verbose_name = "Big Number with Trendline" + verbose_name = _("Big Number with Trendline") credits = 'a Caravel original' is_timeseries = True fieldsets = ({ @@ -843,7 +839,7 @@ class BigNumberTotalViz(BaseViz): """Put emphasis on a single metric with this big number viz""" viz_type = "big_number_total" - verbose_name = "Big Number" + verbose_name = _("Big Number") credits = 'a Caravel original' is_timeseries = False fieldsets = ({ @@ -889,7 +885,7 @@ class NVD3TimeSeriesViz(NVD3Viz): """A rich line chart component with tons of options""" viz_type = "line" - verbose_name = "Time Series - Line Chart" + verbose_name = _("Time Series - Line Chart") sort_series = False is_timeseries = True fieldsets = ({ @@ -1041,7 +1037,7 @@ class NVD3TimeSeriesBarViz(NVD3TimeSeriesViz): viz_type = "bar" sort_series = True - verbose_name = "Time Series - Bar Chart" + verbose_name = _("Time Series - Bar Chart") fieldsets = [NVD3TimeSeriesViz.fieldsets[0]] + [{ 'label': 'Chart Options', 'fields': ( @@ -1059,7 +1055,7 @@ class NVD3CompareTimeSeriesViz(NVD3TimeSeriesViz): """A line chart component where you can compare the % change over time""" viz_type = 'compare' - verbose_name = "Time Series - Percent Change" + verbose_name = _("Time Series - Percent Change") class NVD3TimeSeriesStackedViz(NVD3TimeSeriesViz): @@ -1067,7 +1063,7 @@ class NVD3TimeSeriesStackedViz(NVD3TimeSeriesViz): """A rich stack area chart""" viz_type = "area" - verbose_name = "Time Series - Stacked" + verbose_name = _("Time Series - Stacked") sort_series = True fieldsets = [NVD3TimeSeriesViz.fieldsets[0]] + [{ 'label': 'Chart Options', @@ -1086,7 +1082,7 @@ class DistributionPieViz(NVD3Viz): """Annoy visualization snobs with this controversial pie chart""" viz_type = "pie" - verbose_name = "Distribution - NVD3 - Pie Chart" + verbose_name = _("Distribution - NVD3 - Pie Chart") is_timeseries = False fieldsets = ({ 'label': None, @@ -1122,7 +1118,7 @@ class DistributionBarViz(DistributionPieViz): """A good old bar chart""" viz_type = "dist_bar" - verbose_name = "Distribution - Bar Chart" + verbose_name = _("Distribution - Bar Chart") is_timeseries = False fieldsets = ({ 'label': 'Chart Options', @@ -1202,7 +1198,7 @@ class SunburstViz(BaseViz): """A multi level sunburst chart""" viz_type = "sunburst" - verbose_name = "Sunburst" + verbose_name = _("Sunburst") is_timeseries = False credits = ( 'Kerry Rodden ' @@ -1268,7 +1264,7 @@ class SankeyViz(BaseViz): """A Sankey diagram that requires a parent-child dataset""" viz_type = "sankey" - verbose_name = "Sankey" + verbose_name = _("Sankey") is_timeseries = False credits = 'd3-sankey on npm' fieldsets = ({ @@ -1332,7 +1328,7 @@ class DirectedForceViz(BaseViz): """An animated directed force layout graph visualization""" viz_type = "directed_force" - verbose_name = "Directed Force Layout" + verbose_name = _("Directed Force Layout") credits = 'd3noob @bl.ocks.org' is_timeseries = False fieldsets = ({ @@ -1374,7 +1370,7 @@ class WorldMapViz(BaseViz): """A country centric world map""" viz_type = "world_map" - verbose_name = "World Map" + verbose_name = _("World Map") is_timeseries = False credits = 'datamaps on npm' fieldsets = ({ @@ -1448,7 +1444,7 @@ class FilterBoxViz(BaseViz): """A multi filter, multi-choice filter box to make dashboards interactive""" viz_type = "filter_box" - verbose_name = "Filters" + verbose_name = _("Filters") is_timeseries = False credits = 'a Caravel original' fieldsets = ({ @@ -1496,7 +1492,7 @@ class IFrameViz(BaseViz): """You can squeeze just about anything in this iFrame component""" viz_type = "iframe" - verbose_name = "iFrame" + verbose_name = _("iFrame") credits = 'a Caravel original' is_timeseries = False fieldsets = ({ @@ -1514,7 +1510,7 @@ class ParallelCoordinatesViz(BaseViz): """ viz_type = "para" - verbose_name = "Parallel Coordinates" + verbose_name = _("Parallel Coordinates") credits = ( '' 'Syntagmatic\'s library') @@ -1550,7 +1546,7 @@ class HeatmapViz(BaseViz): """A nice heatmap visualization that support high density through canvas""" viz_type = "heatmap" - verbose_name = "Heatmap" + verbose_name = _("Heatmap") is_timeseries = False credits = ( 'inspired from mbostock @' @@ -1618,7 +1614,7 @@ class HorizonViz(NVD3TimeSeriesViz): """ viz_type = "horizon" - verbose_name = "Horizon Charts" + verbose_name = _("Horizon Charts") credits = ( '' 'd3-horizon-chart') diff --git a/docs/druid.rst b/docs/druid.rst new file mode 100644 index 000000000000..5b4450cbeedd --- /dev/null +++ b/docs/druid.rst @@ -0,0 +1,48 @@ +Druid +===== + +Caravel works well with Druid, though currently not all +advanced features out of Druid are covered. This page clarifies what is +covered and what isn't and explains how to use some of the features. + +.. note :: + Currently Airbnb runs against Druid ``0.8.x`` and previous / + following versions are not tested against. + +Supported +''''''''' + +Aggregations +------------ + +Common aggregations, or Druid metrics can be defined and used in Caravel. +The first and simpler use case is to use the checkbox matrix expose in your +datasource's edit view (``Sources -> Druid Datasources -> +[your datasource] -> Edit -> [tab] List Druid Column``). +Clicking the ``GroupBy`` and ``Filterable`` checkboxes will make the column +appear in the related dropdowns while in explore view. Checking +``Count Distinct``, ``Min``, ``Max`` or ``Sum`` will result in creating +new metrics that will appear in the ``List Druid Metric`` tab upon saving the +datasource. By editing these metrics, you'll notice that they their ``json`` +element correspond to Druid aggregation definition. You can create your own +aggregations manually from the ``List Druid Metric`` tab following Druid +documentation. + +.. image:: _static/img/druid_agg.png + :scale: 50 % + +Post-Aggregations +----------------- + +Druid supports post aggregation and this works in Caravel. All you have to +do is creating a metric, much like you would create an aggregation manually, +but specify ``postagg`` as a ``Metric Type``. You then have to provide a valid +json post-aggregation definition (as specified in the Druid docs) in the +Json field. + + +Not yet supported +''''''''''''''''' + +- Regex filters +- Lookups / joins diff --git a/docs/index.rst b/docs/index.rst index 29478e2b3956..044548c17a92 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -32,8 +32,10 @@ Contents installation tutorial + security videos gallery + druid faq diff --git a/docs/installation.rst b/docs/installation.rst index 314e1f0eccc1..0de9a287b983 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -28,7 +28,7 @@ the required dependencies are installed: :: For **Fedora** and **RHEL-derivatives**, the following command will ensure that the required dependencies are installed: :: - + sudo yum upgrade python-setuptools sudo yum install gcc libffi-devel python-devel python-pip python-wheel openssl-devel @@ -228,6 +228,20 @@ Note that you can run the ``caravel refresh_druid`` command to refresh the metadata from your Druid cluster(s) +CORS +----- + +The extra CORS Dependency must be installed: + + caravel[cors] + + +The following keys in `caravel_config.py` can be specified to configure CORS: + + +* ``ENABLE_CORS``: Must be set to True in order to enable CORS +* ``CORS_OPTIONS``: options passed to Flask-CORS (`documentation `) + Upgrading --------- diff --git a/docs/security.rst b/docs/security.rst new file mode 100644 index 000000000000..0dfc73626fce --- /dev/null +++ b/docs/security.rst @@ -0,0 +1,70 @@ +Security +======== +Security in Caravel is handled by Flask AppBuilder (FAB). FAB is a +"Simple and rapid application development framework, built on top of Flask.". +FAB provides authentication, user management, permissions and roles. + + +Provided Roles +-------------- +Caravel ships with 3 roles that are handled by Caravel itself. You can +assume that these 3 roles will stay up-to-date as Caravel evolves. + +Admin +""""" +Admins have all rights, including granting or revoking rights from other +users and altering other people's slices and dashboards. + +Alpha +""""" +Alpha have access to all data sources, but they cannot grant or revoke access +from other users. They are also limited to altering the objects that they +own. Alpha users can add and alter data sources. + +Gamma +""""" +Gamma have limited access. They can only consume data coming from data sources +they have been giving access to through another complementary role. +They only have access to view the slices and +dashboards made from data sources that they have access to. Currently Gamma +users are not able to alter or add data sources. We assume that they are +mostly content consumers, though they can create slices and dashboards. + +Also note that when Gamma users look at the dashboards and slices list view, +they will only see the objects that they have access to. + + +Managing Gamma per data source access +------------------------------------- +Here's how to provide users access to only specific datasets. First make +sure the users with limited access have [only] the Gamma role assigned to +them. Second, create a new role (``Menu -> Security -> List Roles``) and +click the ``+`` sign. + +.. image:: _static/img/create_role.png + :scale: 50 % + +This new window allows you to give this new role a name, attribute it to users +and select the tables in the ``Permissions`` dropdown. To select the data +sources you want to associate with this role, simply click in the dropdown +and use the typeahead to search for your table names. + +You can then confirm with your Gamma users that they see the objects +(dashboards and slices) associated with the tables related to their roles. + + +Customizing +----------- + +The permissions exposed by FAB are very granular and allow for a great level +of customization. FAB creates many permissions automagically for each model +that is create (can_add, can_delete, can_show, can_edit, ...) as well as for +each view. On top of that, Caravel can expose more granular permissions like +``all_datasource_access``. + +We do not recommend altering the 3 base roles as there +are a set of assumptions that Caravel build upon. It is possible though for +you to create your own roles, and union them to existing ones. + +The best way to go is probably to give user ``Gamma`` plus another role +that would add specific permissions needed by this type of users. diff --git a/setup.py b/setup.py index 61cd097edffa..3ede2045c403 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ 'alembic>=0.8.5, <0.9.0', 'babel==2.3.4', 'cryptography>=1.1.1, <2.0.0', - 'flask-appbuilder>=1.6.0, <2.0.0', + 'flask-appbuilder>=1.6.2, <2.0.0', 'Flask-BabelPkg==0.9.6', 'flask-cache>=0.13.1, <0.14.0', 'flask-migrate>=1.5.1, <2.0.0', @@ -31,7 +31,7 @@ 'markdown>=2.6.2, <3.0.0', 'pandas==0.18.0', 'parsedatetime==2.0.0', - 'pydruid>=0.2.2, <0.3', + 'pydruid==0.3.0, <0.4.0', 'python-dateutil>=2.4.2, <3.0.0', 'requests>=2.7.0, <3.0.0', 'sqlalchemy>=1.0.12, <2.0.0', @@ -39,6 +39,9 @@ 'sqlparse>=0.1.16, <0.2.0', 'werkzeug>=0.11.2, <0.12.0', ], + extras_require={ + 'cors': ['Flask-Cors>=2.0.0'], + }, tests_require=['coverage'], author='Maxime Beauchemin', author_email='maximebeauchemin@gmail.com', diff --git a/tests/core_tests.py b/tests/core_tests.py index 3b66dc3209ba..b450c3e81a02 100644 --- a/tests/core_tests.py +++ b/tests/core_tests.py @@ -6,6 +6,7 @@ from datetime import datetime import doctest +import json import imp import os import unittest @@ -36,6 +37,7 @@ def __init__(self, *args, **kwargs): self.client = app.test_client() utils.init(caravel) + admin = appbuilder.sm.find_user('admin') if not admin: appbuilder.sm.add_user( @@ -49,30 +51,42 @@ def __init__(self, *args, **kwargs): 'gamma', 'gamma', 'user', 'gamma@fab.org', appbuilder.sm.find_role('Gamma'), password='general') + + alpha = appbuilder.sm.find_user('alpha') + if not alpha: + appbuilder.sm.add_user( + 'alpha', 'alpha', 'user', 'alpha@fab.org', + appbuilder.sm.find_role('Alpha'), + password='general') + utils.init(caravel) - def login_admin(self): + def login(self, username='admin', password='general'): resp = self.client.post( '/login/', - data=dict(username='admin', password='general'), + data=dict(username=username, password=password), follow_redirects=True) assert 'Welcome' in resp.data.decode('utf-8') - def login_gamma(self): - resp = self.client.post( - '/login/', - data=dict(username='gamma', password='general'), - follow_redirects=True) - assert 'Welcome' in resp.data.decode('utf-8') + def logout(self): + resp = self.client.get('/logout/', follow_redirects=True) - def setup_public_access_for_dashboard(self, dashboard_name): + def setup_public_access_for_dashboard(self, table_name): public_role = appbuilder.sm.find_role('Public') perms = db.session.query(ab_models.PermissionView).all() for perm in perms: - if (perm.permission.name == 'datasource_access' and - perm.view_menu and dashboard_name in perm.view_menu.name): + if ( perm.permission.name == 'datasource_access' and + perm.view_menu and table_name in perm.view_menu.name): appbuilder.sm.add_permission_role(public_role, perm) + def revoke_public_access(self, table_name): + public_role = appbuilder.sm.find_role('Public') + perms = db.session.query(ab_models.PermissionView).all() + for perm in perms: + if ( perm.permission.name == 'datasource_access' and + perm.view_menu and table_name in perm.view_menu.name): + appbuilder.sm.del_permission_role(public_role, perm) + class CoreTests(CaravelTestCase): @@ -97,7 +111,7 @@ def load_examples(self): cli.load_examples(load_test_data=True) def test_save_slice(self): - self.login_admin() + self.login(username='admin') slice_id = ( db.session.query(models.Slice.id) @@ -120,7 +134,7 @@ def test_save_slice(self): def test_slices(self): # Testing by running all the examples - self.login_admin() + self.login(username='admin') Slc = models.Slice urls = [] for slc in db.session.query(Slc).all(): @@ -134,7 +148,7 @@ def test_slices(self): self.client.get(url) def test_dashboard(self): - self.login_admin() + self.login(username='admin') urls = {} for dash in db.session.query(models.Dashboard).all(): urls[dash.dashboard_title] = dash.url @@ -153,23 +167,35 @@ def test_misc(self): assert self.client.get('/ping').data.decode('utf-8') == "OK" def test_shortner(self): - self.login_admin() + self.login(username='admin') data = "//caravel/explore/table/1/?viz_type=sankey&groupby=source&groupby=target&metric=sum__value&row_limit=5000&where=&having=&flt_col_0=source&flt_op_0=in&flt_eq_0=&slice_id=78&slice_name=Energy+Sankey&collapsed_fieldsets=&action=&datasource_name=energy_usage&datasource_id=1&datasource_type=table&previous_viz_type=sankey" resp = self.client.post('/r/shortner/', data=data) assert '/r/' in resp.data.decode('utf-8') - def test_save_dash(self): - self.login_admin() + def test_save_dash(self, username='admin'): + self.login(username=username) dash = db.session.query(models.Dashboard).filter_by(slug="births").first() - data = """{"positions":[{"slice_id":"131","col":8,"row":8,"size_x":2,"size_y":4},{"slice_id":"132","col":10,"row":8,"size_x":2,"size_y":4},{"slice_id":"133","col":1,"row":1,"size_x":2,"size_y":2},{"slice_id":"134","col":3,"row":1,"size_x":2,"size_y":2},{"slice_id":"135","col":5,"row":4,"size_x":3,"size_y":3},{"slice_id":"136","col":1,"row":7,"size_x":7,"size_y":4},{"slice_id":"137","col":9,"row":1,"size_x":3,"size_y":3},{"slice_id":"138","col":5,"row":1,"size_x":4,"size_y":3},{"slice_id":"139","col":1,"row":3,"size_x":4,"size_y":4},{"slice_id":"140","col":8,"row":4,"size_x":4,"size_y":4}],"css":"None","expanded_slices":{}}""" + positions = [] + for i, slc in enumerate(dash.slices): + d = { + 'col': 0, + 'row': i * 4, + 'size_x': 4, + 'size_y': 4, + 'slice_id': '{}'.format(slc.id)} + positions.append(d) + data = { + 'css': '', + 'expanded_slices': {}, + 'positions': positions, + } url = '/caravel/save_dash/{}/'.format(dash.id) - resp = self.client.post(url, data=dict(data=data)) + resp = self.client.post(url, data=dict(data=json.dumps(data))) assert "SUCCESS" in resp.data.decode('utf-8') def test_gamma(self): - self.login_gamma() + self.login(username='gamma') resp = self.client.get('/slicemodelview/list/') - print(resp.data.decode('utf-8')) assert "List Slice" in resp.data.decode('utf-8') resp = self.client.get('/dashboardmodelview/list/') @@ -177,50 +203,67 @@ def test_gamma(self): def test_public_user_dashboard_access(self): # Try access before adding appropriate permissions. + self.revoke_public_access('birth_names') + self.logout() + resp = self.client.get('/slicemodelview/list/') data = resp.data.decode('utf-8') - assert 'birth_names' not in data - resp = self.client.get('/dashboardmodelview/list/') - data = resp.data.decode('utf-8') - assert '' not in data + assert 'birth_names' not in data - resp = self.client.get('/caravel/explore/table/3/', follow_redirects=True) + resp = self.client.get('/dashboardmodelview/list/') data = resp.data.decode('utf-8') - assert "You don't seem to have access to this datasource" in data + assert '/caravel/dashboard/births/' not in data self.setup_public_access_for_dashboard('birth_names') # Try access after adding appropriate permissions. resp = self.client.get('/slicemodelview/list/') data = resp.data.decode('utf-8') - assert 'birth_names' in data + assert 'birth_names' in data resp = self.client.get('/dashboardmodelview/list/') data = resp.data.decode('utf-8') - assert '' in data + assert "/caravel/dashboard/births/" in data resp = self.client.get('/caravel/dashboard/births/') data = resp.data.decode('utf-8') - assert '[dashboard] Births' in data - - resp = self.client.get('/caravel/explore/table/3/') - data = resp.data.decode('utf-8') - assert '[explore] birth_names' in data + assert 'Births' in data # Confirm that public doesn't have access to other datasets. - resp = self.client.get('/slicemodelview/list/') - data = resp.data.decode('utf-8') - assert 'wb_health_population' not in data - resp = self.client.get('/dashboardmodelview/list/') data = resp.data.decode('utf-8') - assert '' not in data + assert "/caravel/dashboard/world_health/" not in data - resp = self.client.get('/caravel/explore/table/2/', follow_redirects=True) - data = resp.data.decode('utf-8') - assert "You don't seem to have access to this datasource" in data + def test_only_owners_can_save(self): + dash = ( + db.session + .query(models.Dashboard) + .filter_by(slug="births") + .first() + ) + dash.owners = [] + db.session.merge(dash) + db.session.commit() + self.test_save_dash('admin') + + self.logout() + self.assertRaises( + utils.CaravelSecurityException, self.test_save_dash, 'alpha') + + alpha = appbuilder.sm.find_user('alpha') + + dash = ( + db.session + .query(models.Dashboard) + .filter_by(slug="births") + .first() + ) + dash.owners = [alpha] + db.session.merge(dash) + db.session.commit() + self.test_save_dash('alpha') SEGMENT_METADATA = [{ "id": "some_id", @@ -278,7 +321,7 @@ def __init__(self, *args, **kwargs): @patch('caravel.models.PyDruid') def test_client(self, PyDruid): - self.login_admin() + self.login(username='admin') instance = PyDruid.return_value instance.time_boundary.return_value = [ {'result': {'maxTime': '2016-01-01'}}] @@ -319,9 +362,8 @@ def test_client(self, PyDruid): df = pd.DataFrame(nres) instance.export_pandas.return_value = df instance.query_dict = {} + instance.query_builder.last_query.query_dict = {} resp = self.client.get('/caravel/explore/druid/1/?viz_type=table&granularity=one+day&druid_time_origin=&since=7+days+ago&until=now&row_limit=5000&include_search=false&metrics=count&groupby=name&flt_col_0=dim1&flt_op_0=in&flt_eq_0=&slice_id=&slice_name=&collapsed_fieldsets=&action=&datasource_name=test_datasource&datasource_id=1&datasource_type=druid&previous_viz_type=table&json=true&force=true') - print('-'*300) - print(resp.data.decode('utf-8')) assert "Canada" in resp.data.decode('utf-8') From 298a6ce60127039e4c3b3dd3d555e4678f74cf19 Mon Sep 17 00:00:00 2001 From: bnpysse Date: Sat, 11 Jun 2016 17:55:55 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=83=A8=E5=88=86?= =?UTF-8?q?=E7=BF=BB=E8=AF=91=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- caravel/translations/zh/LC_MESSAGES/messages.po | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/caravel/translations/zh/LC_MESSAGES/messages.po b/caravel/translations/zh/LC_MESSAGES/messages.po index 9961cc692f2b..82973125e62c 100644 --- a/caravel/translations/zh/LC_MESSAGES/messages.po +++ b/caravel/translations/zh/LC_MESSAGES/messages.po @@ -415,27 +415,27 @@ msgstr "" #: caravel/viz.py:1378 msgid "World Map" -msgstr "" +msgstr "世界地图" #: caravel/viz.py:1452 msgid "Filters" -msgstr "" +msgstr "过滤器" #: caravel/viz.py:1500 msgid "iFrame" -msgstr "" +msgstr "框架" #: caravel/viz.py:1518 msgid "Parallel Coordinates" -msgstr "" +msgstr "并行坐标系" #: caravel/viz.py:1554 msgid "Heatmap" -msgstr "" +msgstr "堆图" #: caravel/viz.py:1622 msgid "Horizon Charts" -msgstr "" +msgstr "水平图表" #: caravel/templates/appbuilder/navbar_right.html:34 msgid "Profile" From 563a826270d9574fb49ce08ad3966eb91387890d Mon Sep 17 00:00:00 2001 From: bnpysse Date: Sat, 11 Jun 2016 22:08:08 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E5=90=88=E5=B9=B6=E6=BA=90=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/.name | 1 + .idea/caravel.iml | 19 + .idea/cssxfire.xml | 8 + .idea/dbnavigator.xml | 448 ++++++++++ .idea/dictionaries/donglida.xml | 3 + .idea/encodings.xml | 6 + .idea/inspectionProfiles/Project_Default.xml | 16 + .../inspectionProfiles/profiles_settings.xml | 7 + .idea/misc.xml | 20 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + .idea/workspace.xml | 788 ++++++++++++++++++ 12 files changed, 1330 insertions(+) create mode 100644 .idea/.name create mode 100644 .idea/caravel.iml create mode 100644 .idea/cssxfire.xml create mode 100644 .idea/dbnavigator.xml create mode 100644 .idea/dictionaries/donglida.xml create mode 100644 .idea/encodings.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 .idea/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 000000000000..f272578fd4fc --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +caravel \ No newline at end of file diff --git a/.idea/caravel.iml b/.idea/caravel.iml new file mode 100644 index 000000000000..ce274b4dd876 --- /dev/null +++ b/.idea/caravel.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/cssxfire.xml b/.idea/cssxfire.xml new file mode 100644 index 000000000000..61699e71239e --- /dev/null +++ b/.idea/cssxfire.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/dbnavigator.xml b/.idea/dbnavigator.xml new file mode 100644 index 000000000000..93dcb3aa4745 --- /dev/null +++ b/.idea/dbnavigator.xml @@ -0,0 +1,448 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/dictionaries/donglida.xml b/.idea/dictionaries/donglida.xml new file mode 100644 index 000000000000..fbe49dfa224a --- /dev/null +++ b/.idea/dictionaries/donglida.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 000000000000..97626ba45445 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 000000000000..c16527e5d030 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 000000000000..3b312839bf2e --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 000000000000..766bc8f85e11 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 000000000000..3fcb92cedcfc --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 000000000000..94a25f7f4cb4 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 000000000000..cff2a83fe433 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,788 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + DEFINITION_ORDER + + + + + + + + + + + + Python + + + + + PyCompatibilityInspection + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + project + + + + + + + + + + + + + + + + project + + + true + + bdd + + DIRECTORY + + false + + + + + + + + + + + + + + + + + + + + + + + + + + 1463616810402 + + + 1465406842385 + + + 1465638955607 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 8b92c9978712b6ca67c61d76053bc187bf1a7f5a Mon Sep 17 00:00:00 2001 From: bnpysse Date: Tue, 14 Jun 2016 21:51:18 +0800 Subject: [PATCH 4/5] change blank lines of views.py --- caravel/views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/caravel/views.py b/caravel/views.py index d2e8e5b9db52..17b89b60dc0d 100644 --- a/caravel/views.py +++ b/caravel/views.py @@ -180,7 +180,6 @@ class TableColumnInlineView(CompactCRUDMixin, CaravelModelView): # noqa appbuilder.add_view_no_menu(TableColumnInlineView) - class DruidColumnInlineView(CompactCRUDMixin, CaravelModelView): # noqa datamodel = SQLAInterface(models.DruidColumn) edit_columns = [ @@ -429,7 +428,6 @@ class DruidClusterModelView(CaravelModelView, DeleteMixin): # noqa category_icon='fa-database',) - class SliceModelView(CaravelModelView, DeleteMixin): # noqa datamodel = SQLAInterface(models.Slice) add_template = "caravel/add_slice.html" From f1696911033e14a72587e7c4caf8df79839eb099 Mon Sep 17 00:00:00 2001 From: bnpysse Date: Thu, 16 Jun 2016 17:57:07 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=88=B00.9.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/inspectionProfiles/Project_Default.xml | 9 + .idea/workspace.xml | 512 +++++++++++++------ 2 files changed, 368 insertions(+), 153 deletions(-) diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index c16527e5d030..26de18431cde 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -12,5 +12,14 @@ + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 535a3e4d89d8..eba3a2f52b9a 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -2,44 +2,8 @@ - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -53,7 +17,7 @@ - + - - - - - + + - - + + - - + + - - + + - - + + - - - + + + + + - - + + - - + + - - - - - + + - - + + - - + + - - + + - - - - - - - - - - - - @@ -157,8 +105,15 @@ @@ -265,15 +220,7 @@ - @@ -640,6 +574,9 @@ + + + @@ -667,22 +604,22 @@ - + - + - + - - + + - - + + @@ -697,8 +634,8 @@ - - + + @@ -722,6 +659,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -783,7 +881,6 @@ - @@ -826,25 +923,32 @@ - + - - + + - + - + - - + - - + + + + + + + + + + @@ -852,33 +956,113 @@ - + + + + + + + + + + + + + - + - - - + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -887,30 +1071,52 @@ - - - - + + + + + + + + + + + + + + + - - - - - + + + - + - - - + + + + + + + + \ No newline at end of file