From b7691e0ead40afea2cb267b52fe3918c0d29cdde Mon Sep 17 00:00:00 2001 From: davidakennedy Date: Tue, 19 Feb 2013 18:02:46 -0500 Subject: [PATCH 001/230] Add base.css file for open source version and add tweaks to links so does not conflict with Intranet. --- src/idea/static/base.css | 453 ++++++++++++++++++++++++++++++ src/idea/static/idea/css/idea.css | 14 +- 2 files changed, 460 insertions(+), 7 deletions(-) create mode 100644 src/idea/static/base.css diff --git a/src/idea/static/base.css b/src/idea/static/base.css new file mode 100644 index 0000000..e0dc271 --- /dev/null +++ b/src/idea/static/base.css @@ -0,0 +1,453 @@ +/* HTML5 declarations */ +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +menu, +nav, +section, +dialog { + display: block; +} +audio[controls], +canvas, +video { + display: inline-block; + *display: inline; + zoom: 1; +} + +/* Base */ +html { + height: 100%; + font-size: 100%; + overflow-y: scroll; + /* Force scrollbar in non-IE and Remove iOS text size adjust without disabling user zoom */ + -webkit-text-size-adjust: 100%; +} +body { + margin: 0; + min-height: 100%; + -webkit-font-smoothing: antialiased; + font-smoothing: antialiased; + text-rendering: optimizeLegibility; + background: #fff; +} +/* Default font settings */ +/* 16px base font size with 150% (24px) friendly, unitless line height and margin for vertical rhythm */ +/* Font-size percentage is based on 16px browser default size */ +body, +button, +input, +select, +textarea { + font: 100%/1.5 Georgia, "Times New Roman", serif; + /* IE7 and older can't resize px based text */ + *font-size: 1em; + color: #333; +} +p, +blockquote, +q, +pre, +address, +hr, +code, +samp, +dl, +ol, +ul, +form, +table, +fieldset, +menu, +img { + margin: 0 0 1.5em; + padding: 0; +} + +/* Typography */ +/* Composed to a scale of 12px, 14px, 16px, 18px, 21px, 24px, 36px, 48px, 60px and 72px */ +h1, h2, h3, h4, h5, h6 { + font-family: Arial, Helvetica, sans-serif; + color: #222; +} +h1 { + margin: 0; + /* 60px / 72px */ + font-size: 3.75em; + line-height: 1.2em; + margin-bottom: 0.4em; +} +h2 {margin: 0; + /* 48px / 48px */ + font-size: 3em; + line-height: 1em; + margin-bottom: 0.5em; +} +h3 { + margin: 0; + /* 36px / 48px */ + font-size: 2.25em; + line-height: 1.3333333333333333333333333333333em; + margin-bottom: 0.6667em; +} +h4 { + margin: 0; + /* 24px / 24px */ + font-size: 1.5em; + line-height: 1em; + margin-bottom: 1em; +} +h5 { + margin: 0; + /* 21px / 24px */ + font-size: 1.3125em; + line-height: 1.1428571428571428571428571428571em; + margin-bottom: 1.1428571428571428571428571428571em; +} +h6 { + margin: 0; + /* 18px / 24px */ + font-size: 1.125em; + line-height: 1.3333333333333333333333333333333em; + margin-bottom: 1.3333333333333333333333333333333em; +} +p, ul, blockquote, pre, td, th, label { + margin: 0; + /* 16px / 24px */ + font-size: 1em; + line-height: 1.5em; + margin-bottom: 1.5em; +} +small, p.small { + margin: 0; + /* 14px / 24px */ + font-size: 0.875em; + line-height: 1.7142857142857142857142857142857em; + margin-bottom: 1.7142857142857142857142857142857em; +} + +/* Code */ +pre { + white-space: pre; + white-space: pre-wrap; + /* Allow line wrapping of 'pre' */ + word-wrap: break-word; +} +pre, code, kbd, samp { + font-size: 1em; + line-height: 1.5em; + margin-bottom: 1.5em; + font-family: Menlo, Consolas, 'DejaVu Sans Mono', Monaco, monospace; +} + +/* Tables */ +table { + border-collapse: collapse; + border-spacing: 0; + margin-bottom: 1.5em; +} +th { + text-align: left; +} +tr, +th, +td { + padding-right: 1.5em; + border-bottom: 0 solid #333; +} + +/* Forms */ +form { + margin: 0; +} +fieldset { + border: 0; + padding: 0; +} +textarea { + overflow: auto; + vertical-align: top; +} +legend { + *margin-left: -.75em; +} +button, +input, +select, +textarea { + vertical-align: baseline; + /* IE7 and older */ + *vertical-align: middle; +} +button, +input { + line-height: normal; + *overflow: visible; +} +button, +input[type="button"], +input[type="reset"], +input[type="submit"] { + cursor: pointer; + -webkit-appearance: button; +} +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; +} +input[type="search"] { + -webkit-appearance: textfield; + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; + box-sizing: content-box; +} +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +/* Quotes */ +blockquote, +q { + quotes: none; +} +blockquote:before, +blockquote:after, +q:before, +q:after { + content: ''; + content: none; +} +blockquote, +q, +cite { + font-style: italic; +} +blockquote { + padding-left: 1.5em; + border-left: 3px solid #ccc; +} +blockquote > p { + padding: 0; +} + +/* Lists */ +ul, +ol { + list-style-position: inside; + padding: 0; +} +li ul, +li ol { + margin: 0 1.5em; +} +dl dd { + margin-left: 1.5em; +} +dt { + font-family: Arial, Helvetica, sans-serif; +} + +/* Hyperlinks */ +a { + text-decoration: none; + color: royalblue; +} +a:hover { + color: midnightblue; + text-decoration: underline; +} +a:focus { + outline: thin dotted; +} +a:hover, +a:active { + /* Better CSS Outline Suppression */ + outline: none; +} + +/* Media */ +figure { + margin: 0; +} +img, +object, +embed, +video { + max-width: 100%; + /* Fluid images */ + _width: 100%; +} +img { + border: 0; + /* Improve IE's resizing of images */ + -ms-interpolation-mode: bicubic; +} +/* Correct IE9 overflow */ +svg:not(:root) { + overflow: hidden; +} + +/* Abbreviation */ +abbr[title], +dfn[title] { + border-bottom: 1px dotted #333; + cursor: help; +} + +/* Marked/Inserted/Deleted and Selected text */ +ins, +mark { + background: #fff9c0; + text-decoration: none; +} +del { + text-decoration: line-through; +} +/* Selected text */ +::-moz-selection { + background: #555; + color: #fff; +} +::selection { + background: #555; + color: #fff; +} + +/* Others */ +strong, +b, +dt { + font-weight: bold; +} +dfn { + font-style: italic; +} +var, +address { + font-style: normal; +} +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + /* Position 'sub' and 'sup' without affecting line-height */ + vertical-align: baseline; +} +sup { + /* Move superscripted text up */ + top: -0.5em; +} +sub { + /* Move subscripted text down */ + bottom: -0.25em; +} + +/* Adding basic structure tweaks */ +body { + background: #fff; + padding: 0.825em 2em; +} +#content { + margin: 0 auto; + max-width: 70em; /* 1120px */ +} +.search-form input[type="text"], +.search-form textarea { + border: 1px solid #aaa; + height: 2.5em !important; +} + +/* Grid */ +.row, .cf { + zoom: 1; +} +.row:before, .cf:before, .row:after, .cf:after { + content: " "; + display: table; +} + +.row:after, .cf:after { + clear: both; +} +[class*="span"] { + border: 0px solid rgba(0, 0, 0, 0); + margin-left: 1.9%; + margin-right: -0.25em; + padding: 0; + display: inline-block; + vertical-align: top; + zoom: 1; + *display: inline; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-background-clip: padding-box !important; + -moz-background-clip: padding-box !important; + background-clip: padding-box !important; +} +[class*="span"]:first-child { + margin-left: 0; +} + [class*="span"].right { + margin-left: 0; + margin-right: 1.9%; +} +[class*="span"].right:first-child { + margin-left: 0; + margin-right: 0; +} + .span1 { + width: 6.6%; +} + .span2 { + width: 15.0833333333%; +} + .span3 { + width: 23.575%; +} + .span4, .span_side { + width: 32.0666666667%; +} + .span5 { + width: 40.5583333333%; +} + .span6 { + width: 49.05%; +} + .span7 { + width: 57.5416666667%; +} + .span8, .span_main { + width: 66.0333333333%; +} + .span9 { + width: 74.525%; +} + .span10 { + width: 83.0166666667%; +} + .span11 { + width: 91.5083333333%; +} + .span12 { + margin-left: 0; + width: 100%; +} + .offset4 { + margin-left: 32.0666666667%; +} + .offset6 { + margin-left: 49.05%; +} \ No newline at end of file diff --git a/src/idea/static/idea/css/idea.css b/src/idea/static/idea/css/idea.css index ef6f74b..901869f 100644 --- a/src/idea/static/idea/css/idea.css +++ b/src/idea/static/idea/css/idea.css @@ -6,21 +6,21 @@ font-family: "Avenir Next", Arial, sans-serif; font-weight: 400; } -a { +.ic a { color: #43b02a; text-decoration: underline; } -a:hover, -a:active, -a:focus { +.ic a:hover, +.ic a:active, +.ic a:focus { text-decoration: none; } -a:focus { +.ic a:focus { outline: thin dotted #111; } /* Improve readability when focused and hovered in all browsers: h5bp.com/h */ -a:hover, -a:active { +.ic a:hover, +.ic a:active { outline: 0; } From a2bf1bf12b10a525ffe3f7e866ac59eba78adf48 Mon Sep 17 00:00:00 2001 From: davidakennedy Date: Tue, 19 Feb 2013 18:07:35 -0500 Subject: [PATCH 002/230] Small tweaks to base.html and base.css for spacing. --- src/idea/static/base.css | 3 ++- src/idea/templates/base.html | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/idea/static/base.css b/src/idea/static/base.css index e0dc271..10647d7 100644 --- a/src/idea/static/base.css +++ b/src/idea/static/base.css @@ -83,7 +83,8 @@ h1 { line-height: 1.2em; margin-bottom: 0.4em; } -h2 {margin: 0; +h2 { + margin: 0; /* 48px / 48px */ font-size: 3em; line-height: 1em; diff --git a/src/idea/templates/base.html b/src/idea/templates/base.html index 821878b..ea8aa55 100644 --- a/src/idea/templates/base.html +++ b/src/idea/templates/base.html @@ -1,6 +1,7 @@ + {%block "css_files" %}{% endblock %} @@ -12,4 +13,4 @@ {%block "js_scripts" %}{% endblock %} - + \ No newline at end of file From a63933490a6b86b06aa21bf2c7ecdaab3df688b7 Mon Sep 17 00:00:00 2001 From: davidakennedy Date: Wed, 20 Feb 2013 11:26:48 -0500 Subject: [PATCH 003/230] Tweak line heights and add components in base.css, tweak comment form spacing in idea.css and correct missing form label for tags in detail.html. --- src/idea/static/base.css | 26 ++++++++++++++++---------- src/idea/static/idea/css/idea.css | 2 +- src/idea/templates/idea/detail.html | 2 +- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/idea/static/base.css b/src/idea/static/base.css index 10647d7..699abb3 100644 --- a/src/idea/static/base.css +++ b/src/idea/static/base.css @@ -80,56 +80,56 @@ h1 { margin: 0; /* 60px / 72px */ font-size: 3.75em; - line-height: 1.2em; + line-height: 1.2; margin-bottom: 0.4em; } h2 { margin: 0; /* 48px / 48px */ font-size: 3em; - line-height: 1em; + line-height: 1; margin-bottom: 0.5em; } h3 { margin: 0; /* 36px / 48px */ font-size: 2.25em; - line-height: 1.3333333333333333333333333333333em; + line-height: 1.3333333333333333333333333333333; margin-bottom: 0.6667em; } h4 { margin: 0; /* 24px / 24px */ font-size: 1.5em; - line-height: 1em; + line-height: 1; margin-bottom: 1em; } h5 { margin: 0; /* 21px / 24px */ font-size: 1.3125em; - line-height: 1.1428571428571428571428571428571em; + line-height: 1.1428571428571428571428571428571; margin-bottom: 1.1428571428571428571428571428571em; } h6 { margin: 0; /* 18px / 24px */ font-size: 1.125em; - line-height: 1.3333333333333333333333333333333em; + line-height: 1.3333333333333333333333333333333; margin-bottom: 1.3333333333333333333333333333333em; } -p, ul, blockquote, pre, td, th, label { +p, ul, blockquote, pre, td, th { margin: 0; /* 16px / 24px */ font-size: 1em; - line-height: 1.5em; + line-height: 1.5; margin-bottom: 1.5em; } small, p.small { margin: 0; /* 14px / 24px */ font-size: 0.875em; - line-height: 1.7142857142857142857142857142857em; + line-height: 1.7142857142857142857142857142857; margin-bottom: 1.7142857142857142857142857142857em; } @@ -142,7 +142,7 @@ pre { } pre, code, kbd, samp { font-size: 1em; - line-height: 1.5em; + line-height: 1.5; margin-bottom: 1.5em; font-family: Menlo, Consolas, 'DejaVu Sans Mono', Monaco, monospace; } @@ -369,6 +369,12 @@ body { border: 1px solid #aaa; height: 2.5em !important; } +.left { + float: left; +} +.right { + float: right; +} /* Grid */ .row, .cf { diff --git a/src/idea/static/idea/css/idea.css b/src/idea/static/idea/css/idea.css index 901869f..67fd490 100644 --- a/src/idea/static/idea/css/idea.css +++ b/src/idea/static/idea/css/idea.css @@ -205,7 +205,7 @@ } .comment-form { background-color: #f2f2f2; - padding: 0.625em 0.625em 2.5em; + padding: 0.625em 0.625em 2.8em; margin: 1.875em 0 0; } .comment-form textarea { diff --git a/src/idea/templates/idea/detail.html b/src/idea/templates/idea/detail.html index b25f1ca..8fc8752 100644 --- a/src/idea/templates/idea/detail.html +++ b/src/idea/templates/idea/detail.html @@ -97,7 +97,7 @@

Idea Tags

{% csrf_token %}
- + {{ tag_form.tags }} Separate tags with comma From 1ea707210facadddf909e72c6a88888ea4073113 Mon Sep 17 00:00:00 2001 From: davidakennedy Date: Wed, 20 Feb 2013 11:55:52 -0500 Subject: [PATCH 004/230] Add Fluid Baseline Grid info to TERMS and base.css, plus add static URL path to base.html. --- TERMS | 8 ++++++++ src/idea/static/base.css | 9 +++++++++ src/idea/templates/base.html | 2 +- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/TERMS b/TERMS index b136da2..883dab6 100644 --- a/TERMS +++ b/TERMS @@ -6,3 +6,11 @@ Images Images originate from: Font Awesome by Dave Gandy - http://fortawesome.github.com/Font-Awesome and are licensed under CC BY 3.0 + +Base Styles + +Much of base.css originates from: +Fluid Baseline Grid v1.0.0 +https://github.com/thedayhascome/Fluid-Baseline-Grid +Designed & Built by Josh Hopkins and 40 Horse, http://40horse.com +Licensed under Unlicense, http://unlicense.org/ \ No newline at end of file diff --git a/src/idea/static/base.css b/src/idea/static/base.css index 699abb3..97ab41f 100644 --- a/src/idea/static/base.css +++ b/src/idea/static/base.css @@ -1,3 +1,12 @@ +/* + The base stlyesheet is here as a starting point for you, and much of it is based on the Fluid Baseline Grid by Josh Hopkins and 40 Horse. We hope it halps, but feel free to hack away! + + Fluid Baseline Grid v1.0.0 + https://github.com/thedayhascome/Fluid-Baseline-Grid + Designed & Built by Josh Hopkins and 40 Horse, http://40horse.com + Licensed under Unlicense, http://unlicense.org/ +*/ + /* HTML5 declarations */ article, aside, diff --git a/src/idea/templates/base.html b/src/idea/templates/base.html index ea8aa55..bd73684 100644 --- a/src/idea/templates/base.html +++ b/src/idea/templates/base.html @@ -1,7 +1,7 @@ - + {%block "css_files" %}{% endblock %} From af5893c04b0fc83cac97fc884770c9a13bc2c3bc Mon Sep 17 00:00:00 2001 From: davidakennedy Date: Wed, 20 Feb 2013 13:58:56 -0500 Subject: [PATCH 005/230] Tweak comment styling on detail.html. --- src/idea/static/idea/css/idea.css | 22 ++++++------------ .../static/idea/img/comment-icon-detail.png | Bin 2523 -> 1811 bytes src/idea/templates/idea/detail.html | 5 ++-- 3 files changed, 9 insertions(+), 18 deletions(-) mode change 100644 => 100755 src/idea/static/idea/img/comment-icon-detail.png diff --git a/src/idea/static/idea/css/idea.css b/src/idea/static/idea/css/idea.css index 67fd490..1c3097c 100644 --- a/src/idea/static/idea/css/idea.css +++ b/src/idea/static/idea/css/idea.css @@ -186,27 +186,19 @@ .comments { position: relative; } -.comment-total { - background: url(../img/comment-icon-detail.png) no-repeat; - color: #fff; - display: block; - font-weight: 600; - height: 40px; - padding: 0.25em 0; - position: absolute; - top: 1.875em; - left: -4em; - text-align: center; - width: 47px; -} .comments h4 { + background: url(../img/comment-icon-detail.png) 0 0 no-repeat; + font-size: 100%; font-weight: bold; + line-height: 1.66; + min-height: 1.75em; + padding-left: 3.3125em; text-transform: uppercase; } .comment-form { background-color: #f2f2f2; padding: 0.625em 0.625em 2.8em; - margin: 1.875em 0 0; + margin: 1.5625em 0 0; } .comment-form textarea { height: 6.25em; @@ -382,7 +374,7 @@ .idea-single .comments { border-top: 3px solid #f2f2f2; margin-top: 2.5em; - padding-top: 1.875em; + padding-top: 1.5625em; } .idea-single .vote-cta { padding-bottom: 0.625em; diff --git a/src/idea/static/idea/img/comment-icon-detail.png b/src/idea/static/idea/img/comment-icon-detail.png old mode 100644 new mode 100755 index fc253b264088dd0fb999366b2b679b6b7d50a274..b3d0c471b80a3e4bd77c3f11bd3f08a284e3375d GIT binary patch delta 1136 zcmcaDJeg0iGr-TCmrII^fq{Y7)59eQNGk%d3Z^Lp~d73CK;F_N1Te5Fd1klFzskk*I{C267p}^IGI}Dl!mo%4ho{sq!b!qQx6w=D7{#ezQx9rzZ|Fe1a zW6LD71UrF?4L|41jjV2c#W{)lnff39^WE3GcP9rhKay4Gxb(WCxjw97-vn=?ZzqHr zSS;2GT26g0{Oq`X_uGg69S#UZ*e6}~WeL=Ae!Ec5S-U-$J4k0!?Y}=&d$^Ch|D4y% zTXxp%^ay7$-0~tG;soDflZp;#rk23&Wm=wM+A7n!P)F)=jeIbo?))^Rrcd zOlmwcMf%>J|0h?fP8I#~FlJwNy=Q8F?^aG(kJR`yySt*7 zNlrP+RJ>j|f2nBid(T{tty8<}a((${M3iD$+5BJX ze+y||Z|hFF+kHZAfyK47Qz_jGr+G>oHatC5w94}|L*TVjGR=A%R_8uFJJZ(UIIqaj zuvT%UcH@y&<(o@SbzS*!ZdcYc^95$#{uc3XXvuokT(2+oX6Bu?twoEUNwfO-baFpi z;OBH{=Rt?7f|EH;ef`B07Jb*9eO_?xF*W-MeShl?#-i>T|| z@@&7VsuL}rZgAwTmCW_Ly|=BCuW8?YWbD}U^3v`ViNAVl0zW!jpSer+j{wW+PtCkX zlwQB+SYIUVnO9+b)s$)T>QB2_MU%=)%{wMpcCS|sSKaugupy%U`N{2V1q}g$MrZD- zueqTnw`F=!|Gvsy>@F*p_)Se0T^9N~yrIhW<4>VQ-olEG$?fw+4#z5#e0){>%+0Rh zjJiURzs#DVtFo0k6_?`q{<9r4Gs)ew@|M-zckVNvFm$q<5WDo~cYMlD)5J{m`WlHv z>n_V*=grYd(EMksY|*kpPQy^qQ_aq!oHv&9r+c4@-{bWR%nTbXBF^gD@!0_j5e83J KKbLh*2~7ZIAM4}* delta 1853 zcmZ{lc{~%0AIE1}?o5>Qq|7xr=Exc4*xb=_gq)8n$}vAjj5TK(vxod-t!&PShY*Q9 z=9-`TTujb6!lp=aKH~Am@AskN4~S=l7d^W=a%-L)w}P@n7Tz002Uk76`kO zt$H#Fyr)kxM7l_<1_A|2nd-vf2z6a;ZC$v!y1J=`p882e=oxFNnVRY9AvCfBfS3M- zbbbjH1J$nn-=z5~NLng9!p_hCx^H$EQ1oA=*01PnHEt7Z#If570C3vI5@GBRJ^820 zfQyW!V9tRIS17NV1QFaHtXB&$;#Cs?Kv-bbhu97X{@Mftl$xK@)Z}oxTtl!!dy&m) zd64s%O#Y}W{X>6h&Ts>_BJwtpP=EPP=PtCjU-bQIDDMfJVbX?8ikF=YDZd zx7vQgX4SoBn;&pWvzfrBj%`(Hx~f$nWl&YDg_ZbmC9VzOKM%$1I1W*kLD831qyB>k z5W0>YFL>ZH0=DKQ2K6ox=%`6|{8aC9^o1G5A*N{YN>hu=G|oVt5a@Bow}k2>`U=}9 zKBxsEnYV%5^g|V-5DQ)h59Bn|)Z{#vtj%ME2v-g4HHI-!)*PWYmsN z-NS-ALbBA1F)muJn3_e@#a)^wmiyK{V41)bsk+SBH;Q)Awx5OEx3M&p+X{bjksS_{)k9s+kCe4c*o1;qQ+yuM(5IRQxHiW?{ zp|S;?c4SuTr1iH!@bv+}@idcLyEM1(p(DFiTT-2vsxYpMHI;o4!Yg}K zkuQ7{S5;nd>cC6YivxUSFU^0+H(s7r#-;onaEg;X{Svf7D-Q`zdjTVr?V9tXrRber zntw76JM)c5_T@vSlWxS)Il~!y4uO5W$)3!3UpzX_2(WNr1VVbH@ASqR>D zEgn^t@W5MQKBBi=!7HkBc`IXAr)?=batBgZfzMy%F5dOB^rc@RkR|kmH2aNkvmugh^eRDQ> zQj4@UE#*Pai`@k@gi%k*vO|HN*zR~!>1-YXEh zj9{NnBLp`dVuOV_V8jBPriSm)H$l!(3z4%D~B^&b#DHzdX ztPbDYYn|(`y|ymsb^YdBT4=?=Lyx>+lpVVV2lu4$h(KKd^%-H$Du=W2UP9D1gK2YAlRJoiqa@(zw^dQA#nn4N~W@mfHz@nCzJbrPeXvc|q}Ul0Y1( zEBdCDf7W+zKZK;1j4vdHyO_$Dw<%LG(+Zl=j8|Ek1YU+i&gKgsjR9*Zv!8#qNlxb$ z#f~G2i?*aYi1nC>b%wX!x;I-urv_s@=xRp1v9Jsowr2{FUWRj+n=#CYlt<3g(1Ws> zQfyc?gW@vZPBP?5(A)ojn3K1f0T480jbKgIY~6&QCPu9>5x;)ce zvuC#MbW1J?!fE3DRs92#uVap^u(Rd+CBeapsM*2wvp=evm>u= z3@J+t)cFo)Fz$=Z>_Mn2KS1||4P4uqe=JJ8XT_So7|6ko9TltusqeURSTo7qxHbIA z!Tl~Sk*V`K(^;ATznj^ekT1~ePAuQt(aIUD2Yq!ty1YXHa03(}ffcCKj-`{22(UD@ KMO2x1CH@1)Yhd{R diff --git a/src/idea/templates/idea/detail.html b/src/idea/templates/idea/detail.html index 8fc8752..54a11c9 100644 --- a/src/idea/templates/idea/detail.html +++ b/src/idea/templates/idea/detail.html @@ -44,10 +44,9 @@

{{idea.title}}

{{idea.text}}

{% get_comment_list for idea.idea idea.id as comment_list %} -
-

Comments

+
- {{comment_list|length}} +

{{comment_list|length}} Comments

From 941600d4a38ea6479ea6141b12703a50381338ef Mon Sep 17 00:00:00 2001 From: davidakennedy Date: Thu, 21 Feb 2013 17:42:48 -0500 Subject: [PATCH 006/230] Links now bold, make result number bold and remove global font-weight: normal attribute. --- src/idea/static/idea/css/idea.css | 11 ++++++++--- src/idea/templates/idea/add.html | 4 ++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/idea/static/idea/css/idea.css b/src/idea/static/idea/css/idea.css index 1c3097c..b66f02d 100644 --- a/src/idea/static/idea/css/idea.css +++ b/src/idea/static/idea/css/idea.css @@ -4,16 +4,16 @@ /* Base */ .ic * { font-family: "Avenir Next", Arial, sans-serif; - font-weight: 400; } .ic a { color: #43b02a; - text-decoration: underline; + font-weight: bold; + text-decoration: none; } .ic a:hover, .ic a:active, .ic a:focus { - text-decoration: none; + text-decoration: underline; } .ic a:focus { outline: thin dotted #111; @@ -155,6 +155,7 @@ } .idea-title h3 { font-size: 150%; /* 24px */ + font-weight: 600; line-height: 1.33; margin-bottom: 0.25em; } @@ -282,6 +283,7 @@ background-color: #f4dc9a; color: #101820; font-size: 87.5%; + font-weight: normal; padding: 0.625em 0 0.625em 0.625em; text-decoration: none; text-transform: uppercase; @@ -506,6 +508,9 @@ span.btn-archive:hover { .idea-add .alert-none p { display: block; margin-bottom: 0.25em; +} +.result-number { + font-weight: bold; } /* Image replacement */ .ir { diff --git a/src/idea/templates/idea/add.html b/src/idea/templates/idea/add.html index fcdbdaa..f93f73d 100644 --- a/src/idea/templates/idea/add.html +++ b/src/idea/templates/idea/add.html @@ -11,10 +11,10 @@

Idea
Box

{% if not similar %}
-

{{similar|length}} similar idea{{similar|pluralize}}

+

{{similar|length}} similar idea{{similar|pluralize}}

{% else %} From aadf7eaf1c1bfc45f977d24f7d87c602af09b83c Mon Sep 17 00:00:00 2001 From: davidakennedy Date: Fri, 22 Feb 2013 10:42:55 -0500 Subject: [PATCH 007/230] Update main menu links and styles. --- src/idea/static/idea/css/idea.css | 1 + src/idea/templates/idea/list.html | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/idea/static/idea/css/idea.css b/src/idea/static/idea/css/idea.css index b66f02d..d82beaa 100644 --- a/src/idea/static/idea/css/idea.css +++ b/src/idea/static/idea/css/idea.css @@ -126,6 +126,7 @@ .main-nav ul li a.active { background: #43b02a; color: #fff; + text-decoration: none; } /* Content */ diff --git a/src/idea/templates/idea/list.html b/src/idea/templates/idea/list.html index 85da7ed..0470d1a 100644 --- a/src/idea/templates/idea/list.html +++ b/src/idea/templates/idea/list.html @@ -28,8 +28,8 @@

Main Navigation for Idea Box

From 573d41191f9c93912708dfd5c320957a7040a45c Mon Sep 17 00:00:00 2001 From: davidakennedy Date: Fri, 22 Feb 2013 11:24:18 -0500 Subject: [PATCH 008/230] Adjust start button size to better match design. --- src/idea/static/idea/css/idea.css | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/idea/static/idea/css/idea.css b/src/idea/static/idea/css/idea.css index d82beaa..e354c1c 100644 --- a/src/idea/static/idea/css/idea.css +++ b/src/idea/static/idea/css/idea.css @@ -39,7 +39,7 @@ display: inline; font-size: 312.5%; /* 50px */ font-weight: bold; - line-height: 0.78; + line-height: 0.95; text-transform: uppercase; } .project-search { @@ -63,7 +63,7 @@ } .search-form input[type="text"], .search-form textarea { - height: 2em; + height: 2.5em; width: 80%; } .start-btn { @@ -72,8 +72,9 @@ color: #fff; font-size: 150%; /* 24px */ font-weight: 600; + height: 2em; margin-left: -0.2em; - padding: 0.20em 1em; + padding: 0.25em 1em; } .start-btn:hover, .start-btn:active, From 873795ab63f17134a5634272e021e5327771f937 Mon Sep 17 00:00:00 2001 From: davidakennedy Date: Fri, 22 Feb 2013 11:51:24 -0500 Subject: [PATCH 009/230] Fix text-decoration on logo and HTML error in add.html. --- src/idea/static/idea/css/idea.css | 3 ++- src/idea/templates/idea/add.html | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/idea/static/idea/css/idea.css b/src/idea/static/idea/css/idea.css index e354c1c..5e0d032 100644 --- a/src/idea/static/idea/css/idea.css +++ b/src/idea/static/idea/css/idea.css @@ -31,7 +31,8 @@ .logo { padding: 0 0 0 5em; } -.logo a { +.logo a, +.logo a:hover { text-decoration: none; } .project-title { diff --git a/src/idea/templates/idea/add.html b/src/idea/templates/idea/add.html index f93f73d..38769ec 100644 --- a/src/idea/templates/idea/add.html +++ b/src/idea/templates/idea/add.html @@ -17,7 +17,7 @@

{{similar|length}} similar idea{{similar|pluralize}}

{% endif %} -
+
+
{% for idea in similar %} From d656f14e85943720dcadbdcbf13e4886683bdc4d Mon Sep 17 00:00:00 2001 From: davidakennedy Date: Fri, 22 Feb 2013 15:03:31 -0500 Subject: [PATCH 010/230] Remove hover and border on no-results posts and make add.html alert box be a link. --- src/idea/static/idea/css/idea.css | 17 +++++++++++++---- src/idea/templates/idea/add.html | 14 +++++++++----- src/idea/templates/idea/list.html | 4 ++-- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/idea/static/idea/css/idea.css b/src/idea/static/idea/css/idea.css index 5e0d032..9a8a4a0 100644 --- a/src/idea/static/idea/css/idea.css +++ b/src/idea/static/idea/css/idea.css @@ -136,6 +136,9 @@ border-bottom: 1px solid #f2f2f2; padding: 2.5em 0; } +.no-results { + padding: 2.5em 0; +} .idea-entry.hover { background-color: #f2f2f2; } @@ -477,12 +480,18 @@ span.btn-archive:hover { } .idea-add .alert { color: #d14124; + display: block; margin-top: 1.6em; text-align: center; text-transform: uppercase; } -.idea-add .alert a { +.idea-add a .alert { color: #d14124; + text-decoration: none; +} +.idea-add a:hover .alert, +.idea-add a:focus .alert { + border-color: #d14124 !important; } .idea-add .alert .down { background: url(../img/alert-arrow.png) 0 50% no-repeat; @@ -495,8 +504,8 @@ span.btn-archive:hover { border: 3px solid #999; box-sizing: border-box; color: #999; - margin-bottom: 2em; - margin-top: 1.6em; + display: block; + margin: 1.25em 0; padding: 0.5em 1em ; text-align: center; text-transform: uppercase; @@ -507,7 +516,7 @@ span.btn-archive:hover { padding: 0.6875em; width: 22px; } -.idea-add .alert p, +.idea-add .alert .alert-content, .idea-add .alert-none p { display: block; margin-bottom: 0.25em; diff --git a/src/idea/templates/idea/add.html b/src/idea/templates/idea/add.html index 38769ec..205284a 100644 --- a/src/idea/templates/idea/add.html +++ b/src/idea/templates/idea/add.html @@ -12,12 +12,16 @@ {% if not similar %}

{{similar|length}} similar idea{{similar|pluralize}}

- {% else %} - +
+ {% else %} + + + {{similar|length}} similar idea{{similar|pluralize}} + + + + {% endif %}
'); + } + // Prettify the comment rating. + comment.pretty_rating = comment.rating + ' point' + + (comment.rating == 1 ? '' : 's'); + // Make a class (for displaying not yet moderated comments differently) + comment.css_class = comment.displayed ? '' : ' moderate'; + // Create a div for this comment. + var context = $.extend({}, opts, comment); + var div = $(renderTemplate(commentTemplate, context)); + + // If the user has voted on this comment, highlight the correct arrow. + if (comment.vote) { + var direction = (comment.vote == 1) ? 'u' : 'd'; + div.find('#' + direction + 'v' + comment.id).hide(); + div.find('#' + direction + 'u' + comment.id).show(); + } + + if (opts.moderator || comment.text != '[deleted]') { + div.find('a.reply').show(); + if (comment.proposal_diff) + div.find('#sp' + comment.id).show(); + if (opts.moderator && !comment.displayed) + div.find('#cm' + comment.id).show(); + if (opts.moderator || (opts.username == comment.username)) + div.find('#dc' + comment.id).show(); + } + return div; + } + + /** + * A simple template renderer. Placeholders such as <%id%> are replaced + * by context['id'] with items being escaped. Placeholders such as <#id#> + * are not escaped. + */ + function renderTemplate(template, context) { + var esc = $(document.createElement('div')); + + function handle(ph, escape) { + var cur = context; + $.each(ph.split('.'), function() { + cur = cur[this]; + }); + return escape ? esc.text(cur || "").html() : cur; + } + + return template.replace(/<([%#])([\w\.]*)\1>/g, function() { + return handle(arguments[2], arguments[1] == '%' ? true : false); + }); + } + + /** Flash an error message briefly. */ + function showError(message) { + $(document.createElement('div')).attr({'class': 'popup-error'}) + .append($(document.createElement('div')) + .attr({'class': 'error-message'}).text(message)) + .appendTo('body') + .fadeIn("slow") + .delay(2000) + .fadeOut("slow"); + } + + /** Add a link the user uses to open the comments popup. */ + $.fn.comment = function() { + return this.each(function() { + var id = $(this).attr('id').substring(1); + var count = COMMENT_METADATA[id]; + var title = count + ' comment' + (count == 1 ? '' : 's'); + var image = count > 0 ? opts.commentBrightImage : opts.commentImage; + var addcls = count == 0 ? ' nocomment' : ''; + $(this) + .append( + $(document.createElement('a')).attr({ + href: '#', + 'class': 'sphinx-comment-open' + addcls, + id: 'ao' + id + }) + .append($(document.createElement('img')).attr({ + src: image, + alt: 'comment', + title: title + })) + .click(function(event) { + event.preventDefault(); + show($(this).attr('id').substring(2)); + }) + ) + .append( + $(document.createElement('a')).attr({ + href: '#', + 'class': 'sphinx-comment-close hidden', + id: 'ah' + id + }) + .append($(document.createElement('img')).attr({ + src: opts.closeCommentImage, + alt: 'close', + title: 'close' + })) + .click(function(event) { + event.preventDefault(); + hide($(this).attr('id').substring(2)); + }) + ); + }); + }; + + var opts = { + processVoteURL: '/_process_vote', + addCommentURL: '/_add_comment', + getCommentsURL: '/_get_comments', + acceptCommentURL: '/_accept_comment', + deleteCommentURL: '/_delete_comment', + commentImage: '/static/_static/comment.png', + closeCommentImage: '/static/_static/comment-close.png', + loadingImage: '/static/_static/ajax-loader.gif', + commentBrightImage: '/static/_static/comment-bright.png', + upArrow: '/static/_static/up.png', + downArrow: '/static/_static/down.png', + upArrowPressed: '/static/_static/up-pressed.png', + downArrowPressed: '/static/_static/down-pressed.png', + voting: false, + moderator: false + }; + + if (typeof COMMENT_OPTIONS != "undefined") { + opts = jQuery.extend(opts, COMMENT_OPTIONS); + } + + var popupTemplate = '\ +
\ +

\ + Sort by:\ + best rated\ + newest\ + oldest\ +

\ +
Comments
\ +
\ + loading comments...
\ +
    \ +
    \ +

    Add a comment\ + (markup):

    \ +
    \ + reStructured text markup: *emph*, **strong**, \ + ``code``, \ + code blocks: :: and an indented block after blank line
    \ + \ + \ +

    \ + \ + Propose a change ▹\ + \ + \ + Propose a change ▿\ + \ +

    \ + \ + \ + \ + \ + \ +
    \ +
    '; + + var commentTemplate = '\ +
    \ +
    \ +
    \ + \ + \ + \ + \ + \ + \ +
    \ +
    \ + \ + \ + \ + \ + \ + \ +
    \ +
    \ +
    \ +

    \ + <%username%>\ + <%pretty_rating%>\ + <%time.delta%>\ +

    \ +
    <#text#>
    \ +

    \ + \ + reply ▿\ + proposal ▹\ + proposal ▿\ + \ + \ +

    \ +
    \
    +<#proposal_diff#>\
    +        
    \ +
      \ +
      \ +
      \ +
      \ +
      '; + + var replyTemplate = '\ +
    • \ +
      \ +
      \ + \ + \ + \ + \ + \ +
      \ +
      \ +
    • '; + + $(document).ready(function() { + init(); + }); +})(jQuery); + +$(document).ready(function() { + // add comment anchors for all paragraphs that are commentable + $('.sphinx-has-comment').comment(); + + // highlight search words in search results + $("div.context").each(function() { + var params = $.getQueryParameters(); + var terms = (params.q) ? params.q[0].split(/\s+/) : []; + var result = $(this); + $.each(terms, function() { + result.highlightText(this.toLowerCase(), 'highlighted'); + }); + }); + + // directly open comment window if requested + var anchor = document.location.hash; + if (anchor.substring(0, 9) == '#comment-') { + $('#ao' + anchor.substring(9)).click(); + document.location.hash = '#s' + anchor.substring(9); + } +}); diff --git a/doc/build/html/api/idea.buildout.html b/doc/build/html/api/idea.buildout.html new file mode 100644 index 0000000..150d5d6 --- /dev/null +++ b/doc/build/html/api/idea.buildout.html @@ -0,0 +1,116 @@ + + + + + + + + + + buildout Package — Idea Box 0.1.0 documentation + + + + + + + + + + + + + +
      +
      +
      +
      + +
      +

      buildout Package

      +
      +

      search_sites Module

      +
      +
      +

      testsettings Module

      +
      +
      +

      urls Module

      +
      +
      + + +
      +
      +
      +
      +
      +

      Table Of Contents

      + + +

      This Page

      + + + +
      +
      +
      +
      + + + + \ No newline at end of file diff --git a/doc/build/html/api/idea.html b/doc/build/html/api/idea.html new file mode 100644 index 0000000..541d8bb --- /dev/null +++ b/doc/build/html/api/idea.html @@ -0,0 +1,549 @@ + + + + + + + + + + idea Package — Idea Box 0.1.0 documentation + + + + + + + + + + + + + +
      +
      +
      +
      + +
      +

      idea Package

      +
      +

      admin Module

      +
      +
      +

      forms Module

      +
      +
      +class idea.forms.IdeaForm(data=None, files=None, auto_id='id_%s', prefix=None, initial=None, error_class=<class 'django.forms.util.ErrorList'>, label_suffix=':', empty_permitted=False, instance=None)
      +

      Bases: django.forms.models.ModelForm

      +
      +
      +class Meta
      +
      +
      +exclude = ('creator', 'time', 'state')
      +
      + +
      +
      +model
      +

      alias of Idea

      +
      + +
      + +
      +
      +IdeaForm.base_fields = {'title': <django.forms.fields.CharField object at 0x3b0ebd0>, 'text': <django.forms.fields.CharField object at 0x3b0e7d0>, 'tags': <taggit.forms.TagField object at 0x3b0ea10>}
      +
      + +
      +
      +IdeaForm.clean_tags()
      +

      Force tags to lowercase, since tags are case-sensitive otherwise.

      +
      + +
      +
      +IdeaForm.declared_fields = {}
      +
      + +
      +
      +IdeaForm.media
      +
      + +
      + +
      +
      +class idea.forms.IdeaTagForm(data=None, files=None, auto_id='id_%s', prefix=None, initial=None, error_class=<class 'django.forms.util.ErrorList'>, label_suffix=':', empty_permitted=False)
      +

      Bases: django.forms.forms.Form

      +
      +
      +base_fields = {'tags': <django.forms.fields.CharField object at 0x3b0e450>}
      +
      + +
      +
      +clean_tags()
      +

      Force tags to lowercase, since tags are case-sensitive otherwise.

      +
      + +
      +
      +media
      +
      + +
      + +
      +
      +class idea.forms.UpVoteForm(data=None, files=None, auto_id='id_%s', prefix=None, initial=None, error_class=<class 'django.forms.util.ErrorList'>, label_suffix=':', empty_permitted=False)
      +

      Bases: django.forms.forms.Form

      +
      +
      +base_fields = {'idea_id': <django.forms.fields.IntegerField object at 0x3b0e810>, 'next': <django.forms.fields.CharField object at 0x3b0e390>}
      +
      + +
      +
      +media
      +
      + +
      + +
      +
      +

      models Module

      +
      +
      +class idea.models.Banner(*args, **kwargs)
      +

      Bases: django.db.models.base.Model

      +

      The banner text at the beginning of IdeaBox pages, asking the question. +This can be used to run informal campaigns soliciting ideas around specific +topics.

      +
      +
      +exception DoesNotExist
      +

      Bases: django.core.exceptions.ObjectDoesNotExist

      +
      + +
      +
      +exception Banner.MultipleObjectsReturned
      +

      Bases: django.core.exceptions.MultipleObjectsReturned

      +
      + +
      +
      +Banner.get_next_by_start_date(*moreargs, **morekwargs)
      +
      + +
      +
      +Banner.get_previous_by_start_date(*moreargs, **morekwargs)
      +
      + +
      +
      +Banner.objects = <django.db.models.manager.Manager object at 0x2b186d0>
      +
      + +
      + +
      +
      +class idea.models.Idea(*args, **kwargs)
      +

      Bases: idea.models.UserTrackable

      +

      Idea(id, creator_id, time, title, text, state_id)

      +
      +
      +exception DoesNotExist
      +

      Bases: django.core.exceptions.ObjectDoesNotExist

      +
      + +
      +
      +exception Idea.MultipleObjectsReturned
      +

      Bases: django.core.exceptions.MultipleObjectsReturned

      +
      + +
      +
      +Idea.creator
      +
      + +
      +
      +Idea.get_creator_profile()
      +
      + +
      +
      +Idea.get_next_by_time(*moreargs, **morekwargs)
      +
      + +
      +
      +Idea.get_previous_by_time(*moreargs, **morekwargs)
      +
      + +
      +
      +Idea.objects = <idea.models.IdeaManager object at 0x2b18bd0>
      +
      + +
      +
      +Idea.state
      +
      + +
      +
      +Idea.tagged_items
      +

      This class provides the functionality that makes the related-object +managers available as attributes on a model class, for fields that have +multiple “remote” values and have a GenericRelation defined in their model +(rather than having another model pointed at them). In the example +“article.publications”, the publications attribute is a +ReverseGenericRelatedObjectsDescriptor instance.

      +
      + +
      +
      +Idea.tags = <taggit.managers._TaggableManager object at 0x3d1f950>
      +
      + +
      +
      +Idea.url()
      +

      Lookup the view url for this idea.

      +
      + +
      +
      +Idea.vote_set
      +
      + +
      + +
      +
      +class idea.models.IdeaManager
      +

      Bases: django.db.models.manager.Manager

      +
      +
      +related_with_counts()
      +
      + +
      + +
      +
      +class idea.models.State(*args, **kwargs)
      +

      Bases: django.db.models.base.Model

      +

      The state an idea goes through.

      +
      +
      +exception DoesNotExist
      +

      Bases: django.core.exceptions.ObjectDoesNotExist

      +
      + +
      +
      +exception State.MultipleObjectsReturned
      +

      Bases: django.core.exceptions.MultipleObjectsReturned

      +
      + +
      +
      +State.idea_set
      +
      + +
      +
      +State.objects = <django.db.models.manager.Manager object at 0x2b18950>
      +
      + +
      +
      +State.previous
      +
      + +
      +
      +State.state
      +
      + +
      + +
      +
      +class idea.models.UserTrackable(*args, **kwargs)
      +

      Bases: django.db.models.base.Model

      +
      +
      +class Meta
      +
      +
      +abstract = False
      +
      + +
      + +
      +
      +UserTrackable.creator
      +
      + +
      +
      +UserTrackable.get_next_by_time(*moreargs, **morekwargs)
      +
      + +
      +
      +UserTrackable.get_previous_by_time(*moreargs, **morekwargs)
      +
      + +
      + +
      +
      +class idea.models.Vote(*args, **kwargs)
      +

      Bases: idea.models.UserTrackable

      +

      Vote(id, creator_id, time, vote, idea_id)

      +
      +
      +exception DoesNotExist
      +

      Bases: django.core.exceptions.ObjectDoesNotExist

      +
      + +
      +
      +exception Vote.MultipleObjectsReturned
      +

      Bases: django.core.exceptions.MultipleObjectsReturned

      +
      + +
      +
      +Vote.creator
      +
      + +
      +
      +Vote.get_next_by_time(*moreargs, **morekwargs)
      +
      + +
      +
      +Vote.get_previous_by_time(*moreargs, **morekwargs)
      +
      + +
      +
      +Vote.get_vote_display(*moreargs, **morekwargs)
      +
      + +
      +
      +Vote.idea
      +
      + +
      +
      +Vote.objects = <django.db.models.manager.Manager object at 0x2b1c690>
      +
      + +
      + +
      +
      +

      search_indexes Module

      +
      +
      +class idea.search_indexes.IdeaIndex(model, backend=None)
      +

      Bases: haystack.indexes.RealTimeSearchIndex

      +
      +
      +fields = {'text': <haystack.fields.CharField object at 0x3af9890>}
      +
      + +
      +
      +index_queryset()
      +
      + +
      + +
      +
      +

      urls Module

      +
      +
      +

      views Module

      +
      +
      +idea.views.add_idea(request, *args, **kwargs)
      +
      + +
      +
      +idea.views.detail(request, *args, **kwargs)
      +

      Detail view; idea_id must be a string containing an int.

      +
      + +
      +
      +idea.views.get_banner()
      +
      + +
      +
      +idea.views.list(request, *args, **kwargs)
      +
      + +
      +
      +idea.views.more_like_text(text, klass)
      +

      Return more entries like the provided chunk of text. We have to jump +through some hoops to get this working as the haystack API does not +account for this case. In particular, this is a solr-specific hack.

      +
      + +
      +
      +idea.views.up_vote(request, *args, **kwargs)
      +
      + +
      +
      +idea.views.vote_up(idea, user)
      +
      + +
      + +
      + + +
      +
      +
      +
      +
      +

      Table Of Contents

      + + +

      This Page

      + + + +
      +
      +
      +
      + + + + \ No newline at end of file diff --git a/doc/build/html/api/idea.migrations.html b/doc/build/html/api/idea.migrations.html new file mode 100644 index 0000000..ddb569b --- /dev/null +++ b/doc/build/html/api/idea.migrations.html @@ -0,0 +1,351 @@ + + + + + + + + + + migrations Package — Idea Box 0.1.0 documentation + + + + + + + + + + + + + +
      +
      +
      +
      + +
      +

      migrations Package

      +
      +

      0001_initial Module

      +
      +
      +class idea.migrations.0001_initial.Migration
      +

      Bases: south.v2.SchemaMigration

      +
      +
      +backwards(orm)
      +
      + +
      +
      +complete_apps = ['idea']
      +
      + +
      +
      +forwards(orm)
      +
      + +
      +
      +models = {'auth.permission': {'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})}, 'contenttypes.contenttype': {'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'})}, 'idea.comment': {'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), 'text': ('django.db.models.fields.TextField', [], {}), 'Meta': {'object_name': 'Comment'}, 'time': ('django.db.models.fields.DateField', [], {'auto_now': 'True', 'blank': 'True'}), 'issue': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['idea.Idea']"}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})}, 'auth.user': {'username': ('django.db.models.fields.CharField', [], {'max_length': '30', 'unique': 'True'}), 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'blank': 'True', 'symmetrical': 'False'}), 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True', 'symmetrical': 'False'}), 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'Meta': {'object_name': 'User'}, 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'})}, 'idea.vote': {'idea': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['idea.Idea']"}), 'time': ('django.db.models.fields.DateField', [], {'auto_now': 'True', 'blank': 'True'}), 'Meta': {'object_name': 'Vote'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})}, 'idea.state': {'Meta': {'object_name': 'State'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})}, 'auth.group': {'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True', 'symmetrical': 'False'}), 'Meta': {'object_name': 'Group'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True'})}, 'idea.idea': {'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), 'text': ('django.db.models.fields.TextField', [], {}), 'title': ('django.db.models.fields.CharField', [], {'max_length': '140'}), 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['idea.State']"}), 'Meta': {'object_name': 'Idea'}, 'time': ('django.db.models.fields.DateField', [], {'auto_now': 'True', 'blank': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})}}
      +
      + +
      + +
      +
      +

      0002_auto__del_field_comment_issue__add_field_comment_idea Module

      +
      +
      +class idea.migrations.0002_auto__del_field_comment_issue__add_field_comment_idea.Migration
      +

      Bases: south.v2.SchemaMigration

      +
      +
      +backwards(orm)
      +
      + +
      +
      +complete_apps = ['idea']
      +
      + +
      +
      +forwards(orm)
      +
      + +
      +
      +models = {'auth.permission': {'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})}, 'contenttypes.contenttype': {'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'})}, 'idea.comment': {'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), 'text': ('django.db.models.fields.TextField', [], {}), 'idea': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['idea.Idea']"}), 'Meta': {'object_name': 'Comment'}, 'time': ('django.db.models.fields.DateField', [], {'auto_now': 'True', 'blank': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})}, 'auth.user': {'username': ('django.db.models.fields.CharField', [], {'max_length': '30', 'unique': 'True'}), 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'blank': 'True', 'symmetrical': 'False'}), 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True', 'symmetrical': 'False'}), 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'Meta': {'object_name': 'User'}, 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'})}, 'idea.vote': {'idea': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['idea.Idea']"}), 'time': ('django.db.models.fields.DateField', [], {'auto_now': 'True', 'blank': 'True'}), 'Meta': {'object_name': 'Vote'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})}, 'idea.state': {'Meta': {'object_name': 'State'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})}, 'auth.group': {'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True', 'symmetrical': 'False'}), 'Meta': {'object_name': 'Group'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True'})}, 'idea.idea': {'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), 'text': ('django.db.models.fields.TextField', [], {}), 'title': ('django.db.models.fields.CharField', [], {'max_length': '140'}), 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['idea.State']"}), 'Meta': {'object_name': 'Idea'}, 'time': ('django.db.models.fields.DateField', [], {'auto_now': 'True', 'blank': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})}}
      +
      + +
      + +
      +
      +

      0003_auto__add_field_state_previous__chg_field_idea_text__chg_field_idea_ti Module

      +
      +
      +class idea.migrations.0003_auto__add_field_state_previous__chg_field_idea_text__chg_field_idea_ti.Migration
      +

      Bases: south.v2.SchemaMigration

      +
      +
      +backwards(orm)
      +
      + +
      +
      +complete_apps = ['idea']
      +
      + +
      +
      +forwards(orm)
      +
      + +
      +
      +models = {'auth.permission': {'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})}, 'contenttypes.contenttype': {'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'})}, 'idea.comment': {'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), 'text': ('django.db.models.fields.TextField', [], {}), 'idea': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['idea.Idea']"}), 'Meta': {'object_name': 'Comment'}, 'time': ('django.db.models.fields.DateField', [], {'auto_now': 'True', 'blank': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})}, 'auth.user': {'username': ('django.db.models.fields.CharField', [], {'max_length': '30', 'unique': 'True'}), 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'blank': 'True', 'symmetrical': 'False'}), 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True', 'symmetrical': 'False'}), 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'Meta': {'object_name': 'User'}, 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'})}, 'idea.vote': {'idea': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['idea.Idea']"}), 'time': ('django.db.models.fields.DateField', [], {'auto_now': 'True', 'blank': 'True'}), 'Meta': {'object_name': 'Vote'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})}, 'idea.state': {'previous': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['idea.State']", 'unique': 'True', 'null': 'True', 'blank': 'True'}), 'Meta': {'object_name': 'State'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})}, 'auth.group': {'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True', 'symmetrical': 'False'}), 'Meta': {'object_name': 'Group'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True'})}, 'idea.idea': {'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), 'text': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 'title': ('django.db.models.fields.CharField', [], {'max_length': '140'}), 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['idea.State']"}), 'Meta': {'object_name': 'Idea'}, 'time': ('django.db.models.fields.DateField', [], {'default': 'datetime.datetime(2013, 1, 31, 0, 0)'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})}}
      +
      + +
      + +
      +
      +

      0004_statuses Module

      +
      +
      +class idea.migrations.0004_statuses.Migration
      +

      Bases: south.v2.DataMigration

      +
      +
      +backwards(orm)
      +

      No backwards.

      +
      + +
      +
      +complete_apps = ['idea']
      +
      + +
      +
      +forwards(orm)
      +

      Load the two statuses.

      +
      + +
      +
      +models = {'auth.permission': {'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})}, 'contenttypes.contenttype': {'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'})}, 'idea.comment': {'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), 'text': ('django.db.models.fields.TextField', [], {}), 'idea': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['idea.Idea']"}), 'Meta': {'object_name': 'Comment'}, 'time': ('django.db.models.fields.DateField', [], {'auto_now': 'True', 'blank': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})}, 'auth.user': {'username': ('django.db.models.fields.CharField', [], {'max_length': '30', 'unique': 'True'}), 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'blank': 'True', 'symmetrical': 'False'}), 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True', 'symmetrical': 'False'}), 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'Meta': {'object_name': 'User'}, 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'})}, 'idea.vote': {'idea': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['idea.Idea']"}), 'time': ('django.db.models.fields.DateField', [], {'auto_now': 'True', 'blank': 'True'}), 'Meta': {'object_name': 'Vote'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})}, 'idea.state': {'previous': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['idea.State']", 'unique': 'True', 'null': 'True', 'blank': 'True'}), 'Meta': {'object_name': 'State'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})}, 'auth.group': {'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True', 'symmetrical': 'False'}), 'Meta': {'object_name': 'Group'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True'})}, 'idea.idea': {'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), 'text': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 'title': ('django.db.models.fields.CharField', [], {'max_length': '140'}), 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['idea.State']"}), 'Meta': {'object_name': 'Idea'}, 'time': ('django.db.models.fields.DateField', [], {'default': 'datetime.datetime(2013, 1, 31, 0, 0)'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})}}
      +
      + +
      +
      +symmetrical = True
      +
      + +
      + +
      +
      +

      0005_auto__chg_field_vote_time__chg_field_comment_time Module

      +
      +
      +class idea.migrations.0005_auto__chg_field_vote_time__chg_field_comment_time.Migration
      +

      Bases: south.v2.SchemaMigration

      +
      +
      +backwards(orm)
      +
      + +
      +
      +complete_apps = ['idea']
      +
      + +
      +
      +forwards(orm)
      +
      + +
      +
      +models = {'taggit.tag': {'slug': ('django.db.models.fields.SlugField', [], {'max_length': '100', 'unique': 'True'}), 'Meta': {'object_name': 'Tag'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'})}, 'idea.idea': {'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), 'text': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 'title': ('django.db.models.fields.CharField', [], {'max_length': '140'}), 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['idea.State']"}), 'Meta': {'object_name': 'Idea'}, 'time': ('django.db.models.fields.DateField', [], {'default': 'datetime.datetime(2013, 2, 1, 0, 0)'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})}, 'idea.comment': {'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), 'text': ('django.db.models.fields.TextField', [], {}), 'idea': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['idea.Idea']"}), 'Meta': {'object_name': 'Comment'}, 'time': ('django.db.models.fields.DateField', [], {'default': 'datetime.datetime(2013, 2, 1, 0, 0)'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})}, 'idea.state': {'previous': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['idea.State']", 'unique': 'True', 'null': 'True', 'blank': 'True'}), 'Meta': {'object_name': 'State'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})}, 'auth.group': {'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True', 'symmetrical': 'False'}), 'Meta': {'object_name': 'Group'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True'})}, 'auth.user': {'username': ('django.db.models.fields.CharField', [], {'max_length': '30', 'unique': 'True'}), 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'blank': 'True', 'symmetrical': 'False'}), 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True', 'symmetrical': 'False'}), 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'Meta': {'object_name': 'User'}, 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'})}, 'auth.permission': {'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})}, 'contenttypes.contenttype': {'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'})}, 'idea.vote': {'idea': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['idea.Idea']"}), 'time': ('django.db.models.fields.DateField', [], {'default': 'datetime.datetime(2013, 2, 1, 0, 0)'}), 'Meta': {'object_name': 'Vote'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})}, 'taggit.tagcategory': {'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'unique': 'True'}), 'Meta': {'object_name': 'TagCategory'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'unique': 'True'})}, 'taggit.taggeditem': {'tag_creator': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_related'", 'null': 'True', 'to': "orm['auth.User']"}), 'Meta': {'object_name': 'TaggedItem'}, 'create_timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}), 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_items'", 'to': "orm['taggit.Tag']"}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), 'tag_category': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['taggit.TagCategory']", 'null': 'True'})}}
      +
      + +
      + +
      +
      +

      0006_auto__chg_field_vote_time__chg_field_comment_time__chg_field_idea_time Module

      +
      +
      +class idea.migrations.0006_auto__chg_field_vote_time__chg_field_comment_time__chg_field_idea_time.Migration
      +

      Bases: south.v2.SchemaMigration

      +
      +
      +backwards(orm)
      +
      + +
      +
      +complete_apps = ['idea']
      +
      + +
      +
      +forwards(orm)
      +
      + +
      +
      +models = {'taggit.tag': {'slug': ('django.db.models.fields.SlugField', [], {'max_length': '100', 'unique': 'True'}), 'Meta': {'object_name': 'Tag'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'})}, 'idea.idea': {'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), 'text': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 'title': ('django.db.models.fields.CharField', [], {'max_length': '140'}), 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['idea.State']"}), 'Meta': {'object_name': 'Idea'}, 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 2, 1, 0, 0)'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})}, 'idea.comment': {'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), 'text': ('django.db.models.fields.TextField', [], {}), 'idea': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['idea.Idea']"}), 'Meta': {'object_name': 'Comment'}, 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 2, 1, 0, 0)'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})}, 'idea.state': {'previous': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['idea.State']", 'unique': 'True', 'null': 'True', 'blank': 'True'}), 'Meta': {'object_name': 'State'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})}, 'auth.group': {'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True', 'symmetrical': 'False'}), 'Meta': {'object_name': 'Group'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True'})}, 'auth.user': {'username': ('django.db.models.fields.CharField', [], {'max_length': '30', 'unique': 'True'}), 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'blank': 'True', 'symmetrical': 'False'}), 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True', 'symmetrical': 'False'}), 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'Meta': {'object_name': 'User'}, 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'})}, 'auth.permission': {'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})}, 'contenttypes.contenttype': {'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'})}, 'idea.vote': {'idea': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['idea.Idea']"}), 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 2, 1, 0, 0)'}), 'Meta': {'object_name': 'Vote'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})}, 'taggit.tagcategory': {'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'unique': 'True'}), 'Meta': {'object_name': 'TagCategory'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'unique': 'True'})}, 'taggit.taggeditem': {'tag_creator': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_related'", 'null': 'True', 'to': "orm['auth.User']"}), 'Meta': {'object_name': 'TaggedItem'}, 'create_timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}), 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_items'", 'to': "orm['taggit.Tag']"}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), 'tag_category': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['taggit.TagCategory']", 'null': 'True'})}}
      +
      + +
      + +
      +
      +

      0007_auto__del_comment__add_field_vote_vote Module

      +
      +
      +class idea.migrations.0007_auto__del_comment__add_field_vote_vote.Migration
      +

      Bases: south.v2.SchemaMigration

      +
      +
      +backwards(orm)
      +
      + +
      +
      +complete_apps = ['idea']
      +
      + +
      +
      +forwards(orm)
      +
      + +
      +
      +models = {'auth.permission': {'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})}, 'contenttypes.contenttype': {'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'})}, 'idea.vote': {'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), 'idea': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['idea.Idea']"}), 'Meta': {'object_name': 'Vote'}, 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 2, 4, 0, 0)'}), 'vote': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})}, 'auth.user': {'username': ('django.db.models.fields.CharField', [], {'max_length': '30', 'unique': 'True'}), 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'blank': 'True', 'symmetrical': 'False'}), 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True', 'symmetrical': 'False'}), 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'Meta': {'object_name': 'User'}, 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'})}, 'taggit.tagcategory': {'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'unique': 'True'}), 'Meta': {'object_name': 'TagCategory'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'unique': 'True'})}, 'idea.state': {'previous': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['idea.State']", 'unique': 'True', 'null': 'True', 'blank': 'True'}), 'Meta': {'object_name': 'State'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})}, 'auth.group': {'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True', 'symmetrical': 'False'}), 'Meta': {'object_name': 'Group'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True'})}, 'taggit.tag': {'slug': ('django.db.models.fields.SlugField', [], {'max_length': '100', 'unique': 'True'}), 'Meta': {'object_name': 'Tag'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'})}, 'idea.idea': {'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), 'text': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 'title': ('django.db.models.fields.CharField', [], {'max_length': '140'}), 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['idea.State']"}), 'Meta': {'object_name': 'Idea'}, 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 2, 4, 0, 0)'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})}, 'taggit.taggeditem': {'tag_creator': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_related'", 'null': 'True', 'to': "orm['auth.User']"}), 'Meta': {'object_name': 'TaggedItem'}, 'create_timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}), 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_items'", 'to': "orm['taggit.Tag']"}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), 'tag_category': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['taggit.TagCategory']", 'null': 'True'})}}
      +
      + +
      + +
      +
      +

      0008_auto__add_banner Module

      +
      +
      +class idea.migrations.0008_auto__add_banner.Migration
      +

      Bases: south.v2.SchemaMigration

      +
      +
      +backwards(orm)
      +
      + +
      +
      +complete_apps = ['idea']
      +
      + +
      +
      +forwards(orm)
      +
      + +
      +
      +models = {'taggit.tag': {'slug': ('django.db.models.fields.SlugField', [], {'max_length': '100', 'unique': 'True'}), 'Meta': {'object_name': 'Tag'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'})}, 'idea.idea': {'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), 'text': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), 'title': ('django.db.models.fields.CharField', [], {'max_length': '140'}), 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['idea.State']"}), 'Meta': {'object_name': 'Idea'}, 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 2, 19, 0, 0)'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})}, 'idea.state': {'previous': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['idea.State']", 'unique': 'True', 'null': 'True', 'blank': 'True'}), 'Meta': {'object_name': 'State'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})}, 'auth.group': {'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True', 'symmetrical': 'False'}), 'Meta': {'object_name': 'Group'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '80', 'unique': 'True'})}, 'idea.banner': {'text': ('django.db.models.fields.CharField', [], {'max_length': '512'}), 'Meta': {'object_name': 'Banner'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'end_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), 'start_date': ('django.db.models.fields.DateField', [], {})}, 'auth.user': {'username': ('django.db.models.fields.CharField', [], {'max_length': '30', 'unique': 'True'}), 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'blank': 'True', 'symmetrical': 'False'}), 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True', 'symmetrical': 'False'}), 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'Meta': {'object_name': 'User'}, 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'})}, 'auth.permission': {'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})}, 'contenttypes.contenttype': {'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'})}, 'idea.vote': {'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), 'idea': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['idea.Idea']"}), 'Meta': {'object_name': 'Vote'}, 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 2, 19, 0, 0)'}), 'vote': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})}, 'taggit.tagcategory': {'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'unique': 'True'}), 'Meta': {'object_name': 'TagCategory'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'unique': 'True'})}, 'taggit.taggeditem': {'tag_creator': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_related'", 'null': 'True', 'to': "orm['auth.User']"}), 'Meta': {'object_name': 'TaggedItem'}, 'create_timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}), 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_items'", 'to': "orm['taggit.Tag']"}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), 'tag_category': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['taggit.TagCategory']", 'null': 'True'})}}
      +
      + +
      + +
      +
      + + +
      +
      +
      + +
      +
      + + + + \ No newline at end of file diff --git a/doc/build/html/api/idea.tests.html b/doc/build/html/api/idea.tests.html new file mode 100644 index 0000000..29017a0 --- /dev/null +++ b/doc/build/html/api/idea.tests.html @@ -0,0 +1,449 @@ + + + + + + + + + + tests Package — Idea Box 0.1.0 documentation + + + + + + + + + + + + + +
      +
      +
      +
      + +
      +

      tests Package

      +
      +

      tests Package

      +
      +
      +

      addidea Module

      +
      +
      +class idea.tests.addidea.AddIdeaTest(methodName='runTest')
      +

      Bases: django.test.testcases.TestCase

      +
      +
      +fixtures = ['state']
      +
      + +
      +
      +setUp()
      +
      + +
      +
      +test_good_idea()
      +

      Test an normal POST submission to add an idea.

      +
      + +
      +
      +test_must_be_logged_in()
      +

      A user must be logged in to create an idea.

      +
      + +
      +
      +test_similar(*args, **keywargs)
      +

      List of similar ideas should make sense.

      +
      + +
      + +
      +
      +

      banner_tests Module

      +
      +
      +class idea.tests.banner_tests.BannerTest(methodName='runTest')
      +

      Bases: django.test.testcases.TestCase

      +
      +
      +test_indefinite_banner()
      +
      + +
      +
      +test_no_banner()
      +
      + +
      +
      +test_outside_timed()
      +
      + +
      +
      +test_timebound_banner()
      +
      + +
      +
      +test_timed_before_indefinite()
      +
      + +
      + +
      +
      +idea.tests.banner_tests.get_relative_date(delta_days=0)
      +
      + +
      +
      +

      detailview Module

      +
      +
      +class idea.tests.detailview.DetailViewTest(methodName='runTest')
      +

      Bases: django.test.testcases.TestCase

      +

      Tests for idea.views.detail.

      +
      +
      +fixtures = ['state']
      +
      + +
      +
      +test_404s()
      +

      Should get a 404 when using a bad idea_id.

      +
      + +
      +
      +test_idea_correct(*args, **keywargs)
      +

      The idea passed to the ui should be that identified by the id.

      +
      + +
      +
      +test_idea_no_support(*args, **keywargs)
      +

      Check support = False if no votes.

      +
      + +
      +
      +test_idea_support(*args, **keywargs)
      +

      Check support = True if voted votes.

      +
      + +
      +
      +test_idea_voters(*args, **keywargs)
      +

      The idea passed to the ui should have the correct voters.

      +
      + +
      +
      +test_tag_form_exists(*args, **keywargs)
      +

      Detail page should have a form for adding tags.

      +
      + +
      +
      +test_tag_form_submission()
      +

      Detail page tag form submission should add tags.

      +
      + +
      +
      +test_tags(*args, **keywargs)
      +

      Include a sorted list of tags for this idea.

      +
      + +
      + +
      +
      +

      ideaform Module

      +
      +
      +class idea.tests.ideaform.IdeaTests(methodName='runTest')
      +

      Bases: django.test.testcases.TestCase

      +
      +
      +test_idea_text_optional()
      +

      The field ‘text’ is option when adding an idea.

      +
      + +
      +
      +test_idea_title_required()
      +

      The field ‘title’ is required when adding an idea.

      +
      + +
      + +
      +
      +

      listview Module

      +
      +
      +class idea.tests.listview.ListViewTest(methodName='runTest')
      +

      Bases: django.test.testcases.TestCase

      +

      Tests for idea.views.list

      +
      +
      +fixtures = ['state']
      +
      + +
      +
      +test_idea_fields(*args, **keywargs)
      +

      Verify that the fields needed by the ui are available on all ideas.

      +
      + +
      +
      +test_idea_state_filter(*args, **keywargs)
      +

      Verify that state filters work as we expect.

      +
      + +
      +
      +test_paging(*args, **keywargs)
      +

      Verify that paging works as we would expect.

      +
      + +
      +
      +test_sort_comment(*args, **keywargs)
      +

      Verify that the comments sort works.

      +
      + +
      +
      +test_sort_recent(*args, **keywargs)
      +

      Verify that the recent sort params works.

      +
      + +
      +
      +test_sort_vote(*args, **keywargs)
      +

      Verify that the votes sort works.

      +
      + +
      +
      +test_tag_filter(*args, **keywargs)
      +

      List of ideas should be filterable by tag.

      +
      + +
      +
      +test_tags_active(*args, **keywargs)
      +

      Tag list should include if tag was active in this search.

      +
      + +
      +
      +test_tags_count(*args, **keywargs)
      +

      Tag list should include tag count.

      +
      + +
      +
      +test_tags_exist(*args, **keywargs)
      +

      Check that the tag list is populated and only shows the top ten +tags.

      +
      + +
      +
      +test_tags_top_ten(*args, **keywargs)
      +

      Tag list should be in proper order.

      +
      + +
      + +
      +
      +

      search Module

      +
      +
      +class idea.tests.search.SearchTest(methodName='runTest')
      +

      Bases: django.test.testcases.TestCase

      +
      +
      +fixtures = ['state']
      +
      + +
      +
      +setUp()
      +
      + +
      +
      +test_add_idea_tag(*args, **kwargs)
      +

      Check that adding a new idea allows the associated tag to be +immediately searchable.

      +
      + +
      +
      +test_add_idea_title()
      +

      Check that adding a new idea allows title to be immediately +searchable.

      +
      + +
      +
      +test_edit_idea_tag(*args, **kwargs)
      +

      Check that adding a new idea allows the associated tag to be +immediately searchable.

      +
      + +
      + +
      +
      +

      utils Module

      +
      +
      +idea.tests.utils.mock_req(path='/', user=None)
      +

      Mock request – with user!

      +
      + +
      +
      +idea.tests.utils.random_user()
      +
      + +
      +
      +

      voting Module

      +
      +
      +class idea.tests.voting.VotingTests(methodName='runTest')
      +

      Bases: django.test.testcases.TestCase

      +
      +
      +fixtures = ['state']
      +
      + +
      +
      +setUp()
      +
      + +
      +
      +test_good_vote()
      +
      + +
      +
      +test_must_logged_in()
      +

      A user must be logged in to vote.

      +
      + +
      +
      +test_vote_twice()
      +

      Voting twice shouldn’t do anything to the vote count (it’s +idempotent).

      +
      + +
      + +
      +
      + + +
      +
      +
      +
      +
      +

      Table Of Contents

      + + +

      This Page

      + + + +
      +
      +
      +
      + + + + \ No newline at end of file diff --git a/doc/build/html/api/idea.utility.html b/doc/build/html/api/idea.utility.html new file mode 100644 index 0000000..5806326 --- /dev/null +++ b/doc/build/html/api/idea.utility.html @@ -0,0 +1,114 @@ + + + + + + + + + + utility Package — Idea Box 0.1.0 documentation + + + + + + + + + + + + + +
      +
      +
      +
      + +
      +

      utility Package

      +
      +

      state_helper Module

      +
      +
      +idea.utility.state_helper.get_first_state()
      +

      Get the first state for an idea.

      +
      + +
      +
      + + +
      +
      +
      +
      +
      +

      Table Of Contents

      + + +

      This Page

      + + + +
      +
      +
      +
      + + + + \ No newline at end of file diff --git a/doc/build/html/api/modules.html b/doc/build/html/api/modules.html new file mode 100644 index 0000000..5314ed3 --- /dev/null +++ b/doc/build/html/api/modules.html @@ -0,0 +1,146 @@ + + + + + + + + + + API — Idea Box 0.1.0 documentation + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/doc/build/html/genindex.html b/doc/build/html/genindex.html new file mode 100644 index 0000000..0412ef0 --- /dev/null +++ b/doc/build/html/genindex.html @@ -0,0 +1,1037 @@ + + + + + + + + + + + + Index — Idea Box 0.1.0 documentation + + + + + + + + + + + + + +
      +
      +
      +
      + + +

      Index

      + +
      + A + | B + | C + | D + | E + | F + | G + | I + | L + | M + | O + | P + | R + | S + | T + | U + | V + +
      +

      A

      + + + +
      + +
      abstract (idea.models.UserTrackable.Meta attribute) +
      + + +
      add_idea() (in module idea.views) +
      + +
      + +
      AddIdeaTest (class in idea.tests.addidea) +
      + +
      + +

      B

      + + + +
      + +
      backwards() (idea.migrations.0001_initial.Migration method) +
      + +
      + +
      (idea.migrations.0002_auto__del_field_comment_issue__add_field_comment_idea.Migration method) +
      + + +
      (idea.migrations.0003_auto__add_field_state_previous__chg_field_idea_text__chg_field_idea_ti.Migration method) +
      + + +
      (idea.migrations.0004_statuses.Migration method) +
      + + +
      (idea.migrations.0005_auto__chg_field_vote_time__chg_field_comment_time.Migration method) +
      + + +
      (idea.migrations.0006_auto__chg_field_vote_time__chg_field_comment_time__chg_field_idea_time.Migration method) +
      + + +
      (idea.migrations.0007_auto__del_comment__add_field_vote_vote.Migration method) +
      + + +
      (idea.migrations.0008_auto__add_banner.Migration method) +
      + +
      + +
      Banner (class in idea.models) +
      + + +
      Banner.DoesNotExist +
      + +
      + +
      Banner.MultipleObjectsReturned +
      + + +
      BannerTest (class in idea.tests.banner_tests) +
      + + +
      base_fields (idea.forms.IdeaForm attribute) +
      + +
      + +
      (idea.forms.IdeaTagForm attribute) +
      + + +
      (idea.forms.UpVoteForm attribute) +
      + +
      +
      + +

      C

      + + + +
      + +
      clean_tags() (idea.forms.IdeaForm method) +
      + +
      + +
      (idea.forms.IdeaTagForm method) +
      + +
      + +
      complete_apps (idea.migrations.0001_initial.Migration attribute) +
      + +
      + +
      (idea.migrations.0002_auto__del_field_comment_issue__add_field_comment_idea.Migration attribute) +
      + + +
      (idea.migrations.0003_auto__add_field_state_previous__chg_field_idea_text__chg_field_idea_ti.Migration attribute) +
      + + +
      (idea.migrations.0004_statuses.Migration attribute) +
      + + +
      (idea.migrations.0005_auto__chg_field_vote_time__chg_field_comment_time.Migration attribute) +
      + + +
      (idea.migrations.0006_auto__chg_field_vote_time__chg_field_comment_time__chg_field_idea_time.Migration attribute) +
      + + +
      (idea.migrations.0007_auto__del_comment__add_field_vote_vote.Migration attribute) +
      + + +
      (idea.migrations.0008_auto__add_banner.Migration attribute) +
      + +
      +
      + +
      creator (idea.models.Idea attribute) +
      + +
      + +
      (idea.models.UserTrackable attribute) +
      + + +
      (idea.models.Vote attribute) +
      + +
      +
      + +

      D

      + + + +
      + +
      declared_fields (idea.forms.IdeaForm attribute) +
      + + +
      detail() (in module idea.views) +
      + +
      + +
      DetailViewTest (class in idea.tests.detailview) +
      + +
      + +

      E

      + + +
      + +
      exclude (idea.forms.IdeaForm.Meta attribute) +
      + +
      + +

      F

      + + + +
      + +
      fields (idea.search_indexes.IdeaIndex attribute) +
      + + +
      fixtures (idea.tests.addidea.AddIdeaTest attribute) +
      + +
      + +
      (idea.tests.detailview.DetailViewTest attribute) +
      + + +
      (idea.tests.listview.ListViewTest attribute) +
      + + +
      (idea.tests.search.SearchTest attribute) +
      + + +
      (idea.tests.voting.VotingTests attribute) +
      + +
      +
      + +
      forwards() (idea.migrations.0001_initial.Migration method) +
      + +
      + +
      (idea.migrations.0002_auto__del_field_comment_issue__add_field_comment_idea.Migration method) +
      + + +
      (idea.migrations.0003_auto__add_field_state_previous__chg_field_idea_text__chg_field_idea_ti.Migration method) +
      + + +
      (idea.migrations.0004_statuses.Migration method) +
      + + +
      (idea.migrations.0005_auto__chg_field_vote_time__chg_field_comment_time.Migration method) +
      + + +
      (idea.migrations.0006_auto__chg_field_vote_time__chg_field_comment_time__chg_field_idea_time.Migration method) +
      + + +
      (idea.migrations.0007_auto__del_comment__add_field_vote_vote.Migration method) +
      + + +
      (idea.migrations.0008_auto__add_banner.Migration method) +
      + +
      +
      + +

      G

      + + + +
      + +
      get_banner() (in module idea.views) +
      + + +
      get_creator_profile() (idea.models.Idea method) +
      + + +
      get_first_state() (in module idea.utility.state_helper) +
      + + +
      get_next_by_start_date() (idea.models.Banner method) +
      + + +
      get_next_by_time() (idea.models.Idea method) +
      + +
      + +
      (idea.models.UserTrackable method) +
      + + +
      (idea.models.Vote method) +
      + +
      +
      + +
      get_previous_by_start_date() (idea.models.Banner method) +
      + + +
      get_previous_by_time() (idea.models.Idea method) +
      + +
      + +
      (idea.models.UserTrackable method) +
      + + +
      (idea.models.Vote method) +
      + +
      + +
      get_relative_date() (in module idea.tests.banner_tests) +
      + + +
      get_vote_display() (idea.models.Vote method) +
      + +
      + +

      I

      + + + +
      + +
      Idea (class in idea.models) +
      + + +
      idea (idea.models.Vote attribute) +
      + + +
      idea.admin (module) +
      + + +
      idea.buildout.search_sites (module) +
      + + +
      idea.buildout.testsettings (module) +
      + + +
      idea.buildout.urls (module) +
      + + +
      Idea.DoesNotExist +
      + + +
      idea.forms (module) +
      + + +
      idea.migrations.0001_initial (module) +
      + + +
      idea.migrations.0002_auto__del_field_comment_issue__add_field_comment_idea (module) +
      + + +
      idea.migrations.0003_auto__add_field_state_previous__chg_field_idea_text__chg_field_idea_ti (module) +
      + + +
      idea.migrations.0004_statuses (module) +
      + + +
      idea.migrations.0005_auto__chg_field_vote_time__chg_field_comment_time (module) +
      + + +
      idea.migrations.0006_auto__chg_field_vote_time__chg_field_comment_time__chg_field_idea_time (module) +
      + + +
      idea.migrations.0007_auto__del_comment__add_field_vote_vote (module) +
      + + +
      idea.migrations.0008_auto__add_banner (module) +
      + + +
      idea.models (module) +
      + + +
      Idea.MultipleObjectsReturned +
      + + +
      idea.search_indexes (module) +
      + + +
      idea.tests (module) +
      + +
      + +
      idea.tests.addidea (module) +
      + + +
      idea.tests.banner_tests (module) +
      + + +
      idea.tests.detailview (module) +
      + + +
      idea.tests.ideaform (module) +
      + + +
      idea.tests.listview (module) +
      + + +
      idea.tests.search (module) +
      + + +
      idea.tests.utils (module) +
      + + +
      idea.tests.voting (module) +
      + + +
      idea.urls (module) +
      + + +
      idea.utility.state_helper (module) +
      + + +
      idea.views (module) +
      + + +
      idea_set (idea.models.State attribute) +
      + + +
      IdeaForm (class in idea.forms) +
      + + +
      IdeaForm.Meta (class in idea.forms) +
      + + +
      IdeaIndex (class in idea.search_indexes) +
      + + +
      IdeaManager (class in idea.models) +
      + + +
      IdeaTagForm (class in idea.forms) +
      + + +
      IdeaTests (class in idea.tests.ideaform) +
      + + +
      index_queryset() (idea.search_indexes.IdeaIndex method) +
      + +
      + +

      L

      + + + +
      + +
      list() (in module idea.views) +
      + +
      + +
      ListViewTest (class in idea.tests.listview) +
      + +
      + +

      M

      + + + +
      + +
      media (idea.forms.IdeaForm attribute) +
      + +
      + +
      (idea.forms.IdeaTagForm attribute) +
      + + +
      (idea.forms.UpVoteForm attribute) +
      + +
      + +
      Migration (class in idea.migrations.0001_initial) +
      + +
      + +
      (class in idea.migrations.0002_auto__del_field_comment_issue__add_field_comment_idea) +
      + + +
      (class in idea.migrations.0003_auto__add_field_state_previous__chg_field_idea_text__chg_field_idea_ti) +
      + + +
      (class in idea.migrations.0004_statuses) +
      + + +
      (class in idea.migrations.0005_auto__chg_field_vote_time__chg_field_comment_time) +
      + + +
      (class in idea.migrations.0006_auto__chg_field_vote_time__chg_field_comment_time__chg_field_idea_time) +
      + + +
      (class in idea.migrations.0007_auto__del_comment__add_field_vote_vote) +
      + + +
      (class in idea.migrations.0008_auto__add_banner) +
      + +
      + +
      mock_req() (in module idea.tests.utils) +
      + +
      + +
      model (idea.forms.IdeaForm.Meta attribute) +
      + + +
      models (idea.migrations.0001_initial.Migration attribute) +
      + +
      + +
      (idea.migrations.0002_auto__del_field_comment_issue__add_field_comment_idea.Migration attribute) +
      + + +
      (idea.migrations.0003_auto__add_field_state_previous__chg_field_idea_text__chg_field_idea_ti.Migration attribute) +
      + + +
      (idea.migrations.0004_statuses.Migration attribute) +
      + + +
      (idea.migrations.0005_auto__chg_field_vote_time__chg_field_comment_time.Migration attribute) +
      + + +
      (idea.migrations.0006_auto__chg_field_vote_time__chg_field_comment_time__chg_field_idea_time.Migration attribute) +
      + + +
      (idea.migrations.0007_auto__del_comment__add_field_vote_vote.Migration attribute) +
      + + +
      (idea.migrations.0008_auto__add_banner.Migration attribute) +
      + +
      + +
      more_like_text() (in module idea.views) +
      + +
      + +

      O

      + + +
      + +
      objects (idea.models.Banner attribute) +
      + +
      + +
      (idea.models.Idea attribute) +
      + + +
      (idea.models.State attribute) +
      + + +
      (idea.models.Vote attribute) +
      + +
      +
      + +

      P

      + + +
      + +
      previous (idea.models.State attribute) +
      + +
      + +

      R

      + + + +
      + +
      random_user() (in module idea.tests.utils) +
      + +
      + +
      related_with_counts() (idea.models.IdeaManager method) +
      + +
      + +

      S

      + + + +
      + +
      SearchTest (class in idea.tests.search) +
      + + +
      setUp() (idea.tests.addidea.AddIdeaTest method) +
      + +
      + +
      (idea.tests.search.SearchTest method) +
      + + +
      (idea.tests.voting.VotingTests method) +
      + +
      + +
      State (class in idea.models) +
      + + +
      state (idea.models.Idea attribute) +
      + +
      + +
      (idea.models.State attribute) +
      + +
      +
      + +
      State.DoesNotExist +
      + + +
      State.MultipleObjectsReturned +
      + + +
      symmetrical (idea.migrations.0004_statuses.Migration attribute) +
      + +
      + +

      T

      + + + +
      + +
      tagged_items (idea.models.Idea attribute) +
      + + +
      tags (idea.models.Idea attribute) +
      + + +
      test_404s() (idea.tests.detailview.DetailViewTest method) +
      + + +
      test_add_idea_tag() (idea.tests.search.SearchTest method) +
      + + +
      test_add_idea_title() (idea.tests.search.SearchTest method) +
      + + +
      test_edit_idea_tag() (idea.tests.search.SearchTest method) +
      + + +
      test_good_idea() (idea.tests.addidea.AddIdeaTest method) +
      + + +
      test_good_vote() (idea.tests.voting.VotingTests method) +
      + + +
      test_idea_correct() (idea.tests.detailview.DetailViewTest method) +
      + + +
      test_idea_fields() (idea.tests.listview.ListViewTest method) +
      + + +
      test_idea_no_support() (idea.tests.detailview.DetailViewTest method) +
      + + +
      test_idea_state_filter() (idea.tests.listview.ListViewTest method) +
      + + +
      test_idea_support() (idea.tests.detailview.DetailViewTest method) +
      + + +
      test_idea_text_optional() (idea.tests.ideaform.IdeaTests method) +
      + + +
      test_idea_title_required() (idea.tests.ideaform.IdeaTests method) +
      + + +
      test_idea_voters() (idea.tests.detailview.DetailViewTest method) +
      + + +
      test_indefinite_banner() (idea.tests.banner_tests.BannerTest method) +
      + + +
      test_must_be_logged_in() (idea.tests.addidea.AddIdeaTest method) +
      + + +
      test_must_logged_in() (idea.tests.voting.VotingTests method) +
      + +
      + +
      test_no_banner() (idea.tests.banner_tests.BannerTest method) +
      + + +
      test_outside_timed() (idea.tests.banner_tests.BannerTest method) +
      + + +
      test_paging() (idea.tests.listview.ListViewTest method) +
      + + +
      test_similar() (idea.tests.addidea.AddIdeaTest method) +
      + + +
      test_sort_comment() (idea.tests.listview.ListViewTest method) +
      + + +
      test_sort_recent() (idea.tests.listview.ListViewTest method) +
      + + +
      test_sort_vote() (idea.tests.listview.ListViewTest method) +
      + + +
      test_tag_filter() (idea.tests.listview.ListViewTest method) +
      + + +
      test_tag_form_exists() (idea.tests.detailview.DetailViewTest method) +
      + + +
      test_tag_form_submission() (idea.tests.detailview.DetailViewTest method) +
      + + +
      test_tags() (idea.tests.detailview.DetailViewTest method) +
      + + +
      test_tags_active() (idea.tests.listview.ListViewTest method) +
      + + +
      test_tags_count() (idea.tests.listview.ListViewTest method) +
      + + +
      test_tags_exist() (idea.tests.listview.ListViewTest method) +
      + + +
      test_tags_top_ten() (idea.tests.listview.ListViewTest method) +
      + + +
      test_timebound_banner() (idea.tests.banner_tests.BannerTest method) +
      + + +
      test_timed_before_indefinite() (idea.tests.banner_tests.BannerTest method) +
      + + +
      test_vote_twice() (idea.tests.voting.VotingTests method) +
      + +
      + +

      U

      + + + +
      + +
      up_vote() (in module idea.views) +
      + + +
      UpVoteForm (class in idea.forms) +
      + + +
      url() (idea.models.Idea method) +
      + +
      + +
      UserTrackable (class in idea.models) +
      + + +
      UserTrackable.Meta (class in idea.models) +
      + +
      + +

      V

      + + + +
      + +
      Vote (class in idea.models) +
      + + +
      Vote.DoesNotExist +
      + + +
      Vote.MultipleObjectsReturned +
      + +
      + +
      vote_set (idea.models.Idea attribute) +
      + + +
      vote_up() (in module idea.views) +
      + + +
      VotingTests (class in idea.tests.voting) +
      + +
      + + + +
      +
      +
      +
      +
      + + + + + +
      +
      +
      +
      + + + + \ No newline at end of file diff --git a/doc/build/html/index.html b/doc/build/html/index.html new file mode 100644 index 0000000..42171d3 --- /dev/null +++ b/doc/build/html/index.html @@ -0,0 +1,118 @@ + + + + + + + + + + Welcome to Idea Box’s documentation! — Idea Box 0.1.0 documentation + + + + + + + + + + + + + +
      +
      +
      +
      + +
      +

      Welcome to Idea Box’s documentation!

      +

      Contents:

      +
      +
        +
      +
      +
      +
      +

      Indices and tables

      + +
      + + +
      +
      +
      +
      +
      +

      Table Of Contents

      + + +

      This Page

      + + + +
      +
      +
      +
      + + + + \ No newline at end of file diff --git a/doc/build/html/objects.inv b/doc/build/html/objects.inv new file mode 100644 index 0000000000000000000000000000000000000000..e663fb8f76b44b148ab330c93a5a6f585cb42af5 GIT binary patch literal 2064 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 literal 0 HcmV?d00001 diff --git a/doc/build/html/py-modindex.html b/doc/build/html/py-modindex.html new file mode 100644 index 0000000..41be800 --- /dev/null +++ b/doc/build/html/py-modindex.html @@ -0,0 +1,245 @@ + + + + + + + + + + Python Module Index — Idea Box 0.1.0 documentation + + + + + + + + + + + + + + + + +
      +
      +
      +
      + + +

      Python Module Index

      + +
      + i +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
       
      + i
      + idea +
          + idea.admin +
          + idea.buildout.search_sites +
          + idea.buildout.testsettings +
          + idea.buildout.urls +
          + idea.forms +
          + idea.migrations.0001_initial +
          + idea.migrations.0002_auto__del_field_comment_issue__add_field_comment_idea +
          + idea.migrations.0003_auto__add_field_state_previous__chg_field_idea_text__chg_field_idea_ti +
          + idea.migrations.0004_statuses +
          + idea.migrations.0005_auto__chg_field_vote_time__chg_field_comment_time +
          + idea.migrations.0006_auto__chg_field_vote_time__chg_field_comment_time__chg_field_idea_time +
          + idea.migrations.0007_auto__del_comment__add_field_vote_vote +
          + idea.migrations.0008_auto__add_banner +
          + idea.models +
          + idea.search_indexes +
          + idea.tests +
          + idea.tests.addidea +
          + idea.tests.banner_tests +
          + idea.tests.detailview +
          + idea.tests.ideaform +
          + idea.tests.listview +
          + idea.tests.search +
          + idea.tests.utils +
          + idea.tests.voting +
          + idea.urls +
          + idea.utility.state_helper +
          + idea.views +
      + + +
      +
      +
      +
      +
      + + +
      +
      +
      +
      + + + + \ No newline at end of file diff --git a/doc/build/html/search.html b/doc/build/html/search.html new file mode 100644 index 0000000..99faf3d --- /dev/null +++ b/doc/build/html/search.html @@ -0,0 +1,104 @@ + + + + + + + + + + Search — Idea Box 0.1.0 documentation + + + + + + + + + + + + + + + + + +
      +
      +
      +
      + +

      Search

      +
      + +

      + Please activate JavaScript to enable the search + functionality. +

      +
      +

      + From here you can search these documents. Enter your search + words into the box below and click "search". Note that the search + function will automatically search for all of the words. Pages + containing fewer words won't appear in the result list. +

      +
      + + + +
      + +
      + +
      + +
      +
      +
      +
      +
      +
      +
      +
      +
      + + + + \ No newline at end of file diff --git a/doc/build/html/searchindex.js b/doc/build/html/searchindex.js new file mode 100644 index 0000000..14e50e3 --- /dev/null +++ b/doc/build/html/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({objects:{"idea.tests.detailview.DetailViewTest":{test_idea_support:[6,1,1,""],test_tag_form_exists:[6,1,1,""],test_tags:[6,1,1,""],test_idea_voters:[6,1,1,""],test_404s:[6,1,1,""],test_idea_no_support:[6,1,1,""],test_tag_form_submission:[6,1,1,""],fixtures:[6,3,1,""],test_idea_correct:[6,1,1,""]},"idea.migrations.0005_auto__chg_field_vote_time__chg_field_comment_time.Migration":{models:[4,3,1,""],backwards:[4,1,1,""],complete_apps:[4,3,1,""],forwards:[4,1,1,""]},"idea.migrations.0004_statuses":{Migration:[4,2,1,""]},"idea.models.Idea":{vote_set:[3,3,1,""],creator:[3,3,1,""],url:[3,1,1,""],MultipleObjectsReturned:[3,5,1,""],tags:[3,3,1,""],get_creator_profile:[3,1,1,""],state:[3,3,1,""],objects:[3,3,1,""],get_next_by_time:[3,1,1,""],get_previous_by_time:[3,1,1,""],DoesNotExist:[3,5,1,""],tagged_items:[3,3,1,""]},idea:{tests:[6,0,1,""],search_indexes:[3,0,1,""],views:[3,0,1,""],models:[3,0,1,""],forms:[3,0,1,""],admin:[3,0,1,""],urls:[3,0,1,""]},"idea.utility.state_helper":{get_first_state:[5,4,1,""]},"idea.views":{more_like_text:[3,4,1,""],up_vote:[3,4,1,""],vote_up:[3,4,1,""],list:[3,4,1,""],detail:[3,4,1,""],add_idea:[3,4,1,""],get_banner:[3,4,1,""]},"idea.forms.UpVoteForm":{media:[3,3,1,""],base_fields:[3,3,1,""]},"idea.search_indexes":{IdeaIndex:[3,2,1,""]},"idea.migrations":{"0003_auto__add_field_state_previous__chg_field_idea_text__chg_field_idea_ti":[4,0,1,""],"0002_auto__del_field_comment_issue__add_field_comment_idea":[4,0,1,""],"0007_auto__del_comment__add_field_vote_vote":[4,0,1,""],"0004_statuses":[4,0,1,""],"0006_auto__chg_field_vote_time__chg_field_comment_time__chg_field_idea_time":[4,0,1,""],"0008_auto__add_banner":[4,0,1,""],"0005_auto__chg_field_vote_time__chg_field_comment_time":[4,0,1,""],"0001_initial":[4,0,1,""]},"idea.tests.voting.VotingTests":{setUp:[6,1,1,""],test_vote_twice:[6,1,1,""],fixtures:[6,3,1,""],test_good_vote:[6,1,1,""],test_must_logged_in:[6,1,1,""]},"idea.tests.banner_tests":{BannerTest:[6,2,1,""],get_relative_date:[6,4,1,""]},"idea.migrations.0003_auto__add_field_state_previous__chg_field_idea_text__chg_field_idea_ti":{Migration:[4,2,1,""]},"idea.forms.IdeaTagForm":{media:[3,3,1,""],base_fields:[3,3,1,""],clean_tags:[3,1,1,""]},"idea.models":{IdeaManager:[3,2,1,""],Idea:[3,2,1,""],UserTrackable:[3,2,1,""],State:[3,2,1,""],Vote:[3,2,1,""],Banner:[3,2,1,""]},"idea.migrations.0007_auto__del_comment__add_field_vote_vote.Migration":{models:[4,3,1,""],backwards:[4,1,1,""],complete_apps:[4,3,1,""],forwards:[4,1,1,""]},"idea.tests.addidea":{AddIdeaTest:[6,2,1,""]},"idea.migrations.0001_initial":{Migration:[4,2,1,""]},"idea.migrations.0006_auto__chg_field_vote_time__chg_field_comment_time__chg_field_idea_time.Migration":{models:[4,3,1,""],backwards:[4,1,1,""],complete_apps:[4,3,1,""],forwards:[4,1,1,""]},"idea.migrations.0008_auto__add_banner":{Migration:[4,2,1,""]},"idea.tests.search.SearchTest":{setUp:[6,1,1,""],fixtures:[6,3,1,""],test_edit_idea_tag:[6,1,1,""],test_add_idea_tag:[6,1,1,""],test_add_idea_title:[6,1,1,""]},"idea.migrations.0001_initial.Migration":{models:[4,3,1,""],backwards:[4,1,1,""],complete_apps:[4,3,1,""],forwards:[4,1,1,""]},"idea.models.Vote":{creator:[3,3,1,""],MultipleObjectsReturned:[3,5,1,""],idea:[3,3,1,""],get_previous_by_time:[3,1,1,""],objects:[3,3,1,""],get_next_by_time:[3,1,1,""],get_vote_display:[3,1,1,""],DoesNotExist:[3,5,1,""]},"idea.migrations.0007_auto__del_comment__add_field_vote_vote":{Migration:[4,2,1,""]},"idea.forms":{IdeaTagForm:[3,2,1,""],IdeaForm:[3,2,1,""],UpVoteForm:[3,2,1,""]},"idea.tests.ideaform.IdeaTests":{test_idea_text_optional:[6,1,1,""],test_idea_title_required:[6,1,1,""]},"idea.migrations.0003_auto__add_field_state_previous__chg_field_idea_text__chg_field_idea_ti.Migration":{models:[4,3,1,""],backwards:[4,1,1,""],complete_apps:[4,3,1,""],forwards:[4,1,1,""]},"idea.tests":{search:[6,0,1,""],voting:[6,0,1,""],banner_tests:[6,0,1,""],utils:[6,0,1,""],ideaform:[6,0,1,""],addidea:[6,0,1,""],listview:[6,0,1,""],detailview:[6,0,1,""]},"idea.tests.ideaform":{IdeaTests:[6,2,1,""]},"idea.tests.detailview":{DetailViewTest:[6,2,1,""]},"idea.forms.IdeaForm.Meta":{exclude:[3,3,1,""],model:[3,3,1,""]},"idea.tests.banner_tests.BannerTest":{test_timebound_banner:[6,1,1,""],test_no_banner:[6,1,1,""],test_outside_timed:[6,1,1,""],test_timed_before_indefinite:[6,1,1,""],test_indefinite_banner:[6,1,1,""]},"idea.migrations.0002_auto__del_field_comment_issue__add_field_comment_idea.Migration":{models:[4,3,1,""],backwards:[4,1,1,""],complete_apps:[4,3,1,""],forwards:[4,1,1,""]},"idea.migrations.0005_auto__chg_field_vote_time__chg_field_comment_time":{Migration:[4,2,1,""]},"idea.tests.addidea.AddIdeaTest":{setUp:[6,1,1,""],fixtures:[6,3,1,""],test_must_be_logged_in:[6,1,1,""],test_good_idea:[6,1,1,""],test_similar:[6,1,1,""]},"idea.utility":{state_helper:[5,0,1,""]},"idea.migrations.0004_statuses.Migration":{models:[4,3,1,""],symmetrical:[4,3,1,""],backwards:[4,1,1,""],complete_apps:[4,3,1,""],forwards:[4,1,1,""]},"idea.tests.listview":{ListViewTest:[6,2,1,""]},"idea.models.Banner":{MultipleObjectsReturned:[3,5,1,""],objects:[3,3,1,""],DoesNotExist:[3,5,1,""],get_next_by_start_date:[3,1,1,""],get_previous_by_start_date:[3,1,1,""]},"idea.migrations.0006_auto__chg_field_vote_time__chg_field_comment_time__chg_field_idea_time":{Migration:[4,2,1,""]},"idea.models.UserTrackable":{get_previous_by_time:[3,1,1,""],Meta:[3,2,1,""],get_next_by_time:[3,1,1,""],creator:[3,3,1,""]},"idea.models.State":{MultipleObjectsReturned:[3,5,1,""],state:[3,3,1,""],objects:[3,3,1,""],idea_set:[3,3,1,""],DoesNotExist:[3,5,1,""],previous:[3,3,1,""]},"idea.tests.listview.ListViewTest":{test_idea_fields:[6,1,1,""],test_tags_exist:[6,1,1,""],test_tags_active:[6,1,1,""],test_paging:[6,1,1,""],test_tags_top_ten:[6,1,1,""],test_idea_state_filter:[6,1,1,""],test_sort_vote:[6,1,1,""],test_tag_filter:[6,1,1,""],test_sort_comment:[6,1,1,""],fixtures:[6,3,1,""],test_sort_recent:[6,1,1,""],test_tags_count:[6,1,1,""]},"idea.migrations.0008_auto__add_banner.Migration":{models:[4,3,1,""],backwards:[4,1,1,""],complete_apps:[4,3,1,""],forwards:[4,1,1,""]},"idea.tests.utils":{random_user:[6,4,1,""],mock_req:[6,4,1,""]},"idea.models.IdeaManager":{related_with_counts:[3,1,1,""]},"idea.buildout":{search_sites:[1,0,1,""],urls:[1,0,1,""],testsettings:[1,0,1,""]},"idea.search_indexes.IdeaIndex":{fields:[3,3,1,""],index_queryset:[3,1,1,""]},"idea.models.UserTrackable.Meta":{"abstract":[3,3,1,""]},"idea.tests.search":{SearchTest:[6,2,1,""]},"idea.forms.IdeaForm":{declared_fields:[3,3,1,""],base_fields:[3,3,1,""],clean_tags:[3,1,1,""],Meta:[3,2,1,""],media:[3,3,1,""]},"idea.migrations.0002_auto__del_field_comment_issue__add_field_comment_idea":{Migration:[4,2,1,""]},"idea.tests.voting":{VotingTests:[6,2,1,""]}},terms:{all:6,test_tags_exist:6,is_act:4,prefix:3,onetoonefield:4,voter:6,sensit:3,articl:3,content_typ:4,solicit:3,sens:6,more_like_text:3,fals:[4,6,3],account:3,util:[5,2,3,6],test_good_vot:6,get_next_by_start_d:3,contenttyp:4,list:[6,3],last_nam:4,listview:[2,3,6],content_type__app_label:4,ten:6,core:3,jump:3,clean_tag:3,index:[0,3],"0x3b0e810":3,last_login:4,primary_kei:4,"new":6,symmetr:4,"public":3,errorlist:3,shouldn:6,"0001_initi":[4,2,3],path:6,sinc:3,valu:3,"0007_auto__del_comment__add_field_vote_vot":[4,2,3],box:0,search:[0,2,3,6],get_next_by_tim:3,tag_categori:4,declared_field:3,related_nam:4,taggit_taggeditem_item:4,campaign:3,modul:[0,1,2,3,4,5,6],api:[2,3],codenam:4,morearg:3,two:4,"0002_auto__del_field_comment_issue__add_field_comment_idea":[4,2,3],next:3,more:3,sort:6,idempot:6,ideaform:[2,3,6],test_must_logged_in:6,start_dat:4,particular:3,get_bann:3,realtimesearchindex:3,must:[6,3],none:[6,3],alia:3,setup:6,work:[6,3],uniqu:4,can:3,test_timed_before_indefinit:6,object_nam:4,"0005_auto__chg_field_vote_time__chg_field_comment_tim":[4,2,3],indic:0,topic:3,manytomanyfield:4,tag:[4,6,3],"0x3af9890":3,string:3,datetimefield:4,multipl:3,first_nam:4,rather:3,anoth:3,related_with_count:3,verifi:6,vote_set:3,test_tag_filt:6,"0008_auto__add_bann":[4,2,3],user_permiss:4,associ:6,exclud:3,issu:4,inform:3,ideaindex:3,south:4,allow:6,doesnotexist:3,test_idea_correct:6,order:[4,6],test_idea_support:6,upvoteform:3,objectdoesnotexist:3,through:3,test_tag:6,end_dat:4,group:4,test_outside_tim:6,add_idea:3,them:3,"return":3,initi:3,base:[4,6,3],now:4,document:0,name:4,anyth:6,index_queryset:3,"0003_auto__add_field_state_previous__chg_field_idea_text__chg_field_idea_ti":[4,2,3],ideabox:3,chunk:3,idea:[0,2,3,4,5,6],runtest:6,vote:[4,2,3,6],meta:[4,3],expect:6,addidea:[2,3,6],"0x2b18950":3,votingtest:6,goe:3,content:0,test_idea_no_support:6,django:[4,6,3],modelform:3,correct:6,empty_permit:3,migrat:[4,2,3],orm:4,ask:3,test_idea_text_opt:6,test_no_bann:6,test_vote_twic:6,mock_req:6,filter:6,creator_id:3,app_label:4,charfield:[4,3],view:[2,3,6],support:6,detailview:[2,3,6],first:5,hoop:3,blank:4,test_idea_field:6,auto_id:3,data:3,top:6,max_length:4,option:6,"0x3b0ea10":3,"0x3b0ebd0":3,textfield:4,than:3,test_sort_rec:6,provid:3,test_sort_com:6,pass:6,packag:[1,2,3,4,5,6],have:[6,3],tabl:0,need:6,"null":4,get_previous_by_start_d:3,object_id:4,begin:3,taggit:[4,3],integerfield:[4,3],state_help:[5,2,3],usernam:4,previou:[4,3],label_suffix:3,ideamanag:3,"class":[4,6,3],datamigr:4,url:[1,2,3],request:[6,3],doe:3,idea_set:3,show:6,text:[4,6,3],create_timestamp:4,permiss:4,hack:3,test_must_be_logged_in:6,onli:6,addideatest:6,activ:6,should:6,content_type__model:4,test_pag:6,get:[5,6,3],bannertest:6,emailfield:4,banner:[4,3],requir:6,db_tabl:4,twice:6,bad:6,contain:3,up_vot:3,creator:[4,3],methodnam:6,is_staff:4,arg:[6,3],testcas:6,taggit_taggeditem_tagged_item:4,testset:[1,2,3],user:[4,6,3],tagfield:3,test_tags_top_ten:6,tag_creat:4,state:[4,5,6,3],ideatagform:3,email:4,attribut:3,lowercas:3,state_id:3,solr:3,popul:6,smallintegerfield:4,test_tag_form_exist:6,autofield:4,admin:[2,3],similar:6,"0x2b186d0":3,instanc:3,"0x3b0e450":3,load:4,point:3,slugfield:4,"0004_status":[4,2,3],backend:3,get_vote_displai:3,is_superus:4,immedi:6,complete_app:4,tagcategori:4,error_class:3,detailviewtest:6,test_tag_form_submiss:6,"case":3,"0x2b18bd0":3,keywarg:6,defin:3,fixtur:6,test_idea_vot:6,kwarg:[6,3],datefield:4,welcom:0,datetim:4,media:3,make:[6,3],test_404:6,search_sit:[1,2,3],status:4,remot:3,delta_dai:6,auth:4,"0x3b0e390":3,recent:6,subpackag:[2,3],test_sort_vot:6,entri:3,searchabl:6,object:3,exampl:3,thi:[6,3],model:[4,2,3],test_indefinite_bann:6,comment:[4,6],identifi:6,test_similar:6,tagged_item:3,test_add_idea_tag:6,except:3,param:6,"0x3d1f950":3,add:6,"0x2b1c690":3,haystack:3,taggeditem:4,django_content_typ:4,around:3,auto_now:4,search_index:[2,3],test_edit_idea_tag:6,password:4,"0x3b0e7d0":3,like:3,specif:3,get_first_st:5,get_creator_profil:3,unique_togeth:4,page:[0,6,3],some:3,get_previous_by_tim:3,proper:6,buildout:[1,2,3],normal:6,random_us:6,genericrel:3,db_index:4,id_:3,"0006_auto__chg_field_vote_time__chg_field_comment_time__chg_field_idea_tim":[4,2,3],run:3,vote_up:3,post:6,slug:4,would:6,manag:3,reversegenericrelatedobjectsdescriptor:3,schemamigr:4,test_good_idea:6,date_join:4,test_tags_act:6,ideatest:6,searchtest:6,log:6,test_add_idea_titl:6,question:3,avail:[6,3],includ:6,forward:4,submiss:6,usertrack:3,"function":3,form:[2,3,6],listviewtest:6,forc:3,idea_id:[6,3],test_idea_title_requir:6,test_timebound_bann:6,banner_test:[2,3,6],"true":[4,6],count:6,base_field:3,"default":4,taggit_taggeditem_rel:4,foreignkei:4,otherwis:3,booleanfield:4,test_idea_state_filt:6,creat:6,multipleobjectsreturn:3,"int":3,"abstract":3,file:3,check:6,test_tags_count:6,titl:[4,6,3],morekwarg:3,when:6,detail:[6,3],field:[4,6,3],"_taggablemanag":3,lookup:3,test:[2,3,6],mock:6,relat:[4,3],get_relative_d:6,klass:3,time:[4,3],backward:4},objtypes:{"0":"py:module","1":"py:method","2":"py:class","3":"py:attribute","4":"py:function","5":"py:exception"},titles:["Welcome to Idea Box’s documentation!","buildout Package","API","idea Package","migrations Package","utility Package","tests Package"],objnames:{"0":["py","module","Python module"],"1":["py","method","Python method"],"2":["py","class","Python class"],"3":["py","attribute","Python attribute"],"4":["py","function","Python function"],"5":["py","exception","Python exception"]},filenames:["index","api/idea.buildout","api/modules","api/idea","api/idea.migrations","api/idea.utility","api/idea.tests"]}) \ No newline at end of file diff --git a/doc/source/api/idea.buildout.rst b/doc/source/api/idea.buildout.rst new file mode 100644 index 0000000..bb7e3dd --- /dev/null +++ b/doc/source/api/idea.buildout.rst @@ -0,0 +1,27 @@ +buildout Package +================ + +:mod:`search_sites` Module +-------------------------- + +.. automodule:: idea.buildout.search_sites + :members: + :undoc-members: + :show-inheritance: + +:mod:`testsettings` Module +-------------------------- + +.. automodule:: idea.buildout.testsettings + :members: + :undoc-members: + :show-inheritance: + +:mod:`urls` Module +------------------ + +.. automodule:: idea.buildout.urls + :members: + :undoc-members: + :show-inheritance: + diff --git a/doc/source/api/idea.migrations.rst b/doc/source/api/idea.migrations.rst new file mode 100644 index 0000000..c80e6e3 --- /dev/null +++ b/doc/source/api/idea.migrations.rst @@ -0,0 +1,67 @@ +migrations Package +================== + +:mod:`0001_initial` Module +-------------------------- + +.. automodule:: idea.migrations.0001_initial + :members: + :undoc-members: + :show-inheritance: + +:mod:`0002_auto__del_field_comment_issue__add_field_comment_idea` Module +------------------------------------------------------------------------ + +.. automodule:: idea.migrations.0002_auto__del_field_comment_issue__add_field_comment_idea + :members: + :undoc-members: + :show-inheritance: + +:mod:`0003_auto__add_field_state_previous__chg_field_idea_text__chg_field_idea_ti` Module +----------------------------------------------------------------------------------------- + +.. automodule:: idea.migrations.0003_auto__add_field_state_previous__chg_field_idea_text__chg_field_idea_ti + :members: + :undoc-members: + :show-inheritance: + +:mod:`0004_statuses` Module +--------------------------- + +.. automodule:: idea.migrations.0004_statuses + :members: + :undoc-members: + :show-inheritance: + +:mod:`0005_auto__chg_field_vote_time__chg_field_comment_time` Module +-------------------------------------------------------------------- + +.. automodule:: idea.migrations.0005_auto__chg_field_vote_time__chg_field_comment_time + :members: + :undoc-members: + :show-inheritance: + +:mod:`0006_auto__chg_field_vote_time__chg_field_comment_time__chg_field_idea_time` Module +----------------------------------------------------------------------------------------- + +.. automodule:: idea.migrations.0006_auto__chg_field_vote_time__chg_field_comment_time__chg_field_idea_time + :members: + :undoc-members: + :show-inheritance: + +:mod:`0007_auto__del_comment__add_field_vote_vote` Module +--------------------------------------------------------- + +.. automodule:: idea.migrations.0007_auto__del_comment__add_field_vote_vote + :members: + :undoc-members: + :show-inheritance: + +:mod:`0008_auto__add_banner` Module +----------------------------------- + +.. automodule:: idea.migrations.0008_auto__add_banner + :members: + :undoc-members: + :show-inheritance: + diff --git a/doc/source/api/idea.rst b/doc/source/api/idea.rst new file mode 100644 index 0000000..216c833 --- /dev/null +++ b/doc/source/api/idea.rst @@ -0,0 +1,61 @@ +idea Package +============ + +:mod:`admin` Module +------------------- + +.. automodule:: idea.admin + :members: + :undoc-members: + :show-inheritance: + +:mod:`forms` Module +------------------- + +.. automodule:: idea.forms + :members: + :undoc-members: + :show-inheritance: + +:mod:`models` Module +-------------------- + +.. automodule:: idea.models + :members: + :undoc-members: + :show-inheritance: + +:mod:`search_indexes` Module +---------------------------- + +.. automodule:: idea.search_indexes + :members: + :undoc-members: + :show-inheritance: + +:mod:`urls` Module +------------------ + +.. automodule:: idea.urls + :members: + :undoc-members: + :show-inheritance: + +:mod:`views` Module +------------------- + +.. automodule:: idea.views + :members: + :undoc-members: + :show-inheritance: + +Subpackages +----------- + +.. toctree:: + + idea.buildout + idea.migrations + idea.tests + idea.utility + diff --git a/doc/source/api/idea.tests.rst b/doc/source/api/idea.tests.rst new file mode 100644 index 0000000..e2d6b60 --- /dev/null +++ b/doc/source/api/idea.tests.rst @@ -0,0 +1,75 @@ +tests Package +============= + +:mod:`tests` Package +-------------------- + +.. automodule:: idea.tests + :members: + :undoc-members: + :show-inheritance: + +:mod:`addidea` Module +--------------------- + +.. automodule:: idea.tests.addidea + :members: + :undoc-members: + :show-inheritance: + +:mod:`banner_tests` Module +-------------------------- + +.. automodule:: idea.tests.banner_tests + :members: + :undoc-members: + :show-inheritance: + +:mod:`detailview` Module +------------------------ + +.. automodule:: idea.tests.detailview + :members: + :undoc-members: + :show-inheritance: + +:mod:`ideaform` Module +---------------------- + +.. automodule:: idea.tests.ideaform + :members: + :undoc-members: + :show-inheritance: + +:mod:`listview` Module +---------------------- + +.. automodule:: idea.tests.listview + :members: + :undoc-members: + :show-inheritance: + +:mod:`search` Module +-------------------- + +.. automodule:: idea.tests.search + :members: + :undoc-members: + :show-inheritance: + +:mod:`utils` Module +------------------- + +.. automodule:: idea.tests.utils + :members: + :undoc-members: + :show-inheritance: + +:mod:`voting` Module +-------------------- + +.. automodule:: idea.tests.voting + :members: + :undoc-members: + :show-inheritance: + diff --git a/doc/source/api/idea.utility.rst b/doc/source/api/idea.utility.rst new file mode 100644 index 0000000..cb33f77 --- /dev/null +++ b/doc/source/api/idea.utility.rst @@ -0,0 +1,11 @@ +utility Package +=============== + +:mod:`state_helper` Module +-------------------------- + +.. automodule:: idea.utility.state_helper + :members: + :undoc-members: + :show-inheritance: + diff --git a/doc/source/api/modules.rst b/doc/source/api/modules.rst new file mode 100644 index 0000000..c179ee1 --- /dev/null +++ b/doc/source/api/modules.rst @@ -0,0 +1,7 @@ +API +=== + +.. toctree:: + :maxdepth: 4 + + idea diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 0000000..fe05045 --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,249 @@ +# -*- coding: utf-8 -*- +# +# Idea Box documentation build configuration file, created by +# sphinx-quickstart on Thu Feb 21 21:42:31 2013. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +sys.path.insert(0, os.path.abspath('../../src')) + +# Use the testsettings django environment for settings. +from django.core.management import setup_environ +from idea.buildout import testsettings +setup_environ(testsettings) + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Idea Box' +copyright = u'2013, Shashank Khandelwal, CM Lubinski, Jennifer Ehler, Jui Dai, David Kennedy' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.1.0' +# The full version, including alpha/beta/rc tags. +release = '0.1.0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +html_show_copyright = False + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'IdeaBoxdoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'IdeaBox.tex', u'Idea Box Documentation', + u'Shashank Khandelwal, CM Lubinski, Jennifer Ehler, Jui Dai, David Kennedy', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'ideabox', u'Idea Box Documentation', + [u'Shashank Khandelwal, CM Lubinski, Jennifer Ehler, Jui Dai, David Kennedy'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------------ + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'IdeaBox', u'Idea Box Documentation', + u'Shashank Khandelwal, CM Lubinski, Jennifer Ehler, Jui Dai, David Kennedy', 'IdeaBox', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 0000000..1b7807a --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,24 @@ +.. Idea Box documentation master file, created by + sphinx-quickstart on Thu Feb 21 21:42:31 2013. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to Idea Box's documentation! +==================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + api/modules + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + From 7218143b2e184b1d79a14806b3cda2fb01d46676 Mon Sep 17 00:00:00 2001 From: CM Lubinski Date: Tue, 19 Mar 2013 16:06:10 +0100 Subject: [PATCH 013/230] Add profile screen shot --- doc/images/profile.png | Bin 0 -> 93542 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 doc/images/profile.png diff --git a/doc/images/profile.png b/doc/images/profile.png new file mode 100644 index 0000000000000000000000000000000000000000..9f3c74a731a3bbb61ec3844c9627aafb70d8aa88 GIT binary patch literal 93542 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
      diff --git a/src/idea/templates/idea/detail.html b/src/idea/templates/idea/detail.html index cfcdea2..25b6647 100644 --- a/src/idea/templates/idea/detail.html +++ b/src/idea/templates/idea/detail.html @@ -20,13 +20,13 @@

      {{idea.title}}

      {% if idea.state.name == 'Archive' %} Archived {% elif support %} - Voted + Voted {% else %}
      {% csrf_token %} - +
      {% endif %} {{voters|length}} {{voters|pluralize:"Person,People"}} Voted @@ -60,7 +60,7 @@

      {{comment_list|length}} Comments

      {{comment_form.timestamp}} {{comment_form.security_hash}} - +
        @@ -85,11 +85,11 @@

        {{comment_list|length}} Comments

      Q1 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 E0N@12TmS$7 literal 0 HcmV?d00001 From 6247264569c74d9cf688597ae0a63839ed3a0665 Mon Sep 17 00:00:00 2001 From: CM Lubinski Date: Tue, 19 Mar 2013 16:08:53 +0100 Subject: [PATCH 014/230] Include screenshot in README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 80556af..f178c60 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Collecting and surfacing ideas from everyone. +![Idea Profile](https://raw.github.com/cfpb/idea-box/master/doc/images/profile.png) + ## Requirements * django (1.4.3) - This is a django app, so you need django. * django-haystack (1.2.7) - A mapper between django models and search From ef603c2c72bcf7c7ff286a08f4af6ce6ecfc4122 Mon Sep 17 00:00:00 2001 From: CM Lubinski Date: Tue, 19 Mar 2013 16:09:56 +0100 Subject: [PATCH 015/230] Would be helpful to have the proper project name in the README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f178c60..77dc2e3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Idea Collection +# Idea-Box Collecting and surfacing ideas from everyone. From 0a1166b9a0b95d32d50121c701eb5a56f4a8602c Mon Sep 17 00:00:00 2001 From: CM Lubinski Date: Fri, 22 Mar 2013 18:56:35 +0100 Subject: [PATCH 016/230] Add a more descriptive description. Also, discuss campaign banner --- README.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 77dc2e3..2f31534 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,21 @@ # Idea-Box -Collecting and surfacing ideas from everyone. +Idea-Box is a django app for collecting and surfacing ideas from users, in the vein of +IdeaScale, IdeaX, and Django Voice. Idea-Box differs from these projects in its minimal, +easily integrate-able interface. Idea-Box also takes a strong stance on transparency, +such that ideas, votes, etc. are tied to specific users. + +## Features +* Searching (via Solr's "more-like-this") +* Idea Submission +* Tagging (via taggit) +* Voting +* Comments +* Listing by recent, comment count, vote count +* Separate state for archived ideas +* Customizable banner for specific campaigns + +## Screen shot ![Idea Profile](https://raw.github.com/cfpb/idea-box/master/doc/images/profile.png) @@ -114,3 +129,10 @@ $ pip install zc.buildout distribute $ buildout ``` Then, run the django binary in the ```bin``` directory. + +### Campaign Banner + +To create a campaign banner, use django's administrative page to add a Banner model. The +text field will be displayed at the top of the Idea-Box idea listing page. The banner +will only be displayed between Start Date and End Date (or indefinitely after the Start +Date if the End Date is empty.) From 4a6e76c28f87507aa39ee256c6234d5d32913e49 Mon Sep 17 00:00:00 2001 From: Diego Lapiduz Date: Mon, 25 Nov 2013 11:23:00 -0500 Subject: [PATCH 017/230] Update django code to 1.5.4 (IN-177) --- src/idea/models.py | 2 +- src/idea/templates/idea/add.html | 6 +++--- src/idea/templates/idea/detail.html | 8 ++++---- src/idea/templates/idea/list.html | 16 ++++++++-------- src/idea/urls.py | 4 ++-- src/idea/views.py | 16 ++++++++-------- 6 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/idea/models.py b/src/idea/models.py index 1f3438d..5598187 100644 --- a/src/idea/models.py +++ b/src/idea/models.py @@ -6,7 +6,7 @@ from django.core.urlresolvers import reverse from django.db import models from django.utils.timezone import get_default_timezone -from taggit.managers import TaggableManager +from core.taggit.managers import TaggableManager class UserTrackable(models.Model): creator = models.ForeignKey(User) diff --git a/src/idea/templates/idea/add.html b/src/idea/templates/idea/add.html index 205284a..005d9bb 100644 --- a/src/idea/templates/idea/add.html +++ b/src/idea/templates/idea/add.html @@ -8,7 +8,7 @@

      @@ -105,7 +105,7 @@

      Idea Tags

      + + + + + {% endblock %} diff --git a/src/idea/templates/idea/idea-base.html b/src/idea/templates/idea/idea-base.html index 96eecbf..4e019d7 100644 --- a/src/idea/templates/idea/idea-base.html +++ b/src/idea/templates/idea/idea-base.html @@ -1,16 +1,16 @@ {% extends "base.html" %} {% block "css_files" %} - + - {% if page.description %} - - {% endif %} + {% if page.description %} + + {% endif %} {% endblock %} {% block "content" %}{% endblock %} {% block "js_scripts" %} - + {% endblock %} diff --git a/src/idea/templates/idea/list.html b/src/idea/templates/idea/list.html index 26cfd74..7f6920f 100644 --- a/src/idea/templates/idea/list.html +++ b/src/idea/templates/idea/list.html @@ -2,103 +2,103 @@ {% block "content" %} -
      -
      -
      -
      -
      -
      -
      - -
      -
      -
      -

      Main Navigation for Idea Box

      -
      - +
      +
      +
      +
      {% if ideas %} {% for idea in ideas %} +
      +
      + {{idea.vote_count}} Like{{idea.vote_count|pluralize:",s"}} +
      +
      +
      +

      {{idea.title}}

      +
      +
      + {{ idea.text|truncatechars:250 }} +
      +
      + Read More +
      + +
      +
      + {% endfor %} {% else %} -
      -
      -

      There are no ideas to display.

      -
      +
      +
      +

      There are no ideas to display.

      +
      {% endif %} -
      -
      -
      + + +
      +
      - - + +
      +
      Current Challenge:
      +

      {{banner.title}}

      + +
      + + + + {% endblock %} {% block "js_scripts" %} - + {% block "design_js_scripts" %}{% endblock %} {% endblock %} From b00bbb641594a08a2f1fa8e137d95dcea056e8f1 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Fri, 4 Apr 2014 12:33:49 +0000 Subject: [PATCH 048/230] pep8 cleanup --- src/idea/admin.py | 2 +- src/idea/forms.py | 7 ++- src/idea/models.py | 40 +++++++++----- src/idea/urls.py | 6 ++- src/idea/views.py | 126 +++++++++++++++++++++++++-------------------- 5 files changed, 105 insertions(+), 76 deletions(-) diff --git a/src/idea/admin.py b/src/idea/admin.py index 4595ba9..f4398d2 100644 --- a/src/idea/admin.py +++ b/src/idea/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from idea.models import Idea,State,Vote, Banner +from idea.models import Idea, State, Vote, Banner admin.site.register(State) admin.site.register(Idea) diff --git a/src/idea/forms.py b/src/idea/forms.py index a842ac6..111a3cc 100644 --- a/src/idea/forms.py +++ b/src/idea/forms.py @@ -1,7 +1,9 @@ from django import forms from idea.models import Idea + class IdeaForm(forms.ModelForm): + class Meta: model = Idea exclude = ('creator', 'time', 'state',) @@ -9,19 +11,20 @@ class Meta: def clean_tags(self): """ Force tags to lowercase, since tags are case-sensitive otherwise. """ - if 'tags' in self.cleaned_data: + if 'tags' in self.cleaned_data: tags = self.cleaned_data['tags'] # Account for taggit's odd special case (when a tag with spaces # but no commas is split) if 'tags' in self.data and u"," not in self.data['tags'] and len(tags) > 1: tags = [self.data['tags'].strip()] return [t.lower() for t in tags] - + class UpVoteForm(forms.Form): idea_id = forms.IntegerField(widget=forms.HiddenInput()) next = forms.CharField(max_length=512, widget=forms.HiddenInput()) + class IdeaTagForm(forms.Form): tags = forms.CharField(max_length=512) diff --git a/src/idea/models.py b/src/idea/models.py index 0b2c59b..9337651 100644 --- a/src/idea/models.py +++ b/src/idea/models.py @@ -17,38 +17,46 @@ class UserTrackable(models.Model): creator = models.ForeignKey(User) # use a lambda so that this is evaluated upon creation (rather than # once upon class import) - time = models.DateTimeField(default = - lambda:datetime.utcnow().replace(tzinfo=get_default_timezone())) + time = models.DateTimeField( + default=lambda: datetime.utcnow().replace(tzinfo=get_default_timezone())) class Meta: abstract = True - + + class Banner(models.Model): + """ The banner text at the beginning of IdeaBox pages, asking the question. This can be used to run informal campaigns soliciting ideas around specific topics. """ title = models.CharField(max_length=50) text = models.CharField(max_length=2000, verbose_name="description") - start_date = models.DateField(help_text="The date from which this banner will be displayed.") - end_date = models.DateField(null=True, blank=True, - help_text="Empty indicates that the banner should be continued indefinitely. ") + start_date = models.DateField( + help_text="The date from which this banner will be displayed.") + end_date = models.DateField(null=True, blank=True, + help_text="Empty indicates that the banner " + + "should be continued indefinitely. ") def __unicode__(self): return u'%s (%s to %s)' % (self.title, self.start_date, self.end_date) + class State(models.Model): + """ The state an idea goes through. """ name = models.CharField(max_length=50) - #States are ordered with respect to each other. - #The first State has no previous. + # States are ordered with respect to each other. + # The first State has no previous. previous = models.OneToOneField('self', null=True, blank=True) def __unicode__(self): return u'%s' % self.name + class IdeaManager(models.Manager): + def related_with_counts(self): idea_type = ContentType.objects.get(app_label="idea", model="idea") return self.select_related().extra(select={ @@ -71,12 +79,15 @@ def related_with_counts(self): 'vote_count': """ SELECT count(*) FROM idea_vote WHERE idea_id = idea_idea.id """ - }, select_params=[idea_type.id]) + }, select_params=[idea_type.id]) + class Idea(UserTrackable): title = models.CharField(max_length=50, blank=False, null=False) - text = models.TextField(max_length=2000, blank=True, null=True, verbose_name="description") - banner = models.ForeignKey(Banner, verbose_name="challenge", blank=True, null=True) + text = models.TextField( + max_length=2000, blank=True, null=True, verbose_name="description") + banner = models.ForeignKey( + Banner, verbose_name="challenge", blank=True, null=True) state = models.ForeignKey(State) tags = TaggableManager(blank=True) @@ -93,15 +104,16 @@ def url(self): def get_creator_profile(self): try: return self.creator.get_profile() - except (ObjectDoesNotExist, SiteProfileNotAvailable): + except (ObjectDoesNotExist, SiteProfileNotAvailable): return None objects = IdeaManager() -#We can only upvote. +# We can only upvote. UP_VOTE = 1 VOTE_CHOICES = ((u'+1', UP_VOTE),) + class Vote(UserTrackable): - vote = models.SmallIntegerField(choices=VOTE_CHOICES, default=1) + vote = models.SmallIntegerField(choices=VOTE_CHOICES, default=1) idea = models.ForeignKey(Idea) diff --git a/src/idea/urls.py b/src/idea/urls.py index b5c02a1..f9379ad 100644 --- a/src/idea/urls.py +++ b/src/idea/urls.py @@ -1,12 +1,14 @@ from django.conf.urls import patterns, url -urlpatterns = patterns('idea.views', +urlpatterns = patterns( + 'idea.views', url(r'^$', 'list'), url(r'^add/$', 'add_idea', name='add_idea'), url(r'^list/$', 'list', name='idea_list'), url(r'^list/(?P\w+)/$', 'list', name='idea_list'), url(r'^detail/(?P\d+)/$', 'detail', name='idea_detail'), url(r'^vote/up/$', 'up_vote', name='upvote_idea'), - url(r'^challenge/(?P\d+)/$', 'banner_detail', name='banner_detail'), + url(r'^challenge/(?P\d+)/$', + 'banner_detail', name='banner_detail'), ) diff --git a/src/idea/views.py b/src/idea/views.py index b410aa7..d66f2d0 100644 --- a/src/idea/views.py +++ b/src/idea/views.py @@ -30,24 +30,27 @@ def _render(req, template_name, context={}): context['app_link'] = reverse('idea:idea_list') return render(req, template_name, context) + def get_banner(): today = date.today() - timed_banners = Banner.objects.filter(start_date__lte=today, - end_date__isnull=False, end_date__gt=today) + timed_banners = Banner.objects.filter(start_date__lte=today, + end_date__isnull=False, + end_date__gt=today) if timed_banners: return timed_banners[0] else: - indefinite_banners = Banner.objects.filter(start_date__lte=today, - end_date__isnull=True) + indefinite_banners = Banner.objects.filter(start_date__lte=today, + end_date__isnull=True) if indefinite_banners: return indefinite_banners[0] else: return None + @login_required def list(request, sort_or_state=None): - tag_strs= request.GET.get('tags', '').split(',') + tag_strs = request.GET.get('tags', '').split(',') tag_strs = [t for t in tag_strs if t != u''] tag_ids = [tag.id for tag in Tag.objects.filter(slug__in=tag_strs)] page_num = request.GET.get('page_num') @@ -61,7 +64,7 @@ def list(request, sort_or_state=None): # URL Filter - either archive or one of the sorts if sort_or_state == 'archived': ideas = ideas.filter(state=State.objects.get(name='Archive') - ).order_by('-vote_count') + ).order_by('-vote_count') else: ideas = ideas.filter(state=State.objects.get(name='Active')) if sort_or_state == 'vote': @@ -73,7 +76,7 @@ def list(request, sort_or_state=None): ideas = ideas.order_by('-recent_activity') IDEAS_PER_PAGE = getattr(settings, 'IDEAS_PER_PAGE', 10) - pager = Paginator(ideas, IDEAS_PER_PAGE) + pager = Paginator(ideas, IDEAS_PER_PAGE) # Boiler plate paging -- @todo abstract this try: page = pager.page(page_num) @@ -84,9 +87,9 @@ def list(request, sort_or_state=None): # List of tags tags = Tag.objects.filter( - taggit_taggeditem_items__content_type__name='idea' + taggit_taggeditem_items__content_type__name='idea' ).annotate(count=Count('taggit_taggeditem_items') - ).order_by('-count', 'name')[:10] + ).order_by('-count', 'name')[:10] for tag in tags: if tag.slug in tag_strs: @@ -95,29 +98,32 @@ def list(request, sort_or_state=None): tag_slugs = ",".join(tag_strs + [tag.slug]) # Minor tweak: Links just turn on/off a single tag if tag.slug in tag_strs: - tag.tag_url = "%s" % (reverse('idea:idea_list', - args=(sort_or_state,))) + tag.tag_url = "%s" % (reverse('idea:idea_list', + args=(sort_or_state,))) tag.active = True else: - tag.tag_url = "%s?tags=%s" % (reverse('idea:idea_list', - args=(sort_or_state,)), tag.slug) + tag.tag_url = "%s?tags=%s" % (reverse('idea:idea_list', + args=(sort_or_state,)), + tag.slug) tag.active = False banner = get_banner() return _render(request, 'idea/list.html', { 'sort_or_state': sort_or_state, - 'ideas': page, - 'tags': tags, # list of popular tags + 'ideas': page, + 'tags': tags, # list of popular tags 'banner': banner, }) + def vote_up(idea, user): vote = Vote() vote.idea = idea vote.creator = user vote.save() + @require_POST @login_required def up_vote(request): @@ -129,14 +135,16 @@ def up_vote(request): idea = Idea.objects.get(pk=idea_id) - #Up voting is idempotent - existing_votes = Vote.objects.filter(idea=idea, creator=request.user, vote=UP_VOTE) + # Up voting is idempotent + existing_votes = Vote.objects.filter( + idea=idea, creator=request.user, vote=UP_VOTE) if not existing_votes.exists(): vote_up(idea, request.user) return HttpResponseRedirect(next_url) + def more_like_text(text, klass): """ Return more entries like the provided chunk of text. We have to jump @@ -147,28 +155,29 @@ def more_like_text(text, klass): if hasattr(back, 'conn'): query = {'query': { - 'filtered': { - 'query' : { - 'fuzzy_like_this' : { - 'like_text' : text - } - }, - 'filter': { - 'bool': { - 'must': { - 'term': { 'django_ct': 'idea.idea' } - } - } + 'filtered': { + 'query': { + 'fuzzy_like_this': { + 'like_text': text + } + }, + 'filter': { + 'bool': { + 'must': { + 'term': {'django_ct': 'idea.idea'} } } } - } + } + + } results = back.conn.search(query) return back._process_results(results)['results'] else: return [] + @login_required def detail(request, idea_id): """ @@ -179,11 +188,11 @@ def detail(request, idea_id): tag_form = IdeaTagForm(request.POST) if tag_form.is_valid(): data = tag_form.clean()['tags'] - tags = [tag.strip() for tag in data.split(',') + tags = [tag.strip() for tag in data.split(',') if tag.strip() != ''] idea.tags.add(*tags) return HttpResponseRedirect( - reverse('idea:idea_detail', args=(idea.id,))) + reverse('idea:idea_detail', args=(idea.id,))) else: tag_form = IdeaTagForm() @@ -191,30 +200,31 @@ def detail(request, idea_id): for v in voters: try: - v.profile = v.get_profile() + v.profile = v.get_profile() except (ObjectDoesNotExist, SiteProfileNotAvailable): v.profile = None - idea_type = ContentType.objects.get(app_label="idea", model="idea") tags = idea.tags.extra(select={ 'tag_count': """ - SELECT COUNT(*) from taggit_taggeditem tt WHERE tt.tag_id = taggit_tag.id + SELECT COUNT(*) from taggit_taggeditem tt + WHERE tt.tag_id = taggit_tag.id AND content_type_id = %s """ }, select_params=[idea_type.id]).order_by('name') for tag in tags: - tag.tag_url = "%s?tags=%s" % (reverse('idea:idea_list'), tag.slug) + tag.tag_url = "%s?tags=%s" % (reverse('idea:idea_list'), tag.slug) return _render(request, 'idea/detail.html', { - 'idea': idea, # title, body, user name, user photo, time + 'idea': idea, # title, body, user name, user photo, time 'support': request.user in voters, - 'tags': tags, + 'tags': tags, 'voters': voters, 'tag_form': tag_form - }) + }) + @login_required def add_idea(request): @@ -225,17 +235,18 @@ def add_idea(request): if form.is_valid(): new_idea = form.save() vote_up(new_idea, request.user) - return HttpResponseRedirect(reverse('idea:idea_detail', args=(idea.id,))) + return HttpResponseRedirect(reverse('idea:idea_detail', + args=(idea.id,))) else: return HttpResponse('Idea is archived', status=403) else: idea_title = request.GET.get('idea_title', '') - form = IdeaForm(initial={'title':idea_title}) + form = IdeaForm(initial={'title': idea_title}) return _render(request, 'idea/add.html', { - 'form':form, - 'similar': [r.object for r in more_like_text(idea_title, - Idea)] - }) + 'form': form, + 'similar': [r.object for r in more_like_text(idea_title, Idea)] + }) + @login_required def banner_detail(request, banner_id): @@ -244,7 +255,7 @@ def banner_detail(request, banner_id): """ banner = Banner.objects.get(id=banner_id) - tag_strs= request.GET.get('tags', '').split(',') + tag_strs = request.GET.get('tags', '').split(',') tag_strs = [t for t in tag_strs if t != u''] tag_ids = [tag.id for tag in Tag.objects.filter(slug__in=tag_strs)] page_num = request.GET.get('page_num') @@ -256,7 +267,7 @@ def banner_detail(request, banner_id): ideas = ideas.filter(tags__pk__in=tag_ids).distinct() IDEAS_PER_PAGE = getattr(settings, 'IDEAS_PER_PAGE', 10) - pager = Paginator(ideas, IDEAS_PER_PAGE) + pager = Paginator(ideas, IDEAS_PER_PAGE) # Boiler plate paging -- @todo abstract this try: page = pager.page(page_num) @@ -268,12 +279,12 @@ def banner_detail(request, banner_id): # List of tags that are associated with an idea in the banner list banner_ideas = Idea.objects.filter(banner=banner) banner_tags = Tag.objects.filter( - taggit_taggeditem_items__content_type__name='idea', - taggit_taggeditem_items__object_id__in=banner_ideas) + taggit_taggeditem_items__content_type__name='idea', + taggit_taggeditem_items__object_id__in=banner_ideas) tags = banner_tags.filter( - taggit_taggeditem_items__content_type__name='idea' + taggit_taggeditem_items__content_type__name='idea' ).annotate(count=Count('taggit_taggeditem_items') - ).order_by('-count', 'name')[:10] + ).order_by('-count', 'name')[:10] for tag in tags: if tag.slug in tag_strs: @@ -282,16 +293,17 @@ def banner_detail(request, banner_id): tag_slugs = ",".join(tag_strs + [tag.slug]) # Minor tweak: Links just turn on/off a single tag if tag.slug in tag_strs: - tag.tag_url = "%s" % (reverse('idea:banner_detail', - args=(banner_id,))) + tag.tag_url = "%s" % (reverse('idea:banner_detail', + args=(banner_id,))) tag.active = True else: - tag.tag_url = "%s?tags=%s" % (reverse('idea:banner_detail', - args=(banner_id,)), tag.slug) + tag.tag_url = "%s?tags=%s" % (reverse('idea:banner_detail', + args=(banner_id,)), + tag.slug) tag.active = False return _render(request, 'idea/banner_detail.html', { - 'ideas': page, - 'tags': tags, # list of tags associated with banner ideas + 'ideas': page, + 'tags': tags, # list of tags associated with banner ideas 'banner': banner, }) From 06ee97ca22b83714f9deb0ac00a2dbaed43dee7a Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Mon, 7 Apr 2014 14:36:51 +0000 Subject: [PATCH 049/230] IN-318 - Add search results for idea and challenge --- src/idea/search_indexes.py | 69 +++++++++++++++++-- .../search/indexes/idea/banner_text.txt | 2 + .../indexes/idea}/idea_text.txt | 2 + src/idea/tests/search_tests.py | 67 ++++++++++++++++++ 4 files changed, 135 insertions(+), 5 deletions(-) create mode 100644 src/idea/templates/search/indexes/idea/banner_text.txt rename src/idea/templates/{idea/index => search/indexes/idea}/idea_text.txt (67%) diff --git a/src/idea/search_indexes.py b/src/idea/search_indexes.py index 0488b80..806f8b1 100644 --- a/src/idea/search_indexes.py +++ b/src/idea/search_indexes.py @@ -1,12 +1,71 @@ from haystack import indexes -from models import Idea +from models import Idea, Banner +from django.core.urlresolvers import reverse +from time import mktime, strptime class IdeaIndex(indexes.SearchIndex, indexes.Indexable): - text = indexes.CharField(document=True, use_template=True, - template_name='idea/index/idea_text.txt') + text = indexes.EdgeNgramField(document=True, use_template=True) + display = indexes.CharField(model_attr='title') + description = indexes.CharField(model_attr="summary", null=True) + index_name = indexes.CharField(indexed=False) + index_priority = indexes.IntegerField(indexed=False) + index_sort = indexes.IntegerField(indexed=False, null=True) + url = indexes.CharField(indexed=False, null=True) - def index_queryset(self, using=None): - return Idea.objects.related_with_counts() + PRIORITY = 4 + + def prepare_index_name(self, obj): + return "Ideas" + + def prepare_index_priority(self, obj): + return self.PRIORITY + + def prepare_index_sort(self, obj): + # want a positive number so banner results appear above/before ideas + #9999999999 =~ year 2286 + return 9999999999 - int(mktime(strptime(obj.recent_activity, "%Y-%m-%d %H:%M:%S"))) + + def prepare_url(self, obj): + return reverse('idea:idea_detail', args=(obj.id,)) def get_model(self): return Idea + + def index_queryset(self, using=None): + """Used when the entire index for model is updated.""" + # State 2 = Archived + return self.get_model().objects.related_with_counts().exclude(state=2) + + +class BannerIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.EdgeNgramField(document=True, use_template=True) + display = indexes.CharField(model_attr='title') + description = indexes.CharField(model_attr="text") + index_name = indexes.CharField(indexed=False) + index_priority = indexes.IntegerField(indexed=False) + index_sort = indexes.IntegerField(indexed=False, null=True) + url = indexes.CharField(indexed=False, null=True) + + PRIORITY = 4 + + def prepare_index_name(self, obj): + return "Ideas" + + def prepare_index_priority(self, obj): + return self.PRIORITY + + def prepare_index_sort(self, obj): + return 0 - int(mktime(obj.start_date.timetuple())) + + def prepare_url(self, obj): + return reverse('idea:banner_detail', args=(obj.id,)) + + def prepare_display(self, obj): + return "Challenge: " + obj.title + + def get_model(self): + return Banner + + def index_queryset(self, using=None): + """Used when the entire index for model is updated.""" + return self.get_model().objects.all() diff --git a/src/idea/templates/search/indexes/idea/banner_text.txt b/src/idea/templates/search/indexes/idea/banner_text.txt new file mode 100644 index 0000000..61e3e65 --- /dev/null +++ b/src/idea/templates/search/indexes/idea/banner_text.txt @@ -0,0 +1,2 @@ +{{ object.title }} +{{ object.text }} diff --git a/src/idea/templates/idea/index/idea_text.txt b/src/idea/templates/search/indexes/idea/idea_text.txt similarity index 67% rename from src/idea/templates/idea/index/idea_text.txt rename to src/idea/templates/search/indexes/idea/idea_text.txt index bf289bf..7acdd07 100644 --- a/src/idea/templates/idea/index/idea_text.txt +++ b/src/idea/templates/search/indexes/idea/idea_text.txt @@ -1,4 +1,6 @@ {{ object.title }} +{{ object.summary }} +{{ object.text }} {% for tag in object.tags.all %} {{ tag.name }} {% endfor %} diff --git a/src/idea/tests/search_tests.py b/src/idea/tests/search_tests.py index 06848cf..cfd98db 100644 --- a/src/idea/tests/search_tests.py +++ b/src/idea/tests/search_tests.py @@ -4,6 +4,7 @@ from haystack import connections from idea import models, views from idea.tests.utils import random_user +from datetime import date class SearchTest(TestCase): fixtures = ['state'] @@ -33,6 +34,42 @@ def test_add_idea_title(self): results = self.backend.search('example_title') self.assertEqual(1, results['hits']) + @unittest.skipIf(backend_type == 'SimpleSearchBackend', + "See https://github.com/toastdriven/django-haystack/issues/908") + def test_add_idea_summary(self): + """ + Check that adding a new idea allows title to be immediately + searchable. + """ + req = RequestFactory().post('/', { + 'title':'test title', + 'summary': 'example_summary', + 'text': 'test text', + 'tags': 'test, tags' + }) + req.user = random_user() + views.add_idea(req) + results = self.backend.search('example_summary') + self.assertEqual(1, results['hits']) + + @unittest.skipIf(backend_type == 'SimpleSearchBackend', + "See https://github.com/toastdriven/django-haystack/issues/908") + def test_add_idea_text(self): + """ + Check that adding a new idea allows title to be immediately + searchable. + """ + req = RequestFactory().post('/', { + 'title':'test title', + 'summary': 'test summary', + 'text': 'example_text', + 'tags': 'test, tags' + }) + req.user = random_user() + views.add_idea(req) + results = self.backend.search('example_text') + self.assertEqual(1, results['hits']) + @unittest.skipIf(backend_type == 'SimpleSearchBackend', "Simple backend doesn't handle tags") def test_add_idea_tag(self): @@ -74,3 +111,33 @@ def test_edit_idea_tag(self): views.detail(req, str(idea.id)) results = self.backend.search('example_tag') self.assertEqual(1, results['hits']) + + @unittest.skipIf(backend_type == 'SimpleSearchBackend', + "See https://github.com/toastdriven/django-haystack/issues/908") + def test_banner_title(self): + """ + Check that adding a new idea allows title to be immediately + searchable. + """ + banner = models.Banner() + banner.title = "example_title" + banner.text = "test text" + banner.start_date = date.today() + banner.save() + results = self.backend.search('example_title') + self.assertEqual(1, results['hits']) + + @unittest.skipIf(backend_type == 'SimpleSearchBackend', + "See https://github.com/toastdriven/django-haystack/issues/908") + def test_banner_text(self): + """ + Check that adding a new idea allows title to be immediately + searchable. + """ + banner = models.Banner() + banner.title = "test title" + banner.text = "example_text" + banner.start_date = date.today() + banner.save() + results = self.backend.search('example_text') + self.assertEqual(1, results['hits']) From 544318b02b8b8f79d47ab6ab4322056d212f7fe7 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Mon, 7 Apr 2014 14:51:33 +0000 Subject: [PATCH 050/230] IN-318 - Remove idea sort by recent activity, doesn't work in test environment --- src/idea/search_indexes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/idea/search_indexes.py b/src/idea/search_indexes.py index 806f8b1..f53239b 100644 --- a/src/idea/search_indexes.py +++ b/src/idea/search_indexes.py @@ -9,7 +9,8 @@ class IdeaIndex(indexes.SearchIndex, indexes.Indexable): description = indexes.CharField(model_attr="summary", null=True) index_name = indexes.CharField(indexed=False) index_priority = indexes.IntegerField(indexed=False) - index_sort = indexes.IntegerField(indexed=False, null=True) +# TODO causes all tests to fail +# index_sort = indexes.IntegerField(indexed=False, null=True) url = indexes.CharField(indexed=False, null=True) PRIORITY = 4 From 366a5059ce5cbc6f85fddf2c866b9438f15b2b03 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Mon, 7 Apr 2014 15:15:13 +0000 Subject: [PATCH 051/230] Remove template line missed in merge 48553b8 --- src/idea/templates/idea/detail.html | 1 - 1 file changed, 1 deletion(-) diff --git a/src/idea/templates/idea/detail.html b/src/idea/templates/idea/detail.html index d69dc37..9b22985 100644 --- a/src/idea/templates/idea/detail.html +++ b/src/idea/templates/idea/detail.html @@ -43,7 +43,6 @@

      {{idea.title}}

      {{idea.time|date:"H:m A; M d, Y "}}

      -

      {{idea.text}}

      Summary: {{idea.summary}}

      Detail: {{idea.text}}

      {% get_comment_list for idea.idea idea.id as comment_list %} From e2b6712b74c20cae3d0180bc8f86ab51ebed4580 Mon Sep 17 00:00:00 2001 From: Diego Lapiduz Date: Mon, 7 Apr 2014 14:51:07 -0400 Subject: [PATCH 052/230] Add a smoke test for the home page --- buildout.cfg | 3 +++ src/idea/tests/smoke_tests.py | 39 +++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 src/idea/tests/smoke_tests.py diff --git a/buildout.cfg b/buildout.cfg index 388f3c0..73feb8a 100644 --- a/buildout.cfg +++ b/buildout.cfg @@ -20,4 +20,7 @@ eggs = ${buildout:eggs} django-nose django-taggit django-haystack + exam + webtest + django-webtest pysolr diff --git a/src/idea/tests/smoke_tests.py b/src/idea/tests/smoke_tests.py new file mode 100644 index 0000000..13b44aa --- /dev/null +++ b/src/idea/tests/smoke_tests.py @@ -0,0 +1,39 @@ +import os +from django.utils import timezone +from django_webtest import WebTest +from exam.decorators import fixture +from exam.cases import Exam +from django.core.urlresolvers import reverse + + +class SmokeTest(Exam, WebTest): + csrf_checks = False + fixtures = ['state'] + + @fixture + def user(self): + try: + from collab.django_factories import UserF + return UserF(username="test1@example.com", person__title='') + except ImportError: + from django.contrib.auth.models import User + user = User() + user.username = "test1@example.com" + user.first_name = 'first' + user.last_name = 'last' + user.email = '"test1@example.com"' + user.password = 'pbkdf2_sha256$10000$ggAKkiHobFL8$xQzwPeHNX1vWr9uNmZ/gKbd17uLGZVM8QNcgmaIEAUs=' + user.is_staff = False + user.is_active = True + user.is_superuser = False + user.last_login = timezone.now() + user.date_joined = timezone.now() + user.save() + return user + + def get(self, url): + return self.app.get(url, user=self.user) + + def test_idea_home(self): + page = self.get(reverse('idea:idea_list')) + self.assertEquals(200, page.status_code) From 2cd66f4ec6f8cb5554d06c419e95abb2903bc548 Mon Sep 17 00:00:00 2001 From: Diego Lapiduz Date: Tue, 8 Apr 2014 10:26:18 -0400 Subject: [PATCH 053/230] Check for banner on home page --- src/idea/templates/idea/list.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/idea/templates/idea/list.html b/src/idea/templates/idea/list.html index 7f6920f..6cda3c7 100644 --- a/src/idea/templates/idea/list.html +++ b/src/idea/templates/idea/list.html @@ -75,11 +75,13 @@

      Idea Post Navigation

      + {% if banner %}
      Current Challenge:

      {{banner.title}}

      + {% endif %} -
      - - diff --git a/src/idea/templates/idea/list.html b/src/idea/templates/idea/list.html index cae75f9..f14a4e1 100644 --- a/src/idea/templates/idea/list.html +++ b/src/idea/templates/idea/list.html @@ -8,7 +8,9 @@

      Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod @@ -37,8 +39,10 @@

      Main Navigation for Idea Box

      {% for idea in ideas %}
      -
      - {{idea.vote_count}}
      Like{{idea.vote_count|pluralize:",s"}}
      +
      +
      {{idea.vote_count}}
      +
      Like{{idea.vote_count|pluralize:",s"}}
      +
      {% if idea.state.name == 'Archive' %} Archived {% elif request.user in idea.voters.all %} @@ -51,6 +55,7 @@

      Main Navigation for Idea Box

      {% endif %} +
      @@ -59,7 +64,7 @@

      Main Navigation for Idea Box

      {{ idea.summary|truncatechars:250 }} - Read More  +  Read More 
      From 7f00b5298a465cd01cda378d66d9d92d156f4eff Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Tue, 15 Apr 2014 20:58:04 +0000 Subject: [PATCH 066/230] IN-462 - Add challenge text --- src/idea/templates/idea/list.html | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/idea/templates/idea/list.html b/src/idea/templates/idea/list.html index 7a7cd1d..8049fe7 100644 --- a/src/idea/templates/idea/list.html +++ b/src/idea/templates/idea/list.html @@ -13,12 +13,11 @@

      Idea Box

      - Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod - tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, - quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo - consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse - cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non - proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + Idea Box is a place where everyone in the CFPB can share their + ideas on how to improve the work we do for consumers and the way + we do it. Challenges give you the opportunity to share your + solution to a specific problem. Submit your ideas and join the + conversation!

      From 03476542ddb72f0558b94abb7f1af90e074b21c2 Mon Sep 17 00:00:00 2001 From: Diego Lapiduz Date: Tue, 15 Apr 2014 19:16:38 -0400 Subject: [PATCH 067/230] Add mptt to buildout --- buildout.cfg | 1 + src/idea/buildout/testsettings.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/buildout.cfg b/buildout.cfg index 73feb8a..8a702fd 100644 --- a/buildout.cfg +++ b/buildout.cfg @@ -20,6 +20,7 @@ eggs = ${buildout:eggs} django-nose django-taggit django-haystack + django-mptt exam webtest django-webtest diff --git a/src/idea/buildout/testsettings.py b/src/idea/buildout/testsettings.py index 9681e8c..465516d 100644 --- a/src/idea/buildout/testsettings.py +++ b/src/idea/buildout/testsettings.py @@ -36,7 +36,7 @@ 'django_nose', 'django.contrib.comments', 'django.contrib.staticfiles', - 'django-mptt', + 'mptt', ] ROOT_URLCONF = 'idea.buildout.urls' From 77b2363072dcd4c29c6af20dbf04e3f68124c063 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Wed, 16 Apr 2014 12:56:08 +0000 Subject: [PATCH 068/230] Fix some issues from merging cfpb/idea-box#19 and cfpb/idea-box#20 --- src/idea/static/idea/css/idea.css | 8 ++++---- src/idea/templates/idea/detail.html | 24 ------------------------ 2 files changed, 4 insertions(+), 28 deletions(-) diff --git a/src/idea/static/idea/css/idea.css b/src/idea/static/idea/css/idea.css index f7e5629..2a58c3d 100644 --- a/src/idea/static/idea/css/idea.css +++ b/src/idea/static/idea/css/idea.css @@ -39,6 +39,8 @@ .idea-home { overflow: auto; + overflow-y:hidden; + overflow-x:hidden; } /* Improve readability when focused and hovered in all browsers: h5bp.com/h */ @@ -204,19 +206,17 @@ ul.section-nav li.active a { position: relative; } .comments h4 { - background: url(../img/comment-icon-detail.png) 0 0 no-repeat; font-size: 100%; - font-weight: bold; + font-family: "Avenir Next Demi", Arial, sans-serif; line-height: 1.66; min-height: 1.75em; - padding-left: 3.3125em; text-transform: uppercase; } .comment-meta { font-size: 16px; } .comment-form { - padding: 0.625em 0.625em 2.8em; + padding: 0 0 2.8em; margin: 1.5625em 0 0; } .comment-form textarea { diff --git a/src/idea/templates/idea/detail.html b/src/idea/templates/idea/detail.html index 42d552a..c729d67 100644 --- a/src/idea/templates/idea/detail.html +++ b/src/idea/templates/idea/detail.html @@ -38,21 +38,6 @@

      {{idea.title}}

      -
      - {% if idea.state.name == 'Archive' %} - Archived - {% elif support %} - Liked - {% else %} -
      - {% csrf_token %} - - - -
      - {% endif %} - Liked by {{voters|length}} {{voters|pluralize:"person,people"}} -

      Summary: {{idea.summary}}

      @@ -63,16 +48,7 @@

      {{idea.title}}

      {% else %} {{idea.creator.first_name}} {{idea.creator.last_name}} {% endif %} -<<<<<<< HEAD -

      {{idea.time|date:"M d, Y"}}

      -
      -
      -

      Summary: {{idea.summary}}

      -

      Detail: {{idea.text}}

      -======= on {{idea.time|date:"M d, Y "}} - ->>>>>>> e7e700dd8675de4d7fbed906ef6f03c053bd2b97 {% get_comment_list for idea.idea idea.id as comment_list %}
      From 7b99b1739e296885ecc7ae1299d02d9e3fbe8c1b Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Wed, 16 Apr 2014 14:40:53 +0000 Subject: [PATCH 069/230] IN-314 - Fix indented child comments in IE --- src/idea/templates/idea/detail.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/idea/templates/idea/detail.html b/src/idea/templates/idea/detail.html index c729d67..1115346 100644 --- a/src/idea/templates/idea/detail.html +++ b/src/idea/templates/idea/detail.html @@ -89,7 +89,9 @@

      {{comment_list|length}} Comments

      {{node.comment}}

      {% if not node.is_leaf_node %} - {{ children }} +
        + {{ children }} +
      > {% endif %} From 4e43c51343958103588450cdd083d1dcc796b360 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Wed, 16 Apr 2014 15:28:07 +0000 Subject: [PATCH 070/230] Fix fontawesome icon for Read More link in IE --- src/idea/templates/idea/list.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/idea/templates/idea/list.html b/src/idea/templates/idea/list.html index e87647c..3fcd86b 100644 --- a/src/idea/templates/idea/list.html +++ b/src/idea/templates/idea/list.html @@ -63,7 +63,7 @@

      Main Navigation for Idea Box

      {{ idea.summary|truncatechars:250 }} -  Read More  +  Read More 
      From 1a89cfe0790ab4aa46bfcdd535c55c2da13c2378 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Wed, 16 Apr 2014 16:05:10 +0000 Subject: [PATCH 071/230] IN-449 - Add help text for idea create page --- src/idea/models.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/idea/models.py b/src/idea/models.py index faf418c..6d9b950 100644 --- a/src/idea/models.py +++ b/src/idea/models.py @@ -83,15 +83,23 @@ def related_with_counts(self): class Idea(UserTrackable): - title = models.CharField(max_length=50, blank=False, null=False) - summary = models.CharField(max_length=200) - text = models.TextField(max_length=2000, null=False, verbose_name="detail") + title = models.CharField(max_length=50, blank=False, null=False, + help_text="Give your idea a descriptive name.") + summary = models.CharField(max_length=200, help_text="\ + The first 200 characters display on the main page.") + text = models.TextField(max_length=2000, null=False, + verbose_name="detail", help_text="\ + Please add more information to explain your idea.") banner = models.ForeignKey( - Banner, verbose_name="challenge", blank=True, null=True) + Banner, verbose_name="challenge", blank=True, null=True, help_text="\ + Select if your idea relates to a particular challenge.") state = models.ForeignKey(State) - tags = TaggableManager(blank=False) - voters = models.ManyToManyField(User, through="Vote", related_name="idea_vote_creator", null=True) + tags = TaggableManager(blank=False, help_text="\ + You must add at least 1 tag. We suggest the office to which the \ + idea relates.") + voters = models.ManyToManyField(User, through="Vote", null=True, + related_name="idea_vote_creator") def __unicode__(self): return u'%s' % self.title From 5b4e455b5c085045ff2c3dab57febbfd65c89a87 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Wed, 16 Apr 2014 16:08:19 +0000 Subject: [PATCH 072/230] pep8 cleanup --- src/idea/models.py | 6 +++--- src/idea/views.py | 11 ++++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/idea/models.py b/src/idea/models.py index 6d9b950..88015ed 100644 --- a/src/idea/models.py +++ b/src/idea/models.py @@ -112,9 +112,9 @@ def url(self): @property def comments(self): - return Comment.objects.for_model(self.__class__).filter(is_public = True, - is_removed = False, - object_pk = self.pk) + return Comment.objects.for_model(self.__class__).filter(is_public=True, + is_removed=False, + object_pk=self.pk) @property def members(self): diff --git a/src/idea/views.py b/src/idea/views.py index 28b5b60..80527cf 100644 --- a/src/idea/views.py +++ b/src/idea/views.py @@ -30,9 +30,11 @@ def _render(req, template_name, context={}): context['app_link'] = reverse('idea:idea_list') return render(req, template_name, context) + def get_current_banners(): return Banner.objects.filter(start_date__lte=date.today()).exclude(end_date__lt=date.today()) + def get_banner(): today = date.today() timed_banners = Banner.objects.filter(start_date__lte=today, @@ -106,7 +108,7 @@ def list(request, sort_or_state=None): else: tag.tag_url = "%s?tags=%s" % (reverse('idea:idea_list', args=(sort_or_state,)), - tag.slug) + tag.slug) tag.active = False banner = get_banner() @@ -228,7 +230,6 @@ def detail(request, idea_id): }) - @login_required def add_idea(request): if request.method == 'POST': @@ -242,7 +243,7 @@ def add_idea(request): args=(idea.id,))) else: form.fields["banner"].queryset = get_current_banners() - return _render(request, 'idea/add.html', {'form':form, }) + return _render(request, 'idea/add.html', {'form': form, }) else: return HttpResponse('Idea is archived', status=403) else: @@ -252,7 +253,7 @@ def add_idea(request): return _render(request, 'idea/add.html', { 'form': form, 'similar': [r.object for r in more_like_text(idea_title, Idea)] - }) + }) @login_required @@ -306,7 +307,7 @@ def banner_detail(request, banner_id): else: tag.tag_url = "%s?tags=%s" % (reverse('idea:banner_detail', args=(banner_id,)), - tag.slug) + tag.slug) tag.active = False return _render(request, 'idea/banner_detail.html', { From affdd536f5c1dd3ae64f36f53db0e7110fe300dc Mon Sep 17 00:00:00 2001 From: CM Lubinski Date: Thu, 17 Apr 2014 16:23:28 +0200 Subject: [PATCH 073/230] Peg django version, thereby reduce the number of tests we need to skip --- buildout.cfg | 3 +++ src/idea/tests/search_tests.py | 15 ++------------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/buildout.cfg b/buildout.cfg index 8a702fd..afaccc5 100644 --- a/buildout.cfg +++ b/buildout.cfg @@ -8,6 +8,9 @@ recipe = zc.recipe.egg interpreter = python eggs = ${buildout:eggs} +[versions] +Django = 1.5.4 + [django] recipe = djangorecipe project = idea diff --git a/src/idea/tests/search_tests.py b/src/idea/tests/search_tests.py index cfd98db..c4442e4 100644 --- a/src/idea/tests/search_tests.py +++ b/src/idea/tests/search_tests.py @@ -11,13 +11,10 @@ class SearchTest(TestCase): backend = connections['default'].get_backend() backend_type = connections['default'].backend.__name__ - @unittest.skipIf(backend_type == 'SimpleSearchBackend', - "Simple backend doesn't allow itself to be cleared") def setUp(self): - self.backend.clear() + if SearchTest.backend_type != 'SimpleSearchBackend': + self.backend.clear() - @unittest.skipIf(backend_type == 'SimpleSearchBackend', - "See https://github.com/toastdriven/django-haystack/issues/908") def test_add_idea_title(self): """ Check that adding a new idea allows title to be immediately @@ -34,8 +31,6 @@ def test_add_idea_title(self): results = self.backend.search('example_title') self.assertEqual(1, results['hits']) - @unittest.skipIf(backend_type == 'SimpleSearchBackend', - "See https://github.com/toastdriven/django-haystack/issues/908") def test_add_idea_summary(self): """ Check that adding a new idea allows title to be immediately @@ -52,8 +47,6 @@ def test_add_idea_summary(self): results = self.backend.search('example_summary') self.assertEqual(1, results['hits']) - @unittest.skipIf(backend_type == 'SimpleSearchBackend', - "See https://github.com/toastdriven/django-haystack/issues/908") def test_add_idea_text(self): """ Check that adding a new idea allows title to be immediately @@ -112,8 +105,6 @@ def test_edit_idea_tag(self): results = self.backend.search('example_tag') self.assertEqual(1, results['hits']) - @unittest.skipIf(backend_type == 'SimpleSearchBackend', - "See https://github.com/toastdriven/django-haystack/issues/908") def test_banner_title(self): """ Check that adding a new idea allows title to be immediately @@ -127,8 +118,6 @@ def test_banner_title(self): results = self.backend.search('example_title') self.assertEqual(1, results['hits']) - @unittest.skipIf(backend_type == 'SimpleSearchBackend', - "See https://github.com/toastdriven/django-haystack/issues/908") def test_banner_text(self): """ Check that adding a new idea allows title to be immediately From f6ae06085f1a2c8c5fc1bf6f2984b35285851167 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Thu, 17 Apr 2014 18:39:31 +0000 Subject: [PATCH 074/230] IN-481 - Add autocomplete for existing idea tags --- src/idea/forms.py | 3 ++- src/idea/templates/idea/detail.html | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/idea/forms.py b/src/idea/forms.py index eb97a1e..c172322 100644 --- a/src/idea/forms.py +++ b/src/idea/forms.py @@ -30,7 +30,8 @@ class UpVoteForm(forms.Form): class IdeaTagForm(forms.Form): - tags = forms.CharField(max_length=512) + tags = forms.CharField(max_length=512, + widget=forms.TextInput(attrs={'class':'tags_autocomplete'})) def clean_tags(self): """ Force tags to lowercase, since tags are case-sensitive otherwise. """ diff --git a/src/idea/templates/idea/detail.html b/src/idea/templates/idea/detail.html index 42d552a..19742e1 100644 --- a/src/idea/templates/idea/detail.html +++ b/src/idea/templates/idea/detail.html @@ -163,3 +163,9 @@

      Idea Tags

      {% endblock %} + +{% block "js_ready" %} + $(".tags_autocomplete").autocomplete({ + source: "{% url "search:model_tags_json" "idea" %}", + }); +{% endblock %} From 93185c15c99217dbd6c6314fdca21c126e646348 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Fri, 18 Apr 2014 17:03:09 +0000 Subject: [PATCH 075/230] IN-482 - The user can now remove tags he or she added --- src/idea/templates/idea/detail.html | 3 +- src/idea/tests/tag_tests.py | 74 +++++++++++++++++++++++++++++ src/idea/urls.py | 2 + src/idea/views.py | 34 +++++++++++-- 4 files changed, 108 insertions(+), 5 deletions(-) create mode 100644 src/idea/tests/tag_tests.py diff --git a/src/idea/templates/idea/detail.html b/src/idea/templates/idea/detail.html index 1115346..13de7ef 100644 --- a/src/idea/templates/idea/detail.html +++ b/src/idea/templates/idea/detail.html @@ -121,7 +121,8 @@

      Liked by

      Idea Tags

        {% for tag in tags %} -
      • {{tag}} {{tag.tag_count}}
      • +
      • {{tag}} + {% if tag.name in tags_created_by_user %}x
      • {% endif %} {% endfor %}
      diff --git a/src/idea/tests/tag_tests.py b/src/idea/tests/tag_tests.py new file mode 100644 index 0000000..cc8fbb0 --- /dev/null +++ b/src/idea/tests/tag_tests.py @@ -0,0 +1,74 @@ +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse +from django.test import TestCase +from django.utils import unittest +from django.test.client import RequestFactory +from idea import models, views +from idea.tests.utils import mock_req, random_user +try: + from core.taggit.utils import add_tags + from core.taggit.models import TaggedItem + ADD_TAGS = True; +except ImportError: + from taggit.models import TaggedItem + ADD_TAGS = False; + +class TagTest(TestCase): + fixtures = ['state', 'core-test-fixtures'] + + @unittest.skipIf(ADD_TAGS == False, "Remove only works with collab's core.taggit") + def test_tag_remove_exists_for_creator(self): + """ + Detail page allows for removal of tags created by the current user + """ + self.client.login(username='test1@example.com', password='1') + user1 = User.objects.get(username='test1@example.com') + idea = models.Idea(creator=user1, title='AAAA', + state = models.State.objects.get(name='Active')) + idea.save() + add_tags(idea, 'AAA', None, user1, 'idea') + + response = self.client.get(reverse("idea:idea_detail", args=(str(idea.id),))) + self.assertContains(response, 'aaa') + self.assertContains(response, 'tag_remove') + + @unittest.skipIf(ADD_TAGS == False, "Remove only works with collab's core.taggit") + def test_tag_remove_not_exists_for_random_user(self): + """ + Detail page does not allow for removal of tags created by a different user + """ + self.client.login(username='test1@example.com', password='1') + user1 = random_user() + idea = models.Idea(creator=user1, title='AAAA', + state = models.State.objects.get(name='Active')) + idea.save() + add_tags(idea, 'AAA', None, user1, 'idea') + + response = self.client.get(reverse("idea:idea_detail", args=(str(idea.id),))) + self.assertContains(response, 'aaa') + self.assertNotContains(response, 'tag_remove') + + @unittest.skipIf(ADD_TAGS == False, "Remove only works with collab's core.taggit") + def test_tag_remove(self): + """ + Detail page tag form submission should add tags. + """ + user1 = random_user() + idea = models.Idea(creator=user1, title='AAAA', + state = models.State.objects.get(name='Active')) + idea.save() + add_tags(idea, 'AAA', None, user1, 'idea') + + # Attempting to remove as a different user fails + req = RequestFactory().post('/', {}) + req.user = random_user() + response = views.remove_tag(req, str(idea.id), 'aaa') + self.assertEqual(response.status_code, 302) + self.assertIn('aaa', set([tag.slug for tag in idea.tags.all()])) + + # Attempting to remove as the creator succeeds + req.user = user1 + response = views.remove_tag(req, str(idea.id), 'aaa') + self.assertEqual(response.status_code, 302) + self.assertNotIn('aaa', set([tag.slug for tag in idea.tags.all()])) + diff --git a/src/idea/urls.py b/src/idea/urls.py index f9379ad..325164d 100644 --- a/src/idea/urls.py +++ b/src/idea/urls.py @@ -8,6 +8,8 @@ url(r'^list/$', 'list', name='idea_list'), url(r'^list/(?P\w+)/$', 'list', name='idea_list'), url(r'^detail/(?P\d+)/$', 'detail', name='idea_detail'), + url(r'^detail/(?P\d+)/remove_tag/(?P\w+)/$', + 'remove_tag', name='remove_tag'), url(r'^vote/up/$', 'up_vote', name='upvote_idea'), url(r'^challenge/(?P\d+)/$', 'banner_detail', name='banner_detail'), diff --git a/src/idea/views.py b/src/idea/views.py index 80527cf..af6158e 100644 --- a/src/idea/views.py +++ b/src/idea/views.py @@ -8,7 +8,7 @@ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.core.urlresolvers import reverse from django.db.models import Count -from django.http import HttpResponseRedirect, HttpResponse +from django.http import HttpResponseRedirect, HttpResponseForbidden, HttpResponse from django.shortcuts import get_object_or_404, render from django.views.decorators.http import require_POST @@ -18,9 +18,10 @@ from idea.models import UP_VOTE try: - from core.taggit.models import Tag + from core.taggit.models import Tag, TaggedItem + from core.taggit.utils import add_tags except ImportError: - from taggit.models import Tag + from taggit.models import Tag, TaggedItem from haystack import connections @@ -194,7 +195,11 @@ def detail(request, idea_id): data = tag_form.clean()['tags'] tags = [tag.strip() for tag in data.split(',') if tag.strip() != ''] - idea.tags.add(*tags) + try: + for t in tags: + add_tags(idea, t, None, request.user, 'idea') + except NameError: # catch if add_tags doesn't exist + idea.tags.add(*tags) return HttpResponseRedirect( reverse('idea:idea_detail', args=(idea.id,))) else: @@ -218,13 +223,19 @@ def detail(request, idea_id): """ }, select_params=[idea_type.id]).order_by('name') + tags_created_by_user = [] for tag in tags: tag.tag_url = "%s?tags=%s" % (reverse('idea:idea_list'), tag.slug) + for ti in tag.taggit_taggeditem_items.all(): + if ti.tag_creator == request.user and \ + ti.content_type.name == "idea": + tags_created_by_user.append(tag.name) return _render(request, 'idea/detail.html', { 'idea': idea, # title, body, user name, user photo, time 'support': request.user in voters, 'tags': tags, + 'tags_created_by_user': tags_created_by_user, 'voters': voters, 'tag_form': tag_form }) @@ -315,3 +326,18 @@ def banner_detail(request, banner_id): 'tags': tags, # list of tags associated with banner ideas 'banner': banner, }) + +@login_required +def remove_tag(request, idea_id, tag_slug): + idea = Idea.objects.get(pk=idea_id) + tag = Tag.objects.get(slug=tag_slug) + try: + taggeditem = TaggedItem.objects.get(tag_creator=request.user, + object_id=idea.id, tag=tag) + taggeditem.delete() + except TaggedItem.DoesNotExist: # catch if object not found + pass + except NameError: # catch if TaggedItem doesn't exist + pass + return HttpResponseRedirect(reverse('idea:idea_detail', args=(idea.id,))) + From 71ee25d9c56d430cec8b208de2d7e374f5b53e67 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Fri, 18 Apr 2014 15:53:37 -0400 Subject: [PATCH 076/230] IN-470 - Use common formatting to top left title --- src/idea/static/idea/css/idea.css | 12 +++++++----- src/idea/templates/idea/add.html | 6 +++--- src/idea/templates/idea/banner_detail.html | 10 +++++----- src/idea/templates/idea/detail.html | 6 +++--- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/idea/static/idea/css/idea.css b/src/idea/static/idea/css/idea.css index 2a58c3d..c931ad7 100644 --- a/src/idea/static/idea/css/idea.css +++ b/src/idea/static/idea/css/idea.css @@ -66,14 +66,14 @@ margin-bottom: 10px; font: 100%/1.375 Georgia, "Times New Roman", serif; /* 16px font size */ } -.compact-header .project-title { +.project-title { margin-top: 0; } .project-title a { color: #2cb34a; - letter-spacing: normal; - font-family: "Avenir Next Medium", Arial, sans-serif; - line-height: 1.25em; + letter-spacing: 1px; + font-family: "Avenir Next Demi", Arial, sans-serif; + line-height: 1.57142857142857em; } h4.project-title { @@ -412,7 +412,9 @@ ul.section-nav li.active a { .challenge-info p.challenge-entry-content { margin-bottom: 0; } -.idea-single { +.idea-add, +.idea-single, +.idea-banner-single { padding-top: 1.5em; } .idea-single .main-content { diff --git a/src/idea/templates/idea/add.html b/src/idea/templates/idea/add.html index a82f479..f8aa76b 100644 --- a/src/idea/templates/idea/add.html +++ b/src/idea/templates/idea/add.html @@ -3,10 +3,10 @@ {% block "content" %}
      -
      -

      Add an idea

      diff --git a/src/idea/templates/idea/banner_detail.html b/src/idea/templates/idea/banner_detail.html index e6825e3..ff30e97 100644 --- a/src/idea/templates/idea/banner_detail.html +++ b/src/idea/templates/idea/banner_detail.html @@ -2,14 +2,14 @@ {% block "content" %} -
      +
      -
      -
      - +
      +
      +

      Current Challenge:

      {{banner.title}}

      diff --git a/src/idea/templates/idea/detail.html b/src/idea/templates/idea/detail.html index 1115346..f21502b 100644 --- a/src/idea/templates/idea/detail.html +++ b/src/idea/templates/idea/detail.html @@ -6,10 +6,10 @@ {% block "content" %}
      -
      -

      +
      + +

      From 2d2767b4e6185517d179b03044ed9da86ac7ac0f Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Fri, 18 Apr 2014 16:14:45 -0400 Subject: [PATCH 077/230] IN-472 - Update summary/detail formatting in detail page --- src/idea/static/idea/css/idea.css | 10 ++++++---- src/idea/templates/idea/detail.html | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/idea/static/idea/css/idea.css b/src/idea/static/idea/css/idea.css index c931ad7..6ba5587 100644 --- a/src/idea/static/idea/css/idea.css +++ b/src/idea/static/idea/css/idea.css @@ -240,10 +240,12 @@ ul.section-nav li.active a { .single-idea-entry { width: 63.71428%; /* slightly smaller than span8 due to wide idea-votes */ } -.single-idea-entry .idea-entry-content .comment-author { - font-size: 87.5%; - line-height: 1.9; - font-weight: 600; +.single-idea-entry .idea-entry-content h4 { + font-family: "Avenir Next Medium", Arial, sans-serif; + margin-top: 1.5em; +} +.single-idea-entry .idea-entry-content p { + margin-top: .5em; } .comment-date { color: #666; diff --git a/src/idea/templates/idea/detail.html b/src/idea/templates/idea/detail.html index f21502b..6b157b5 100644 --- a/src/idea/templates/idea/detail.html +++ b/src/idea/templates/idea/detail.html @@ -40,8 +40,8 @@

      {{idea.title}}

      -

      Summary: {{idea.summary}}

      -

      Detail: {{idea.text}}

      +

      Summary

      {{idea.summary}}

      +

      Detail

      {{idea.text}}

      Posted by {% if idea.get_creator_profile %} {{idea.creator.first_name}} {{idea.creator.last_name}} From b893627ec9e6cc6acafece0224039c8baa30ac80 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Fri, 18 Apr 2014 21:30:29 +0000 Subject: [PATCH 078/230] IN-482 - tags_by_creator should only be used with collab.core.taggit, not regular taggit --- src/idea/tests/tag_tests.py | 12 +++++------- src/idea/views.py | 17 ++++++++++------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/idea/tests/tag_tests.py b/src/idea/tests/tag_tests.py index cc8fbb0..1274862 100644 --- a/src/idea/tests/tag_tests.py +++ b/src/idea/tests/tag_tests.py @@ -7,16 +7,14 @@ from idea.tests.utils import mock_req, random_user try: from core.taggit.utils import add_tags - from core.taggit.models import TaggedItem - ADD_TAGS = True; + COLLAB_TAGS = True; except ImportError: - from taggit.models import TaggedItem - ADD_TAGS = False; + COLLAB_TAGS = False; class TagTest(TestCase): fixtures = ['state', 'core-test-fixtures'] - @unittest.skipIf(ADD_TAGS == False, "Remove only works with collab's core.taggit") + @unittest.skipIf(COLLAB_TAGS == False, "Remove only works with collab's core.taggit") def test_tag_remove_exists_for_creator(self): """ Detail page allows for removal of tags created by the current user @@ -32,7 +30,7 @@ def test_tag_remove_exists_for_creator(self): self.assertContains(response, 'aaa') self.assertContains(response, 'tag_remove') - @unittest.skipIf(ADD_TAGS == False, "Remove only works with collab's core.taggit") + @unittest.skipIf(COLLAB_TAGS == False, "Remove only works with collab's core.taggit") def test_tag_remove_not_exists_for_random_user(self): """ Detail page does not allow for removal of tags created by a different user @@ -48,7 +46,7 @@ def test_tag_remove_not_exists_for_random_user(self): self.assertContains(response, 'aaa') self.assertNotContains(response, 'tag_remove') - @unittest.skipIf(ADD_TAGS == False, "Remove only works with collab's core.taggit") + @unittest.skipIf(COLLAB_TAGS == False, "Remove only works with collab's core.taggit") def test_tag_remove(self): """ Detail page tag form submission should add tags. diff --git a/src/idea/views.py b/src/idea/views.py index af6158e..9ecadfe 100644 --- a/src/idea/views.py +++ b/src/idea/views.py @@ -20,8 +20,10 @@ try: from core.taggit.models import Tag, TaggedItem from core.taggit.utils import add_tags + COLLAB_TAGS = True; except ImportError: - from taggit.models import Tag, TaggedItem + from taggit.models import Tag + COLLAB_TAGS = False; from haystack import connections @@ -224,12 +226,13 @@ def detail(request, idea_id): }, select_params=[idea_type.id]).order_by('name') tags_created_by_user = [] - for tag in tags: - tag.tag_url = "%s?tags=%s" % (reverse('idea:idea_list'), tag.slug) - for ti in tag.taggit_taggeditem_items.all(): - if ti.tag_creator == request.user and \ - ti.content_type.name == "idea": - tags_created_by_user.append(tag.name) + if COLLAB_TAGS: + for tag in tags: + tag.tag_url = "%s?tags=%s" % (reverse('idea:idea_list'), tag.slug) + for ti in tag.taggit_taggeditem_items.all(): + if ti.tag_creator == request.user and \ + ti.content_type.name == "idea": + tags_created_by_user.append(tag.name) return _render(request, 'idea/detail.html', { 'idea': idea, # title, body, user name, user photo, time From 79b3ff9568d45139dcdde6b178cea530f2b3bf97 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Fri, 18 Apr 2014 22:39:54 +0000 Subject: [PATCH 079/230] IN-476 - Update sidebar location, challenge page to reflect design sample --- src/idea/static/idea/css/idea.css | 31 ++++++++++++--- src/idea/templates/idea/banner_detail.html | 46 +++++++++++----------- src/idea/templates/idea/detail.html | 18 +++++---- 3 files changed, 60 insertions(+), 35 deletions(-) diff --git a/src/idea/static/idea/css/idea.css b/src/idea/static/idea/css/idea.css index 6ba5587..e79920c 100644 --- a/src/idea/static/idea/css/idea.css +++ b/src/idea/static/idea/css/idea.css @@ -119,7 +119,7 @@ ul.section-nav { border-bottom: 1px solid #000; padding-left: 0; margin-top: 0; - margin-bottom: .5em; + margin-bottom: 0; list-style: none; overflow: auto; overflow-y:hidden; @@ -160,7 +160,11 @@ ul.section-nav li.active a { .no-results { padding: 2.5em 0; } -.idea-votes { +.idea-single .idea-votes { + width: 6.9%; /* 72 px */ + text-align: center; +} +.idea-entry .idea-votes { text-align: center; } .idea-votes .count { @@ -238,10 +242,15 @@ ul.section-nav li.active a { padding: 0; } .single-idea-entry { - width: 63.71428%; /* slightly smaller than span8 due to wide idea-votes */ + width: 90%; /* slightly smaller than span11 due to wide idea-votes */ +} +.single-idea-entry .body { + margin-top: 1.5em; } .single-idea-entry .idea-entry-content h4 { font-family: "Avenir Next Medium", Arial, sans-serif; +} +.single-idea-entry .idea-entry-content .detail { margin-top: 1.5em; } .single-idea-entry .idea-entry-content p { @@ -283,6 +292,9 @@ ul.section-nav li.active a { } /* Sidebar */ +.sidebar { + //margin-top: 1em; +} .sidebar ul { list-style: none; padding-left: 0; @@ -412,12 +424,13 @@ ul.section-nav li.active a { } .challenge-info p.challenge-entry-content { - margin-bottom: 0; + margin: .5em 0; } .idea-add, .idea-single, .idea-banner-single { padding-top: 1.5em; + overflow: auto; } .idea-single .main-content { padding-top: 1.5em; @@ -553,11 +566,19 @@ span.btn-voted:hover { .challenge-info h2 { font-weight: normal; } - +.challenge-info .challenge-banner { + margin-top: 0.5em; + background-color: #DBEDD4; + padding: 0.75em; +} .challenge-info { + border-bottom: 1px solid #000; padding: 0; padding-bottom: 20px; } +.challenge-info #start { + margin-right: 2.5em; +} section.main-content hr { background-color: #000; diff --git a/src/idea/templates/idea/banner_detail.html b/src/idea/templates/idea/banner_detail.html index ff30e97..1931fb9 100644 --- a/src/idea/templates/idea/banner_detail.html +++ b/src/idea/templates/idea/banner_detail.html @@ -9,29 +9,31 @@
      -
      -
      -

      Current Challenge:

      -

      {{banner.title}}

      -

      - {{banner.text}} -

      - - -
      -
      Challenge Start Date: {{banner.start_date}}
      -
      Challenge End Date: {{banner.end_date}}
      -
      -
      +
      +
      +

      Current Challenge:

      +

      {{banner.title}}

      +
      +

      + {{banner.text}} +

      + +
      + Challenge start date: {{banner.start_date|date:"SHORT_DATE_FORMAT"}} + Challenge end date: {{banner.end_date|date:"SHORT_DATE_FORMAT"}} +
      +
      +
      -
      -
      +
      {% if ideas %} {% for idea in ideas %}
      -
      - {{idea.vote_count}} Like{{idea.vote_count|pluralize:",s"}} +
      +
      {{idea.vote_count}}
      +
      Like{{idea.vote_count|pluralize:",s"}}
      +
      {% if idea.state.name == 'Archive' %} Archived {% elif request.user in idea.voters.all %} @@ -40,10 +42,11 @@

      {{banner.title}}

      {% csrf_token %} - +
      {% endif %} +
      @@ -81,9 +84,7 @@

      Idea Post Navigation

      {% endif %}
      -
      -
      - +
      {% endblock %} diff --git a/src/idea/templates/idea/detail.html b/src/idea/templates/idea/detail.html index 6b157b5..7ed71c2 100644 --- a/src/idea/templates/idea/detail.html +++ b/src/idea/templates/idea/detail.html @@ -12,7 +12,7 @@
      -
      +
      {{voters|length}}
      Like{{voters|pluralize:",s"}}
      @@ -31,17 +31,18 @@
      -
      +
      +
      From 3f7bedc33bc7d8ecadbbf11542a90e681299a3d6 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Fri, 18 Apr 2014 22:48:10 +0000 Subject: [PATCH 080/230] IN-469 - Adjust style for 'Read More' in challenge page --- src/idea/templates/idea/banner_detail.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/idea/templates/idea/banner_detail.html b/src/idea/templates/idea/banner_detail.html index 1931fb9..86a245d 100644 --- a/src/idea/templates/idea/banner_detail.html +++ b/src/idea/templates/idea/banner_detail.html @@ -54,9 +54,9 @@

      {{banner.title}}

      {{ idea.summary|truncatechars:250 }} -
      -
      From 9df80d889be8939ca12b9db4912cb16a233a3680 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Fri, 18 Apr 2014 22:58:27 +0000 Subject: [PATCH 081/230] IN-469 - Adjust border between ideas --- src/idea/static/idea/css/idea.css | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/idea/static/idea/css/idea.css b/src/idea/static/idea/css/idea.css index e79920c..0e4560a 100644 --- a/src/idea/static/idea/css/idea.css +++ b/src/idea/static/idea/css/idea.css @@ -154,9 +154,12 @@ ul.section-nav li.active a { /* Content */ .idea-entry { - border-bottom: 1px solid #f2f2f2; + border-top: 1px solid #000; padding: 20px 20px 20px 0; } +.idea-entry:first-child { + border-top: 0; +} .no-results { padding: 2.5em 0; } From cb0d4901f325f4de3ffd982e2d9e5c080caad211 Mon Sep 17 00:00:00 2001 From: Diego Lapiduz Date: Sun, 20 Apr 2014 21:15:13 -0400 Subject: [PATCH 082/230] Remove overflow auto to remove scroll bars --- src/idea/static/idea/css/idea.css | 1 - 1 file changed, 1 deletion(-) diff --git a/src/idea/static/idea/css/idea.css b/src/idea/static/idea/css/idea.css index 0e4560a..4e29a20 100644 --- a/src/idea/static/idea/css/idea.css +++ b/src/idea/static/idea/css/idea.css @@ -433,7 +433,6 @@ ul.section-nav li.active a { .idea-single, .idea-banner-single { padding-top: 1.5em; - overflow: auto; } .idea-single .main-content { padding-top: 1.5em; From 660a992e14ff49405751f3fc4db50d118b4d2b86 Mon Sep 17 00:00:00 2001 From: Diego Lapiduz Date: Mon, 21 Apr 2014 11:24:24 -0400 Subject: [PATCH 083/230] Add selectize --- TERMS.md | 3 + .../static/idea/css/selectize.default.css | 381 ++++++++++++++++++ src/idea/static/idea/js/selectize.min.js | 3 + 3 files changed, 387 insertions(+) create mode 100644 src/idea/static/idea/css/selectize.default.css create mode 100644 src/idea/static/idea/js/selectize.min.js diff --git a/TERMS.md b/TERMS.md index 09f4361..3456f28 100644 --- a/TERMS.md +++ b/TERMS.md @@ -46,3 +46,6 @@ https://github.com/thedayhascome/Fluid-Baseline-Grid Designed & Built by Josh Hopkins and 40 Horse, http://40horse.com Licensed under Unlicense, http://unlicense.org/ +### Selectize.js +Selectize.js by Brian Reavis & Contributors - https://github.com/brianreavis/selectize.js +Licensed under Apache version 2.0: http://www.apache.org/licenses/LICENSE-2.0 diff --git a/src/idea/static/idea/css/selectize.default.css b/src/idea/static/idea/css/selectize.default.css new file mode 100644 index 0000000..aad860c --- /dev/null +++ b/src/idea/static/idea/css/selectize.default.css @@ -0,0 +1,381 @@ +/** + * selectize.default.css (v0.9.0) - Default Theme + * Copyright (c) 2013 Brian Reavis & contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at: + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + * + * @author Brian Reavis + */ +.selectize-control.plugin-drag_drop.multi > .selectize-input > div.ui-sortable-placeholder { + visibility: visible !important; + background: #f2f2f2 !important; + background: rgba(0, 0, 0, 0.06) !important; + border: 0 none !important; + -webkit-box-shadow: inset 0 0 12px 4px #ffffff; + box-shadow: inset 0 0 12px 4px #ffffff; +} +.selectize-control.plugin-drag_drop .ui-sortable-placeholder::after { + content: '!'; + visibility: hidden; +} +.selectize-control.plugin-drag_drop .ui-sortable-helper { + -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); +} +.selectize-dropdown-header { + position: relative; + padding: 5px 8px; + border-bottom: 1px solid #d0d0d0; + background: #f8f8f8; + -webkit-border-radius: 3px 3px 0 0; + -moz-border-radius: 3px 3px 0 0; + border-radius: 3px 3px 0 0; +} +.selectize-dropdown-header-close { + position: absolute; + right: 8px; + top: 50%; + color: #303030; + opacity: 0.4; + margin-top: -12px; + line-height: 20px; + font-size: 20px !important; +} +.selectize-dropdown-header-close:hover { + color: #000000; +} +.selectize-dropdown.plugin-optgroup_columns .optgroup { + border-right: 1px solid #f2f2f2; + border-top: 0 none; + float: left; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.selectize-dropdown.plugin-optgroup_columns .optgroup:last-child { + border-right: 0 none; +} +.selectize-dropdown.plugin-optgroup_columns .optgroup:before { + display: none; +} +.selectize-dropdown.plugin-optgroup_columns .optgroup-header { + border-top: 0 none; +} +.selectize-control.plugin-remove_button [data-value] { + position: relative; + padding-right: 24px !important; +} +.selectize-control.plugin-remove_button [data-value] .remove { + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 17px; + text-align: center; + font-weight: bold; + font-size: 12px; + color: inherit; + text-decoration: none; + vertical-align: middle; + display: inline-block; + padding: 2px 0 0 0; + border-left: 1px solid #0073bb; + -webkit-border-radius: 0 2px 2px 0; + -moz-border-radius: 0 2px 2px 0; + border-radius: 0 2px 2px 0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.selectize-control.plugin-remove_button [data-value] .remove:hover { + background: rgba(0, 0, 0, 0.05); +} +.selectize-control.plugin-remove_button [data-value].active .remove { + border-left-color: #00578d; +} +.selectize-control.plugin-remove_button .disabled [data-value] .remove:hover { + background: none; +} +.selectize-control.plugin-remove_button .disabled [data-value] .remove { + border-left-color: #aaaaaa; +} +.selectize-control { + position: relative; +} +.selectize-dropdown, +.selectize-input, +.selectize-input input { + color: #303030; + font-family: inherit; + font-size: 13px; + line-height: 18px; + -webkit-font-smoothing: inherit; +} +.selectize-input, +.selectize-control.single .selectize-input.input-active { + background: #ffffff; + cursor: text; + display: inline-block; +} +.selectize-input { + border: 1px solid #d0d0d0; + padding: 8px 8px; + display: inline-block; + width: 100%; + overflow: hidden; + position: relative; + z-index: 1; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1); + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +.selectize-control.multi .selectize-input.has-items { + padding: 5px 8px 2px; +} +.selectize-input.full { + background-color: #ffffff; +} +.selectize-input.disabled, +.selectize-input.disabled * { + cursor: default !important; +} +.selectize-input.focus { + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15); +} +.selectize-input.dropdown-active { + -webkit-border-radius: 3px 3px 0 0; + -moz-border-radius: 3px 3px 0 0; + border-radius: 3px 3px 0 0; +} +.selectize-input > * { + vertical-align: baseline; + display: -moz-inline-stack; + display: inline-block; + zoom: 1; + *display: inline; +} +.selectize-control.multi .selectize-input > div { + cursor: pointer; + margin: 0 3px 3px 0; + padding: 2px 6px; + background: #1da7ee; + color: #ffffff; + border: 1px solid #0073bb; +} +.selectize-control.multi .selectize-input > div.active { + background: #92c836; + color: #ffffff; + border: 1px solid #00578d; +} +.selectize-control.multi .selectize-input.disabled > div, +.selectize-control.multi .selectize-input.disabled > div.active { + color: #ffffff; + background: #d2d2d2; + border: 1px solid #aaaaaa; +} +.selectize-input > input { + padding: 0 !important; + min-height: 0 !important; + max-height: none !important; + max-width: 100% !important; + margin: 0 1px !important; + text-indent: 0 !important; + border: 0 none !important; + background: none !important; + line-height: inherit !important; + -webkit-user-select: auto !important; + -webkit-box-shadow: none !important; + box-shadow: none !important; +} +.selectize-input > input:focus { + outline: none !important; +} +.selectize-input::after { + content: ' '; + display: block; + clear: left; +} +.selectize-input.dropdown-active::before { + content: ' '; + display: block; + position: absolute; + background: #f0f0f0; + height: 1px; + bottom: 0; + left: 0; + right: 0; +} +.selectize-dropdown { + position: absolute; + z-index: 10; + border: 1px solid #d0d0d0; + background: #ffffff; + margin: -1px 0 0 0; + border-top: 0 none; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + -webkit-border-radius: 0 0 3px 3px; + -moz-border-radius: 0 0 3px 3px; + border-radius: 0 0 3px 3px; +} +.selectize-dropdown [data-selectable] { + cursor: pointer; + overflow: hidden; +} +.selectize-dropdown [data-selectable] .highlight { + background: rgba(125, 168, 208, 0.2); + -webkit-border-radius: 1px; + -moz-border-radius: 1px; + border-radius: 1px; +} +.selectize-dropdown [data-selectable], +.selectize-dropdown .optgroup-header { + padding: 5px 8px; +} +.selectize-dropdown .optgroup:first-child .optgroup-header { + border-top: 0 none; +} +.selectize-dropdown .optgroup-header { + color: #303030; + background: #ffffff; + cursor: default; +} +.selectize-dropdown .active { + background-color: #f5fafd; + color: #495c68; +} +.selectize-dropdown .active.create { + color: #495c68; +} +.selectize-dropdown .create { + color: rgba(48, 48, 48, 0.5); +} +.selectize-dropdown-content { + overflow-y: auto; + overflow-x: hidden; + max-height: 200px; +} +.selectize-control.single .selectize-input, +.selectize-control.single .selectize-input input { + cursor: pointer; +} +.selectize-control.single .selectize-input.input-active, +.selectize-control.single .selectize-input.input-active input { + cursor: text; +} +.selectize-control.single .selectize-input:after { + content: ' '; + display: block; + position: absolute; + top: 50%; + right: 15px; + margin-top: -3px; + width: 0; + height: 0; + border-style: solid; + border-width: 5px 5px 0 5px; + border-color: #808080 transparent transparent transparent; +} +.selectize-control.single .selectize-input.dropdown-active:after { + margin-top: -4px; + border-width: 0 5px 5px 5px; + border-color: transparent transparent #808080 transparent; +} +.selectize-control.rtl.single .selectize-input:after { + left: 15px; + right: auto; +} +.selectize-control.rtl .selectize-input > input { + margin: 0 4px 0 -2px !important; +} +.selectize-control .selectize-input.disabled { + opacity: 0.5; + background-color: #fafafa; +} +.selectize-control.multi .selectize-input.has-items { + padding-left: 5px; + padding-right: 5px; +} +.selectize-control.multi .selectize-input.disabled [data-value] { + color: #999; + text-shadow: none; + background: none; + -webkit-box-shadow: none; + box-shadow: none; +} +.selectize-control.multi .selectize-input.disabled [data-value], +.selectize-control.multi .selectize-input.disabled [data-value] .remove { + border-color: #e6e6e6; +} +.selectize-control.multi .selectize-input.disabled [data-value] .remove { + background: none; +} +.selectize-control.multi .selectize-input [data-value] { + text-shadow: 0 1px 0 rgba(0, 51, 83, 0.3); + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + background-color: #1b9dec; + background-image: -moz-linear-gradient(top, #1da7ee, #178ee9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#1da7ee), to(#178ee9)); + background-image: -webkit-linear-gradient(top, #1da7ee, #178ee9); + background-image: -o-linear-gradient(top, #1da7ee, #178ee9); + background-image: linear-gradient(to bottom, #1da7ee, #178ee9); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff1da7ee', endColorstr='#ff178ee9', GradientType=0); + -webkit-box-shadow: 0 1px 0 rgba(0,0,0,0.2),inset 0 1px rgba(255,255,255,0.03); + box-shadow: 0 1px 0 rgba(0,0,0,0.2),inset 0 1px rgba(255,255,255,0.03); +} +.selectize-control.multi .selectize-input [data-value].active { + background-color: #0085d4; + background-image: -moz-linear-gradient(top, #008fd8, #0075cf); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#008fd8), to(#0075cf)); + background-image: -webkit-linear-gradient(top, #008fd8, #0075cf); + background-image: -o-linear-gradient(top, #008fd8, #0075cf); + background-image: linear-gradient(to bottom, #008fd8, #0075cf); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff008fd8', endColorstr='#ff0075cf', GradientType=0); +} +.selectize-control.single .selectize-input { + -webkit-box-shadow: 0 1px 0 rgba(0,0,0,0.05), inset 0 1px 0 rgba(255,255,255,0.8); + box-shadow: 0 1px 0 rgba(0,0,0,0.05), inset 0 1px 0 rgba(255,255,255,0.8); + background-color: #f9f9f9; + background-image: -moz-linear-gradient(top, #fefefe, #f2f2f2); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fefefe), to(#f2f2f2)); + background-image: -webkit-linear-gradient(top, #fefefe, #f2f2f2); + background-image: -o-linear-gradient(top, #fefefe, #f2f2f2); + background-image: linear-gradient(to bottom, #fefefe, #f2f2f2); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffefefe', endColorstr='#fff2f2f2', GradientType=0); +} +.selectize-control.single .selectize-input, +.selectize-dropdown.single { + border-color: #b8b8b8; +} +.selectize-dropdown .optgroup-header { + padding-top: 7px; + font-weight: bold; + font-size: 0.85em; +} +.selectize-dropdown .optgroup { + border-top: 1px solid #f0f0f0; +} +.selectize-dropdown .optgroup:first-child { + border-top: 0 none; +} diff --git a/src/idea/static/idea/js/selectize.min.js b/src/idea/static/idea/js/selectize.min.js new file mode 100644 index 0000000..d647c3b --- /dev/null +++ b/src/idea/static/idea/js/selectize.min.js @@ -0,0 +1,3 @@ +/*! selectize.js - v0.9.0 | https://github.com/brianreavis/selectize.js | Apache License (v2) */ +!function(a,b){"function"==typeof define&&define.amd?define("sifter",b):"object"==typeof exports?module.exports=b():a.Sifter=b()}(this,function(){var a=function(a,b){this.items=a,this.settings=b||{diacritics:!0}};a.prototype.tokenize=function(a){if(a=d(String(a||"").toLowerCase()),!a||!a.length)return[];var b,c,f,h,i=[],j=a.split(/ +/);for(b=0,c=j.length;c>b;b++){if(f=e(j[b]),this.settings.diacritics)for(h in g)g.hasOwnProperty(h)&&(f=f.replace(new RegExp(h,"g"),g[h]));i.push({string:j[b],regex:new RegExp(f,"i")})}return i},a.prototype.iterator=function(a,b){var c;c=f(a)?Array.prototype.forEach||function(a){for(var b=0,c=this.length;c>b;b++)a(this[b],b,this)}:function(a){for(var b in this)this.hasOwnProperty(b)&&a(this[b],b,this)},c.apply(a,[b])},a.prototype.getScoreFunction=function(a,b){var c,d,e,f;c=this,a=c.prepareSearch(a,b),e=a.tokens,d=a.options.fields,f=e.length;var g=function(a,b){var c,d;return a?(a=String(a||""),d=a.search(b.regex),-1===d?0:(c=b.string.length/a.length,0===d&&(c+=.5),c)):0},h=function(){var a=d.length;return a?1===a?function(a,b){return g(b[d[0]],a)}:function(b,c){for(var e=0,f=0;a>e;e++)f+=g(c[d[e]],b);return f/a}:function(){return 0}}();return f?1===f?function(a){return h(e[0],a)}:"and"===a.options.conjunction?function(a){for(var b,c=0,d=0;f>c;c++){if(b=h(e[c],a),0>=b)return 0;d+=b}return d/f}:function(a){for(var b=0,c=0;f>b;b++)c+=h(e[b],a);return c/f}:function(){return 0}},a.prototype.getSortFunction=function(a,c){var d,e,f,g,h,i,j,k,l,m,n;if(f=this,a=f.prepareSearch(a,c),n=!a.query&&c.sort_empty||c.sort,l=function(a,b){return"$score"===a?b.score:f.items[b.id][a]},h=[],n)for(d=0,e=n.length;e>d;d++)(a.query||"$score"!==n[d].field)&&h.push(n[d]);if(a.query){for(m=!0,d=0,e=h.length;e>d;d++)if("$score"===h[d].field){m=!1;break}m&&h.unshift({field:"$score",direction:"desc"})}else for(d=0,e=h.length;e>d;d++)if("$score"===h[d].field){h.splice(d,1);break}for(k=[],d=0,e=h.length;e>d;d++)k.push("desc"===h[d].direction?-1:1);return i=h.length,i?1===i?(g=h[0].field,j=k[0],function(a,c){return j*b(l(g,a),l(g,c))}):function(a,c){var d,e,f;for(d=0;i>d;d++)if(f=h[d].field,e=k[d]*b(l(f,a),l(f,c)))return e;return 0}:null},a.prototype.prepareSearch=function(a,b){if("object"==typeof a)return a;b=c({},b);var d=b.fields,e=b.sort,g=b.sort_empty;return d&&!f(d)&&(b.fields=[d]),e&&!f(e)&&(b.sort=[e]),g&&!f(g)&&(b.sort_empty=[g]),{options:b,query:String(a||"").toLowerCase(),tokens:this.tokenize(a),total:0,items:[]}},a.prototype.search=function(a,b){var c,d,e,f,g=this;return d=this.prepareSearch(a,b),b=d.options,a=d.query,f=b.score||g.getScoreFunction(d),a.length?g.iterator(g.items,function(a,e){c=f(a),(b.filter===!1||c>0)&&d.items.push({score:c,id:e})}):g.iterator(g.items,function(a,b){d.items.push({score:1,id:b})}),e=g.getSortFunction(d,b),e&&d.items.sort(e),d.total=d.items.length,"number"==typeof b.limit&&(d.items=d.items.slice(0,b.limit)),d};var b=function(a,b){return"number"==typeof a&&"number"==typeof b?a>b?1:b>a?-1:0:(a=String(a||"").toLowerCase(),b=String(b||"").toLowerCase(),a>b?1:b>a?-1:0)},c=function(a){var b,c,d,e;for(b=1,c=arguments.length;c>b;b++)if(e=arguments[b])for(d in e)e.hasOwnProperty(d)&&(a[d]=e[d]);return a},d=function(a){return(a+"").replace(/^\s+|\s+$|/g,"")},e=function(a){return(a+"").replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")},f=Array.isArray||$&&$.isArray||function(a){return"[object Array]"===Object.prototype.toString.call(a)},g={a:"[aÀÁÂÃÄÅàáâãäå]",c:"[cÇçćĆčČ]",d:"[dđĐďĎ]",e:"[eÈÉÊËèéêëěĚ]",i:"[iÌÍÎÏìíîï]",n:"[nÑñňŇ]",o:"[oÒÓÔÕÕÖØòóôõöø]",r:"[rřŘ]",s:"[sŠš]",t:"[tťŤ]",u:"[uÙÚÛÜùúûüůŮ]",y:"[yŸÿýÝ]",z:"[zŽž]"};return a}),function(a,b){"function"==typeof define&&define.amd?define("microplugin",b):"object"==typeof exports?module.exports=b():a.MicroPlugin=b()}(this,function(){var a={};a.mixin=function(a){a.plugins={},a.prototype.initializePlugins=function(a){var c,d,e,f=this,g=[];if(f.plugins={names:[],settings:{},requested:{},loaded:{}},b.isArray(a))for(c=0,d=a.length;d>c;c++)"string"==typeof a[c]?g.push(a[c]):(f.plugins.settings[a[c].name]=a[c].options,g.push(a[c].name));else if(a)for(e in a)a.hasOwnProperty(e)&&(f.plugins.settings[e]=a[e],g.push(e));for(;g.length;)f.require(g.shift())},a.prototype.loadPlugin=function(b){var c=this,d=c.plugins,e=a.plugins[b];if(!a.plugins.hasOwnProperty(b))throw new Error('Unable to find "'+b+'" plugin');d.requested[b]=!0,d.loaded[b]=e.fn.apply(c,[c.plugins.settings[b]||{}]),d.names.push(b)},a.prototype.require=function(a){var b=this,c=b.plugins;if(!b.plugins.loaded.hasOwnProperty(a)){if(c.requested[a])throw new Error('Plugin has circular dependency ("'+a+'")');b.loadPlugin(a)}return c.loaded[a]},a.define=function(b,c){a.plugins[b]={name:b,fn:c}}};var b={isArray:Array.isArray||function(a){return"[object Array]"===Object.prototype.toString.call(a)}};return a}),function(a,b){"function"==typeof define&&define.amd?define("selectize",["jquery","sifter","microplugin"],b):"object"==typeof exports?module.exports=b(require("jquery"),require("sifter"),require("microplugin")):a.Selectize=b(a.jQuery,a.Sifter,a.MicroPlugin)}(this,function(a,b,c){"use strict";var d=function(a,b){if("string"!=typeof b||b.length){var c="string"==typeof b?new RegExp(b,"i"):b,d=function(a){var b=0;if(3===a.nodeType){var e=a.data.search(c);if(e>=0&&a.data.length>0){var f=a.data.match(c),g=document.createElement("span");g.className="highlight";var h=a.splitText(e),i=(h.splitText(f[0].length),h.cloneNode(!0));g.appendChild(i),h.parentNode.replaceChild(g,h),b=1}}else if(1===a.nodeType&&a.childNodes&&!/(script|style)/i.test(a.tagName))for(var j=0;j/g,">").replace(/"/g,""")},A=function(a){return(a+"").replace(/\$/g,"$$$$")},B={};B.before=function(a,b,c){var d=a[b];a[b]=function(){return c.apply(a,arguments),d.apply(a,arguments)}},B.after=function(a,b,c){var d=a[b];a[b]=function(){var b=d.apply(a,arguments);return c.apply(a,arguments),b}};var C=function(b,c){if(!a.isArray(c))return c;var d,e,f={};for(d=0,e=c.length;e>d;d++)c[d].hasOwnProperty(b)&&(f[c[d][b]]=c[d]);return f},D=function(a){var b=!1;return function(){b||(b=!0,a.apply(this,arguments))}},E=function(a,b){var c;return function(){var d=this,e=arguments;window.clearTimeout(c),c=window.setTimeout(function(){a.apply(d,e)},b)}},F=function(a,b,c){var d,e=a.trigger,f={};a.trigger=function(){var c=arguments[0];return-1===b.indexOf(c)?e.apply(a,arguments):void(f[c]=arguments)},c.apply(a,[]),a.trigger=e;for(d in f)f.hasOwnProperty(d)&&e.apply(a,f[d])},G=function(a,b,c,d){a.on(b,c,function(b){for(var c=b.target;c&&c.parentNode!==a[0];)c=c.parentNode;return b.currentTarget=c,d.apply(this,[b])})},H=function(a){var b={};if("selectionStart"in a)b.start=a.selectionStart,b.length=a.selectionEnd-b.start;else if(document.selection){a.focus();var c=document.selection.createRange(),d=document.selection.createRange().text.length;c.moveStart("character",-a.value.length),b.start=c.text.length-d,b.length=d}return b},I=function(a,b,c){var d,e,f={};if(c)for(d=0,e=c.length;e>d;d++)f[c[d]]=a.css(c[d]);else f=a.css();b.css(f)},J=function(b,c){if(!b)return 0;var d=a("").css({position:"absolute",top:-99999,left:-99999,width:"auto",padding:0,whiteSpace:"pre"}).text(b).appendTo("body");I(c,d,["letterSpacing","fontSize","fontFamily","fontWeight","textTransform"]);var e=d.width();return d.remove(),e},K=function(a){var b=null,c=function(c){var d,e,f,g,h,i,j,k;c=c||window.event||{},c.metaKey||c.altKey||a.data("grow")!==!1&&(d=a.val(),c.type&&"keydown"===c.type.toLowerCase()&&(e=c.keyCode,f=e>=97&&122>=e||e>=65&&90>=e||e>=48&&57>=e||32===e,e===q||e===p?(k=H(a[0]),k.length?d=d.substring(0,k.start)+d.substring(k.start+k.length):e===p&&k.start?d=d.substring(0,k.start-1)+d.substring(k.start+1):e===q&&"undefined"!=typeof k.start&&(d=d.substring(0,k.start)+d.substring(k.start+1))):f&&(i=c.shiftKey,j=String.fromCharCode(c.keyCode),j=i?j.toUpperCase():j.toLowerCase(),d+=j)),g=a.attr("placeholder")||"",!d.length&&g.length&&(d=g),h=J(d,a)+4,h!==b&&(b=h,a.width(h),a.triggerHandler("resize")))};a.on("keydown keyup update blur",c),c()},L=function(c,d){var e,f,g=this;f=c[0],f.selectize=g,e=window.getComputedStyle?window.getComputedStyle(f,null).getPropertyValue("direction"):f.currentStyle&&f.currentStyle.direction,e=e||c.parents("[dir]:first").attr("dir")||"",a.extend(g,{settings:d,$input:c,tagType:"select"===f.tagName.toLowerCase()?v:w,rtl:/rtl/i.test(e),eventNS:".selectize"+ ++L.count,highlightedValue:null,isOpen:!1,isDisabled:!1,isRequired:c.is("[required]"),isInvalid:!1,isLocked:!1,isFocused:!1,isInputHidden:!1,isSetup:!1,isShiftDown:!1,isCmdDown:!1,isCtrlDown:!1,ignoreFocus:!1,ignoreHover:!1,hasOptions:!1,currentResults:null,lastValue:"",caretPos:0,loading:0,loadedSearches:{},$activeOption:null,$activeItems:[],optgroups:{},options:{},userOptions:{},items:[],renderCache:{},onSearchChange:E(g.onSearchChange,d.loadThrottle)}),g.sifter=new b(this.options,{diacritics:d.diacritics}),a.extend(g.options,C(d.valueField,d.options)),delete g.settings.options,a.extend(g.optgroups,C(d.optgroupValueField,d.optgroups)),delete g.settings.optgroups,g.settings.mode=g.settings.mode||(1===g.settings.maxItems?"single":"multi"),"boolean"!=typeof g.settings.hideSelected&&(g.settings.hideSelected="multi"===g.settings.mode),g.initializePlugins(g.settings.plugins),g.setupCallbacks(),g.setupTemplates(),g.setup()};return e.mixin(L),c.mixin(L),a.extend(L.prototype,{setup:function(){var b,c,d,e,g,h,i,j,k,l,m=this,n=m.settings,o=m.eventNS,p=a(window),q=a(document);i=m.settings.mode,j=m.$input.attr("tabindex")||"",k=m.$input.attr("class")||"",b=a("
      ").addClass(n.wrapperClass).addClass(k).addClass(i),c=a("
      ").addClass(n.inputClass).addClass("items").appendTo(b),d=a('').appendTo(c).attr("tabindex",j),h=a(n.dropdownParent||b),e=a("
      ").addClass(n.dropdownClass).addClass(k).addClass(i).hide().appendTo(h),g=a("
      ").addClass(n.dropdownContentClass).appendTo(e),b.css({width:m.$input[0].style.width}),m.plugins.names.length&&(l="plugin-"+m.plugins.names.join(" plugin-"),b.addClass(l),e.addClass(l)),(null===n.maxItems||n.maxItems>1)&&m.tagType===v&&m.$input.attr("multiple","multiple"),m.settings.placeholder&&d.attr("placeholder",n.placeholder),m.$wrapper=b,m.$control=c,m.$control_input=d,m.$dropdown=e,m.$dropdown_content=g,e.on("mouseenter","[data-selectable]",function(){return m.onOptionHover.apply(m,arguments)}),e.on("mousedown","[data-selectable]",function(){return m.onOptionSelect.apply(m,arguments)}),G(c,"mousedown","*:not(input)",function(){return m.onItemSelect.apply(m,arguments)}),K(d),c.on({mousedown:function(){return m.onMouseDown.apply(m,arguments)},click:function(){return m.onClick.apply(m,arguments)}}),d.on({mousedown:function(a){a.stopPropagation()},keydown:function(){return m.onKeyDown.apply(m,arguments)},keyup:function(){return m.onKeyUp.apply(m,arguments)},keypress:function(){return m.onKeyPress.apply(m,arguments)},resize:function(){m.positionDropdown.apply(m,[])},blur:function(){return m.onBlur.apply(m,arguments)},focus:function(){return m.onFocus.apply(m,arguments)}}),q.on("keydown"+o,function(a){m.isCmdDown=a[f?"metaKey":"ctrlKey"],m.isCtrlDown=a[f?"altKey":"ctrlKey"],m.isShiftDown=a.shiftKey}),q.on("keyup"+o,function(a){a.keyCode===t&&(m.isCtrlDown=!1),a.keyCode===r&&(m.isShiftDown=!1),a.keyCode===s&&(m.isCmdDown=!1)}),q.on("mousedown"+o,function(a){if(m.isFocused){if(a.target===m.$dropdown[0]||a.target.parentNode===m.$dropdown[0])return!1;m.$control.has(a.target).length||a.target===m.$control[0]||m.blur()}}),p.on(["scroll"+o,"resize"+o].join(" "),function(){m.isOpen&&m.positionDropdown.apply(m,arguments)}),p.on("mousemove"+o,function(){m.ignoreHover=!1}),this.revertSettings={$children:m.$input.children().detach(),tabindex:m.$input.attr("tabindex")},m.$input.attr("tabindex",-1).hide().after(m.$wrapper),a.isArray(n.items)&&(m.setValue(n.items),delete n.items),m.$input[0].validity&&m.$input.on("invalid"+o,function(a){a.preventDefault(),m.isInvalid=!0,m.refreshState()}),m.updateOriginalInput(),m.refreshItems(),m.refreshState(),m.updatePlaceholder(),m.isSetup=!0,m.$input.is(":disabled")&&m.disable(),m.on("change",this.onChange),m.trigger("initialize"),n.preload===!0&&m.onSearchChange("")},setupTemplates:function(){var b=this,c=b.settings.labelField,d=b.settings.optgroupLabelField,e={optgroup:function(a){return'
      '+a.html+"
      "},optgroup_header:function(a,b){return'
      '+b(a[d])+"
      "},option:function(a,b){return'
      '+b(a[c])+"
      "},item:function(a,b){return'
      '+b(a[c])+"
      "},option_create:function(a,b){return'
      Add '+b(a.input)+"
      "}};b.settings.render=a.extend({},e,b.settings.render)},setupCallbacks:function(){var a,b,c={initialize:"onInitialize",change:"onChange",item_add:"onItemAdd",item_remove:"onItemRemove",clear:"onClear",option_add:"onOptionAdd",option_remove:"onOptionRemove",option_clear:"onOptionClear",dropdown_open:"onDropdownOpen",dropdown_close:"onDropdownClose",type:"onType"};for(a in c)c.hasOwnProperty(a)&&(b=this.settings[c[a]],b&&this.on(a,b))},onClick:function(a){var b=this;b.isFocused||(b.focus(),a.preventDefault())},onMouseDown:function(b){{var c=this,d=b.isDefaultPrevented();a(b.target)}if(c.isFocused){if(b.target!==c.$control_input[0])return"single"===c.settings.mode?c.isOpen?c.close():c.open():d||c.setActiveItem(null),!1}else d||window.setTimeout(function(){c.focus()},0)},onChange:function(){this.$input.trigger("change")},onKeyPress:function(a){if(this.isLocked)return a&&a.preventDefault();var b=String.fromCharCode(a.keyCode||a.which);return this.settings.create&&b===this.settings.delimiter?(this.createItem(),a.preventDefault(),!1):void 0},onKeyDown:function(a){var b=(a.target===this.$control_input[0],this);if(b.isLocked)return void(a.keyCode!==u&&a.preventDefault());switch(a.keyCode){case g:if(b.isCmdDown)return void b.selectAll();break;case i:return void b.close();case o:if(!a.ctrlKey)break;case n:if(!b.isOpen&&b.hasOptions)b.open();else if(b.$activeOption){b.ignoreHover=!0;var c=b.getAdjacentOption(b.$activeOption,1);c.length&&b.setActiveOption(c,!0,!0)}return void a.preventDefault();case l:if(!a.ctrlKey)break;case k:if(b.$activeOption){b.ignoreHover=!0;var d=b.getAdjacentOption(b.$activeOption,-1);d.length&&b.setActiveOption(d,!0,!0)}return void a.preventDefault();case h:return b.isOpen&&b.$activeOption&&b.onOptionSelect({currentTarget:b.$activeOption}),void a.preventDefault();case j:return void b.advanceSelection(-1,a);case m:return void b.advanceSelection(1,a);case u:return b.isOpen&&b.$activeOption&&b.onOptionSelect({currentTarget:b.$activeOption}),void(b.settings.create&&b.createItem()&&a.preventDefault());case p:case q:return void b.deleteSelection(a)}return b.isFull()||b.isInputHidden?void a.preventDefault():void 0},onKeyUp:function(a){var b=this;if(b.isLocked)return a&&a.preventDefault();var c=b.$control_input.val()||"";b.lastValue!==c&&(b.lastValue=c,b.onSearchChange(c),b.refreshOptions(),b.trigger("type",c))},onSearchChange:function(a){var b=this,c=b.settings.load;c&&(b.loadedSearches.hasOwnProperty(a)||(b.loadedSearches[a]=!0,b.load(function(d){c.apply(b,[a,d])})))},onFocus:function(a){var b=this;return b.isFocused=!0,b.isDisabled?(b.blur(),a&&a.preventDefault(),!1):void(b.ignoreFocus||("focus"===b.settings.preload&&b.onSearchChange(""),b.$activeItems.length||(b.showInput(),b.setActiveItem(null),b.refreshOptions(!!b.settings.openOnFocus)),b.refreshState()))},onBlur:function(){var a=this;a.isFocused=!1,a.ignoreFocus||(a.settings.create&&a.settings.createOnBlur&&a.createItem(!1),a.close(),a.setTextboxValue(""),a.setActiveItem(null),a.setActiveOption(null),a.setCaret(a.items.length),a.refreshState())},onOptionHover:function(a){this.ignoreHover||this.setActiveOption(a.currentTarget,!1)},onOptionSelect:function(b){var c,d,e=this;b.preventDefault&&(b.preventDefault(),b.stopPropagation()),d=a(b.currentTarget),d.hasClass("create")?e.createItem():(c=d.attr("data-value"),c&&(e.lastQuery=null,e.setTextboxValue(""),e.addItem(c),!e.settings.hideSelected&&b.type&&/mouse/.test(b.type)&&e.setActiveOption(e.getOption(c))))},onItemSelect:function(a){var b=this;b.isLocked||"multi"===b.settings.mode&&(a.preventDefault(),b.setActiveItem(a.currentTarget,a))},load:function(a){var b=this,c=b.$wrapper.addClass("loading");b.loading++,a.apply(b,[function(a){b.loading=Math.max(b.loading-1,0),a&&a.length&&(b.addOption(a),b.refreshOptions(b.isFocused&&!b.isInputHidden)),b.loading||c.removeClass("loading"),b.trigger("load",a)}])},setTextboxValue:function(a){this.$control_input.val(a).triggerHandler("update"),this.lastValue=a},getValue:function(){return this.tagType===v&&this.$input.attr("multiple")?this.items:this.items.join(this.settings.delimiter)},setValue:function(a){F(this,["change"],function(){this.clear(),this.addItems(a)})},setActiveItem:function(b,c){var d,e,f,g,h,i,j,k,l=this;if("single"!==l.settings.mode){if(b=a(b),!b.length)return a(l.$activeItems).removeClass("active"),l.$activeItems=[],void(l.isFocused&&l.showInput());if(d=c&&c.type.toLowerCase(),"mousedown"===d&&l.isShiftDown&&l.$activeItems.length){for(k=l.$control.children(".active:last"),g=Array.prototype.indexOf.apply(l.$control[0].childNodes,[k[0]]),h=Array.prototype.indexOf.apply(l.$control[0].childNodes,[b[0]]),g>h&&(j=g,g=h,h=j),e=g;h>=e;e++)i=l.$control[0].childNodes[e],-1===l.$activeItems.indexOf(i)&&(a(i).addClass("active"),l.$activeItems.push(i));c.preventDefault()}else"mousedown"===d&&l.isCtrlDown||"keydown"===d&&this.isShiftDown?b.hasClass("active")?(f=l.$activeItems.indexOf(b[0]),l.$activeItems.splice(f,1),b.removeClass("active")):l.$activeItems.push(b.addClass("active")[0]):(a(l.$activeItems).removeClass("active"),l.$activeItems=[b.addClass("active")[0]]);l.hideInput(),this.isFocused||l.focus()}},setActiveOption:function(b,c,d){var e,f,g,h,i,j=this;j.$activeOption&&j.$activeOption.removeClass("active"),j.$activeOption=null,b=a(b),b.length&&(j.$activeOption=b.addClass("active"),(c||!x(c))&&(e=j.$dropdown_content.height(),f=j.$activeOption.outerHeight(!0),c=j.$dropdown_content.scrollTop()||0,g=j.$activeOption.offset().top-j.$dropdown_content.offset().top+c,h=g,i=g-e+f,g+f>e+c?j.$dropdown_content.stop().animate({scrollTop:i},d?j.settings.scrollDuration:0):c>g&&j.$dropdown_content.stop().animate({scrollTop:h},d?j.settings.scrollDuration:0)))},selectAll:function(){var a=this;"single"!==a.settings.mode&&(a.$activeItems=Array.prototype.slice.apply(a.$control.children(":not(input)").addClass("active")),a.$activeItems.length&&(a.hideInput(),a.close()),a.focus())},hideInput:function(){var a=this;a.setTextboxValue(""),a.$control_input.css({opacity:0,position:"absolute",left:a.rtl?1e4:-1e4}),a.isInputHidden=!0},showInput:function(){this.$control_input.css({opacity:1,position:"relative",left:0}),this.isInputHidden=!1},focus:function(){var a=this;a.isDisabled||(a.ignoreFocus=!0,a.$control_input[0].focus(),window.setTimeout(function(){a.ignoreFocus=!1,a.onFocus()},0))},blur:function(){this.$control_input.trigger("blur")},getScoreFunction:function(a){return this.sifter.getScoreFunction(a,this.getSearchOptions())},getSearchOptions:function(){var a=this.settings,b=a.sortField;return"string"==typeof b&&(b={field:b}),{fields:a.searchField,conjunction:a.searchConjunction,sort:b}},search:function(b){var c,d,e,f=this,g=f.settings,h=this.getSearchOptions();if(g.score&&(e=f.settings.score.apply(this,[b]),"function"!=typeof e))throw new Error('Selectize "score" setting must be a function that returns a function');if(b!==f.lastQuery?(f.lastQuery=b,d=f.sifter.search(b,a.extend(h,{score:e})),f.currentResults=d):d=a.extend(!0,{},f.currentResults),g.hideSelected)for(c=d.items.length-1;c>=0;c--)-1!==f.items.indexOf(y(d.items[c].id))&&d.items.splice(c,1);return d},refreshOptions:function(b){var c,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s;"undefined"==typeof b&&(b=!0);var t=this,u=t.$control_input.val(),v=t.search(u),w=t.$dropdown_content,x=t.$activeOption&&y(t.$activeOption.attr("data-value"));if(g=v.items.length,"number"==typeof t.settings.maxOptions&&(g=Math.min(g,t.settings.maxOptions)),h={},t.settings.optgroupOrder)for(i=t.settings.optgroupOrder,c=0;cc;c++)for(j=t.options[v.items[c].id],k=t.render("option",j),l=j[t.settings.optgroupField]||"",m=a.isArray(l)?l:[l],e=0,f=m&&m.length;f>e;e++)l=m[e],t.optgroups.hasOwnProperty(l)||(l=""),h.hasOwnProperty(l)||(h[l]=[],i.push(l)),h[l].push(k);for(n=[],c=0,g=i.length;g>c;c++)l=i[c],t.optgroups.hasOwnProperty(l)&&h[l].length?(o=t.render("optgroup_header",t.optgroups[l])||"",o+=h[l].join(""),n.push(t.render("optgroup",a.extend({},t.optgroups[l],{html:o})))):n.push(h[l].join(""));if(w.html(n.join("")),t.settings.highlight&&v.query.length&&v.tokens.length)for(c=0,g=v.tokens.length;g>c;c++)d(w,v.tokens[c].regex);if(!t.settings.hideSelected)for(c=0,g=t.items.length;g>c;c++)t.getOption(t.items[c]).addClass("selected");p=t.settings.create&&v.query.length,p&&(w.prepend(t.render("option_create",{input:u})),s=a(w[0].childNodes[0])),t.hasOptions=v.items.length>0||p,t.hasOptions?(v.items.length>0?(r=x&&t.getOption(x),r&&r.length?q=r:"single"===t.settings.mode&&t.items.length&&(q=t.getOption(t.items[0])),q&&q.length||(q=s&&!t.settings.addPrecedence?t.getAdjacentOption(s,1):w.find("[data-selectable]:first"))):q=s,t.setActiveOption(q),b&&!t.isOpen&&t.open()):(t.setActiveOption(null),b&&t.isOpen&&t.close())},addOption:function(b){var c,d,e,f=this;if(a.isArray(b))for(c=0,d=b.length;d>c;c++)f.addOption(b[c]);else e=y(b[f.settings.valueField]),e&&!f.options.hasOwnProperty(e)&&(f.userOptions[e]=!0,f.options[e]=b,f.lastQuery=null,f.trigger("option_add",e,b))},addOptionGroup:function(a,b){this.optgroups[a]=b,this.trigger("optgroup_add",a,b)},updateOption:function(b,c){var d,e,f,g,h,i,j=this;if(b=y(b),f=y(c[j.settings.valueField]),j.options.hasOwnProperty(b)){if(!f)throw new Error("Value must be set in option data");f!==b&&(delete j.options[b],g=j.items.indexOf(b),-1!==g&&j.items.splice(g,1,f)),j.options[f]=c,h=j.renderCache.item,i=j.renderCache.option,x(h)&&(delete h[b],delete h[f]),x(i)&&(delete i[b],delete i[f]),-1!==j.items.indexOf(f)&&(d=j.getItem(b),e=a(j.render("item",c)),d.hasClass("active")&&e.addClass("active"),d.replaceWith(e)),j.isOpen&&j.refreshOptions(!1)}},removeOption:function(a){var b=this;a=y(a),delete b.userOptions[a],delete b.options[a],b.lastQuery=null,b.trigger("option_remove",a),b.removeItem(a)},clearOptions:function(){var a=this;a.loadedSearches={},a.userOptions={},a.options=a.sifter.items={},a.lastQuery=null,a.trigger("option_clear"),a.clear()},getOption:function(a){return this.getElementWithValue(a,this.$dropdown_content.find("[data-selectable]"))},getAdjacentOption:function(b,c){var d=this.$dropdown.find("[data-selectable]"),e=d.index(b)+c;return e>=0&&ed;d++)if(c[d].getAttribute("data-value")===b)return a(c[d]);return a()},getItem:function(a){return this.getElementWithValue(a,this.$control.children())},addItems:function(b){for(var c=a.isArray(b)?b:[b],d=0,e=c.length;e>d;d++)this.isPending=e-1>d,this.addItem(c[d])},addItem:function(b){F(this,["change"],function(){var c,d,e,f,g=this,h=g.settings.mode;return b=y(b),-1!==g.items.indexOf(b)?void("single"===h&&g.close()):void(g.options.hasOwnProperty(b)&&("single"===h&&g.clear(),"multi"===h&&g.isFull()||(c=a(g.render("item",g.options[b])),g.items.splice(g.caretPos,0,b),g.insertAtCaret(c),g.refreshState(),g.isSetup&&(e=g.$dropdown_content.find("[data-selectable]"),this.isPending||(d=g.getOption(b),f=g.getAdjacentOption(d,1).attr("data-value"),g.refreshOptions(g.isFocused&&"single"!==h),f&&g.setActiveOption(g.getOption(f))),!e.length||null!==g.settings.maxItems&&g.items.length>=g.settings.maxItems?g.close():g.positionDropdown(),g.updatePlaceholder(),g.trigger("item_add",b,c),g.updateOriginalInput()))))})},removeItem:function(a){var b,c,d,e=this;b="object"==typeof a?a:e.getItem(a),a=y(b.attr("data-value")),c=e.items.indexOf(a),-1!==c&&(b.remove(),b.hasClass("active")&&(d=e.$activeItems.indexOf(b[0]),e.$activeItems.splice(d,1)),e.items.splice(c,1),e.lastQuery=null,!e.settings.persist&&e.userOptions.hasOwnProperty(a)&&e.removeOption(a),c0),b.$control_input.data("grow",!c&&!d)},isFull:function(){return null!==this.settings.maxItems&&this.items.length>=this.settings.maxItems},updateOriginalInput:function(){var a,b,c,d=this;if("select"===d.$input[0].tagName.toLowerCase()){for(c=[],a=0,b=d.items.length;b>a;a++)c.push('');c.length||this.$input.attr("multiple")||c.push(''),d.$input.html(c.join(""))}else d.$input.val(d.getValue());d.isSetup&&d.trigger("change",d.$input.val())},updatePlaceholder:function(){if(this.settings.placeholder){var a=this.$control_input;this.items.length?a.removeAttr("placeholder"):a.attr("placeholder",this.settings.placeholder),a.triggerHandler("update")}},open:function(){var a=this;a.isLocked||a.isOpen||"multi"===a.settings.mode&&a.isFull()||(a.focus(),a.isOpen=!0,a.refreshState(),a.$dropdown.css({visibility:"hidden",display:"block"}),a.positionDropdown(),a.$dropdown.css({visibility:"visible"}),a.trigger("dropdown_open",a.$dropdown))},close:function(){var a=this,b=a.isOpen;"single"===a.settings.mode&&a.items.length&&a.hideInput(),a.isOpen=!1,a.$dropdown.hide(),a.setActiveOption(null),a.refreshState(),b&&a.trigger("dropdown_close",a.$dropdown)},positionDropdown:function(){var a=this.$control,b="body"===this.settings.dropdownParent?a.offset():a.position();b.top+=a.outerHeight(!0),this.$dropdown.css({width:a.outerWidth(),top:b.top,left:b.left})},clear:function(){var a=this;a.items.length&&(a.$control.children(":not(input)").remove(),a.items=[],a.setCaret(0),a.updatePlaceholder(),a.updateOriginalInput(),a.refreshState(),a.showInput(),a.trigger("clear"))},insertAtCaret:function(b){var c=Math.min(this.caretPos,this.items.length);0===c?this.$control.prepend(b):a(this.$control[0].childNodes[c]).before(b),this.setCaret(c+1)},deleteSelection:function(b){var c,d,e,f,g,h,i,j,k,l=this;if(e=b&&b.keyCode===p?-1:1,f=H(l.$control_input[0]),l.$activeOption&&!l.settings.hideSelected&&(i=l.getAdjacentOption(l.$activeOption,-1).attr("data-value")),g=[],l.$activeItems.length){for(k=l.$control.children(".active:"+(e>0?"last":"first")),h=l.$control.children(":not(input)").index(k),e>0&&h++,c=0,d=l.$activeItems.length;d>c;c++)g.push(a(l.$activeItems[c]).attr("data-value"));b&&(b.preventDefault(),b.stopPropagation())}else(l.isFocused||"single"===l.settings.mode)&&l.items.length&&(0>e&&0===f.start&&0===f.length?g.push(l.items[l.caretPos-1]):e>0&&f.start===l.$control_input.val().length&&g.push(l.items[l.caretPos]));if(!g.length||"function"==typeof l.settings.onDelete&&l.settings.onDelete.apply(l,[g])===!1)return!1;for("undefined"!=typeof h&&l.setCaret(h);g.length;)l.removeItem(g.pop());return l.showInput(),l.positionDropdown(),l.refreshOptions(!0),i&&(j=l.getOption(i),j.length&&l.setActiveOption(j)),!0},advanceSelection:function(a,b){var c,d,e,f,g,h,i=this;0!==a&&(i.rtl&&(a*=-1),c=a>0?"last":"first",d=H(i.$control_input[0]),i.isFocused&&!i.isInputHidden?(f=i.$control_input.val().length,g=0>a?0===d.start&&0===d.length:d.start===f,g&&!f&&i.advanceCaret(a,b)):(h=i.$control.children(".active:"+c),h.length&&(e=i.$control.children(":not(input)").index(h),i.setActiveItem(null),i.setCaret(a>0?e+1:e))))},advanceCaret:function(a,b){var c,d,e=this;0!==a&&(c=a>0?"next":"prev",e.isShiftDown?(d=e.$control_input[c](),d.length&&(e.hideInput(),e.setActiveItem(d),b&&b.preventDefault())):e.setCaret(e.caretPos+a))},setCaret:function(b){var c=this;b="single"===c.settings.mode?c.items.length:Math.max(0,Math.min(c.items.length,b));var d,e,f,g;for(f=c.$control.children(":not(input)"),d=0,e=f.length;e>d;d++)g=a(f[d]).detach(),b>d?c.$control_input.before(g):c.$control.append(g);c.caretPos=b},lock:function(){this.close(),this.isLocked=!0,this.refreshState()},unlock:function(){this.isLocked=!1,this.refreshState()},disable:function(){var a=this;a.$input.prop("disabled",!0),a.isDisabled=!0,a.lock()},enable:function(){var a=this;a.$input.prop("disabled",!1),a.isDisabled=!1,a.unlock()},destroy:function(){var b=this,c=b.eventNS,d=b.revertSettings;b.trigger("destroy"),b.off(),b.$wrapper.remove(),b.$dropdown.remove(),b.$input.html("").append(d.$children).removeAttr("tabindex").attr({tabindex:d.tabindex}).show(),a(window).off(c),a(document).off(c),a(document.body).off(c),delete b.$input[0].selectize},render:function(a,b){var c,d,e="",f=!1,g=this,h=/^[\t ]*<([a-z][a-z0-9\-_]*(?:\:[a-z][a-z0-9\-_]*)?)/i;return("option"===a||"item"===a)&&(c=y(b[g.settings.valueField]),f=!!c),f&&(x(g.renderCache[a])||(g.renderCache[a]={}),g.renderCache[a].hasOwnProperty(c))?g.renderCache[a][c]:(e=g.settings.render[a].apply(this,[b,z]),("option"===a||"option_create"===a)&&(e=e.replace(h,"<$1 data-selectable")),"optgroup"===a&&(d=b[g.settings.optgroupValueField]||"",e=e.replace(h,'<$1 data-group="'+A(z(d))+'"')),("option"===a||"item"===a)&&(e=e.replace(h,'<$1 data-value="'+A(z(c||""))+'"')),f&&(g.renderCache[a][c]=e),e)}}),L.count=0,L.defaults={plugins:[],delimiter:",",persist:!0,diacritics:!0,create:!1,createOnBlur:!1,highlight:!0,openOnFocus:!0,maxOptions:1e3,maxItems:null,hideSelected:null,addPrecedence:!1,preload:!1,scrollDuration:60,loadThrottle:300,dataAttr:"data-data",optgroupField:"optgroup",valueField:"value",labelField:"text",optgroupLabelField:"label",optgroupValueField:"value",optgroupOrder:null,sortField:"$order",searchField:["text"],searchConjunction:"and",mode:null,wrapperClass:"selectize-control",inputClass:"selectize-input",dropdownClass:"selectize-dropdown",dropdownContentClass:"selectize-dropdown-content",dropdownParent:null,render:{}},a.fn.selectize=function(b){var c=a.fn.selectize.defaults,d=a.extend({},c,b),e=d.dataAttr,f=d.labelField,g=d.valueField,h=d.optgroupField,i=d.optgroupLabelField,j=d.optgroupValueField,k=function(b,c){var e,h,i,j,k=a.trim(b.val()||""); +if(k.length){for(i=k.split(d.delimiter),e=0,h=i.length;h>e;e++)j={},j[f]=i[e],j[g]=i[e],c.options[i[e]]=j;c.items=i}},l=function(b,c){var d,k,l,m,n=0,o=c.options,p=function(a){var b=e&&a.attr(e);return"string"==typeof b&&b.length?JSON.parse(b):null},q=function(b,d){var e,i;if(b=a(b),e=b.attr("value")||"",e.length){if(o.hasOwnProperty(e))return void(d&&(o[e].optgroup?a.isArray(o[e].optgroup)?o[e].optgroup.push(d):o[e].optgroup=[o[e].optgroup,d]:o[e].optgroup=d));i=p(b)||{},i[f]=i[f]||b.text(),i[g]=i[g]||e,i[h]=i[h]||d,i.$order=++n,o[e]=i,b.is(":selected")&&c.items.push(e)}},r=function(b){var d,e,f,g,h;for(b=a(b),f=b.attr("label"),f&&(g=p(b)||{},g[i]=f,g[j]=f,c.optgroups[f]=g),h=a("option",b),d=0,e=h.length;e>d;d++)q(h[d],f)};for(c.maxItems=b.attr("multiple")?null:1,m=b.children(),d=0,k=m.length;k>d;d++)l=m[d].tagName.toLowerCase(),"optgroup"===l?r(m[d]):"option"===l&&q(m[d])};return this.each(function(){if(!this.selectize){var d,e=a(this),f=this.tagName.toLowerCase(),g={placeholder:e.children('option[value=""]').text()||e.attr("placeholder"),options:{},optgroups:{},items:[]};"select"===f?l(e,g):k(e,g),d=new L(e,a.extend(!0,{},c,g,b)),e.data("selectize",d),e.addClass("selectized")}})},a.fn.selectize.defaults=L.defaults,L.define("drag_drop",function(){if(!a.fn.sortable)throw new Error('The "drag_drop" plugin requires jQuery UI "sortable".');if("multi"===this.settings.mode){var b=this;b.lock=function(){var a=b.lock;return function(){var c=b.$control.data("sortable");return c&&c.disable(),a.apply(b,arguments)}}(),b.unlock=function(){var a=b.unlock;return function(){var c=b.$control.data("sortable");return c&&c.enable(),a.apply(b,arguments)}}(),b.setup=function(){var c=b.setup;return function(){c.apply(this,arguments);var d=b.$control.sortable({items:"[data-value]",forcePlaceholderSize:!0,disabled:b.isLocked,start:function(a,b){b.placeholder.css("width",b.helper.css("width")),d.css({overflow:"visible"})},stop:function(){d.css({overflow:"hidden"});var c=b.$activeItems?b.$activeItems.slice():null,e=[];d.children("[data-value]").each(function(){e.push(a(this).attr("data-value"))}),b.setValue(e),b.setActiveItem(c)}})}}()}}),L.define("dropdown_header",function(b){var c=this;b=a.extend({title:"Untitled",headerClass:"selectize-dropdown-header",titleRowClass:"selectize-dropdown-header-title",labelClass:"selectize-dropdown-header-label",closeClass:"selectize-dropdown-header-close",html:function(a){return'
      '+a.title+'×
      '}},b),c.setup=function(){var d=c.setup;return function(){d.apply(c,arguments),c.$dropdown_header=a(b.html(b)),c.$dropdown.prepend(c.$dropdown_header)}}()}),L.define("optgroup_columns",function(b){var c=this;b=a.extend({equalizeWidth:!0,equalizeHeight:!0},b),this.getAdjacentOption=function(b,c){var d=b.closest("[data-group]").find("[data-selectable]"),e=d.index(b)+c;return e>=0&&ed;d++)f=Math.max(f,j.eq(d).height());j.css({height:f})}b.equalizeWidth&&(i=c.$dropdown_content.innerWidth(),g=Math.round(i/e),j.css({width:g}),e>1&&(h=i-g*(e-1),j.eq(e-1).css({width:h})))}};(b.equalizeHeight||b.equalizeWidth)&&(B.after(this,"positionDropdown",d),B.after(this,"refreshOptions",d))}),L.define("remove_button",function(b){if("single"!==this.settings.mode){b=a.extend({label:"×",title:"Remove",className:"remove",append:!0},b);var c=this,d=''+b.label+"",e=function(a,b){var c=a.search(/(<\/[^>]+>\s*)$/);return a.substring(0,c)+b+a.substring(c)};this.setup=function(){var f=c.setup;return function(){if(b.append){var g=c.settings.render.item;c.settings.render.item=function(){return e(g.apply(this,arguments),d)}}f.apply(this,arguments),this.$control.on("click","."+b.className,function(b){if(b.preventDefault(),!c.isLocked){var d=a(b.currentTarget).parent();c.setActiveItem(d),c.deleteSelection()&&c.setCaret(c.items.length)}})}}()}}),L.define("restore_on_backspace",function(a){var b=this;a.text=a.text||function(a){return a[this.settings.labelField]},this.onKeyDown=function(){var c=b.onKeyDown;return function(b){var d,e;return b.keyCode===p&&""===this.$control_input.val()&&!this.$activeItems.length&&(d=this.caretPos-1,d>=0&&d Date: Mon, 21 Apr 2014 11:24:46 -0400 Subject: [PATCH 084/230] Use selectize for autocomplete tags in add page --- src/idea/templates/idea/add.html | 71 +++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/src/idea/templates/idea/add.html b/src/idea/templates/idea/add.html index f8aa76b..d1042cf 100644 --- a/src/idea/templates/idea/add.html +++ b/src/idea/templates/idea/add.html @@ -1,5 +1,43 @@ {% extends "idea/idea-base.html" %} + +{%block "css_files" %} + + +{% endblock %} + + {% block "content" %}
      @@ -34,7 +72,7 @@

      Add an idea

      -
      +
      {% endblock %} @@ -47,9 +85,38 @@

      Add an idea

      $("#activate-help").click(function() { $(".help_text span").toggle(); }); + + $('#id_tags').selectize({ + valueField: 'value', + labelField: 'label', + searchField: ['label'], + delimiter: ',', + persist: false, + create: function(input) { + return { + value: input, + text: input + } + }, + load: function(query, callback) { + if (!query.length) return callback(); + $.ajax({ + url: '/search/tags/json/model/idea/?term=' + query, + type: 'GET', + dataType: 'json', + error: function() { + callback(); + }, + success: function(res) { + callback(res); + } + }); + } + }); {% endblock %} {% block "js_scripts" %} - + + {% block "design_js_scripts" %}{% endblock %} {% endblock %} From 2ec35b7becac2aed61ac558a381830237ad5a780 Mon Sep 17 00:00:00 2001 From: Stephanie V Date: Mon, 21 Apr 2014 13:00:46 -0400 Subject: [PATCH 085/230] Update README.md minor edit to remove Solr reference. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f034708..f33cf34 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ easily integrate-able interface. Idea-Box also takes a strong stance on transpar such that ideas, votes, etc. are tied to specific users. ## Features -* Searching (via Solr's "more-like-this") +* Searching * Idea Submission * Tagging (via taggit) * Voting From a918667cdb0b5e1d7a88d291e745a369ea7715b1 Mon Sep 17 00:00:00 2001 From: Stephanie V Date: Mon, 21 Apr 2014 13:20:30 -0400 Subject: [PATCH 086/230] Update models.py remove start date from banner drop down display --- src/idea/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/idea/models.py b/src/idea/models.py index 9337651..312ef8e 100644 --- a/src/idea/models.py +++ b/src/idea/models.py @@ -39,7 +39,7 @@ class Banner(models.Model): "should be continued indefinitely. ") def __unicode__(self): - return u'%s (%s to %s)' % (self.title, self.start_date, self.end_date) + return u'%s (ends %s)' % (self.title, self.end_date) class State(models.Model): From 35163c5990f4990c11fee1d4e615d926ab6b9e70 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Mon, 21 Apr 2014 19:16:53 +0000 Subject: [PATCH 087/230] Lock taggit to older version since newer builds break when Django <= 6 --- buildout.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildout.cfg b/buildout.cfg index afaccc5..1752d17 100644 --- a/buildout.cfg +++ b/buildout.cfg @@ -21,7 +21,7 @@ eggs = ${buildout:eggs} mock south django-nose - django-taggit + django-taggit==0.11 django-haystack django-mptt exam From ee506d672344f5023c7a85fb0efa11880dc9c187 Mon Sep 17 00:00:00 2001 From: Diego Lapiduz Date: Mon, 21 Apr 2014 22:21:02 -0400 Subject: [PATCH 088/230] Add es5 shim for IE --- src/idea/static/idea/js/es5-shim.min.js | 7 +++++++ src/idea/templates/idea/add.html | 1 + 2 files changed, 8 insertions(+) create mode 100644 src/idea/static/idea/js/es5-shim.min.js diff --git a/src/idea/static/idea/js/es5-shim.min.js b/src/idea/static/idea/js/es5-shim.min.js new file mode 100644 index 0000000..5c67537 --- /dev/null +++ b/src/idea/static/idea/js/es5-shim.min.js @@ -0,0 +1,7 @@ +/*! + * https://github.com/es-shims/es5-shim + * @license es5-shim Copyright 2009-2014 by contributors, MIT License + * see https://github.com/es-shims/es5-shim/blob/master/LICENSE + */ +(function(t,r){if(typeof define==="function"&&define.amd){define(r)}else if(typeof exports==="object"){module.exports=r()}else{t.returnExports=r()}})(this,function(){var t=Function.prototype.call;var r=Array.prototype;var e=Object.prototype;var n=r.slice;var i=Array.prototype.splice;var o=Array.prototype.push;var a=Array.prototype.unshift;function u(){}if(!Function.prototype.bind){Function.prototype.bind=function G(t){var r=this;if(typeof r!=="function"){throw new TypeError("Function.prototype.bind called on incompatible "+r)}var e=n.call(arguments,1);var i=function(){if(this instanceof p){var i=r.apply(this,e.concat(n.call(arguments)));if(Object(i)===i){return i}return this}else{return r.apply(t,e.concat(n.call(arguments)))}};var o=Math.max(0,r.length-e.length);var a=[];for(var l=0;l0){if(r<=0){if(t===this.length){o.apply(this,u);return[]}if(t===0){a.apply(this,u);return[]}}e=n.call(this,t,t+r);u.push.apply(u,n.call(this,t+r,this.length));u.unshift.apply(u,n.call(this,0,t));u.unshift(0,this.length);i.apply(this,u);return e}return i.call(this,t,r)}}}if([].unshift(0)!=1){Array.prototype.unshift=function(){a.apply(this,arguments);return this.length}}if(!Array.isArray){Array.isArray=function P(t){return p(t)==="[object Array]"}}var g=Object("a");var v=g[0]!="a"||!(0 in g);var b=function B(t){var r=true;if(t){t.call("foo",function(t,e,n){if(typeof n!=="object"){r=false}})}return!!t&&r};if(!Array.prototype.forEach||!b(Array.prototype.forEach)){Array.prototype.forEach=function H(t){var r=$(this),e=v&&p(this)==="[object String]"?this.split(""):r,n=arguments[1],i=-1,o=e.length>>>0;if(p(t)!="[object Function]"){throw new TypeError}while(++i>>0,i=Array(n),o=arguments[1];if(p(t)!="[object Function]"){throw new TypeError(t+" is not a function")}for(var a=0;a>>0,i=[],o,a=arguments[1];if(p(t)!="[object Function]"){throw new TypeError(t+" is not a function")}for(var u=0;u>>0,i=arguments[1];if(p(t)!="[object Function]"){throw new TypeError(t+" is not a function")}for(var o=0;o>>0,i=arguments[1];if(p(t)!="[object Function]"){throw new TypeError(t+" is not a function")}for(var o=0;o>>0;if(p(t)!="[object Function]"){throw new TypeError(t+" is not a function")}if(!n&&arguments.length===1){throw new TypeError("reduce of empty array with no initial value")}var i=0;var o;if(arguments.length>=2){o=arguments[1]}else{do{if(i in e){o=e[i++];break}if(++i>=n){throw new TypeError("reduce of empty array with no initial value")}}while(true)}for(;i>>0;if(p(t)!="[object Function]"){throw new TypeError(t+" is not a function")}if(!n&&arguments.length===1){throw new TypeError("reduceRight of empty array with no initial value")}var i,o=n-1;if(arguments.length>=2){i=arguments[1]}else{do{if(o in e){i=e[o--];break}if(--o<0){throw new TypeError("reduceRight of empty array with no initial value")}}while(true)}if(o<0){return i}do{if(o in this){i=t.call(void 0,i,e[o],o,r)}}while(o--);return i}}if(!Array.prototype.indexOf||[0,1].indexOf(1,2)!=-1){Array.prototype.indexOf=function V(t){var r=v&&p(this)==="[object String]"?this.split(""):$(this),e=r.length>>>0;if(!e){return-1}var n=0;if(arguments.length>1){n=Z(arguments[1])}n=n>=0?n:Math.max(0,e+n);for(;n>>0;if(!e){return-1}var n=e-1;if(arguments.length>1){n=Math.min(n,Z(arguments[1]))}n=n>=0?n:e-Math.abs(n);for(;n>=0;n--){if(n in r&&t===r[n]){return n}}return-1}}if(!Object.keys){var w=!{toString:null}.propertyIsEnumerable("toString"),m=function(){}.propertyIsEnumerable("prototype"),S=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],A=S.length,j=function tr(t){return p(t)==="[object Function]"},x=function rr(t){var r=p(t);var e=r==="[object Arguments]";if(!e){e=!Array.isArray(r)&&t!==null&&typeof t==="object"&&typeof t.length==="number"&&t.length>=0&&j(t.callee)}return e};Object.keys=function er(t){var r=j(t),e=x(t),n=t!==null&&typeof t==="object",i=n&&p(t)==="[object String]";if(!n&&!r&&!e){throw new TypeError("Object.keys called on a non-object")}var o=[];var a=m&&r;if(i||e){for(var u=0;u9999?"+":"")+("00000"+Math.abs(n)).slice(0<=n&&n<=9999?-4:-6);r=t.length;while(r--){e=t[r];if(e<10){t[r]="0"+e}}return n+"-"+t.slice(0,2).join("-")+"T"+t.slice(2).join(":")+"."+("000"+this.getUTCMilliseconds()).slice(-3)+"Z"}}var T=false;try{T=Date.prototype.toJSON&&new Date(NaN).toJSON()===null&&new Date(O).toJSON().indexOf(N)!==-1&&Date.prototype.toJSON.call({toISOString:function(){return true}})}catch(E){}if(!T){Date.prototype.toJSON=function ir(t){var r=Object(this),e=X(r),n;if(typeof e==="number"&&!isFinite(e)){return null}n=r.toISOString;if(typeof n!="function"){throw new TypeError("toISOString property is not callable")}return n.call(r)}}var F=Date.parse("+033658-09-27T01:46:40.000Z")===1e15;var D=!isNaN(Date.parse("2012-04-04T24:00:00.500Z"))||!isNaN(Date.parse("2012-11-31T23:59:59.000Z"));var _=isNaN(Date.parse("2000-01-01T00:00:00.000Z"));if(!Date.parse||_||D||!F){Date=function(t){function r(e,n,i,o,a,u,l){var p=arguments.length;if(this instanceof t){var f=p===1&&String(e)===e?new t(r.parse(e)):p>=7?new t(e,n,i,o,a,u,l):p>=6?new t(e,n,i,o,a,u):p>=5?new t(e,n,i,o,a):p>=4?new t(e,n,i,o):p>=3?new t(e,n,i):p>=2?new t(e,n):p>=1?new t(e):new t;f.constructor=r;return f}return t.apply(this,arguments)}var e=new RegExp("^"+"(\\d{4}|[+-]\\d{6})"+"(?:-(\\d{2})"+"(?:-(\\d{2})"+"(?:"+"T(\\d{2})"+":(\\d{2})"+"(?:"+":(\\d{2})"+"(?:(\\.\\d{1,}))?"+")?"+"("+"Z|"+"(?:"+"([-+])"+"(\\d{2})"+":(\\d{2})"+")"+")?)?)?)?"+"$");var n=[0,31,59,90,120,151,181,212,243,273,304,334,365];function i(t,r){var e=r>1?1:0;return n[r]+Math.floor((t-1969+e)/4)-Math.floor((t-1901+e)/100)+Math.floor((t-1601+e)/400)+365*(t-1970)}function o(r){return Number(new t(1970,0,1,0,0,0,r))}for(var a in t){r[a]=t[a]}r.now=t.now;r.UTC=t.UTC;r.prototype=t.prototype;r.prototype.constructor=r;r.parse=function u(r){var n=e.exec(r);if(n){var a=Number(n[1]),u=Number(n[2]||1)-1,l=Number(n[3]||1)-1,p=Number(n[4]||0),f=Number(n[5]||0),s=Number(n[6]||0),c=Math.floor(Number(n[7]||0)*1e3),h=Boolean(n[4]&&!n[8]),y=n[9]==="-"?1:-1,g=Number(n[10]||0),v=Number(n[11]||0),b;if(p<(f>0||s>0||c>0?24:25)&&f<60&&s<60&&c<1e3&&u>-1&&u<12&&g<24&&v<60&&l>-1&&l=0){o+=e[i];e[i]=Math.floor(o/n);o=o%n*t}}function a(){var t=r;var n="";while(--t>=0){if(n!==""||t===0||e[t]!==0){var i=String(e[t]);if(n===""){n=i}else{n+="0000000".slice(0,7-i.length)+i}}}return n}function u(t,r,e){return r===0?e:r%2===1?u(t,r-1,e*t):u(t*t,r/2,e)}function l(t){var r=0;while(t>=4096){r+=12;t/=4096}while(t>=2){r+=1;t/=2}return r}Number.prototype.toFixed=function p(t){var r,e,n,p,f,s,c,h;r=Number(t);r=r!==r?0:Math.floor(r);if(r<0||r>20){throw new RangeError("Number.toFixed called with invalid number of decimals")}e=Number(this);if(e!==e){return"NaN"}if(e<=-1e21||e>=1e21){return String(e)}n="";if(e<0){n="-";e=-e}p="0";if(e>1e-21){f=l(e*u(2,69,1))-69;s=f<0?e*u(2,-f,1):e/u(2,f,1);s*=4503599627370496;f=52-f;if(f>0){i(0,s);c=r;while(c>=7){i(1e7,0);c-=7}i(u(10,c,1),0);c=f-1;while(c>=23){o(1<<23);c-=23}o(1<0){h=p.length;if(h<=r){p=n+"0.0000000000000000000".slice(0,r-h+2)+p}else{p=n+p.slice(0,h-r)+"."+p.slice(h-r)}}else{p=n+p}return p}})()}var I=String.prototype.split;if("ab".split(/(?:ab)*/).length!==2||".".split(/(.?)(.?)/).length!==4||"tesst".split(/(s)*/)[1]==="t"||"".split(/.?/).length||".".split(/()()/).length>1){(function(){var t=/()??/.exec("")[1]===void 0;String.prototype.split=function(r,e){var n=this;if(r===void 0&&e===0)return[];if(Object.prototype.toString.call(r)!=="[object RegExp]"){return I.apply(this,arguments)}var i=[],o=(r.ignoreCase?"i":"")+(r.multiline?"m":"")+(r.extended?"x":"")+(r.sticky?"y":""),a=0,u,l,p,f;r=new RegExp(r.source,o+"g");n+="";if(!t){u=new RegExp("^"+r.source+"$(?!\\s)",o)}e=e===void 0?-1>>>0:e>>>0;while(l=r.exec(n)){p=l.index+l[0].length;if(p>a){i.push(n.slice(a,l.index));if(!t&&l.length>1){l[0].replace(u,function(){for(var t=1;t1&&l.index=e){break}}if(r.lastIndex===l.index){r.lastIndex++}}if(a===n.length){if(f||!r.test("")){i.push("")}}else{i.push(n.slice(a))}return i.length>e?i.slice(0,e):i}})()}else if("0".split(void 0,0).length){String.prototype.split=function ar(t,r){if(t===void 0&&r===0)return[];return I.apply(this,arguments)}}if("".substr&&"0b".substr(-1)!=="b"){var M=String.prototype.substr;String.prototype.substr=function ur(t,r){return M.call(this,t<0?(t=this.length+t)<0?0:t:t,r)}}var R=" \n \f\r \xa0\u1680\u180e\u2000\u2001\u2002\u2003"+"\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028"+"\u2029\ufeff";var C="\u200b";if(!String.prototype.trim||R.trim()||!C.trim()){R="["+R+"]";var k=new RegExp("^"+R+R+"*"),U=new RegExp(R+R+"*$");String.prototype.trim=function lr(){if(this===void 0||this===null){throw new TypeError("can't convert "+this+" to object")}return String(this).replace(k,"").replace(U,"")}}if(parseInt(R+"08")!==8||parseInt(R+"0x16")!==22){parseInt=function(t){var r=/^0[xX]/;return function e(n,i){n=String(n).trim();if(!Number(i)){i=r.test(n)?16:10}return t(n,i)}}(parseInt)}function Z(t){t=+t;if(t!==t){t=0}else if(t!==0&&t!==1/0&&t!==-(1/0)){t=(t>0||-1)*Math.floor(Math.abs(t))}return t}function J(t){var r=typeof t;return t===null||r==="undefined"||r==="boolean"||r==="number"||r==="string"}function X(t){var r,e,n;if(J(t)){return t}e=t.valueOf;if(typeof e==="function"){r=e.call(t);if(J(r)){return r}}n=t.toString;if(typeof n==="function"){r=n.call(t);if(J(r)){return r}}throw new TypeError}var $=function(t){if(t==null){throw new TypeError("can't convert "+t+" to object")}return Object(t)}}); +//# sourceMappingURL=es5-shim.map \ No newline at end of file diff --git a/src/idea/templates/idea/add.html b/src/idea/templates/idea/add.html index d1042cf..5ebec2a 100644 --- a/src/idea/templates/idea/add.html +++ b/src/idea/templates/idea/add.html @@ -118,5 +118,6 @@

      Add an idea

      {% block "js_scripts" %} + {% block "design_js_scripts" %}{% endblock %} {% endblock %} From 150b4263f96c49f020305db65f6df339c9774054 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Tue, 22 Apr 2014 14:18:46 +0000 Subject: [PATCH 089/230] IN-478 - Improve style consistency for selectize tool --- src/idea/static/idea/css/idea.css | 39 +++++++++++++++++++++++++++++++ src/idea/templates/idea/add.html | 33 +------------------------- 2 files changed, 40 insertions(+), 32 deletions(-) diff --git a/src/idea/static/idea/css/idea.css b/src/idea/static/idea/css/idea.css index 4e29a20..645adfa 100644 --- a/src/idea/static/idea/css/idea.css +++ b/src/idea/static/idea/css/idea.css @@ -392,6 +392,12 @@ ul.section-nav li.active a { display: inline-block; margin: 0.625em 0; } +.project-add .selectize-control { + width: 49.366%; /* 48.57142% + padding to match other elements */ + margin: 0.625em 0; + vertical-align: top; + display: inline-block; +} .project-add .helptext { margin-left: 25.75%; } @@ -593,3 +599,36 @@ section.main-content hr { padding: 0.71428571428571em 1em; font-size: 16px; } + +/* Selectize */ +.selectize-input { + border: 1px solid #919395; + -webkit-box-shadow: inset 0 1px 2px rgba(0,0,0,0.15); + -moz-box-shadow: inset 0 1px 2px rgba(0,0,0,0.15); + box-shadow: inset 0 1px 3px rgba(0,0,0,0.15); + -webkit-transition: border linear .2s,box-shadow linear .2s; + -moz-transition: border linear .2s,box-shadow linear .2s; + -ms-transition: border linear .2s,box-shadow linear .2s; + -o-transition: border linear .2s,box-shadow linear .2s; + transition: border linear .2s,box-shadow linear .2s; + border-radius: 0; + font-size: 14px; +} +.selectize-input .item { + background: #0072CE !important; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + border: none !important; + color: #FFF; +} +.selectize-input input[type="text"], +.selectize-control textarea { + vertical-align: middle; + width: 375px; +} +.selectize-control div.option span, +.selectize-control div.create span { + margin-left: 0; + display: inline; +} diff --git a/src/idea/templates/idea/add.html b/src/idea/templates/idea/add.html index 5ebec2a..1dcd5a1 100644 --- a/src/idea/templates/idea/add.html +++ b/src/idea/templates/idea/add.html @@ -3,38 +3,7 @@ {%block "css_files" %} - +{{ block.super }} {% endblock %} From 5686025647dba8f1236d8a4c4f42a4477c5f3d11 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Tue, 22 Apr 2014 14:22:34 +0000 Subject: [PATCH 090/230] Update informative text as requested --- src/idea/templates/idea/add.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/idea/templates/idea/add.html b/src/idea/templates/idea/add.html index 1dcd5a1..3ca1387 100644 --- a/src/idea/templates/idea/add.html +++ b/src/idea/templates/idea/add.html @@ -17,7 +17,7 @@

      Add an idea

      -

      Please complete all fields to add the idea

      +

      Please complete all form fields to submit an idea.

      Toggle instructions for each field.
      {% csrf_token %} From 0780851ca5cf7b26f5db863fda6f37cfc8f7640d Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Tue, 22 Apr 2014 15:56:18 +0000 Subject: [PATCH 091/230] IN-482 - Fix remove_tag regex to support multi-word tags --- src/idea/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/idea/urls.py b/src/idea/urls.py index 325164d..8a39003 100644 --- a/src/idea/urls.py +++ b/src/idea/urls.py @@ -8,7 +8,7 @@ url(r'^list/$', 'list', name='idea_list'), url(r'^list/(?P\w+)/$', 'list', name='idea_list'), url(r'^detail/(?P\d+)/$', 'detail', name='idea_detail'), - url(r'^detail/(?P\d+)/remove_tag/(?P\w+)/$', + url(r'^detail/(?P\d+)/remove_tag/(?P[a-zA-Z0-9/\-_]+)/$', 'remove_tag', name='remove_tag'), url(r'^vote/up/$', 'up_vote', name='upvote_idea'), url(r'^challenge/(?P\d+)/$', From de7052d1e026853f9e74f87a7961cc93a8ebaa01 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Tue, 22 Apr 2014 12:25:06 +0000 Subject: [PATCH 092/230] Improve style for pagination of idea lists --- src/idea/static/idea/css/idea.css | 71 ++++++++++++++++------ src/idea/templates/idea/banner_detail.html | 29 ++++++--- src/idea/templates/idea/list.html | 30 ++++++--- 3 files changed, 92 insertions(+), 38 deletions(-) diff --git a/src/idea/static/idea/css/idea.css b/src/idea/static/idea/css/idea.css index 645adfa..76ec14f 100644 --- a/src/idea/static/idea/css/idea.css +++ b/src/idea/static/idea/css/idea.css @@ -275,25 +275,6 @@ ul.section-nav li.active a { border-bottom: 0; } -/* Internal Nav */ -.top-internal-nav { - margin: 1em 0 0; -} -.bottom-internal-nav { - margin-top: 1.5em; -} -.top-internal-nav a, -.bottom-internal-nav a.left{ - float: left; - background: url(../img/back-arrow.png) center left no-repeat; - padding-left: .75em; -} -.bottom-internal-nav a { - float: right; - background: url(../img/more-arrow.png) center right no-repeat; - padding-right: .75em; -} - /* Sidebar */ .sidebar { //margin-top: 1em; @@ -421,7 +402,6 @@ ul.section-nav li.active a { .idea-hero a { border-bottom-width: 0; } -.top-internal-nav a, .main-content a { border-bottom-width: 1px; } @@ -632,3 +612,54 @@ section.main-content hr { margin-left: 0; display: inline; } + +// Pagination + +.pagination { + border: solid #75787b; + border-width: 1px 0; + margin: 20px 0; +} + +.pagination ul { + display: block; + padding-left: 0; + margin: 0; + overflow: auto; +} + +.pagination [class^="icon-"] { + font-size: 0.625em; +} + +.pagination ul > li { + display: inline; +} + +.pagination ul > li > a, +.pagination ul > li > span { + padding: 4px 8px; + float: left; + color: #75787b; + line-height: 20px; + text-decoration: none; + border-bottom-width: 0; +} + +.pagination ul > li > a:hover, +.pagination ul > li > a:focus, +.pagination ul > li > a:hover > i, +.pagination ul > li > a:focus > i, +.pagination ul > .active > a, +.pagination ul > .active > span { + color: #101820; +} + +.pagination ul > .active > a, +.pagination ul > .active > span { + font-family: "Avenir Next Demi", Arial, sans-serif; + font-weight: bold; + cursor: default; +} + +.pagination ul > .disabled > span, diff --git a/src/idea/templates/idea/banner_detail.html b/src/idea/templates/idea/banner_detail.html index 86a245d..aade4ce 100644 --- a/src/idea/templates/idea/banner_detail.html +++ b/src/idea/templates/idea/banner_detail.html @@ -74,15 +74,26 @@

      {{banner.title}}

      {% endif %} - + {% if ideas.object_list.count > 5 %} + Back to top + {% endif %} + + +
      diff --git a/src/idea/views.py b/src/idea/views.py index b487cfb..1bdac50 100644 --- a/src/idea/views.py +++ b/src/idea/views.py @@ -13,7 +13,7 @@ from django.views.decorators.http import require_POST from idea.forms import IdeaForm, IdeaTagForm, UpVoteForm -from idea.models import Idea, State, Vote, Banner +from idea.models import Idea, State, Vote, Banner, Config from idea.utility import state_helper from idea.models import UP_VOTE @@ -115,12 +115,17 @@ def list(request, sort_or_state=None): tag_slugs) banner = get_banner() + try: + about_text = Config.objects.get(key="list_about").value + except Config.DoesNotExist: + about_text = "" return _render(request, 'idea/list.html', { 'sort_or_state': sort_or_state, 'ideas': page, 'tags': tags, # list of popular tags 'banner': banner, + 'about_text': about_text, }) From 4f32cce162c11572cc76f4db2ccd911ce4a01bfe Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Wed, 30 Apr 2014 19:36:49 +0000 Subject: [PATCH 115/230] Remove comma-separated help text that conflicts with autocomplete dropdown --- src/idea/templates/idea/detail.html | 1 - 1 file changed, 1 deletion(-) diff --git a/src/idea/templates/idea/detail.html b/src/idea/templates/idea/detail.html index 6542ba1..1907006 100644 --- a/src/idea/templates/idea/detail.html +++ b/src/idea/templates/idea/detail.html @@ -132,7 +132,6 @@

      Idea Tags

      {{ tag_form.tags }} - Separate tags with comma
      From f2df49da067cdb41aa06ddaf6b1f47cb5215f783 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Wed, 30 Apr 2014 20:11:29 +0000 Subject: [PATCH 116/230] IN-503 - Add fixture for default About Idea Box text --- src/idea/fixtures/config.json | 10 ++ src/idea/migrations/0014_load_config.py | 124 ++++++++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 src/idea/fixtures/config.json create mode 100644 src/idea/migrations/0014_load_config.py diff --git a/src/idea/fixtures/config.json b/src/idea/fixtures/config.json new file mode 100644 index 0000000..c1b66a8 --- /dev/null +++ b/src/idea/fixtures/config.json @@ -0,0 +1,10 @@ +[ +{ + "pk": 1, + "model": "idea.config", + "fields": { + "key": "list_about", + "value": "Idea Box is a place where everyone in the CFPB can share their ideas on how to improve the work we do for consumers and the way we do it. Challenges give you the opportunity to share your solution to a specific problem. Submit your ideas and join the conversation!" + } +} +] diff --git a/src/idea/migrations/0014_load_config.py b/src/idea/migrations/0014_load_config.py new file mode 100644 index 0000000..761138d --- /dev/null +++ b/src/idea/migrations/0014_load_config.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import DataMigration +from django.core.management import call_command +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + """ + Load the two statuses. + """ + call_command('loaddata', 'config.json') + + def backwards(self, orm): + """ + No backwards. + """ + raise RuntimeError('No reverse for config.json') + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'idea.banner': { + 'Meta': {'object_name': 'Banner'}, + 'end_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'start_date': ('django.db.models.fields.DateField', [], {}), + 'text': ('django.db.models.fields.CharField', [], {'max_length': '2000'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'idea.config': { + 'Meta': {'object_name': 'Config'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'value': ('django.db.models.fields.TextField', [], {'max_length': '2000'}) + }, + u'idea.idea': { + 'Meta': {'object_name': 'Idea'}, + 'banner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['idea.Banner']", 'null': 'True', 'blank': 'True'}), + 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['idea.State']"}), + 'summary': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'text': ('django.db.models.fields.TextField', [], {'max_length': '2000'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 4, 30, 0, 0)'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'voters': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'idea_vote_creator'", 'null': 'True', 'through': u"orm['idea.Vote']", 'to': u"orm['auth.User']"}) + }, + u'idea.state': { + 'Meta': {'object_name': 'State'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'previous': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['idea.State']", 'unique': 'True', 'null': 'True', 'blank': 'True'}) + }, + u'idea.vote': { + 'Meta': {'object_name': 'Vote'}, + 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'idea': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['idea.Idea']"}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 4, 30, 0, 0)'}), + 'vote': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}) + }, + u'taggit.tag': { + 'Meta': {'object_name': 'Tag'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100'}) + }, + u'taggit.tagcategory': { + 'Meta': {'object_name': 'TagCategory'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}) + }, + u'taggit.taggeditem': { + 'Meta': {'object_name': 'TaggedItem'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_tagged_items'", 'to': u"orm['contenttypes.ContentType']"}), + 'create_timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_items'", 'to': u"orm['taggit.Tag']"}), + 'tag_category': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['taggit.TagCategory']", 'null': 'True'}), + 'tag_creator': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_related'", 'null': 'True', 'to': u"orm['auth.User']"}) + } + } + + complete_apps = ['idea'] + symmetrical = True From 43e6f954768bfef15afa063a77e57a6e678c4bee Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Tue, 6 May 2014 15:47:25 +0000 Subject: [PATCH 117/230] IN-509 - Update color of tag button in add idea page --- src/idea/static/idea/css/idea.css | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/idea/static/idea/css/idea.css b/src/idea/static/idea/css/idea.css index ff8d191..5c23608 100644 --- a/src/idea/static/idea/css/idea.css +++ b/src/idea/static/idea/css/idea.css @@ -598,12 +598,16 @@ section.main-content hr { border-radius: 0; } .multi.selectize-control .selectize-input [data-value] { - background: #0072CE; + background: #FFCE8D; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; border: none; - color: #FFF; + box-shadow: none; + -webkit-box-shadow: none; + -moz-box-shadow: none; + text-shadow: none; + color: #000; filter: none; font-size: 14px; } From 0ed5c42b040c951fb574635651fe3fa35f8e7ea9 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Thu, 8 May 2014 17:30:30 +0000 Subject: [PATCH 118/230] Update right sidebar headers from h4 to h3 --- src/idea/static/idea/css/idea.css | 4 ---- src/idea/templates/idea/banner_detail.html | 2 +- src/idea/templates/idea/detail.html | 4 ++-- src/idea/templates/idea/list.html | 2 +- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/idea/static/idea/css/idea.css b/src/idea/static/idea/css/idea.css index 5c23608..1fc0c0c 100644 --- a/src/idea/static/idea/css/idea.css +++ b/src/idea/static/idea/css/idea.css @@ -290,10 +290,6 @@ ul.section-nav li.active a { .sidebar-nav { margin-bottom: 2.5em; } -.sidebar h4 { - font-size: 120%; /* 24px */ - line-height: 0.95; -} .sidebar .challenge-banner { margin-bottom: 2.5em; background-color: #DBEDD4; diff --git a/src/idea/templates/idea/banner_detail.html b/src/idea/templates/idea/banner_detail.html index e4e0faf..3c7d530 100644 --- a/src/idea/templates/idea/banner_detail.html +++ b/src/idea/templates/idea/banner_detail.html @@ -102,7 +102,7 @@

      {{banner.title}}

      Submit an idea
      {% endif %}
      -

      Add an idea

      +

      Share your idea

      Please complete all form fields to submit an idea.

      Toggle instructions for each field.
      @@ -37,7 +37,7 @@

      Add an idea

      {% endfor %} - + From 9f8c2d243dd98f4a4a44dad2d4278792a38a57ab Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Wed, 21 May 2014 14:47:42 +0000 Subject: [PATCH 125/230] IN-562 - Add test for case where challenge is hidden --- src/idea/tests/addidea_tests.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/idea/tests/addidea_tests.py b/src/idea/tests/addidea_tests.py index 35e5ead..45d8301 100644 --- a/src/idea/tests/addidea_tests.py +++ b/src/idea/tests/addidea_tests.py @@ -159,3 +159,23 @@ def test_add_idea_with_banner(self, render): self.assertIn(banner1, banner_field._queryset) self.assertIn(banner2, banner_field._queryset) self.assertNotIn(banner3, banner_field._queryset) + + @patch('idea.views.render') + def test_add_idea_with_no_banner(self, render): + """ + Verify that the banner field disappears if no current challenge + """ + + banner1 = models.Banner(id=1, title="AAAA", text="text1", + start_date=date.today() - timedelta(days=2), + end_date=date.today() - timedelta(days=1)) + banner1.save() + banner2 = models.Banner(id=2, title="BBBB", text="text2", + start_date=date.today() + timedelta(days=1)) + banner2.save() + + views.add_idea(mock_req()) + context = render.call_args[0][2] + self.assertTrue('form' in context) + self.assertTrue(isinstance(context['form'], IdeaForm)) + self.assertNotIn('banner', context['form'].fields.keys()) From 1f783a836ef86d2f8f2da3e51f4c21dacf0d879f Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Tue, 27 May 2014 14:16:08 +0000 Subject: [PATCH 126/230] Hotfix duplicate of 7980c71 --- src/idea/templates/idea/add.html | 2 +- src/idea/templates/idea/banner_detail.html | 2 +- src/idea/templates/idea/detail.html | 2 +- src/idea/templates/idea/email/new_comment.html | 4 ++-- src/idea/templates/idea/email/new_comment.txt | 4 ++-- src/idea/templates/idea/list.html | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/idea/templates/idea/add.html b/src/idea/templates/idea/add.html index 04e3c04..61048e4 100644 --- a/src/idea/templates/idea/add.html +++ b/src/idea/templates/idea/add.html @@ -12,7 +12,7 @@
      diff --git a/src/idea/templates/idea/banner_detail.html b/src/idea/templates/idea/banner_detail.html index 3c7d530..b6ff7fd 100644 --- a/src/idea/templates/idea/banner_detail.html +++ b/src/idea/templates/idea/banner_detail.html @@ -5,7 +5,7 @@
      diff --git a/src/idea/templates/idea/detail.html b/src/idea/templates/idea/detail.html index 5e29a4e..488ba36 100644 --- a/src/idea/templates/idea/detail.html +++ b/src/idea/templates/idea/detail.html @@ -8,7 +8,7 @@
      diff --git a/src/idea/templates/idea/email/new_comment.html b/src/idea/templates/idea/email/new_comment.html index e5f9b23..7a7f71d 100644 --- a/src/idea/templates/idea/email/new_comment.html +++ b/src/idea/templates/idea/email/new_comment.html @@ -4,7 +4,7 @@

      Hello {{ n.target.first_name }}!
      -We're writing to let you know {{ n.actor.first_name }} commented on {{ n.obj }} in Idea Box.
      +We're writing to let you know {{ n.actor.first_name }} commented on {{ n.obj }} in IdeaBox.

      -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/idea/templates/idea/email/new_comment.txt b/src/idea/templates/idea/email/new_comment.txt index 0495c08..adf8b41 100644 --- a/src/idea/templates/idea/email/new_comment.txt +++ b/src/idea/templates/idea/email/new_comment.txt @@ -2,5 +2,5 @@ {% block "body" %} Hello {{ n.target.first_name }}! -We're writing to let you know {{ n.actor.first_name }} commented on {{ n.obj }} in Idea Box. -{% endblock %} \ No newline at end of file +We're writing to let you know {{ n.actor.first_name }} commented on {{ n.obj }} in IdeaBox. +{% endblock %} diff --git a/src/idea/templates/idea/list.html b/src/idea/templates/idea/list.html index 307b4ee..6853d1f 100644 --- a/src/idea/templates/idea/list.html +++ b/src/idea/templates/idea/list.html @@ -9,14 +9,14 @@

      {{ about_text }}

      -

      Main Navigation for Idea Box

      +

      Main Navigation for IdeaBox

      • Trending
      • From ac0ac4116d65a253704869c801987dc98b766317 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Fri, 23 May 2014 20:14:52 +0000 Subject: [PATCH 127/230] HTML text can be used in the admin 'config' fields --- src/idea/templates/idea/list.html | 2 +- src/idea/views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/idea/templates/idea/list.html b/src/idea/templates/idea/list.html index 6853d1f..998546e 100644 --- a/src/idea/templates/idea/list.html +++ b/src/idea/templates/idea/list.html @@ -12,7 +12,7 @@

        IdeaBox

      -

      {{ about_text }}

      +

      {{ about_text|safe }}

      diff --git a/src/idea/views.py b/src/idea/views.py index a79fdd8..38f2d72 100644 --- a/src/idea/views.py +++ b/src/idea/views.py @@ -116,7 +116,7 @@ def list(request, sort_or_state=None): banner = get_banner() try: - about_text = Config.objects.get(key="list_about").value + about_text = Config.objects.get(key="list_about").value.replace('', '') except Config.DoesNotExist: about_text = "" From 43a8d5237140b52fcc03d9da32dd8a580b1ad2c5 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Fri, 23 May 2014 16:42:25 +0000 Subject: [PATCH 128/230] IN-555 Add add idea success page --- src/idea/static/idea/css/idea.css | 25 +++++++++ src/idea/templates/idea/add_success.html | 70 ++++++++++++++++++++++++ src/idea/views.py | 9 ++- 3 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 src/idea/templates/idea/add_success.html diff --git a/src/idea/static/idea/css/idea.css b/src/idea/static/idea/css/idea.css index 52461bf..e06e3e6 100644 --- a/src/idea/static/idea/css/idea.css +++ b/src/idea/static/idea/css/idea.css @@ -500,6 +500,31 @@ span.btn-voted:hover { height: 150%; } +.add-summary { + margin-top: 2em; + width: 65.71428%; +} +.add-summary #submit-buttons { + margin: 2em 0; + text-align: center; +} +.add-summary #submit-buttons a { + margin-right: 8em; +} + +.summary-text { + margin: 1em 0; +} +.summary-section { + padding: 1em 0; +} +.summary-section-header { + font-family: "Avenir Next Medium", Arial, sans-serif; +} +.summary-section-body { + margin-top: 0.5em; +} + /* Hide from both screenreaders and browsers: h5bp.com/u */ .hidden { display: none !important; diff --git a/src/idea/templates/idea/add_success.html b/src/idea/templates/idea/add_success.html new file mode 100644 index 0000000..219ac55 --- /dev/null +++ b/src/idea/templates/idea/add_success.html @@ -0,0 +1,70 @@ +{% extends "idea/idea-base.html" %} + +{% block "content" %} + +
      +
      + +
      +
      +

      Thanks for sharing your idea

      + +
      What happens next:
      + +
      +

      + Watch the Likes go up +

      +
      + Gauge support for your idea by watching who has Liked it. Don’t + worry, people can’t unlike it. +
      +
      + +
      +

      + Others can leave comment +

      +
      + Start a conversation yourself, or join it by responding to any + questions or comments others may have. You'll receive a notification + when someone comments on your idea or responds to a comment. +
      +
      + +
      +

      + Add tags to find support or collaboration +

      +
      + You or anyone can add more tags to your idea. This helps filter and + group similar ideas, and find areas that people are talking the most + about. +
      +
      + +
      +

      + Show your support +

      +
      + Check out the ideas submitted by your colleagues on the Idea Box landing + page. You may find potential collaborators for your own ideas by showing + your support. +
      +
      + +
      + Ideas with the greatest interest will receive attention from Project Boldness. +
      + + +
      + +{% endblock %} diff --git a/src/idea/views.py b/src/idea/views.py index 38f2d72..0e3a8ed 100644 --- a/src/idea/views.py +++ b/src/idea/views.py @@ -252,14 +252,19 @@ def detail(request, idea_id): @login_required def add_idea(request, banner_id=None): if request.method == 'POST': + matching_ideas = Idea.objects.filter(creator=request.user, title=request.POST.get('title','')) + if matching_ideas.count() > 0: + # user already submitted this idea + return HttpResponseRedirect(reverse('idea:idea_detail', + args=(matching_ideas[0].id,))) idea = Idea(creator=request.user, state=state_helper.get_first_state()) if idea.state.name == 'Active': form = IdeaForm(request.POST, instance=idea) if form.is_valid(): new_idea = form.save() vote_up(new_idea, request.user) - return HttpResponseRedirect(reverse('idea:idea_detail', - args=(idea.id,))) + return _render(request, 'idea/add_success.html', + {'idea': new_idea,}) else: form.fields["banner"].queryset = get_current_banners() return _render(request, 'idea/add.html', {'form': form, }) From 64121048b5be92e9fceb516b1e89e01710ac17e2 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Fri, 23 May 2014 16:58:45 +0000 Subject: [PATCH 129/230] Add id_idea to context --- src/idea/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/idea/views.py b/src/idea/views.py index 0e3a8ed..9a05680 100644 --- a/src/idea/views.py +++ b/src/idea/views.py @@ -30,6 +30,7 @@ def _render(req, template_name, context={}): context['active_app'] = 'Idea' + context['is_idea'] = True context['app_link'] = reverse('idea:idea_list') return render(req, template_name, context) From 30dad9202628a90a31086f8836bbdd1bb5287a8a Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Fri, 23 May 2014 19:28:26 +0000 Subject: [PATCH 130/230] Update top left header style to 16px non-uppercase --- src/idea/static/idea/css/idea.css | 6 +++--- src/idea/templates/idea/add.html | 4 ++-- src/idea/templates/idea/add_success.html | 4 ++-- src/idea/templates/idea/banner_detail.html | 4 ++-- src/idea/templates/idea/detail.html | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/idea/static/idea/css/idea.css b/src/idea/static/idea/css/idea.css index e06e3e6..1a1e325 100644 --- a/src/idea/static/idea/css/idea.css +++ b/src/idea/static/idea/css/idea.css @@ -71,12 +71,12 @@ } .project-title a { color: #2cb34a; - font-family: FontAwesome, "Avenir Next Demi", Arial, sans-serif; + font-family: FontAwesome, "Avenir Next Medium", Arial, sans-serif; line-height: 1.57142857142857em; } .project-title a:before { - content: "\f053\200a Back To "; - font-family: FontAwesome, "Avenir Next Demi", Arial, sans-serif; + content: "\f053\00a0 Back To "; + font-family: FontAwesome, "Avenir Next Medium", Arial, sans-serif; margin-right: 0; } diff --git a/src/idea/templates/idea/add.html b/src/idea/templates/idea/add.html index 61048e4..427b8ec 100644 --- a/src/idea/templates/idea/add.html +++ b/src/idea/templates/idea/add.html @@ -11,9 +11,9 @@
      - +

      Share your idea

      diff --git a/src/idea/templates/idea/add_success.html b/src/idea/templates/idea/add_success.html index 219ac55..4ec5b57 100644 --- a/src/idea/templates/idea/add_success.html +++ b/src/idea/templates/idea/add_success.html @@ -4,9 +4,9 @@
      - +

      Thanks for sharing your idea

      diff --git a/src/idea/templates/idea/banner_detail.html b/src/idea/templates/idea/banner_detail.html index b6ff7fd..1b36f0b 100644 --- a/src/idea/templates/idea/banner_detail.html +++ b/src/idea/templates/idea/banner_detail.html @@ -4,9 +4,9 @@
      - +
      diff --git a/src/idea/templates/idea/detail.html b/src/idea/templates/idea/detail.html index 488ba36..0eb2492 100644 --- a/src/idea/templates/idea/detail.html +++ b/src/idea/templates/idea/detail.html @@ -7,9 +7,9 @@
      - +
      From e5fd0df220f70a50626ebadd51b3193479c7a048 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Fri, 23 May 2014 19:29:25 +0000 Subject: [PATCH 131/230] Search and replace 'Idea Box' with 'IdeaBox' --- src/idea/fixtures/config.json | 2 +- src/idea/templates/idea/add_success.html | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/idea/fixtures/config.json b/src/idea/fixtures/config.json index c1b66a8..8239e1e 100644 --- a/src/idea/fixtures/config.json +++ b/src/idea/fixtures/config.json @@ -4,7 +4,7 @@ "model": "idea.config", "fields": { "key": "list_about", - "value": "Idea Box is a place where everyone in the CFPB can share their ideas on how to improve the work we do for consumers and the way we do it. Challenges give you the opportunity to share your solution to a specific problem. Submit your ideas and join the conversation!" + "value": "IdeaBox is a place where everyone in the CFPB can share their ideas on how to improve the work we do for consumers and the way we do it. Challenges give you the opportunity to share your solution to a specific problem. Submit your ideas and join the conversation!" } } ] diff --git a/src/idea/templates/idea/add_success.html b/src/idea/templates/idea/add_success.html index 4ec5b57..399ac4f 100644 --- a/src/idea/templates/idea/add_success.html +++ b/src/idea/templates/idea/add_success.html @@ -5,7 +5,7 @@
      @@ -50,7 +50,7 @@

      Show your support

      - Check out the ideas submitted by your colleagues on the Idea Box landing + Check out the ideas submitted by your colleagues on the IdeaBox landing page. You may find potential collaborators for your own ideas by showing your support.
      @@ -61,7 +61,7 @@

      From 3655ecb1c93f2683b86cdd83e422d5cd738a958f Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Fri, 23 May 2014 19:39:23 +0000 Subject: [PATCH 132/230] IN-555 Update tests to reflect c311775 --- src/idea/tests/addidea_tests.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/idea/tests/addidea_tests.py b/src/idea/tests/addidea_tests.py index 45d8301..c00498f 100644 --- a/src/idea/tests/addidea_tests.py +++ b/src/idea/tests/addidea_tests.py @@ -27,13 +27,23 @@ def test_good_idea(self): num_voters = User.objects.filter(vote__idea__pk=1, vote__vote=1).count() self.assertEqual(num_voters, 0) resp = self.client.post(reverse('idea:add_idea'), {'title':'test title', 'summary':'test summary', 'text':'test text', 'tags':'test, tags'}) - self.assertEqual(resp.status_code, 302) - self.assertIn('detail', resp['Location']) + self.assertContains(resp, 'Thanks for sharing your idea') self.assertEquals(models.Idea.objects.all().count(), 1) num_voters = User.objects.filter(vote__idea__pk=1, vote__vote=1).count() self.assertEqual(num_voters, 1) + def test_duplicate_idea(self): + """ Test an duplicate POST submission to add an idea. """ + self.client.login(username='test1@example.com', password='1') + self.assertEquals(models.Idea.objects.all().count(), 0) + resp = self.client.post(reverse('idea:add_idea'), {'title':'test title', 'summary':'test summary', 'text':'test text', 'tags':'test, tags'}) + self.assertEquals(models.Idea.objects.all().count(), 1) + resp = self.client.post(reverse('idea:add_idea'), {'title':'test title', 'summary':'new summary', 'text':'new text', 'tags':'new, tags'}) + self.assertEqual(resp.status_code, 302) + self.assertIn('detail', resp['Location']) + self.assertEquals(models.Idea.objects.all().count(), 1) + def test_bad_idea(self): """ Test an incomplete POST submission to add an idea. """ From 3a8febf38d64394f52308c24920cb5bd70642838 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Fri, 23 May 2014 21:06:06 +0000 Subject: [PATCH 133/230] IN-561 challenge box no longer reappears with incomplete submission --- src/idea/views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/idea/views.py b/src/idea/views.py index 9a05680..920d76b 100644 --- a/src/idea/views.py +++ b/src/idea/views.py @@ -267,7 +267,10 @@ def add_idea(request, banner_id=None): return _render(request, 'idea/add_success.html', {'idea': new_idea,}) else: - form.fields["banner"].queryset = get_current_banners() + if 'banner' in request.POST: + form.fields["banner"].queryset = get_current_banners() + else: + form.fields.pop('banner') return _render(request, 'idea/add.html', {'form': form, }) else: return HttpResponse('Idea is archived', status=403) From ae0b508b6b370c60a399e2c8fcb46eed568d60ca Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Wed, 4 Jun 2014 18:18:07 +0000 Subject: [PATCH 134/230] IN-546 - Update help text for idea create page --- src/idea/models.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/idea/models.py b/src/idea/models.py index 3e96405..57ad5b4 100644 --- a/src/idea/models.py +++ b/src/idea/models.py @@ -84,20 +84,23 @@ def related_with_counts(self): class Idea(UserTrackable): title = models.CharField(max_length=50, blank=False, null=False, - help_text="Give your idea a descriptive name.") + help_text="\ + Make your idea stand out from the rest with a good title.") summary = models.CharField(max_length=200, help_text="\ - The first 200 characters display on the main page.") + Get people's attention and instant support! Only the first 200 \ + characters make it onto the IdeaBox landing page.") text = models.TextField(max_length=2000, null=False, verbose_name="detail", help_text="\ - Please add more information to explain your idea.") + Describe your reasoning to garner deper support. Include links to any \ + research, pages, or even other ideas.") banner = models.ForeignKey( - Banner, verbose_name="challenge", blank=True, null=True, help_text="\ - Select if your idea relates to a particular challenge.") + Banner, verbose_name="challenge", blank=True, null=True) state = models.ForeignKey(State) tags = TaggableManager(blank=False, help_text="\ - You must add at least 1 tag. We suggest the office to which the \ - idea relates.") + Make it easy for supporters to find your idea. See how many other \ + ideas have the same tags for potential collaboration or a little \ + healthy competition.") voters = models.ManyToManyField(User, through="Vote", null=True, related_name="idea_vote_creator") From bd47eb5fdf0e836c9ddd1abba1fb13081f74afbf Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Wed, 4 Jun 2014 18:25:01 +0000 Subject: [PATCH 135/230] IN-546 - Update style of idea create page --- src/idea/forms.py | 37 +++++++++ src/idea/static/idea/css/idea.css | 120 ++++++++++++++++++++++-------- src/idea/templates/idea/add.html | 70 +++++++++++++---- src/idea/views.py | 3 + 4 files changed, 185 insertions(+), 45 deletions(-) diff --git a/src/idea/forms.py b/src/idea/forms.py index dfdea7c..4167814 100644 --- a/src/idea/forms.py +++ b/src/idea/forms.py @@ -16,6 +16,34 @@ def __init__(self, *args, **kwargs): super(IdeaForm, self).__init__(*args, **kwargs) self.fields['banner'].empty_label = "No challenge" + self.fields['title'].label = "What is your idea?" + self.fields['banner'].label = None + self.fields['summary'].label = "Pitch your idea" + self.fields['tags'].label = "Tag it with keywords" + self.fields['text'].label = "Give us the details" + + self.fields['challenge-checkbox'] = forms.BooleanField( + required=False, + label = "My idea is part of a Challenge") + + for field in self.fields: + form_classes = "form-control" + if field == "banner" and 'challenge-checkbox' in self.data.keys() \ + and self.data['challenge-checkbox'] == 'on': + form_classes += " active" + if field in self.data.keys() and self.data[field]: + form_classes += " populated" + self.fields[field].widget.attrs["class"] = form_classes + + + self.fields.keyOrder = [ + 'title', + 'challenge-checkbox', + 'banner', + 'summary', + 'tags', + 'text'] + def save(self, commit=True): instance = super(IdeaForm, self).save(commit=False) # add tags separately @@ -29,6 +57,15 @@ def save(self, commit=True): instance.tags.add(*tags) return instance + def set_error_css(self): + for field in self.fields: + classes_set = set(self.fields[field].widget.attrs["class"].split()) + if field in self.errors.keys(): + classes_set.add("input-error") + else: + classes_set.discard("input-error") + self.fields[field].widget.attrs["class"] = " ".join(classes_set) + def clean_tags(self): """ Force tags to lowercase, since tags are case-sensitive otherwise. """ diff --git a/src/idea/static/idea/css/idea.css b/src/idea/static/idea/css/idea.css index 1a1e325..263d656 100644 --- a/src/idea/static/idea/css/idea.css +++ b/src/idea/static/idea/css/idea.css @@ -43,6 +43,42 @@ overflow-x:hidden; } +input[type="text"], +textarea[type="text"] { + padding: 0.25em; + border: 1px solid #75787B; /* Gray */ + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; + margin: 0; + vertical-align: top; +} + +.selectize-input.input-active, +input:focus, +input.focus, +textarea:focus, +textarea.focus { + border: 1px solid #0072CE; /* Pacific */ + border-radius: 0; + outline: 1px solid #0072CE; /* Pacific */ + outline-offset: 0; + -webkit-box-shadow: none; + box-shadow: none; +} + +.selectize-control.input-error .selectize-input, +input.input-error, +textarea.input-error { + border: 1px solid #D12124; /* Red Orange */ + outline: 1px solid #D12124; /* Red Orange */ +} + +input.populated, +textarea.populated { + color: #888; +} + /* Improve readability when focused and hovered in all browsers: h5bp.com/h */ .ic a:hover, .ic a:active { @@ -330,18 +366,9 @@ ul.section-nav li.active a { .project-add { margin: 1.5em 0 0; } -.project-add h1 { - display: inline-block; - margin-right: 0.25em; -} -.helptext, -.instructions { - color: #75787B; - font-family: "Avenir Next Italic", Arial, sans-serif; - font-style: italic; -} -.project-add p { - display: inline-block; +.project-add form > div { + margin: 1em 0; + position: relative; } .project-add form textarea { height: 6.25em; @@ -350,39 +377,69 @@ ul.section-nav li.active a { margin: 0.625em 0; } .project-add label { - display: inline-block; - font-family: "Avenir Next Demi", Arial, sans-serif; - padding: 0.625em 0 0.625em; + display: block; position: relative; - width: 22.85714%; - margin-right: 2.5%; - text-align: right; } .project-add input#id_title, -.project-add input#id_text, +.project-add select#id_banner, +.project-add textarea#id_text, .project-add input#id_summary, -.project-add input#id_tags { +.project-add input#id_tags, +.selectize-control { width: 48.57142%; + display: block; + margin: 0; +} +.project-add .banner { + margin-top: 0; +} +.project-add .challenge-checkbox { + margin-bottom: 0; +} +.project-add .challenge-checkbox label { display: inline-block; - margin: 0.625em 0; + vertical-align: middle; + margin-left: .5em; } -.project-add .selectize-control { - width: 49.366%; /* 48.57142% + padding to match other elements */ - margin: 0.625em 0; - vertical-align: top; +.project-add .challenge-checkbox input { display: inline-block; + vertical-align: middle; +} +.project-add select#id_banner { + visibility: hidden; + margin-left: 1.5em; +} +.project-add select#id_banner.active { + visibility: visible; +} +.form-field-footer { + position: relative; +} +.project-add .help_text { + margin: 0; + font-style: italic; + width: 48.57142%; + font-size: 0.875em; + visibility: hidden; + display: block; } -.project-add .helptext { - margin-left: 25.75%; +.errorlist { + list-style: none; + padding: 0; } -.project-add .errorlist { - margin-left: 5px; - width: 22.85714%; +.errorlist li { + position: absolute; + top: 0; + left: 2px; + margin: 0; + padding: 0; } .project-add span#submit-buttons { display: block; margin-top: 1.375em; - margin-left: 25.75%; +} +.project-add span#submit-buttons input { + vertical-align: middle; } .project-add #add-idea-btn, .project-add .secondary-action { @@ -611,6 +668,7 @@ section.main-content hr { -o-transition: border linear .2s,box-shadow linear .2s; transition: border linear .2s,box-shadow linear .2s; border-radius: 0; + display: block; } .multi.selectize-control .selectize-input [data-value] { background: #FFCE8D; diff --git a/src/idea/templates/idea/add.html b/src/idea/templates/idea/add.html index 427b8ec..eef327e 100644 --- a/src/idea/templates/idea/add.html +++ b/src/idea/templates/idea/add.html @@ -17,28 +17,39 @@

      Share your idea

      -

      Please complete all form fields to submit an idea.

      Toggle instructions for each field.
      {% csrf_token %} {% for field in form %} -
      -
      @@ -47,10 +58,15 @@

      Share your idea

      {% endblock %} {% block "js_ready" %} - $(".icon-question-sign").tooltip({ - delay: 100, - left: -16 + $('#id_challenge-checkbox').change(function(){ + if($(this).is(":checked")) { + $('#id_banner').addClass("active"); + } else { + $('#id_banner').prop('selectedIndex',0); + $('#id_banner').removeClass("active"); + } }); + $("#activate-help").click(function() { $(".help_text span").toggle(); }); @@ -83,6 +99,32 @@

      Share your idea

      }); } }); + + // Show help_text, hide error text when input box is focused + function field_focus(field) { + field.next().children('.errorlist').css("visibility", "hidden"); + field.next().children('.help_text').css("visibility", "visible"); + field.removeClass("input-error").removeClass("populated").addClass("input-active") + }; + function field_blur(field) { + field.next().children('.help_text').css("visibility", "hidden"); + field.removeClass("input-active") + } + $('.project-add').find(".form-control") + .focus(function(){ + field_focus($(this)); + }) + .blur(function(){ + field_blur($(this)); + }); + $('.project-add').find(".selectize-input").children("input") + .focus(function(){ + field_focus($('.selectize-control')); + }) + .blur(function(){ + field_blur($('.selectize-control')); + }); + {% endblock %} {% block "js_scripts" %} diff --git a/src/idea/views.py b/src/idea/views.py index 920d76b..fa67b6a 100644 --- a/src/idea/views.py +++ b/src/idea/views.py @@ -271,6 +271,8 @@ def add_idea(request, banner_id=None): form.fields["banner"].queryset = get_current_banners() else: form.fields.pop('banner') + form.fields.pop('challenge-checkbox') + form.set_error_css() return _render(request, 'idea/add.html', {'form': form, }) else: return HttpResponse('Idea is archived', status=403) @@ -280,6 +282,7 @@ def add_idea(request, banner_id=None): if current_banners.count() == 0: form = IdeaForm(initial={'title': idea_title}) form.fields.pop('banner') + form.fields.pop('challenge-checkbox') else: if banner_id and Banner.objects.get(id=banner_id) in get_current_banners(): banner = Banner.objects.get(id=banner_id) From 0302989ca2656f696b70cbadb62013ca963f3af2 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Wed, 4 Jun 2014 18:54:24 +0000 Subject: [PATCH 136/230] Autopep8 --- src/idea/forms.py | 10 +++++----- src/idea/models.py | 9 +++++---- src/idea/views.py | 30 +++++++++++++++++++----------- 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/src/idea/forms.py b/src/idea/forms.py index 4167814..e11682e 100644 --- a/src/idea/forms.py +++ b/src/idea/forms.py @@ -6,6 +6,7 @@ except ImportError: pass + class IdeaForm(forms.ModelForm): class Meta: @@ -24,7 +25,7 @@ def __init__(self, *args, **kwargs): self.fields['challenge-checkbox'] = forms.BooleanField( required=False, - label = "My idea is part of a Challenge") + label="My idea is part of a Challenge") for field in self.fields: form_classes = "form-control" @@ -33,8 +34,7 @@ def __init__(self, *args, **kwargs): form_classes += " active" if field in self.data.keys() and self.data[field]: form_classes += " populated" - self.fields[field].widget.attrs["class"] = form_classes - + self.fields[field].widget.attrs["class"] = form_classes self.fields.keyOrder = [ 'title', @@ -65,7 +65,7 @@ def set_error_css(self): else: classes_set.discard("input-error") self.fields[field].widget.attrs["class"] = " ".join(classes_set) - + def clean_tags(self): """ Force tags to lowercase, since tags are case-sensitive otherwise. """ @@ -85,7 +85,7 @@ class UpVoteForm(forms.Form): class IdeaTagForm(forms.Form): tags = forms.CharField(max_length=512, - widget=forms.TextInput(attrs={'class':'tags_autocomplete'})) + widget=forms.TextInput(attrs={'class': 'tags_autocomplete'})) def clean_tags(self): """ Force tags to lowercase, since tags are case-sensitive otherwise. """ diff --git a/src/idea/models.py b/src/idea/models.py index 57ad5b4..977bbe3 100644 --- a/src/idea/models.py +++ b/src/idea/models.py @@ -26,8 +26,8 @@ class Meta: class Banner(models.Model): - """ The banner text at the beginning of IdeaBox pages, asking the question. - This can be used to run informal campaigns soliciting ideas around specific + """ The banner text at the beginning of IdeaBox pages, asking the question. + This can be used to run informal campaigns soliciting ideas around specific topics. """ title = models.CharField(max_length=50) @@ -61,7 +61,7 @@ def related_with_counts(self): idea_type = ContentType.objects.get(app_label="idea", model="idea") return self.select_related().extra(select={ 'comment_count': """ - SELECT count(*) FROM django_comments + SELECT count(*) FROM django_comments WHERE django_comments.content_type_id = %s AND django_comments.object_pk = idea_idea.id """, @@ -72,7 +72,7 @@ def related_with_counts(self): END) FROM idea_idea a LEFT OUTER JOIN django_comments b ON a.id = b.object_pk - LEFT OUTER JOIN idea_vote c ON a.id = c.idea_id + LEFT OUTER JOIN idea_vote c ON a.id = c.idea_id WHERE a.id = idea_idea.id """, # Don't use annotate() as it conflicts with extra() @@ -150,6 +150,7 @@ class Vote(UserTrackable): vote = models.SmallIntegerField(choices=VOTE_CHOICES, default=1) idea = models.ForeignKey(Idea) + class Config(models.Model): key = models.CharField(max_length=50, unique=True) value = models.TextField(max_length=2000) diff --git a/src/idea/views.py b/src/idea/views.py index fa67b6a..453901d 100644 --- a/src/idea/views.py +++ b/src/idea/views.py @@ -36,7 +36,8 @@ def _render(req, template_name, context={}): def get_current_banners(): - return Banner.objects.filter(start_date__lte=date.today()).exclude(end_date__lt=date.today()) + return Banner.objects.filter(start_date__lte=date.today()).exclude( + end_date__lt=date.today()) def get_banner(): @@ -117,14 +118,19 @@ def list(request, sort_or_state=None): banner = get_banner() try: - about_text = Config.objects.get(key="list_about").value.replace('', '') + about_text = Config.objects.get( + key="list_about").value.replace( + '', + '') except Config.DoesNotExist: about_text = "" return _render(request, 'idea/list.html', { 'sort_or_state': sort_or_state, - 'ideas': page, - 'tags': tags, # list of popular tags + 'ideas': page, + 'tags': tags, # list of popular tags 'banner': banner, 'about_text': about_text, }) @@ -226,8 +232,8 @@ def detail(request, idea_id): tags = idea.tags.extra(select={ 'tag_count': """ SELECT COUNT(*) from taggit_taggeditem tt - WHERE tt.tag_id = taggit_tag.id - AND content_type_id = %s + WHERE tt.tag_id = taggit_tag.id + AND content_type_id = %s """ }, select_params=[idea_type.id]).order_by('name') @@ -253,11 +259,13 @@ def detail(request, idea_id): @login_required def add_idea(request, banner_id=None): if request.method == 'POST': - matching_ideas = Idea.objects.filter(creator=request.user, title=request.POST.get('title','')) + matching_ideas = Idea.objects.filter( + creator=request.user, + title=request.POST.get('title', '')) if matching_ideas.count() > 0: # user already submitted this idea return HttpResponseRedirect(reverse('idea:idea_detail', - args=(matching_ideas[0].id,))) + args=(matching_ideas[0].id,))) idea = Idea(creator=request.user, state=state_helper.get_first_state()) if idea.state.name == 'Active': form = IdeaForm(request.POST, instance=idea) @@ -265,7 +273,7 @@ def add_idea(request, banner_id=None): new_idea = form.save() vote_up(new_idea, request.user) return _render(request, 'idea/add_success.html', - {'idea': new_idea,}) + {'idea': new_idea, }) else: if 'banner' in request.POST: form.fields["banner"].queryset = get_current_banners() @@ -351,8 +359,8 @@ def banner_detail(request, banner_id): tag_slugs) return _render(request, 'idea/banner_detail.html', { - 'ideas': page, - 'tags': tags, # list of tags associated with banner ideas + 'ideas': page, + 'tags': tags, # list of tags associated with banner ideas 'banner': banner, 'is_current_banner': is_current_banner, }) From b9fb72149f2fe5c226ee198aebfffc2284c6c943 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Wed, 4 Jun 2014 20:49:06 +0000 Subject: [PATCH 137/230] IN-546 - correct indentation of template if-then, remove a redundant if-then --- src/idea/templates/idea/add.html | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/idea/templates/idea/add.html b/src/idea/templates/idea/add.html index eef327e..4642293 100644 --- a/src/idea/templates/idea/add.html +++ b/src/idea/templates/idea/add.html @@ -30,20 +30,18 @@

      Share your idea

      {% elif 'banner' == field.html_name %} {{ field }} {% else %} - {% if 'banner' != field.html_name %} - - {% endif %} - {{ field }} - + + {{ field }} + {% endif %}
      {% endfor %} From 737bb345b63b965e40557cdf82b443a9817f445b Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Fri, 6 Jun 2014 20:49:47 +0000 Subject: [PATCH 138/230] Challenge dropdown default is 'Select' --- src/idea/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/idea/forms.py b/src/idea/forms.py index e11682e..57929f1 100644 --- a/src/idea/forms.py +++ b/src/idea/forms.py @@ -15,7 +15,7 @@ class Meta: def __init__(self, *args, **kwargs): super(IdeaForm, self).__init__(*args, **kwargs) - self.fields['banner'].empty_label = "No challenge" + self.fields['banner'].empty_label = "Select" self.fields['title'].label = "What is your idea?" self.fields['banner'].label = None From cd05bba4c2c0a9de39c5b29658f54f1b20c6a8d9 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Fri, 6 Jun 2014 20:55:47 +0000 Subject: [PATCH 139/230] Correct some pep8 ugliness --- src/idea/views.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/idea/views.py b/src/idea/views.py index 453901d..231e596 100644 --- a/src/idea/views.py +++ b/src/idea/views.py @@ -119,11 +119,8 @@ def list(request, sort_or_state=None): banner = get_banner() try: about_text = Config.objects.get( - key="list_about").value.replace( - '', - '') + key="list_about").value.replace('','') except Config.DoesNotExist: about_text = "" From 39866350c4304d83391695c2c190c553d3f0e81a Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Mon, 9 Jun 2014 16:51:44 +0000 Subject: [PATCH 140/230] Summary field should be be a multi-line text box --- .../0016_auto__chg_field_idea_summary.py | 121 ++++++++++++++++++ src/idea/models.py | 2 +- 2 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 src/idea/migrations/0016_auto__chg_field_idea_summary.py diff --git a/src/idea/migrations/0016_auto__chg_field_idea_summary.py b/src/idea/migrations/0016_auto__chg_field_idea_summary.py new file mode 100644 index 0000000..e9bc77e --- /dev/null +++ b/src/idea/migrations/0016_auto__chg_field_idea_summary.py @@ -0,0 +1,121 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Changing field 'Idea.summary' + db.alter_column(u'idea_idea', 'summary', self.gf('django.db.models.fields.TextField')(max_length=200)) + + def backwards(self, orm): + + # Changing field 'Idea.summary' + db.alter_column(u'idea_idea', 'summary', self.gf('django.db.models.fields.CharField')(max_length=200)) + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'idea.banner': { + 'Meta': {'object_name': 'Banner'}, + 'end_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'start_date': ('django.db.models.fields.DateField', [], {}), + 'text': ('django.db.models.fields.CharField', [], {'max_length': '2000'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'idea.config': { + 'Meta': {'object_name': 'Config'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50'}), + 'value': ('django.db.models.fields.TextField', [], {'max_length': '2000'}) + }, + u'idea.idea': { + 'Meta': {'object_name': 'Idea'}, + 'banner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['idea.Banner']", 'null': 'True', 'blank': 'True'}), + 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['idea.State']"}), + 'summary': ('django.db.models.fields.TextField', [], {'max_length': '200'}), + 'text': ('django.db.models.fields.TextField', [], {'max_length': '2000'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 6, 9, 0, 0)'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'voters': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'idea_vote_creator'", 'null': 'True', 'through': u"orm['idea.Vote']", 'to': u"orm['auth.User']"}) + }, + u'idea.state': { + 'Meta': {'object_name': 'State'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'previous': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['idea.State']", 'unique': 'True', 'null': 'True', 'blank': 'True'}) + }, + u'idea.vote': { + 'Meta': {'object_name': 'Vote'}, + 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'idea': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['idea.Idea']"}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 6, 9, 0, 0)'}), + 'vote': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}) + }, + u'taggit.tag': { + 'Meta': {'object_name': 'Tag'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100'}) + }, + u'taggit.tagcategory': { + 'Meta': {'object_name': 'TagCategory'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}) + }, + u'taggit.taggeditem': { + 'Meta': {'object_name': 'TaggedItem'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_tagged_items'", 'to': u"orm['contenttypes.ContentType']"}), + 'create_timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_items'", 'to': u"orm['taggit.Tag']"}), + 'tag_category': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['taggit.TagCategory']", 'null': 'True'}), + 'tag_creator': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_related'", 'null': 'True', 'to': u"orm['auth.User']"}) + } + } + + complete_apps = ['idea'] \ No newline at end of file diff --git a/src/idea/models.py b/src/idea/models.py index 977bbe3..7f6781f 100644 --- a/src/idea/models.py +++ b/src/idea/models.py @@ -86,7 +86,7 @@ class Idea(UserTrackable): title = models.CharField(max_length=50, blank=False, null=False, help_text="\ Make your idea stand out from the rest with a good title.") - summary = models.CharField(max_length=200, help_text="\ + summary = models.TextField(max_length=200, help_text="\ Get people's attention and instant support! Only the first 200 \ characters make it onto the IdeaBox landing page.") text = models.TextField(max_length=2000, null=False, From da3dca9e5d0c336bc683142d2f9a4344ef02fc3a Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Mon, 9 Jun 2014 18:17:02 +0000 Subject: [PATCH 141/230] Fix some typos --- src/idea/models.py | 4 ++-- src/idea/templates/idea/add_success.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/idea/models.py b/src/idea/models.py index 7f6781f..e59c044 100644 --- a/src/idea/models.py +++ b/src/idea/models.py @@ -91,8 +91,8 @@ class Idea(UserTrackable): characters make it onto the IdeaBox landing page.") text = models.TextField(max_length=2000, null=False, verbose_name="detail", help_text="\ - Describe your reasoning to garner deper support. Include links to any \ - research, pages, or even other ideas.") + Describe your reasoning to garner deeper support. Include links to \ + any research, pages, or even other ideas.") banner = models.ForeignKey( Banner, verbose_name="challenge", blank=True, null=True) state = models.ForeignKey(State) diff --git a/src/idea/templates/idea/add_success.html b/src/idea/templates/idea/add_success.html index 399ac4f..a1999c3 100644 --- a/src/idea/templates/idea/add_success.html +++ b/src/idea/templates/idea/add_success.html @@ -25,7 +25,7 @@

      - Others can leave comment + Others can leave comments

      Start a conversation yourself, or join it by responding to any From ede600b3b5002c55986683992b925484094150ce Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Tue, 10 Jun 2014 14:20:34 +0000 Subject: [PATCH 142/230] IN-554 - Add ability to edit an existing idea --- src/idea/forms.py | 5 +- src/idea/static/idea/css/idea.css | 3 + src/idea/templates/idea/add.html | 8 ++- src/idea/templates/idea/detail.html | 6 ++ src/idea/templates/idea/edit.html | 8 +++ src/idea/tests/editidea_tests.py | 94 +++++++++++++++++++++++++++++ src/idea/urls.py | 1 + src/idea/views.py | 56 ++++++++++++++++- 8 files changed, 176 insertions(+), 5 deletions(-) create mode 100644 src/idea/templates/idea/edit.html create mode 100644 src/idea/tests/editidea_tests.py diff --git a/src/idea/forms.py b/src/idea/forms.py index 57929f1..80dfe6f 100644 --- a/src/idea/forms.py +++ b/src/idea/forms.py @@ -47,7 +47,10 @@ def __init__(self, *args, **kwargs): def save(self, commit=True): instance = super(IdeaForm, self).save(commit=False) # add tags separately - tags = self.cleaned_data['tags'] + try: + tags = self.cleaned_data['tags'] + except KeyError: + tags = [] self.cleaned_data['tags'] = [] instance.save() try: diff --git a/src/idea/static/idea/css/idea.css b/src/idea/static/idea/css/idea.css index 263d656..a54f64b 100644 --- a/src/idea/static/idea/css/idea.css +++ b/src/idea/static/idea/css/idea.css @@ -292,6 +292,9 @@ ul.section-nav li.active a { .single-idea-entry .idea-entry-content p { margin-top: .5em; } +.single-idea-entry .idea-entry-content .edit-separator { + padding: 0em .5em; +} .comment-date { color: #666; font-weight: normal; diff --git a/src/idea/templates/idea/add.html b/src/idea/templates/idea/add.html index 4642293..0ae708a 100644 --- a/src/idea/templates/idea/add.html +++ b/src/idea/templates/idea/add.html @@ -18,7 +18,7 @@

      Share your idea

      Toggle instructions for each field. -
      + {% csrf_token %} {% for field in form %}
      @@ -46,8 +46,10 @@

      Share your idea

      {% endfor %} + {% block "submit-buttons" %} + {% endblock %}
      @@ -56,6 +58,10 @@

      Share your idea

      {% endblock %} {% block "js_ready" %} + if ($('#id_challenge-checkbox').is(":checked")) { + $('#id_banner').addClass("active"); + }; + $('#id_challenge-checkbox').change(function(){ if($(this).is(":checked")) { $('#id_banner').addClass("active"); diff --git a/src/idea/templates/idea/detail.html b/src/idea/templates/idea/detail.html index 0eb2492..04df150 100644 --- a/src/idea/templates/idea/detail.html +++ b/src/idea/templates/idea/detail.html @@ -50,6 +50,12 @@

      Detail

      {{idea.text}}

      {{idea.creator.first_name}} {{idea.creator.last_name}} {% endif %} on {{idea.time|date:"M d, Y "}} + {% if idea.creator == request.user %} + + | + Edit my Idea + + {% endif %} {% get_comment_list for idea.idea idea.id as comment_list %}
      diff --git a/src/idea/templates/idea/edit.html b/src/idea/templates/idea/edit.html new file mode 100644 index 0000000..f7ed62f --- /dev/null +++ b/src/idea/templates/idea/edit.html @@ -0,0 +1,8 @@ +{% extends "idea/add.html" %} + +{% block "form-url" %}{% url 'idea:edit_idea' idea.id %}{% endblock %} + +{% block "submit-buttons" %} + + +{% endblock %} diff --git a/src/idea/tests/editidea_tests.py b/src/idea/tests/editidea_tests.py new file mode 100644 index 0000000..898b251 --- /dev/null +++ b/src/idea/tests/editidea_tests.py @@ -0,0 +1,94 @@ +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse +from django.test import TestCase +from idea import models +from idea.tests.utils import random_user +from datetime import date + +def create_idea(user=None): + if not user: + user = random_user() + state = models.State.objects.get(name='Active') + idea = models.Idea(creator=user, title='Transit subsidy to Mars', + text='Aliens need assistance.', state=state) + banner = models.Banner(id=1, title="AAAA", text="text1", + start_date=date.today()) + banner.save() + idea.banner = banner + idea.save() + idea.tags.add("test tag") + return idea + +class AddIdeaTest(TestCase): + fixtures = ['state', 'core-test-fixtures'] + + def test_edit_good_idea(self): + """ Test an normal POST submission to edit an idea. """ + idea = create_idea(user=User.objects.get(username='test1@example.com')) + + self.client.login(username='test1@example.com', password='1') + self.assertEquals(models.Idea.objects.all().count(), 1) + new_title = "new title" + new_summary = "new summary" + new_text = "new text" + new_banner = models.Banner(id=2, title="BBB", text="text2", + start_date=date.today()) + new_banner.save() + resp = self.client.post(reverse('idea:edit_idea', args=(idea.id,)), + {'title':new_title, + 'summary':new_summary, + 'text':new_text, + 'banner': new_banner.id}) + self.assertEqual(resp.status_code, 302) + self.assertIn('detail', resp['Location']) + self.assertEquals(models.Idea.objects.all().count(), 1) + + # ensure editing an idea does not up the vote count + # vote count is 0 because votes are added in views.add_idea, which is not used in this test + num_voters = User.objects.filter(vote__idea__pk=idea.id, vote__vote=1).count() + self.assertEqual(num_voters, 0) + + refresh_idea = models.Idea.objects.get(id=idea.id) + self.assertEqual(refresh_idea.title, new_title) + self.assertEqual(refresh_idea.summary, new_summary) + self.assertEqual(refresh_idea.text, new_text) + self.assertEqual(refresh_idea.banner, new_banner) + + # verify the expected fields remain the same + self.assertEqual(refresh_idea.tags.count(), 1) + self.assertEqual(refresh_idea.tags.all()[0].name, "test tag") + self.assertEqual(refresh_idea.creator, idea.creator) + + def test_bad_edit_idea(self): + """ Test an incomplete POST submission to edit an idea. """ + idea = create_idea(user=User.objects.get(username='test1@example.com')) + + self.client.login(username='test1@example.com', password='1') + resp = self.client.post(reverse('idea:edit_idea', args=(idea.id,)), {'text':'new title'}) + self.assertEqual(resp.status_code, 200) + self.assertIn('This field is required.', resp.content) + self.assertEquals(models.Idea.objects.all().count(), 1) + + refresh_idea = models.Idea.objects.get(id=idea.id) + self.assertEqual(refresh_idea.title, idea.title) + self.assertEqual(refresh_idea.banner, idea.banner) + + def test_must_be_logged_in(self): + """ A user must be logged in to edit an idea. """ + idea = create_idea(user=User.objects.get(username='test1@example.com')) + resp = self.client.post(reverse('idea:edit_idea', args=(idea.id,)), {'title':'test title', 'summary':'test summary', 'text':'test text'}) + self.assertEqual(resp.status_code, 302) + self.assertIn('login', resp['Location']) + + def test_edit_ignores_tags(self): + """ A user must be logged in to create an idea. """ + idea = create_idea(user=User.objects.get(username='test1@example.com')) + + self.client.login(username='test1@example.com', password='1') + resp = self.client.post(reverse('idea:edit_idea', args=(idea.id,)), {'title':'test title', 'summary':'test summary', 'text':'test text', 'tags':'sample, newtag'}) + self.assertEqual(resp.status_code, 302) + self.assertIn('detail', resp['Location']) + + refresh_idea = models.Idea.objects.get(id=idea.id) + self.assertEqual(refresh_idea.tags.count(), 1) + self.assertEqual(refresh_idea.tags.all()[0].name, "test tag") diff --git a/src/idea/urls.py b/src/idea/urls.py index e83ebf9..a84e13d 100644 --- a/src/idea/urls.py +++ b/src/idea/urls.py @@ -6,6 +6,7 @@ url(r'^$', 'list'), url(r'^add/$', 'add_idea', name='add_idea'), url(r'^add/(?P\d+)/$', 'add_idea', name='add_idea'), + url(r'^edit/(?P\d+)/$', 'edit_idea', name='edit_idea'), url(r'^list/$', 'list', name='idea_list'), url(r'^list/(?P\w+)/$', 'list', name='idea_list'), url(r'^detail/(?P\d+)/$', 'detail', name='idea_detail'), diff --git a/src/idea/views.py b/src/idea/views.py index 231e596..54d44ec 100644 --- a/src/idea/views.py +++ b/src/idea/views.py @@ -11,6 +11,7 @@ from django.http import HttpResponseRedirect, HttpResponseForbidden, HttpResponse from django.shortcuts import get_object_or_404, render from django.views.decorators.http import require_POST +from django.db.models import Q from idea.forms import IdeaForm, IdeaTagForm, UpVoteForm from idea.models import Idea, State, Vote, Banner, Config @@ -35,9 +36,13 @@ def _render(req, template_name, context={}): return render(req, template_name, context) -def get_current_banners(): - return Banner.objects.filter(start_date__lte=date.today()).exclude( - end_date__lt=date.today()) +def get_current_banners(additional_ids_list=None): + start_date = Q(start_date__lte=date.today()) + end_date = Q(end_date__gte=date.today())|Q(end_date__isnull=True) + banner_filter = (start_date&end_date) + if additional_ids_list: + banner_filter = banner_filter|Q(id__in=additional_ids_list) + return Banner.objects.filter(banner_filter) def get_banner(): @@ -301,6 +306,51 @@ def add_idea(request, banner_id=None): }) +@login_required +def edit_idea(request, idea_id): + idea = get_object_or_404(Idea, pk=int(idea_id)) + original_banner = idea.banner + if idea.creator != request.user: + return HttpResponseRedirect(reverse('idea:idea_detail', + args=(idea_id,))) + + if request.method == 'POST': + form = IdeaForm(request.POST, instance=idea) + form.fields.pop('tags') + if form.is_valid(): + updated_idea = form.save() + return HttpResponseRedirect(reverse('idea:idea_detail', + args=(idea_id,))) + else: + if 'banner' in request.POST: + if original_banner: + current_banners = get_current_banners([original_banner.id]) + else: + current_banners = get_current_banners() + form.fields["banner"].queryset = current_banners + else: + form.fields.pop('banner') + form.fields.pop('challenge-checkbox') + form.set_error_css() + return _render(request, 'idea/edit.html', {'form': form, 'idea': idea }) + else: + form_initial = {} + if original_banner: + current_banners = get_current_banners([original_banner.id]) + form_initial["challenge-checkbox"] = "on" + else: + current_banners = get_current_banners() + form = IdeaForm(instance=idea, initial=form_initial) + form.fields.pop('tags') + if len(current_banners) == 0: + form.fields.pop('banner') + form.fields.pop('challenge-checkbox') + else: + form.fields["banner"].queryset = current_banners + return _render(request, 'idea/edit.html', + {'form': form, 'idea': idea }) + + @login_required def banner_detail(request, banner_id): """ From 88b7cf25e82e183206d0b7b7f39d8fc321d03724 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Tue, 10 Jun 2014 14:57:10 +0000 Subject: [PATCH 143/230] Use better formatting for multi-line strings --- src/idea/models.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/idea/models.py b/src/idea/models.py index e59c044..15a0187 100644 --- a/src/idea/models.py +++ b/src/idea/models.py @@ -84,23 +84,23 @@ def related_with_counts(self): class Idea(UserTrackable): title = models.CharField(max_length=50, blank=False, null=False, - help_text="\ - Make your idea stand out from the rest with a good title.") - summary = models.TextField(max_length=200, help_text="\ - Get people's attention and instant support! Only the first 200 \ - characters make it onto the IdeaBox landing page.") + help_text=""" + Make your idea stand out from the rest with a good title.""") + summary = models.TextField(max_length=200, help_text=""" + Get people's attention and instant support! Only the first 200 + characters make it onto the IdeaBox landing page.""") text = models.TextField(max_length=2000, null=False, - verbose_name="detail", help_text="\ - Describe your reasoning to garner deeper support. Include links to \ - any research, pages, or even other ideas.") + verbose_name="detail", help_text=""" + Describe your reasoning to garner deeper support. Include links to + any research, pages, or even other ideas.""") banner = models.ForeignKey( Banner, verbose_name="challenge", blank=True, null=True) state = models.ForeignKey(State) - tags = TaggableManager(blank=False, help_text="\ - Make it easy for supporters to find your idea. See how many other \ - ideas have the same tags for potential collaboration or a little \ - healthy competition.") + tags = TaggableManager(blank=False, help_text=""" + Make it easy for supporters to find your idea. See how many other + ideas have the same tags for potential collaboration or a little + healthy competition.""") voters = models.ManyToManyField(User, through="Vote", null=True, related_name="idea_vote_creator") From 5422e0223a16fd85be284747b7a70a3b90cd9308 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Tue, 10 Jun 2014 14:59:32 +0000 Subject: [PATCH 144/230] IN-546 - Fix spacing for help text below summary box --- src/idea/static/idea/css/idea.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/idea/static/idea/css/idea.css b/src/idea/static/idea/css/idea.css index a54f64b..6ded14d 100644 --- a/src/idea/static/idea/css/idea.css +++ b/src/idea/static/idea/css/idea.css @@ -386,7 +386,7 @@ ul.section-nav li.active a { .project-add input#id_title, .project-add select#id_banner, .project-add textarea#id_text, -.project-add input#id_summary, +.project-add textarea#id_summary, .project-add input#id_tags, .selectize-control { width: 48.57142%; From c5f674b22a8659dfd9ccb8deac54f752fb686b2f Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Tue, 10 Jun 2014 18:39:59 +0000 Subject: [PATCH 145/230] IN-546 - Remove unused variable --- src/idea/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/idea/views.py b/src/idea/views.py index 54d44ec..13357d0 100644 --- a/src/idea/views.py +++ b/src/idea/views.py @@ -318,7 +318,7 @@ def edit_idea(request, idea_id): form = IdeaForm(request.POST, instance=idea) form.fields.pop('tags') if form.is_valid(): - updated_idea = form.save() + form.save() return HttpResponseRedirect(reverse('idea:idea_detail', args=(idea_id,))) else: From be6ac461705a87a2494a83f0be94e16db4979f20 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Tue, 10 Jun 2014 18:52:25 +0000 Subject: [PATCH 146/230] IN-546 - Improve error catching for cleaned_data[tags] --- src/idea/forms.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/idea/forms.py b/src/idea/forms.py index 80dfe6f..ea69300 100644 --- a/src/idea/forms.py +++ b/src/idea/forms.py @@ -47,10 +47,7 @@ def __init__(self, *args, **kwargs): def save(self, commit=True): instance = super(IdeaForm, self).save(commit=False) # add tags separately - try: - tags = self.cleaned_data['tags'] - except KeyError: - tags = [] + tags = self.cleaned_data.get('tags', []) self.cleaned_data['tags'] = [] instance.save() try: From 41e3ddb364df4c3f3c1c8ebeb0222fb8e17bf6b1 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Tue, 10 Jun 2014 20:06:37 +0000 Subject: [PATCH 147/230] IN-545 - Blue back button for IdeaBox --- src/idea/static/idea/css/idea.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/idea/static/idea/css/idea.css b/src/idea/static/idea/css/idea.css index 263d656..ee6f2a5 100644 --- a/src/idea/static/idea/css/idea.css +++ b/src/idea/static/idea/css/idea.css @@ -106,7 +106,7 @@ textarea.populated { margin-top: 0; } .project-title a { - color: #2cb34a; + color: #0072CE; font-family: FontAwesome, "Avenir Next Medium", Arial, sans-serif; line-height: 1.57142857142857em; } From b6cf6e1a94ace2ddc6e39e698fcf7558182c8c09 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Thu, 12 Jun 2014 14:53:10 +0000 Subject: [PATCH 148/230] IN-546 - Add character counter to textarea fields in form --- src/idea/static/idea/css/idea.css | 24 ++++++++++++++++++++---- src/idea/templates/idea/add.html | 25 ++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/idea/static/idea/css/idea.css b/src/idea/static/idea/css/idea.css index ce008dd..a430a6c 100644 --- a/src/idea/static/idea/css/idea.css +++ b/src/idea/static/idea/css/idea.css @@ -374,10 +374,12 @@ ul.section-nav li.active a { position: relative; } .project-add form textarea { - height: 6.25em; - width: 48.57142%; - display: inline-block; - margin: 0.625em 0; + height: 5.25em; + max-width: 48.57142%; + padding-bottom: 1.25em; +} +.project-add form textarea#id_text { + height: 8.25em; } .project-add label { display: block; @@ -393,6 +395,20 @@ ul.section-nav li.active a { display: block; margin: 0; } +.project-add .textAreaCountMessage { + width: 48.57142%; + position: relative; +} +.project-add span[id$="textcount"] { + font-style: italic; + background-color: #FFF; + line-height: 1em; + font-size: 0.875em; + position: absolute; + bottom: 2px; + right: 0; + text-align: right; +} .project-add .banner { margin-top: 0; } diff --git a/src/idea/templates/idea/add.html b/src/idea/templates/idea/add.html index 0ae708a..98a8aca 100644 --- a/src/idea/templates/idea/add.html +++ b/src/idea/templates/idea/add.html @@ -34,6 +34,9 @@

      Share your idea

      {{ field.label }} {{ field }} +
      + +
      -
      -
      +
      +

      Current Challenges

        {% for banner in current_banners %} -
      • +
      • {{banner.title}}
      • {% endfor %} @@ -29,7 +29,7 @@

        Past Challenges

          {% for banner in past_banners %} -
        • +
        • {{banner.title}}
        • {% endfor %} From aeecf5f0411fc6e912019cb0d10b9e4f310b610e Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Mon, 23 Jun 2014 15:31:28 +0000 Subject: [PATCH 158/230] IN-600 - Add tests, tweaks to banner_list view --- src/idea/templates/idea/banner_detail.html | 6 ++- src/idea/templates/idea/banner_list.html | 12 ++++- src/idea/tests/banner_tests.py | 4 +- src/idea/tests/bannerlistview_tests.py | 63 ++++++++++++++++++++++ src/idea/tests/listview_tests.py | 62 +++++++++++++++++++-- src/idea/views.py | 35 ++++++------ 6 files changed, 153 insertions(+), 29 deletions(-) create mode 100644 src/idea/tests/bannerlistview_tests.py diff --git a/src/idea/templates/idea/banner_detail.html b/src/idea/templates/idea/banner_detail.html index 1b36f0b..98aeb3b 100644 --- a/src/idea/templates/idea/banner_detail.html +++ b/src/idea/templates/idea/banner_detail.html @@ -11,7 +11,11 @@
          -

          Current Challenge:

          + {% if is_current_banner %} +

          Current Challenge:

          + {% else %} +

          Expired Challenge:

          + {% endif %}

          {{banner.title}}

          diff --git a/src/idea/templates/idea/banner_list.html b/src/idea/templates/idea/banner_list.html index 3b641c8..38a37e4 100644 --- a/src/idea/templates/idea/banner_list.html +++ b/src/idea/templates/idea/banner_list.html @@ -10,7 +10,7 @@

          -

          Current Challenges

          +

          Current Challenge{{ current_banners.count|pluralize }}

            @@ -18,13 +18,17 @@

            Current Challenges

          • {{banner.title}}
          • + {% empty %} +
          • + There are no active challenges +
          • {% endfor %}
          -

          Past Challenges

          +

          Past Challenge{{ past_banners.count|pluralize }}

            @@ -32,6 +36,10 @@

            Past Challenges

          • {{banner.title}}
          • + {% empty %} +
          • + There are no expired challenges +
          • {% endfor %}
          diff --git a/src/idea/tests/banner_tests.py b/src/idea/tests/banner_tests.py index f982977..21ef6bf 100644 --- a/src/idea/tests/banner_tests.py +++ b/src/idea/tests/banner_tests.py @@ -63,9 +63,11 @@ def test_outside_timed(self): self.assertIsNone(b) def test_get_current_banners(self): + # Banners should be ordered by end date, nearest end date first yesterday = get_relative_date(-1) today = datetime.date.today() tomorrow = get_relative_date(+1) + self.assertEqual(views.get_current_banners().count(), 0) banner1 = models.Banner(title="How would you improve our vacation policy?", text="We would like to know what we can do to improve your work/life balance", start_date=yesterday, end_date=today) @@ -90,4 +92,4 @@ def test_get_current_banners(self): text="We would like to know what we can do to improve your work/life balance", start_date=today, end_date=tomorrow) banner5.save() - self.assertEqual(list(views.get_current_banners()), [banner1,banner2,banner5]) + self.assertEqual(list(views.get_current_banners()), [banner1,banner5,banner2]) diff --git a/src/idea/tests/bannerlistview_tests.py b/src/idea/tests/bannerlistview_tests.py new file mode 100644 index 0000000..d70e110 --- /dev/null +++ b/src/idea/tests/bannerlistview_tests.py @@ -0,0 +1,63 @@ +from django.contrib.auth.models import User +from django.test import TestCase +from idea import models, views +from idea.tests.utils import mock_req +from mock import patch +import datetime + +def get_relative_date(delta_days=0): + return datetime.date.today() + datetime.timedelta(days=delta_days) + +class ListViewTest(TestCase): + """ + Tests for idea.views.list + """ + fixtures = ['state'] + def _generate_data(self, paramfn=lambda x,y:None, postfn=lambda x,y:None, + entry_data=[(5, 'AAAA'), (9, 'BBBB'), (3, 'CCCC'), (7, 'DDDD'), + (1, 'EEEE'), (11, 'FFFF')]): + """ + Helper function to handle the banner creation. + """ + + def make_banner(nonce, title): + kwargs = {'title': title, 'text': title + ' Text'} + paramfn(kwargs, nonce) + banner = models.Banner(**kwargs) + banner.save() + postfn(banner, nonce) + return banner + + banner = [make_banner(pair[0], pair[1]) for pair in entry_data] + + @patch('idea.views.render') + def test_banner_list(self, render): + """ + Verify that the banner list works. + """ + def add_dates(kwargs, nonce): + baseline_start = -7 + baseline_end = -5 + # -2 0 + # 2 None + # -4 None + # 0 2 + # -6 -4 + # 4 6 + kwargs['start_date'] = get_relative_date(baseline_start + nonce) + if nonce % 3 == 0: + kwargs['end_date'] = None + else: + kwargs['end_date'] = get_relative_date(baseline_end + nonce) + self._generate_data(paramfn=add_dates) + views.banner_list(mock_req()) + + context = render.call_args[0][2] + self.assertTrue('current_banners' in context) + self.assertTrue('past_banners' in context) + self.assertEqual(3, len(context['current_banners'])) + self.assertEqual(1, len(context['past_banners'])) + self.assertEqual('AAAA', context['current_banners'][0].title) + self.assertEqual('DDDD', context['current_banners'][1].title) + self.assertEqual('CCCC', context['current_banners'][2].title) + self.assertEqual('EEEE', context['past_banners'][0].title) diff --git a/src/idea/tests/listview_tests.py b/src/idea/tests/listview_tests.py index f88b6ad..d9c7927 100644 --- a/src/idea/tests/listview_tests.py +++ b/src/idea/tests/listview_tests.py @@ -1,4 +1,4 @@ -from datetime import datetime +import datetime from django.contrib.auth.models import User from django.utils.timezone import get_default_timezone from django.contrib.comments.models import Comment @@ -10,6 +10,16 @@ from mock import patch import string +def get_relative_date(delta_days=0): + return datetime.date.today() + datetime.timedelta(days=delta_days) + +def create_banner(title, delta_days=0): + banner = models.Banner(title=title, text=title+' Text', + start_date=get_relative_date(-delta_days), + end_date=get_relative_date(delta_days)) + banner.save() + return banner + class ListViewTest(TestCase): """ Tests for idea.views.list @@ -56,7 +66,7 @@ def test_sort_recent(self, render): Verify that the recent sort params works. """ def add_time(kwargs, nonce): - kwargs['time'] = datetime(2013, 1, nonce, tzinfo=get_default_timezone()) + kwargs['time'] = datetime.datetime(2013, 1, nonce, tzinfo=get_default_timezone()) self._generate_data(paramfn=add_time) views.list(mock_req(), sort_or_state='recent') self._verify_order(render) @@ -71,20 +81,20 @@ def test_sort_trending(self, render): def add_time(kwargs, nonce): # add future timestamp for item 3 if nonce == 3: - kwargs['time'] = datetime(2050, 1, nonce, tzinfo=get_default_timezone()) + kwargs['time'] = datetime.datetime(2050, 1, nonce, tzinfo=get_default_timezone()) def create_timestamp_event(idea, nonce): # add future timestamp for vote for items 0, 2, 4 if nonce % 2 == 0: models.Vote(creator=idea.creator, idea=idea, - time=datetime(2050, 1, nonce, tzinfo=get_default_timezone()) + time=datetime.datetime(2050, 1, nonce, tzinfo=get_default_timezone()) ).save() # add future timestamp for comment for items 1, 5 elif nonce != 3: Comment(content_type=idea_type, site=site, object_pk=idea.pk, user=idea.creator, comment='Blah', - submit_date=datetime(2050, 1, nonce, tzinfo=get_default_timezone()) + submit_date=datetime.datetime(2050, 1, nonce, tzinfo=get_default_timezone()) ).save() self._generate_data(postfn=create_timestamp_event, paramfn=add_time) views.list(mock_req(), sort_or_state='trending') @@ -195,6 +205,48 @@ def check_state(kwargs, nonce): self.assertTrue('ideas' in context) self.assertEqual(1, len(context['ideas'])) + @patch('idea.views.render') + def test_current_banner(self, render): + """ + Check that the current banner is populated + """ + views.list(mock_req()) + context = render.call_args[0][2] + self.assertTrue('banner' in context) + self.assertIsNone(context['banner']) + + banner = create_banner('AAAA') + views.list(mock_req()) + context = render.call_args[0][2] + self.assertTrue('banner' in context) + self.assertEqual(context['banner'], banner) + + @patch('idea.views.render') + def test_browse_banners(self, render): + """ + Check that the banner list is populated if more than one active banner + """ + views.list(mock_req()) + context = render.call_args[0][2] + self.assertTrue('browse_banners' in context) + self.assertIsNone(context['browse_banners']) + + create_banner('AAAA', 3) + create_banner('BBBB', 2) + create_banner('CCCC', 1) + create_banner('DDDD', 6) + create_banner('EEEE', 5) + create_banner('FFFF', 4) + views.list(mock_req()) + context = render.call_args[0][2] + import pdb; pdb.set_trace() + self.assertTrue('browse_banners' in context) + self.assertEqual(len(context['browse_banners']), 4) + self.assertEqual(context['browse_banners'][0].title, 'BBBB') + self.assertEqual(context['browse_banners'][1].title, 'AAAA') + self.assertEqual(context['browse_banners'][2].title, 'FFFF') + self.assertEqual(context['browse_banners'][3].title, 'EEEE') + @patch('idea.views.render') def test_tags_exist(self, render): """ diff --git a/src/idea/views.py b/src/idea/views.py index 6a4e05c..f11fffa 100644 --- a/src/idea/views.py +++ b/src/idea/views.py @@ -42,24 +42,19 @@ def get_current_banners(additional_ids_list=None): banner_filter = (start_date&end_date) if additional_ids_list: banner_filter = banner_filter|Q(id__in=additional_ids_list) - return Banner.objects.filter(banner_filter).order_by('end_date') + banners = Banner.objects.filter(banner_filter) + # Banners with null end_date should be at the end + banners = banners.extra(select={'null_end_date': 'CASE WHEN idea_banner.end_date IS NULL THEN 0 ELSE 1 END'}) + banners = banners.order_by('-null_end_date', 'end_date') + return banners def get_banner(): - today = date.today() - timed_banners = Banner.objects.filter(start_date__lte=today, - end_date__isnull=False, - end_date__gt=today).order_by('end_date') - - if timed_banners: - return timed_banners[0] + banners = get_current_banners() + if banners: + return banners[0] else: - indefinite_banners = Banner.objects.filter(start_date__lte=today, - end_date__isnull=True) - if indefinite_banners: - return indefinite_banners[0] - else: - return None + return None @login_required @@ -121,9 +116,12 @@ def list(request, sort_or_state=None): args=(sort_or_state,)), tag_slugs) - banner = get_banner() + banner = None + browse_banners = None current_banners = get_current_banners() - browse_banners = current_banners[1:current_banners.count()] + if current_banners: + banner = current_banners[0] + browse_banners = current_banners[1:5] try: about_text = Config.objects.get(key="list_about").value except Config.DoesNotExist: @@ -141,10 +139,7 @@ def list(request, sort_or_state=None): @login_required def banner_list(request): current_banners = get_current_banners() - start_date = Q(start_date__lt=date.today()) - end_date = Q(end_date__lt=date.today())|Q(end_date__isnull=True) - banner_filter = (start_date&end_date) - past_banners = Banner.objects.filter(banner_filter).order_by('end_date') + past_banners = Banner.objects.filter(end_date__lt=date.today()).order_by('end_date') return _render(request, 'idea/banner_list.html', { 'current_banners': current_banners, 'past_banners': past_banners, From c7e4b2262a82a3308e48e29fa4fd1f1220096f7e Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Mon, 23 Jun 2014 16:27:07 +0000 Subject: [PATCH 159/230] IN-600 - Adjust sidebar banner list style --- src/idea/static/idea/css/idea.css | 9 ++++++++- src/idea/templates/idea/list.html | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/idea/static/idea/css/idea.css b/src/idea/static/idea/css/idea.css index bab1dfa..0a6969b 100644 --- a/src/idea/static/idea/css/idea.css +++ b/src/idea/static/idea/css/idea.css @@ -332,10 +332,11 @@ ul.section-nav li.active a { margin-bottom: 2.5em; } .sidebar .challenge-banner { - margin-bottom: 2.5em; + margin-bottom: 1.25em; background-color: #DBEDD4; padding: 1.25em; } +.sidebar a, #challenge-link a { border-bottom-width: 1px; } @@ -354,6 +355,12 @@ ul.section-nav li.active a { font-family: FontAwesome, "Avenir Next", Arial, sans-serif; margin-left: .25em; } +.banners { + margin-bottom: 1.25em; +} +.banners .challenge { + margin: .5em 0; +} .challenge_view_all a:after { content: "\f054"; font-family: FontAwesome, "Avenir Next", Arial, sans-serif; diff --git a/src/idea/templates/idea/list.html b/src/idea/templates/idea/list.html index bbd5ad3..da826eb 100644 --- a/src/idea/templates/idea/list.html +++ b/src/idea/templates/idea/list.html @@ -119,7 +119,7 @@

          Browse by Challenge:

          {% endfor %}
        • - +
        From 1be086ffb3945dfa1dd65a7f81208b89ca569acc Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Mon, 23 Jun 2014 16:28:19 +0000 Subject: [PATCH 160/230] IN-600 - Remove unwanted pdb breakpoint in tests --- src/idea/tests/listview_tests.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/idea/tests/listview_tests.py b/src/idea/tests/listview_tests.py index d9c7927..4a84fb2 100644 --- a/src/idea/tests/listview_tests.py +++ b/src/idea/tests/listview_tests.py @@ -239,7 +239,6 @@ def test_browse_banners(self, render): create_banner('FFFF', 4) views.list(mock_req()) context = render.call_args[0][2] - import pdb; pdb.set_trace() self.assertTrue('browse_banners' in context) self.assertEqual(len(context['browse_banners']), 4) self.assertEqual(context['browse_banners'][0].title, 'BBBB') From e71ab4c55917162b26c13a59effbaeb89eaea976 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Mon, 23 Jun 2014 18:34:45 +0000 Subject: [PATCH 161/230] Revert 75e9d6a --- src/idea/templates/idea/list.html | 2 +- src/idea/views.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/idea/templates/idea/list.html b/src/idea/templates/idea/list.html index 5d459d7..998546e 100644 --- a/src/idea/templates/idea/list.html +++ b/src/idea/templates/idea/list.html @@ -12,7 +12,7 @@

        IdeaBox

        -

        {{ about_text|urlize }}

        +

        {{ about_text|safe }}

      diff --git a/src/idea/views.py b/src/idea/views.py index f1a0fbd..13357d0 100644 --- a/src/idea/views.py +++ b/src/idea/views.py @@ -123,7 +123,9 @@ def list(request, sort_or_state=None): banner = get_banner() try: - about_text = Config.objects.get(key="list_about").value + about_text = Config.objects.get( + key="list_about").value.replace('','') except Config.DoesNotExist: about_text = "" From e3a79f8db686e037a73078353f116996de63cf5c Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Mon, 23 Jun 2014 19:30:31 +0000 Subject: [PATCH 162/230] links in description under the title should be underlined --- src/idea/static/idea/css/idea.css | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/idea/static/idea/css/idea.css b/src/idea/static/idea/css/idea.css index 368c282..b6d0e00 100644 --- a/src/idea/static/idea/css/idea.css +++ b/src/idea/static/idea/css/idea.css @@ -477,9 +477,6 @@ ul.section-nav li.active a { .idea-hero { margin: 0 0 0 0; } -.idea-hero a { - border-bottom-width: 0; -} .main-content a { border-bottom-width: 1px; } From 137ad41560fd63dec93a207af311bee8e77a24a0 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Tue, 24 Jun 2014 16:15:26 +0000 Subject: [PATCH 163/230] Update capitalization of titles and proper nouns --- src/idea/static/idea/css/idea.css | 2 +- src/idea/templates/idea/add.html | 4 ++-- src/idea/templates/idea/add_success.html | 16 ++++++++-------- src/idea/templates/idea/banner_detail.html | 8 ++++---- src/idea/templates/idea/banner_list.html | 4 ++-- src/idea/templates/idea/detail.html | 2 +- src/idea/templates/idea/edit.html | 2 +- src/idea/templates/idea/list.html | 8 ++++---- 8 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/idea/static/idea/css/idea.css b/src/idea/static/idea/css/idea.css index cbd4e2b..5cbc59c 100644 --- a/src/idea/static/idea/css/idea.css +++ b/src/idea/static/idea/css/idea.css @@ -111,7 +111,7 @@ textarea.populated { line-height: 1.57142857142857em; } .project-title a:before { - content: "\f053\00a0 Back To "; + content: "\f053\00a0 Back to "; font-family: FontAwesome, "Avenir Next Medium", Arial, sans-serif; margin-right: 0; } diff --git a/src/idea/templates/idea/add.html b/src/idea/templates/idea/add.html index 98a8aca..9e38585 100644 --- a/src/idea/templates/idea/add.html +++ b/src/idea/templates/idea/add.html @@ -16,7 +16,7 @@
      -

      Share your idea

      +

      Share your Idea

      Toggle instructions for each field.
      {% csrf_token %} @@ -50,7 +50,7 @@

      Share your idea

      {% endfor %} {% block "submit-buttons" %} - + {% endblock %} diff --git a/src/idea/templates/idea/add_success.html b/src/idea/templates/idea/add_success.html index a1999c3..8384932 100644 --- a/src/idea/templates/idea/add_success.html +++ b/src/idea/templates/idea/add_success.html @@ -9,7 +9,7 @@
      -

      Thanks for sharing your idea

      +

      Thanks for sharing your Idea

      What happens next:
      @@ -18,7 +18,7 @@

      Watch the Likes go up

      - Gauge support for your idea by watching who has Liked it. Don’t + Gauge support for your Idea by watching who has Liked it. Don’t worry, people can’t unlike it.
      @@ -30,7 +30,7 @@

      Start a conversation yourself, or join it by responding to any questions or comments others may have. You'll receive a notification - when someone comments on your idea or responds to a comment. + when someone comments on your Idea or responds to a comment.

      @@ -39,8 +39,8 @@

      Add tags to find support or collaboration

      - You or anyone can add more tags to your idea. This helps filter and - group similar ideas, and find areas that people are talking the most + You or anyone can add more tags to your Idea. This helps filter and + group similar Ideas, and find areas that people are talking the most about.
      @@ -50,8 +50,8 @@

      Show your support

      - Check out the ideas submitted by your colleagues on the IdeaBox landing - page. You may find potential collaborators for your own ideas by showing + Check out the Ideas submitted by your colleagues on the IdeaBox landing + page. You may find potential collaborators for your own Ideas by showing your support.
      @@ -62,7 +62,7 @@

      diff --git a/src/idea/templates/idea/banner_detail.html b/src/idea/templates/idea/banner_detail.html index 98aeb3b..8fbc55f 100644 --- a/src/idea/templates/idea/banner_detail.html +++ b/src/idea/templates/idea/banner_detail.html @@ -22,7 +22,7 @@

      {{banner.title}}

      {{banner.text}}

      {% if is_current_banner %} - + {% endif %}
      Challenge start date: {{banner.start_date|date:"SHORT_DATE_FORMAT"}} @@ -61,7 +61,7 @@

      {{banner.title}}

      {{ idea.summary|truncatechars:250 }} - Read More  + Read more 
      @@ -76,7 +76,7 @@

      {{banner.title}}

      {% else %}
      -

      There are no ideas to display.

      +

      There are no Ideas to display.

      {% endif %} @@ -103,7 +103,7 @@

      {{banner.title}}

      From eb3f5b2e0e1a7d7043571bedecf563321a878016 Mon Sep 17 00:00:00 2001 From: Seph Coster Date: Wed, 2 Jul 2014 13:58:07 -0400 Subject: [PATCH 177/230] Updated chevron styles, fonts --- src/idea/static/idea/css/idea.css | 14 -------------- src/idea/templates/idea/banner_detail.html | 2 +- src/idea/templates/idea/detail.html | 4 +--- src/idea/templates/idea/list.html | 6 +++--- 4 files changed, 5 insertions(+), 21 deletions(-) diff --git a/src/idea/static/idea/css/idea.css b/src/idea/static/idea/css/idea.css index 739ef6f..8e1a9cc 100644 --- a/src/idea/static/idea/css/idea.css +++ b/src/idea/static/idea/css/idea.css @@ -114,7 +114,6 @@ textarea.populated { color: #0072CE; border-bottom: 1px dotted; line-height: 1em; - font-family: FontAwesome, "Avenir Next Medium", Arial, sans-serif; display: inline-block; margin-left: 1em; } @@ -380,19 +379,6 @@ ul.section-nav li.active a { margin-bottom: 0; } -#challenge-link a { - position: relative; -} - -#challenge-link a:after { - content: "\f054"; - font-family: FontAwesome, "Avenir Next", Arial, sans-serif; - margin-left: .25em; - border: none; - position: absolute; - left: 100%; - top:0; -} .banners { margin-bottom: 1.25em; } diff --git a/src/idea/templates/idea/banner_detail.html b/src/idea/templates/idea/banner_detail.html index 8fbc55f..5dcd48a 100644 --- a/src/idea/templates/idea/banner_detail.html +++ b/src/idea/templates/idea/banner_detail.html @@ -22,7 +22,7 @@

      {{banner.title}}

      {{banner.text}}

      {% if is_current_banner %} - + {% endif %}
      Challenge start date: {{banner.start_date|date:"SHORT_DATE_FORMAT"}} diff --git a/src/idea/templates/idea/detail.html b/src/idea/templates/idea/detail.html index bfd7bff..b6c61aa 100644 --- a/src/idea/templates/idea/detail.html +++ b/src/idea/templates/idea/detail.html @@ -133,9 +133,7 @@

      Liked by

      {% endif %} {% endfor %} - {% if voters.count < 11 %} - - {% else %} + {% if voters.count > 10 %}

      {{voters|length}} Like{{voters|pluralize:",s"}}

      {% endif %} diff --git a/src/idea/templates/idea/list.html b/src/idea/templates/idea/list.html index 87e00a4..9f05bf1 100644 --- a/src/idea/templates/idea/list.html +++ b/src/idea/templates/idea/list.html @@ -58,8 +58,8 @@

      Main Navigation for IdeaBox

      {{ idea.summary|truncatechars:250 }} - - Read more + + Read more
      - Ideas with the greatest interest will receive attention from Project Boldness. + Ideas with the greatest interest will receive attention from the IdeaBox team.
      From 3f6fbf51b722bd138c747b6cb8bfc28792f39eee Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Thu, 16 Oct 2014 14:57:42 +0000 Subject: [PATCH 197/230] Add comments for why similar is being disabled --- src/idea/views.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/idea/views.py b/src/idea/views.py index e84ceb7..2ae4fcd 100644 --- a/src/idea/views.py +++ b/src/idea/views.py @@ -335,6 +335,16 @@ def add_idea(request, banner_id=None): form.fields["banner"].queryset = current_banners return _render(request, 'idea/add.html', { 'form': form, + # Similar keywords was used to provide suggestions for idea titles + # (or notify you that an idea already exists) when creating a new + # idea. This functionality was removed from the front end a while + # ago when we redesigned the Add Idea page. As such, the similar + # object is not currently being utilized. + # + # The specific reason why this is being removed right now is we've + # seen instances where elasticsearch complains about too many + # requests which results in a 500 error when clicking the + # "Add Idea" button. # 'similar': [r.object for r in more_like_text(idea_title, Idea)] }) From 819ef857c3cda8594f67d91a9a10ed0a702a25f9 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Thu, 16 Oct 2014 20:29:28 +0000 Subject: [PATCH 198/230] Update README with info on the sample style --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index f33cf34..d4aa2f3 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,26 @@ this to link users to a profile page. This is specified through the AUTH_PROFILE_MODULE setting. Your profile module will also have to specify a get_absolute_url() method. +### CSS + +The default CSS style for Idea-Box is not ready for production. This was done +intentionally so that Idea-Box's style can match the style of the platform in +which it resides (i.e. not everyone wants a green header). The simplest way to +improve the styling is to source `src/idea/static/idea/css/sample_style.css` +in the `css_files` block in the `/src/idea/templates/idea/idea-base.html` template: + +``` +{% block "css_files" %} + # ... existing code ... + +{% endblock %} +``` + +Ideally, your Django platform will provide styles that can be sourced in the +`base.html` template described in the section above so Idea-Box can match the +look and feel of your system. + + ### Buildout To use buildout, run the following: ```bash From 6427dc5cd7aedb87b0ed088eb9457e5f8483d936 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Thu, 16 Oct 2014 13:14:35 +0000 Subject: [PATCH 199/230] Remove dependencies on Collab's core.collabuser and core.taggit --- buildout.cfg | 1 + setup.py | 4 +- src/idea/buildout/testsettings.py | 10 ++-- src/idea/fixtures/core-test-fixtures.json | 20 -------- src/idea/forms.py | 5 +- ...ld_vote_creator__chg_field_idea_creator.py | 46 ++++++++++++------- .../0018_auto__chg_field_banner_text.py | 38 +++++++-------- src/idea/models.py | 4 +- src/idea/tests/addidea_tests.py | 14 +++--- src/idea/tests/editidea_tests.py | 7 ++- src/idea/tests/model_tests.py | 5 +- src/idea/tests/smoke_tests.py | 7 ++- src/idea/tests/tag_tests.py | 12 +++-- src/idea/tests/utils.py | 3 ++ src/idea/tests/voting_tests.py | 5 +- src/idea/views.py | 4 +- 16 files changed, 100 insertions(+), 85 deletions(-) delete mode 100644 src/idea/fixtures/core-test-fixtures.json diff --git a/buildout.cfg b/buildout.cfg index b8e985b..026124d 100644 --- a/buildout.cfg +++ b/buildout.cfg @@ -35,3 +35,4 @@ eggs = ${buildout:eggs} pysolr django-cache-tools pillow + django-taggit diff --git a/setup.py b/setup.py index 593b9c0..22049f8 100644 --- a/setup.py +++ b/setup.py @@ -2,11 +2,11 @@ setup( name = "idea-collection", - version = "0.1.0", + version = "0.2.0", #url = "TBD", license = "public domain", description = "An idea collection tool for django", - author = "Jui Dai, Jennifer Ehlers, David Kennedy, Shashank Khandelwal, CM Lubinski", + author = "Jui Dai, Jennifer Ehlers, David Kennedy, Shashank Khandelwal, CM Lubinski, Mike Brown", packages = find_packages('src'), package_dir = {'':'src'}, install_requires = ['setuptools'], diff --git a/src/idea/buildout/testsettings.py b/src/idea/buildout/testsettings.py index 7dd562c..f0ac944 100644 --- a/src/idea/buildout/testsettings.py +++ b/src/idea/buildout/testsettings.py @@ -36,8 +36,8 @@ 'django.contrib.comments', 'django.contrib.staticfiles', 'mptt', - 'core', # collab core - 'core.taggit', + 'core.custom_comments', # from Collab + 'taggit', ] ROOT_URLCONF = 'idea.buildout.urls' @@ -56,4 +56,8 @@ TEST_RUNNER = 'django_nose.runner.NoseTestSuiteRunner' -AUTH_USER_MODEL = 'core.CollabUser' +SOUTH_MIGRATION_MODULES = { + 'taggit': 'taggit.south_migrations', +} + +COMMENTS_APP = 'core.custom_comments' diff --git a/src/idea/fixtures/core-test-fixtures.json b/src/idea/fixtures/core-test-fixtures.json deleted file mode 100644 index 5afce07..0000000 --- a/src/idea/fixtures/core-test-fixtures.json +++ /dev/null @@ -1,20 +0,0 @@ -[ - { - "pk": 1, - "model": "core.CollabUser", - "fields": { - "username": "test1@example.com", - "first_name": "John", - "last_name": "Smith", - "is_active": true, - "is_superuser": true, - "is_staff": true, - "last_login": "2012-08-28T14:11:00Z", - "groups": [], - "user_permissions": [], - "password": "pbkdf2_sha256$10000$ggAKkiHobFL8$xQzwPeHNX1vWr9uNmZ/gKbd17uLGZVM8QNcgmaIEAUs=", - "email": "test1.1@example.com", - "date_joined": "2012-03-01T16:03:14Z" - } - } -] diff --git a/src/idea/forms.py b/src/idea/forms.py index ea69300..b7059fc 100644 --- a/src/idea/forms.py +++ b/src/idea/forms.py @@ -1,9 +1,10 @@ from django import forms +from django.conf import settings from idea.models import Idea -try: +if 'core.taggit' in settings.INSTALLED_APPS: from core.taggit.utils import add_tags -except ImportError: +else: pass diff --git a/src/idea/migrations/0017_auto__chg_field_vote_creator__chg_field_idea_creator.py b/src/idea/migrations/0017_auto__chg_field_vote_creator__chg_field_idea_creator.py index fe2398f..8445af8 100644 --- a/src/idea/migrations/0017_auto__chg_field_vote_creator__chg_field_idea_creator.py +++ b/src/idea/migrations/0017_auto__chg_field_vote_creator__chg_field_idea_creator.py @@ -4,16 +4,28 @@ from south.v2 import SchemaMigration from django.db import models +# custom user model workaround from http://kevindias.com/writing/django-custom-user-models-south-and-reusable-apps/ +try: + from django.contrib.auth import get_user_model +except ImportError: + from django.contrib.auth.models import User +else: + User = get_user_model() + +# With the default User model these will be 'auth.User' and 'auth.user' +# so instead of using orm['auth.User'] we can use orm[user_orm_label] +user_orm_label = '%s.%s' % (User._meta.app_label, User._meta.object_name) +user_model_label = '%s.%s' % (User._meta.app_label, User._meta.module_name) class Migration(SchemaMigration): def forwards(self, orm): # Changing field 'Vote.creator' - db.alter_column(u'idea_vote', 'creator_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['core.CollabUser'])) + db.alter_column(u'idea_vote', 'creator_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm[user_orm_label])) # Changing field 'Idea.creator' - db.alter_column(u'idea_idea', 'creator_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['core.CollabUser'])) + db.alter_column(u'idea_idea', 'creator_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm[user_orm_label])) def backwards(self, orm): @@ -37,20 +49,13 @@ def backwards(self, orm): u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) }, - u'contenttypes.contenttype': { - 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) - }, - u'core.collabuser': { - 'Meta': {'object_name': 'CollabUser'}, + user_model_label: { + 'Meta': {'object_name': User.__name__, 'db_table': "'%s'" % User._meta.db_table}, 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 'email': ('django.db.models.fields.EmailField', [], {'max_length': '254', 'blank': 'True'}), 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '75', 'blank': 'True'}), 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + User._meta.pk.column: ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), @@ -60,6 +65,13 @@ def backwards(self, orm): 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '75'}) }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, u'idea.banner': { 'Meta': {'object_name': 'Banner'}, 'end_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), @@ -77,14 +89,14 @@ def backwards(self, orm): u'idea.idea': { 'Meta': {'object_name': 'Idea'}, 'banner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['idea.Banner']", 'null': 'True', 'blank': 'True'}), - 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['core.CollabUser']"}), + 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['%s']" % user_orm_label}), u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['idea.State']"}), 'summary': ('django.db.models.fields.TextField', [], {'max_length': '200'}), 'text': ('django.db.models.fields.TextField', [], {'max_length': '2000'}), 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 6, 30, 0, 0)'}), 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}), - 'voters': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'idea_vote_creator'", 'null': 'True', 'through': u"orm['idea.Vote']", 'to': u"orm['core.CollabUser']"}) + 'voters': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'idea_vote_creator'", 'null': 'True', 'through': u"orm['idea.Vote']", 'to': u"orm['%s']" % user_orm_label}) }, u'idea.state': { 'Meta': {'object_name': 'State'}, @@ -94,7 +106,7 @@ def backwards(self, orm): }, u'idea.vote': { 'Meta': {'object_name': 'Vote'}, - 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['core.CollabUser']"}), + 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['%s']" % user_orm_label}), u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'idea': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['idea.Idea']"}), 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 6, 30, 0, 0)'}), @@ -120,8 +132,8 @@ def backwards(self, orm): 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_items'", 'to': u"orm['taggit.Tag']"}), 'tag_category': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['taggit.TagCategory']", 'null': 'True'}), - 'tag_creator': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_related'", 'null': 'True', 'to': u"orm['core.CollabUser']"}) + 'tag_creator': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_related'", 'null': 'True', 'to': u"orm['%s']" % user_orm_label}) } } - complete_apps = ['idea'] \ No newline at end of file + complete_apps = ['idea'] diff --git a/src/idea/migrations/0018_auto__chg_field_banner_text.py b/src/idea/migrations/0018_auto__chg_field_banner_text.py index e4e4058..5a5042e 100644 --- a/src/idea/migrations/0018_auto__chg_field_banner_text.py +++ b/src/idea/migrations/0018_auto__chg_field_banner_text.py @@ -31,28 +31,28 @@ def backwards(self, orm): u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) }, - u'contenttypes.contenttype': { - 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) - }, - u'core.collabuser': { - 'Meta': {'object_name': 'CollabUser'}, + u'auth.user': { + 'Meta': {'object_name': 'User'}, 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '254', 'blank': 'True'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '75', 'blank': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '75', 'blank': 'True'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '75'}) + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) }, u'idea.banner': { 'Meta': {'object_name': 'Banner'}, @@ -71,14 +71,14 @@ def backwards(self, orm): u'idea.idea': { 'Meta': {'object_name': 'Idea'}, 'banner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['idea.Banner']", 'null': 'True', 'blank': 'True'}), - 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['core.CollabUser']"}), + 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}), u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['idea.State']"}), 'summary': ('django.db.models.fields.TextField', [], {'max_length': '200'}), 'text': ('django.db.models.fields.TextField', [], {'max_length': '2000'}), - 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 9, 9, 0, 0)'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 10, 15, 0, 0)'}), 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}), - 'voters': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'idea_vote_creator'", 'null': 'True', 'through': u"orm['idea.Vote']", 'to': u"orm['core.CollabUser']"}) + 'voters': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'idea_vote_creator'", 'null': 'True', 'through': u"orm['idea.Vote']", 'to': u"orm['auth.User']"}) }, u'idea.state': { 'Meta': {'object_name': 'State'}, @@ -88,10 +88,10 @@ def backwards(self, orm): }, u'idea.vote': { 'Meta': {'object_name': 'Vote'}, - 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['core.CollabUser']"}), + 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}), u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'idea': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['idea.Idea']"}), - 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 9, 9, 0, 0)'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 10, 15, 0, 0)'}), 'vote': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}) }, u'taggit.tag': { @@ -114,7 +114,7 @@ def backwards(self, orm): 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_items'", 'to': u"orm['taggit.Tag']"}), 'tag_category': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['taggit.TagCategory']", 'null': 'True'}), - 'tag_creator': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_related'", 'null': 'True', 'to': u"orm['core.CollabUser']"}) + 'tag_creator': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_related'", 'null': 'True', 'to': u"orm['auth.User']"}) } } diff --git a/src/idea/models.py b/src/idea/models.py index 5aa4064..4ad42cb 100644 --- a/src/idea/models.py +++ b/src/idea/models.py @@ -8,9 +8,9 @@ from django.db import models from django.utils.timezone import get_default_timezone -try: +if 'core.taggit' in settings.INSTALLED_APPS: from core.taggit.managers import TaggableManager -except ImportError: +else: from taggit.managers import TaggableManager class UserTrackable(models.Model): diff --git a/src/idea/tests/addidea_tests.py b/src/idea/tests/addidea_tests.py index 4f7fb6d..2d4a5ab 100644 --- a/src/idea/tests/addidea_tests.py +++ b/src/idea/tests/addidea_tests.py @@ -1,28 +1,30 @@ from django.contrib.auth import get_user_model from django.core.urlresolvers import reverse +from django.conf import settings from django.test import TestCase from django.utils import unittest from haystack import connections from idea import models, views -from idea.tests.utils import mock_req, random_user, login +from idea.tests.utils import mock_req, random_user, login, create_superuser from mock import patch from datetime import date, timedelta from idea.forms import IdeaForm -try: +if 'core.taggit' in settings.INSTALLED_APPS: from core.taggit.utils import add_tags from core.taggit.models import TaggedItem COLLAB_TAGS = True; -except ImportError: +else: COLLAB_TAGS = False; class AddIdeaTest(TestCase): - # core-test-fixtures required for integration with Collab - fixtures = ['state', 'core-test-fixtures'] + fixtures = ['state'] + + def setUp(self): + create_superuser() def test_good_idea(self): """ Test an normal POST submission to add an idea. """ - login(self) self.assertEquals(models.Idea.objects.all().count(), 0) num_voters = get_user_model().objects.filter(vote__idea__pk=1, vote__vote=1).count() diff --git a/src/idea/tests/editidea_tests.py b/src/idea/tests/editidea_tests.py index dbb3c72..5c4f204 100644 --- a/src/idea/tests/editidea_tests.py +++ b/src/idea/tests/editidea_tests.py @@ -2,7 +2,7 @@ from django.core.urlresolvers import reverse from django.test import TestCase from idea import models -from idea.tests.utils import random_user, login +from idea.tests.utils import random_user, login, create_superuser from datetime import date def create_idea(user=None): @@ -20,7 +20,10 @@ def create_idea(user=None): return idea class AddIdeaTest(TestCase): - fixtures = ['state', 'core-test-fixtures'] + fixtures = ['state'] + + def setUp(self): + create_superuser() def test_edit_good_idea(self): """ Test an normal POST submission to edit an idea. """ diff --git a/src/idea/tests/model_tests.py b/src/idea/tests/model_tests.py index d7575aa..b2f665a 100644 --- a/src/idea/tests/model_tests.py +++ b/src/idea/tests/model_tests.py @@ -1,14 +1,15 @@ from django.test import TestCase from idea import models -from idea.tests.utils import random_user +from idea.tests.utils import random_user, create_superuser from django.contrib.auth import get_user_model from django.contrib.comments.models import Comment class VotingTests(TestCase): - fixtures = ['state', 'core-test-fixtures'] + fixtures = ['state'] def setUp(self): + create_superuser() self.state = models.State.objects.get(name='Active') def test_members(self): diff --git a/src/idea/tests/smoke_tests.py b/src/idea/tests/smoke_tests.py index 874f643..bde4fcf 100644 --- a/src/idea/tests/smoke_tests.py +++ b/src/idea/tests/smoke_tests.py @@ -5,12 +5,15 @@ from exam.cases import Exam from django.core.urlresolvers import reverse from django.contrib.auth import get_user_model -from idea.tests.utils import get_login_user +from idea.tests.utils import get_login_user, create_superuser class SmokeTest(Exam, WebTest): csrf_checks = False - fixtures = ['state', 'core-test-fixtures'] + fixtures = ['state'] + + def setUp(self): + create_superuser() @fixture def user(self): diff --git a/src/idea/tests/tag_tests.py b/src/idea/tests/tag_tests.py index ecd81dd..8949977 100644 --- a/src/idea/tests/tag_tests.py +++ b/src/idea/tests/tag_tests.py @@ -1,18 +1,22 @@ from django.contrib.auth import get_user_model from django.core.urlresolvers import reverse +from django.conf import settings from django.test import TestCase from django.utils import unittest from django.test.client import RequestFactory from idea import models, views -from idea.tests.utils import random_user, login -try: +from idea.tests.utils import random_user, login, create_superuser +if 'core.taggit' in settings.INSTALLED_APPS: from core.taggit.utils import add_tags COLLAB_TAGS = True; -except ImportError: +else: COLLAB_TAGS = False; class TagTest(TestCase): - fixtures = ['state', 'core-test-fixtures'] + fixtures = ['state'] + + def setUp(self): + create_superuser() @unittest.skipIf(COLLAB_TAGS == False, "Remove only works with collab's core.taggit") def test_tag_remove_exists_for_creator(self): diff --git a/src/idea/tests/utils.py b/src/idea/tests/utils.py index dca3796..469fe68 100644 --- a/src/idea/tests/utils.py +++ b/src/idea/tests/utils.py @@ -7,6 +7,9 @@ def random_user(): return get_user_model().objects.create_user( ''.join(random.choice(string.lowercase) for _ in range(12))) +def create_superuser(): + get_user_model().objects.create_superuser('test1@example.com', 'test1@example.com', '1') + def get_login_user(): # required for Collab integration if get_user_model().objects.filter(username='test1@example.com').exists(): diff --git a/src/idea/tests/voting_tests.py b/src/idea/tests/voting_tests.py index 28fefca..cc21e07 100644 --- a/src/idea/tests/voting_tests.py +++ b/src/idea/tests/voting_tests.py @@ -1,13 +1,14 @@ from django.core.urlresolvers import reverse from django.test import TestCase from idea import models -from idea.tests.utils import random_user, login +from idea.tests.utils import random_user, login, create_superuser class VotingTests(TestCase): - fixtures = ['state', 'core-test-fixtures'] + fixtures = ['state'] def setUp(self): + create_superuser() self.state = models.State.objects.get(name='Active') def test_good_vote(self): diff --git a/src/idea/views.py b/src/idea/views.py index 2ae4fcd..7a3316b 100644 --- a/src/idea/views.py +++ b/src/idea/views.py @@ -18,11 +18,11 @@ from idea.utility import state_helper from idea.models import UP_VOTE -try: +if 'core.taggit' in settings.INSTALLED_APPS: from core.taggit.models import Tag, TaggedItem from core.taggit.utils import add_tags COLLAB_TAGS = True -except ImportError: +else: from taggit.models import Tag COLLAB_TAGS = False From 8626873cecbbbca1c52e2bc34b47db68c8961bc4 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Fri, 17 Oct 2014 00:25:30 +0000 Subject: [PATCH 200/230] Remove haystack/search functionality as it is not being used --- README.md | 77 ++++++---------- buildout.cfg | 2 - src/idea/buildout/search_sites.py | 2 - src/idea/buildout/testsettings.py | 7 -- src/idea/search_indexes.py | 72 --------------- src/idea/templates/idea/detail.html | 4 + src/idea/tests/addidea_tests.py | 36 -------- src/idea/tests/search_tests.py | 132 ---------------------------- src/idea/views.py | 45 ---------- 9 files changed, 33 insertions(+), 344 deletions(-) delete mode 100644 src/idea/buildout/search_sites.py delete mode 100644 src/idea/search_indexes.py delete mode 100644 src/idea/tests/search_tests.py diff --git a/README.md b/README.md index d4aa2f3..cc8f185 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,13 @@ easily integrate-able interface. Idea-Box also takes a strong stance on transpar such that ideas, votes, etc. are tied to specific users. ## Features -* Searching * Idea Submission * Tagging (via taggit) * Voting * Comments -* Listing by recent, comment count, vote count -* Separate state for archived ideas -* Customizable banner for specific campaigns +* Listing by trending, likes, and recently added +* Archive/hide ideas +* Customizable challenges for specific campaigns ## Screen shot @@ -23,49 +22,37 @@ such that ideas, votes, etc. are tied to specific users. ## Requirements * django (1.5.4) - This is a django app, so you need django. -* django-haystack (2.0.0) - A mapper between django models and search -backends. -* pyelasticsearch - Library for communicating with elasticsearch. * django-taggit - A library for Tags within django * mock - A library for creating mock objects for testing. -* south - A library for schema and data migrations. +* south - A library for schema and data migrations. +* [collab](http://github.com/cfpb/collab) - Intranet platform, specifically needed for idea comments +* mptt - A library enabling nested/reply-to comments + +### Optional +* collab platform - Installing idea-box as an app inside a collab platform provides several convenience features: + * Autocomplete when adding new tags (requires elasticsearch server) + * User can delete tags he/she created + * Email notifications -* elasticsearch - A Search backend. Unfortunately, we currently require -elasticsearch (rather than another backend) because we need specific functionality -that haystack doesn't give us direct access to. Eventually, we'll get a -pull request to haystack which will reduce our elasticsearch requirement. ## Installation +* Use pip to install the dependencies listed above +* If not using collab as the Django platform, you still need to install collab for `custom_comments` + +``` +pip install git+https://github.com/cfpb/collab.git#egg=collab +``` + ### Settings File Modify your settings file to add the following apps: * django.contrib.comments -* haystack +* django-taggit +* south +* mptt +* core.custom_comments * idea -You will also need to configure haystack. See the haystack -[documentation](http://django-haystack.readthedocs.org/en/v1.2.7/tutorial.html#configuration) - -If you'd prefer to take the quick route, add the following to your -settings.py: -```python -HAYSTACK_CONNECTIONS = { - 'default': { - 'ENGINE': 'haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine', - 'URL': 'http://127.0.0.1:9200/', - 'INDEX_NAME': 'haystack', - }, -} -``` - -If you are going that route, make sure that you have a search_sites.py -module in the root of your project with something like the following: - -```python -import haystack -haystack.autodiscover() -``` - ### Folder Structure You will need to get the contents of the ```src/idea``` directory into @@ -82,24 +69,17 @@ mydjango_project/ ### URLs -Add the idea.urls, haystack.urls, and comments.urls to you url.py. For +Add the idea.urls and comments.urls to you url.py. For example: ```python if 'idea' in settings.INSTALLED_APPS and \ - 'django.contrib.comments' in settings.INSTALLED_APPS and\ - 'haystack' in settings.INSTALLED_APPS: - urlpatterns.append(url(r'^haystack/', include('haystack.urls'))) + 'django.contrib.comments' in settings.INSTALLED_APPS: urlpatterns.append(url(r'^comments/', include('django.contrib.comments.urls'))) urlpatterns.append(url(r'^idea/', include('idea.urls'))) ``` -### Elasticsearch - -You will need to have elasticsearch installed and running. You can use -[this guide](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/setup.html#setup-installation) to install it. - ### Migrations @@ -140,7 +120,8 @@ look and feel of your system. ### Buildout -To use buildout, run the following: +An alternative way to install the software is to use the buildout configuration. +To use buildout to create a working project, run the following: ```bash $ pip install zc.buildout distribute $ buildout @@ -149,7 +130,7 @@ Then, run the django binary in the ```bin``` directory. ### Campaign Banner -To create a campaign banner, use django's administrative page to add a Banner model. The -text field will be displayed at the top of the Idea-Box idea listing page. The banner +To create a challenge, use django's administrative page to add a Banner model object. The +text field will be displayed at the right of the Idea-Box idea listing page. The banner will only be displayed between Start Date and End Date (or indefinitely after the Start Date if the End Date is empty.) diff --git a/buildout.cfg b/buildout.cfg index 026124d..5b7d0de 100644 --- a/buildout.cfg +++ b/buildout.cfg @@ -27,12 +27,10 @@ eggs = ${buildout:eggs} mock south django-nose - django-haystack django-mptt exam webtest django-webtest - pysolr django-cache-tools pillow django-taggit diff --git a/src/idea/buildout/search_sites.py b/src/idea/buildout/search_sites.py deleted file mode 100644 index e8d4e13..0000000 --- a/src/idea/buildout/search_sites.py +++ /dev/null @@ -1,2 +0,0 @@ -import haystack -haystack.autodiscover() diff --git a/src/idea/buildout/testsettings.py b/src/idea/buildout/testsettings.py index f0ac944..dbadfb4 100644 --- a/src/idea/buildout/testsettings.py +++ b/src/idea/buildout/testsettings.py @@ -31,7 +31,6 @@ 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', - 'haystack', 'django_nose', 'django.contrib.comments', 'django.contrib.staticfiles', @@ -42,12 +41,6 @@ ROOT_URLCONF = 'idea.buildout.urls' -HAYSTACK_CONNECTIONS = { - 'default': { - 'ENGINE': 'haystack.backends.simple_backend.SimpleEngine', - }, -} - DEBUG = True STATIC_URL = '/static/' diff --git a/src/idea/search_indexes.py b/src/idea/search_indexes.py deleted file mode 100644 index f53239b..0000000 --- a/src/idea/search_indexes.py +++ /dev/null @@ -1,72 +0,0 @@ -from haystack import indexes -from models import Idea, Banner -from django.core.urlresolvers import reverse -from time import mktime, strptime - -class IdeaIndex(indexes.SearchIndex, indexes.Indexable): - text = indexes.EdgeNgramField(document=True, use_template=True) - display = indexes.CharField(model_attr='title') - description = indexes.CharField(model_attr="summary", null=True) - index_name = indexes.CharField(indexed=False) - index_priority = indexes.IntegerField(indexed=False) -# TODO causes all tests to fail -# index_sort = indexes.IntegerField(indexed=False, null=True) - url = indexes.CharField(indexed=False, null=True) - - PRIORITY = 4 - - def prepare_index_name(self, obj): - return "Ideas" - - def prepare_index_priority(self, obj): - return self.PRIORITY - - def prepare_index_sort(self, obj): - # want a positive number so banner results appear above/before ideas - #9999999999 =~ year 2286 - return 9999999999 - int(mktime(strptime(obj.recent_activity, "%Y-%m-%d %H:%M:%S"))) - - def prepare_url(self, obj): - return reverse('idea:idea_detail', args=(obj.id,)) - - def get_model(self): - return Idea - - def index_queryset(self, using=None): - """Used when the entire index for model is updated.""" - # State 2 = Archived - return self.get_model().objects.related_with_counts().exclude(state=2) - - -class BannerIndex(indexes.SearchIndex, indexes.Indexable): - text = indexes.EdgeNgramField(document=True, use_template=True) - display = indexes.CharField(model_attr='title') - description = indexes.CharField(model_attr="text") - index_name = indexes.CharField(indexed=False) - index_priority = indexes.IntegerField(indexed=False) - index_sort = indexes.IntegerField(indexed=False, null=True) - url = indexes.CharField(indexed=False, null=True) - - PRIORITY = 4 - - def prepare_index_name(self, obj): - return "Ideas" - - def prepare_index_priority(self, obj): - return self.PRIORITY - - def prepare_index_sort(self, obj): - return 0 - int(mktime(obj.start_date.timetuple())) - - def prepare_url(self, obj): - return reverse('idea:banner_detail', args=(obj.id,)) - - def prepare_display(self, obj): - return "Challenge: " + obj.title - - def get_model(self): - return Banner - - def index_queryset(self, using=None): - """Used when the entire index for model is updated.""" - return self.get_model().objects.all() diff --git a/src/idea/templates/idea/detail.html b/src/idea/templates/idea/detail.html index b387eee..54fbb47 100644 --- a/src/idea/templates/idea/detail.html +++ b/src/idea/templates/idea/detail.html @@ -165,6 +165,10 @@

      Idea Tags

      {% endblock %} +{% comment %} +Section js_ready calls search:model_tags_json, which is a collab +callback for autocomplete. This will not currently work without collab +{% endcomment %} {% block "js_ready" %} $(".tags_autocomplete").autocomplete({ source: "{% url "search:model_tags_json" "idea" %}", diff --git a/src/idea/tests/addidea_tests.py b/src/idea/tests/addidea_tests.py index 2d4a5ab..97afc8f 100644 --- a/src/idea/tests/addidea_tests.py +++ b/src/idea/tests/addidea_tests.py @@ -3,7 +3,6 @@ from django.conf import settings from django.test import TestCase from django.utils import unittest -from haystack import connections from idea import models, views from idea.tests.utils import mock_req, random_user, login, create_superuser from mock import patch @@ -69,41 +68,6 @@ def test_must_be_logged_in(self): self.assertIn('login', resp['Location']) self.assertEqual(models.Idea.objects.all().count(), 0) - @unittest.skip("the 'similar' functionality has been disabled") - @patch('idea.views.render') - def test_similar(self, render): - """ - List of similar ideas should make sense. - """ - class Mock(): - pass - with patch('idea.views.more_like_text') as mlt: - backend = connections['default'].get_backend() - backend.clear() - user = random_user() - state = models.State.objects.get(name='Active') - similar1 = models.Idea(creator=user, title='airplanes', state=state, - text="Title is enough said.") - similar1.save() - similar2 = models.Idea(creator=user, title='exexex', state=state, - text="I, too, love submarines.") - similar2.save() - - models.Idea(creator=user, title='AAAAAA', state=state, - text='BBBBBB').save() - - m1, m2 = Mock(), Mock() - m1.object = similar1 - m2.object = similar2 - mlt.return_value = [m1, m2] - - views.add_idea(mock_req('/?idea_title=' + - 'Airplanes%20and%20submarines')) - context = render.call_args[0][2] - self.assertTrue('similar' in context) - self.assertEqual(2, len(context['similar'])) - self.assertEqual(set(context['similar']), set([similar1, similar2])) - @unittest.skipIf(COLLAB_TAGS == False, "TaggedItem creator field requires collab's core.taggit") def test_tagged_item_creator(self): """ Test tag fields from a normal POST submission to add an idea. """ diff --git a/src/idea/tests/search_tests.py b/src/idea/tests/search_tests.py deleted file mode 100644 index c4442e4..0000000 --- a/src/idea/tests/search_tests.py +++ /dev/null @@ -1,132 +0,0 @@ -from django.test import TestCase -from django.test.client import RequestFactory -from django.utils import unittest -from haystack import connections -from idea import models, views -from idea.tests.utils import random_user -from datetime import date - -class SearchTest(TestCase): - fixtures = ['state'] - backend = connections['default'].get_backend() - backend_type = connections['default'].backend.__name__ - - def setUp(self): - if SearchTest.backend_type != 'SimpleSearchBackend': - self.backend.clear() - - def test_add_idea_title(self): - """ - Check that adding a new idea allows title to be immediately - searchable. - """ - req = RequestFactory().post('/', { - 'title':'example_title', - 'summary': 'test summary', - 'text': 'test text', - 'tags': 'test, tags' - }) - req.user = random_user() - views.add_idea(req) - results = self.backend.search('example_title') - self.assertEqual(1, results['hits']) - - def test_add_idea_summary(self): - """ - Check that adding a new idea allows title to be immediately - searchable. - """ - req = RequestFactory().post('/', { - 'title':'test title', - 'summary': 'example_summary', - 'text': 'test text', - 'tags': 'test, tags' - }) - req.user = random_user() - views.add_idea(req) - results = self.backend.search('example_summary') - self.assertEqual(1, results['hits']) - - def test_add_idea_text(self): - """ - Check that adding a new idea allows title to be immediately - searchable. - """ - req = RequestFactory().post('/', { - 'title':'test title', - 'summary': 'test summary', - 'text': 'example_text', - 'tags': 'test, tags' - }) - req.user = random_user() - views.add_idea(req) - results = self.backend.search('example_text') - self.assertEqual(1, results['hits']) - - @unittest.skipIf(backend_type == 'SimpleSearchBackend', - "Simple backend doesn't handle tags") - def test_add_idea_tag(self): - """ - Check that adding a new idea allows the associated tag to be - immediately searchable. - """ - req = RequestFactory().post('/', { - 'title':'title', - 'summary': 'test summary', - 'text': 'test text', - 'tags': 'example_tag' - }) - req.user = random_user() - views.add_idea(req) - results = self.backend.search('example_tag') - self.assertEqual(1, results['hits']) - - @unittest.skipIf(backend_type == 'SimpleSearchBackend', - "Simple backend doesn't handle tags") - def test_edit_idea_tag(self): - """ - Check that adding a new idea allows the associated tag to be - immediately searchable. - """ - idea = models.Idea(creator=random_user(), title='title', - state = models.State.objects.get(name='Active')) - idea.save() - results = self.backend.search('example_tag') - self.assertEqual(0, results['hits']) - - req = RequestFactory().post('/', { - 'title':'title', - 'summary': 'test summary', - 'text': 'test text', - 'tags': 'example_tag' - }) - req.user = random_user() - views.detail(req, str(idea.id)) - results = self.backend.search('example_tag') - self.assertEqual(1, results['hits']) - - def test_banner_title(self): - """ - Check that adding a new idea allows title to be immediately - searchable. - """ - banner = models.Banner() - banner.title = "example_title" - banner.text = "test text" - banner.start_date = date.today() - banner.save() - results = self.backend.search('example_title') - self.assertEqual(1, results['hits']) - - def test_banner_text(self): - """ - Check that adding a new idea allows title to be immediately - searchable. - """ - banner = models.Banner() - banner.title = "test title" - banner.text = "example_text" - banner.start_date = date.today() - banner.save() - results = self.backend.search('example_text') - self.assertEqual(1, results['hits']) diff --git a/src/idea/views.py b/src/idea/views.py index 7a3316b..4432d0b 100644 --- a/src/idea/views.py +++ b/src/idea/views.py @@ -26,8 +26,6 @@ from taggit.models import Tag COLLAB_TAGS = False -from haystack import connections - def _render(req, template_name, context={}): context['active_app'] = 'Idea' @@ -177,38 +175,6 @@ def up_vote(request): return HttpResponseRedirect(next_url) -def more_like_text(text, klass): - """ - Return more entries like the provided chunk of text. We have to jump - through some hoops to get this working as the haystack API does not - account for this case. In particular, this is a solr-specific hack. - """ - back = connections['default'].get_backend() - - if hasattr(back, 'conn'): - query = {'query': { - 'filtered': { - 'query': { - 'fuzzy_like_this': { - 'like_text': text - } - }, - 'filter': { - 'bool': { - 'must': { - 'term': {'django_ct': 'idea.idea'} - } - } - } - } - } - - } - results = back.conn.search(query) - return back._process_results(results)['results'] - else: - return [] - @login_required def detail(request, idea_id): @@ -335,17 +301,6 @@ def add_idea(request, banner_id=None): form.fields["banner"].queryset = current_banners return _render(request, 'idea/add.html', { 'form': form, - # Similar keywords was used to provide suggestions for idea titles - # (or notify you that an idea already exists) when creating a new - # idea. This functionality was removed from the front end a while - # ago when we redesigned the Add Idea page. As such, the similar - # object is not currently being utilized. - # - # The specific reason why this is being removed right now is we've - # seen instances where elasticsearch complains about too many - # requests which results in a 500 error when clicking the - # "Add Idea" button. - # 'similar': [r.object for r in more_like_text(idea_title, Idea)] }) From 04883157bd1960880aa8e61a683770eaa57b5ce5 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Mon, 20 Oct 2014 10:39:43 -0400 Subject: [PATCH 201/230] Re-add search_indexes.py, which is used by collab --- src/idea/search_indexes.py | 72 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/idea/search_indexes.py diff --git a/src/idea/search_indexes.py b/src/idea/search_indexes.py new file mode 100644 index 0000000..f53239b --- /dev/null +++ b/src/idea/search_indexes.py @@ -0,0 +1,72 @@ +from haystack import indexes +from models import Idea, Banner +from django.core.urlresolvers import reverse +from time import mktime, strptime + +class IdeaIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.EdgeNgramField(document=True, use_template=True) + display = indexes.CharField(model_attr='title') + description = indexes.CharField(model_attr="summary", null=True) + index_name = indexes.CharField(indexed=False) + index_priority = indexes.IntegerField(indexed=False) +# TODO causes all tests to fail +# index_sort = indexes.IntegerField(indexed=False, null=True) + url = indexes.CharField(indexed=False, null=True) + + PRIORITY = 4 + + def prepare_index_name(self, obj): + return "Ideas" + + def prepare_index_priority(self, obj): + return self.PRIORITY + + def prepare_index_sort(self, obj): + # want a positive number so banner results appear above/before ideas + #9999999999 =~ year 2286 + return 9999999999 - int(mktime(strptime(obj.recent_activity, "%Y-%m-%d %H:%M:%S"))) + + def prepare_url(self, obj): + return reverse('idea:idea_detail', args=(obj.id,)) + + def get_model(self): + return Idea + + def index_queryset(self, using=None): + """Used when the entire index for model is updated.""" + # State 2 = Archived + return self.get_model().objects.related_with_counts().exclude(state=2) + + +class BannerIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.EdgeNgramField(document=True, use_template=True) + display = indexes.CharField(model_attr='title') + description = indexes.CharField(model_attr="text") + index_name = indexes.CharField(indexed=False) + index_priority = indexes.IntegerField(indexed=False) + index_sort = indexes.IntegerField(indexed=False, null=True) + url = indexes.CharField(indexed=False, null=True) + + PRIORITY = 4 + + def prepare_index_name(self, obj): + return "Ideas" + + def prepare_index_priority(self, obj): + return self.PRIORITY + + def prepare_index_sort(self, obj): + return 0 - int(mktime(obj.start_date.timetuple())) + + def prepare_url(self, obj): + return reverse('idea:banner_detail', args=(obj.id,)) + + def prepare_display(self, obj): + return "Challenge: " + obj.title + + def get_model(self): + return Banner + + def index_queryset(self, using=None): + """Used when the entire index for model is updated.""" + return self.get_model().objects.all() From 910cffb279d7f7ba932321da21a1c07d80d74ab8 Mon Sep 17 00:00:00 2001 From: Dan Ford Date: Wed, 22 Oct 2014 10:25:52 -0400 Subject: [PATCH 202/230] readme updates --- README.md | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index cc8f185..83b50fc 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,10 @@ such that ideas, votes, etc. are tied to specific users. * django-taggit - A library for Tags within django * mock - A library for creating mock objects for testing. * south - A library for schema and data migrations. -* [collab](http://github.com/cfpb/collab) - Intranet platform, specifically needed for idea comments -* mptt - A library enabling nested/reply-to comments +* django-mptt - A library enabling nested/reply-to comments ### Optional -* collab platform - Installing idea-box as an app inside a collab platform provides several convenience features: +* [collab platform](http://github.com/cfpb/collab) - Installing idea-box as an app inside a collab platform provides several convenience features: * Autocomplete when adding new tags (requires elasticsearch server) * User can delete tags he/she created * Email notifications @@ -45,13 +44,22 @@ pip install git+https://github.com/cfpb/collab.git#egg=collab ``` ### Settings File -Modify your settings file to add the following apps: -* django.contrib.comments -* django-taggit -* south -* mptt -* core.custom_comments -* idea +Modify your settings file to add the following to your `INSTALLED_APPS`: +``` +'django.contrib.comments', +'taggit', +'south', +'mptt', +'core.custom_comments', +'idea' +``` + +If using a newer version of django-taggit, add the following to your settings file: +``` +SOUTH_MIGRATION_MODULES = { + 'taggit': 'taggit.south_migrations', +} +``` ### Folder Structure @@ -73,17 +81,19 @@ Add the idea.urls and comments.urls to you url.py. For example: ```python +from mydjango_project import settings + if 'idea' in settings.INSTALLED_APPS and \ 'django.contrib.comments' in settings.INSTALLED_APPS: urlpatterns.append(url(r'^comments/', include('django.contrib.comments.urls'))) - urlpatterns.append(url(r'^idea/', include('idea.urls'))) + urlpatterns.append(url(r'^idea/', include('idea.urls', namespace='idea'))) ``` ### Migrations -From your project root, synchronize and migrate the new apps. +From your project root, synchronize and migrate the new apps. Make sure to set your database settings. ```bash $ python ./manage.py syncdb --noinput --migrate From 1e134be5c1d49d83de4e167f540acb9410e4d91d Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Mon, 8 Dec 2014 13:54:28 -0500 Subject: [PATCH 203/230] Initial commit for private room functionality --- src/idea/forms.py | 12 ++ ...d_field_banner_private__chg_field_vote_.py | 131 ++++++++++++++++++ src/idea/models.py | 44 ++++++ src/idea/static/idea/css/idea.css | 7 +- src/idea/templates/idea/add.html | 14 +- src/idea/templates/idea/add_success.html | 6 +- src/idea/templates/idea/banner_detail.html | 58 +++++--- src/idea/templates/idea/detail.html | 21 ++- src/idea/templates/idea/list.html | 4 +- src/idea/urls.py | 4 +- src/idea/views.py | 93 +++++++++---- 11 files changed, 337 insertions(+), 57 deletions(-) create mode 100644 src/idea/migrations/0019_auto__add_field_banner_slug__add_field_banner_private__chg_field_vote_.py diff --git a/src/idea/forms.py b/src/idea/forms.py index b7059fc..87e4533 100644 --- a/src/idea/forms.py +++ b/src/idea/forms.py @@ -79,6 +79,18 @@ def clean_tags(self): return [t.lower() for t in tags] +class PrivateIdeaForm(IdeaForm): + def __init__(self, *args, **kwargs): + super(PrivateIdeaForm, self).__init__(*args, **kwargs) + + self.fields['challenge-checkbox'] = forms.BooleanField( + label="My idea will only be visible in this private room:", + widget=forms.HiddenInput(), required=False, initial=True) + self.fields["challenge-checkbox"].widget.attrs["class"] = "form-control active" + self.fields["banner"].widget.attrs["class"] = "form-control active" + self.fields['banner'].empty_label = None + + class UpVoteForm(forms.Form): idea_id = forms.IntegerField(widget=forms.HiddenInput()) next = forms.CharField(max_length=512, widget=forms.HiddenInput()) diff --git a/src/idea/migrations/0019_auto__add_field_banner_slug__add_field_banner_private__chg_field_vote_.py b/src/idea/migrations/0019_auto__add_field_banner_slug__add_field_banner_private__chg_field_vote_.py new file mode 100644 index 0000000..bb9b1da --- /dev/null +++ b/src/idea/migrations/0019_auto__add_field_banner_slug__add_field_banner_private__chg_field_vote_.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Banner.slug' + db.add_column(u'idea_banner', 'slug', + self.gf('django.db.models.fields.SlugField')(default='', unique=True, max_length=50, blank=True), + keep_default=False) + + # Adding field 'Banner.private' + db.add_column(u'idea_banner', 'private', + self.gf('django.db.models.fields.BooleanField')(default=False), + keep_default=False) + + def backwards(self, orm): + # Deleting field 'Banner.slug' + db.delete_column(u'idea_banner', 'slug') + + # Deleting field 'Banner.private' + db.delete_column(u'idea_banner', 'private') + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'idea.banner': { + 'Meta': {'object_name': 'Banner'}, + 'end_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50', 'blank': 'True'}), + 'start_date': ('django.db.models.fields.DateField', [], {}), + 'text': ('django.db.models.fields.TextField', [], {'max_length': '2000'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'idea.config': { + 'Meta': {'object_name': 'Config'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50'}), + 'value': ('django.db.models.fields.TextField', [], {'max_length': '2000'}) + }, + u'idea.idea': { + 'Meta': {'object_name': 'Idea'}, + 'banner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['idea.Banner']", 'null': 'True', 'blank': 'True'}), + 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['idea.State']"}), + 'summary': ('django.db.models.fields.TextField', [], {'max_length': '200'}), + 'text': ('django.db.models.fields.TextField', [], {'max_length': '2000'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 12, 8, 0, 0)'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'voters': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'idea_vote_creator'", 'null': 'True', 'through': u"orm['idea.Vote']", 'to': u"orm['auth.User']"}) + }, + u'idea.state': { + 'Meta': {'object_name': 'State'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'previous': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['idea.State']", 'unique': 'True', 'null': 'True', 'blank': 'True'}) + }, + u'idea.vote': { + 'Meta': {'object_name': 'Vote'}, + 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'idea': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['idea.Idea']"}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 12, 8, 0, 0)'}), + 'vote': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}) + }, + u'taggit.tag': { + 'Meta': {'object_name': 'Tag'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100'}) + }, + u'taggit.tagcategory': { + 'Meta': {'object_name': 'TagCategory'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}) + }, + u'taggit.taggeditem': { + 'Meta': {'object_name': 'TaggedItem'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_tagged_items'", 'to': u"orm['contenttypes.ContentType']"}), + 'create_timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_items'", 'to': u"orm['taggit.Tag']"}), + 'tag_category': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['taggit.TagCategory']", 'null': 'True'}), + 'tag_creator': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_related'", 'null': 'True', 'to': u"orm['auth.User']"}) + } + } + + complete_apps = ['idea'] diff --git a/src/idea/models.py b/src/idea/models.py index 4ad42cb..31bd0a4 100644 --- a/src/idea/models.py +++ b/src/idea/models.py @@ -7,12 +7,50 @@ from django.core.urlresolvers import reverse from django.db import models from django.utils.timezone import get_default_timezone +from django.utils.translation import ugettext_lazy if 'core.taggit' in settings.INSTALLED_APPS: from core.taggit.managers import TaggableManager else: from taggit.managers import TaggableManager + +def unique_slug(item, slug_source, slug_field): + """ + Ensures a unique slug field by appending an integer counter to duplicate + slugs. + + The item's slug field is first prepopulated by slugify-ing the source + field. If that value already exists, a counter is appended to the slug, + and the counter incremented upward until the value is unique. + + For instance, if you save an object titled Daily Roundup, and the slug + daily-roundup is already taken, this function will try daily-roundup-2, + daily-roundup-3, daily-roundup-4, etc, until a unique value is found. + + Call from within a model's custom save() method like so: + unique_slug(item, slug_source='field1', slug_field='field2') + where the value of field slug_source will be used to prepopulate the value + of slug_field. + """ + if not getattr(item, slug_field): # if it already has slug, do nothing. + from django.template.defaultfilters import slugify + slug = slugify(getattr(item, slug_source)) + itemModel = item.__class__ + # the following gets all existing slug values + allSlugs = [sl.values()[0] + for sl in itemModel.objects.values(slug_field)] + if slug in allSlugs: + import re + counterFinder = re.compile(r'-\d+$') + counter = 2 + slug = "%s-%i" % (slug, counter) + while slug in allSlugs: + slug = re.sub(counterFinder, "-%i" % counter, slug) + counter += 1 + setattr(item, slug_field, slug) + + class UserTrackable(models.Model): creator = models.ForeignKey(settings.AUTH_USER_MODEL) # use a lambda so that this is evaluated upon creation (rather than @@ -31,12 +69,18 @@ class Banner(models.Model): topics. """ title = models.CharField(max_length=50) + slug = models.SlugField(ugettext_lazy("Slug"), editable=False, unique=True, blank=True) text = models.TextField(max_length=2000, verbose_name="description") start_date = models.DateField( help_text="The date from which this banner will be displayed.") end_date = models.DateField(null=True, blank=True, help_text="Empty indicates that the banner " + "should be continued indefinitely. ") + private = models.BooleanField(default=False) + + def save(self, *args, **kwargs): + unique_slug(self, 'title', 'slug') + super(Banner, self).save(*args, **kwargs) def __unicode__(self): return u'%s (ends %s)' % (self.title, self.end_date) diff --git a/src/idea/static/idea/css/idea.css b/src/idea/static/idea/css/idea.css index c30e7e5..aea01b8 100644 --- a/src/idea/static/idea/css/idea.css +++ b/src/idea/static/idea/css/idea.css @@ -383,8 +383,8 @@ ul.section-nav li.active a { border-bottom-width: 1px; } -#challenge-link { - margin-bottom: 20px; +#date-range { + margin-top: 20px; } .sidebar .challenge-banner #challenge-link { margin-top: 1.5em; @@ -498,11 +498,11 @@ ul.section-nav li.active a { } .project-add .challenge-checkbox { margin-bottom: 0; + margin-right: .5em; } .project-add .challenge-checkbox label { display: inline-block; vertical-align: middle; - margin-left: .5em; } .project-add .challenge-checkbox input { display: inline-block; @@ -510,7 +510,6 @@ ul.section-nav li.active a { } .project-add select#id_banner { visibility: hidden; - margin-left: 1.5em; } .project-add select#id_banner.active { visibility: visible; diff --git a/src/idea/templates/idea/add.html b/src/idea/templates/idea/add.html index 9e38585..7e7a73b 100644 --- a/src/idea/templates/idea/add.html +++ b/src/idea/templates/idea/add.html @@ -12,13 +12,17 @@

      Share your Idea

      Toggle instructions for each field. -
      + {% csrf_token %} {% for field in form %}
      @@ -51,7 +55,11 @@

      Share your Idea

      {% block "submit-buttons" %} - + {% if banner and banner.private %} + + {% else %} + + {% endif %} {% endblock %} diff --git a/src/idea/templates/idea/add_success.html b/src/idea/templates/idea/add_success.html index 014e91d..e982f1f 100644 --- a/src/idea/templates/idea/add_success.html +++ b/src/idea/templates/idea/add_success.html @@ -61,7 +61,11 @@

      - Take me to IdeaBox + {% if idea.banner and idea.banner.private %} + Take me to the private Idea room + {% else %} + Take me to IdeaBox + {% endif %} Take me to my Idea
      diff --git a/src/idea/templates/idea/banner_detail.html b/src/idea/templates/idea/banner_detail.html index f7c72aa..fc3faba 100644 --- a/src/idea/templates/idea/banner_detail.html +++ b/src/idea/templates/idea/banner_detail.html @@ -2,17 +2,28 @@ {% block "content" %} -
      -
      - -
      + {% if banner.private %} +
      + + {% else %} +
      +
      + +
      + {% endif %}
      {% if is_current_banner %}

      Current Challenge:

      + {% elif banner.private == True %} +

      Private Room

      {% else %}

      Expired Challenge:

      {% endif %} @@ -22,12 +33,16 @@

      {{banner.title}}

      {{banner.text|urlize|linebreaksbr}}

      {% if is_current_banner %} - + + {% elif banner.private %} + + {% endif %} + {% if not banner.private %} +
      + Challenge start date: {{banner.start_date|date:"SHORT_DATE_FORMAT"}} + Challenge end date: {{banner.end_date|date:"SHORT_DATE_FORMAT"}} +
      {% endif %} -
      - Challenge start date: {{banner.start_date|date:"SHORT_DATE_FORMAT"}} - Challenge end date: {{banner.end_date|date:"SHORT_DATE_FORMAT"}} -
      @@ -42,14 +57,16 @@

      {{banner.title}}

      {% if idea.state.name == 'Archive' %} Archived - {% elif request.user in idea.voters.all %} - Liked {% else %} -
      + {% csrf_token %} - - + + {% if request.user in idea.voters.all%} + + {% else %} + + {% endif %}
      {% endif %}
      @@ -81,7 +98,7 @@

      {{banner.title}}

      {% endif %} {% if ideas.object_list.count > 5 %} - Back to top + Back to top {% endif %} -
      {% endblock %} diff --git a/src/idea/templates/idea/detail.html b/src/idea/templates/idea/detail.html index 54fbb47..25b37a8 100644 --- a/src/idea/templates/idea/detail.html +++ b/src/idea/templates/idea/detail.html @@ -8,7 +8,11 @@
      @@ -44,8 +48,13 @@

      {{idea.title}}

      {% if idea.banner %} - Challenge: - {{idea.banner.title}} + {% if idea.banner.private %} + Room: + {{idea.banner.title}} + {% else %} + Challenge: + {{idea.banner.title}} + {% endif %} {% endif %}
      @@ -120,7 +129,11 @@

      {{comment_list|length}} Comments

      @@ -66,11 +71,13 @@

      Likes '{{idea.title}}'

      {% endfor %}
      + {% if idea.banner and not idea.banner.private %} + {% endif %}
      From 7f427e8ec9951a80639291c589a831b69b253583 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Tue, 9 Dec 2014 11:26:30 -0500 Subject: [PATCH 210/230] Ensure private ideas don't appear in search results --- src/idea/search_indexes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/idea/search_indexes.py b/src/idea/search_indexes.py index de2fc1e..103e000 100644 --- a/src/idea/search_indexes.py +++ b/src/idea/search_indexes.py @@ -35,7 +35,7 @@ def get_model(self): def index_queryset(self, using=None): """Used when the entire index for model is updated.""" # State 2 = Archived - return self.get_model().objects.related_with_counts().exclude(state=2) + return self.get_model().objects.related_with_counts().exclude(state=2).exclude(banner__private=True) class BannerIndex(indexes.SearchIndex, indexes.Indexable): @@ -69,4 +69,4 @@ def get_model(self): def index_queryset(self, using=None): """Used when the entire index for model is updated.""" - return self.get_model().objects.all() + return self.get_model().objects.exclude(private=True) From 6a451f2c85ffef8e7a9d245b1fcd22fe342b7de8 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Tue, 9 Dec 2014 14:20:10 -0500 Subject: [PATCH 211/230] Tests now run when bundled with collab --- src/idea/tests/utils.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/idea/tests/utils.py b/src/idea/tests/utils.py index 469fe68..e4b1356 100644 --- a/src/idea/tests/utils.py +++ b/src/idea/tests/utils.py @@ -1,5 +1,6 @@ from django.contrib.auth import get_user_model from django.test.client import RequestFactory +from django.conf import settings import random import string @@ -8,7 +9,13 @@ def random_user(): ''.join(random.choice(string.lowercase) for _ in range(12))) def create_superuser(): - get_user_model().objects.create_superuser('test1@example.com', 'test1@example.com', '1') + user = get_user_model().objects.create_superuser('test1@example.com', 'test1@example.com', '1') + # If using collab, person objects needs to be created too + if 'core' in settings.INSTALLED_APPS: + from core.models import Person + person = Person() + person.user = user + person.save() def get_login_user(): # required for Collab integration From 941abff8ff1d2a37282e8a0f8ad90680fc20de1d Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Tue, 9 Dec 2014 17:53:28 -0500 Subject: [PATCH 212/230] Add tests for private room functionality --- src/idea/tests/addidea_tests.py | 56 +++- src/idea/tests/editidea_tests.py | 55 +++- src/idea/tests/listview_tests.py | 46 ++- src/idea/tests/privatebannerview_tests.py | 365 ++++++++++++++++++++++ src/idea/views.py | 4 +- 5 files changed, 516 insertions(+), 10 deletions(-) create mode 100644 src/idea/tests/privatebannerview_tests.py diff --git a/src/idea/tests/addidea_tests.py b/src/idea/tests/addidea_tests.py index 97afc8f..35a0300 100644 --- a/src/idea/tests/addidea_tests.py +++ b/src/idea/tests/addidea_tests.py @@ -7,7 +7,7 @@ from idea.tests.utils import mock_req, random_user, login, create_superuser from mock import patch from datetime import date, timedelta -from idea.forms import IdeaForm +from idea.forms import IdeaForm, PrivateIdeaForm if 'core.taggit' in settings.INSTALLED_APPS: from core.taggit.utils import add_tags @@ -106,6 +106,7 @@ def test_add_idea_with_banner(self, render): banner_field = context['form'].fields['banner'] selected = context['form'].initial['banner'] self.assertEqual(None, selected) + self.assertEqual(context['form'].fields['banner'].widget.choices.field.empty_label, 'Select') self.assertIn(banner1, banner_field._queryset) self.assertIn(banner2, banner_field._queryset) self.assertNotIn(banner3, banner_field._queryset) @@ -114,7 +115,8 @@ def test_add_idea_with_banner(self, render): context = render.call_args[0][2] banner_field = context['form'].fields['banner'] selected = context['form'].initial['banner'] - self.assertEqual(banner1, selected) + self.assertEqual(banner1.id, selected) + self.assertEqual(context['form'].fields['banner'].widget.choices.field.empty_label, 'Select') self.assertIn(banner1, banner_field._queryset) self.assertIn(banner2, banner_field._queryset) self.assertNotIn(banner3, banner_field._queryset) @@ -123,7 +125,8 @@ def test_add_idea_with_banner(self, render): context = render.call_args[0][2] banner_field = context['form'].fields['banner'] selected = context['form'].initial['banner'] - self.assertEqual(banner2, selected) + self.assertEqual(banner2.id, selected) + self.assertEqual(context['form'].fields['banner'].widget.choices.field.empty_label, 'Select') self.assertIn(banner1, banner_field._queryset) self.assertIn(banner2, banner_field._queryset) self.assertNotIn(banner3, banner_field._queryset) @@ -133,10 +136,57 @@ def test_add_idea_with_banner(self, render): banner_field = context['form'].fields['banner'] selected = context['form'].initial['banner'] self.assertEqual(None, selected) + self.assertEqual(context['form'].fields['banner'].widget.choices.field.empty_label, 'Select') self.assertIn(banner1, banner_field._queryset) self.assertIn(banner2, banner_field._queryset) self.assertNotIn(banner3, banner_field._queryset) + @patch('idea.views.render') + def test_add_idea_with_private_banner(self, render): + """ + Verify that the private banner field auto-populates properly + """ + + banner1 = models.Banner(id=1, title="AAAA", text="text1", + start_date=date.today(), private=True) + banner1.save() + banner2 = models.Banner(id=2, title="BBBB", text="text2", + start_date=date.today()) + banner2.save() + + views.add_idea(mock_req()) + context = render.call_args[0][2] + self.assertTrue('form' in context) + self.assertTrue(isinstance(context['form'], IdeaForm)) + self.assertFalse(isinstance(context['form'], PrivateIdeaForm)) + banner_field = context['form'].fields['banner'] + selected = context['form'].initial['banner'] + self.assertEqual(None, selected) + self.assertEqual(context['form'].fields['banner'].widget.choices.field.empty_label, 'Select') + self.assertNotIn(banner1, banner_field._queryset) + self.assertIn(banner2, banner_field._queryset) + + views.add_idea(mock_req(), banner1.id) + context = render.call_args[0][2] + self.assertTrue(isinstance(context['form'], PrivateIdeaForm)) + banner_field = context['form'].fields['banner'] + selected = context['form'].initial['banner'] + self.assertEqual(banner1.id, selected) + self.assertEqual(context['form'].fields['banner'].widget.choices.field.empty_label, None) + self.assertIn(banner1, banner_field._queryset) + self.assertNotIn(banner2, banner_field._queryset) + + views.add_idea(mock_req(), banner2.id) + context = render.call_args[0][2] + self.assertTrue(isinstance(context['form'], IdeaForm)) + self.assertFalse(isinstance(context['form'], PrivateIdeaForm)) + banner_field = context['form'].fields['banner'] + selected = context['form'].initial['banner'] + self.assertEqual(context['form'].fields['banner'].widget.choices.field.empty_label, 'Select') + self.assertEqual(banner2.id, selected) + self.assertNotIn(banner1, banner_field._queryset) + self.assertIn(banner2, banner_field._queryset) + @patch('idea.views.render') def test_add_idea_with_no_banner(self, render): """ diff --git a/src/idea/tests/editidea_tests.py b/src/idea/tests/editidea_tests.py index 5c4f204..5655184 100644 --- a/src/idea/tests/editidea_tests.py +++ b/src/idea/tests/editidea_tests.py @@ -1,9 +1,11 @@ from django.contrib.auth import get_user_model from django.core.urlresolvers import reverse from django.test import TestCase -from idea import models -from idea.tests.utils import random_user, login, create_superuser +from idea import models, views +from idea.forms import IdeaForm, PrivateIdeaForm +from idea.tests.utils import mock_req, random_user, login, create_superuser from datetime import date +from mock import patch def create_idea(user=None): if not user: @@ -19,7 +21,7 @@ def create_idea(user=None): idea.tags.add("test tag") return idea -class AddIdeaTest(TestCase): +class EditIdeaTest(TestCase): fixtures = ['state'] def setUp(self): @@ -97,3 +99,50 @@ def test_edit_ignores_tags(self): refresh_idea = models.Idea.objects.get(id=idea.id) self.assertEqual(refresh_idea.tags.count(), 1) self.assertEqual(refresh_idea.tags.all()[0].name, "test tag") + + @patch('idea.views.render') + def test_edit_idea_with_private_banner(self, render): + """ + Verify that the private banner field auto-populates properly + """ + user = login(self) + state = models.State.objects.get(name='Active') + + idea1 = models.Idea(creator=user, title='Transit subsidy to Venus', + text='Aliens need assistance.', state=state) + banner1 = models.Banner(id=1, title="AAAA", text="text1", + start_date=date.today(), private=True) + banner1.save() + idea1.banner = banner1 + idea1.save() + + idea2 = models.Idea(creator=user, title='Transit subsidy to Venus', + text='Aliens need assistance.', state=state) + banner2 = models.Banner(id=2, title="BBBB", text="text2", + start_date=date.today()) + banner2.save() + idea2.banner = banner2 + idea2.save() + + views.edit_idea(mock_req(user=user), idea1.id) + context = render.call_args[0][2] + self.assertTrue('form' in context) + self.assertTrue(isinstance(context['form'], PrivateIdeaForm)) + banner_field = context['form'].fields['banner'] + selected = context['form'].initial['banner'] + self.assertEqual(banner1.id, selected) + self.assertEqual(context['form'].fields['banner'].widget.choices.field.empty_label, None) + self.assertIn(banner1, banner_field._queryset) + self.assertNotIn(banner2, banner_field._queryset) + + views.edit_idea(mock_req(user=user), idea2.id) + context = render.call_args[0][2] + self.assertTrue('form' in context) + self.assertTrue(isinstance(context['form'], IdeaForm)) + self.assertFalse(isinstance(context['form'], PrivateIdeaForm)) + banner_field = context['form'].fields['banner'] + selected = context['form'].initial['banner'] + self.assertEqual(banner2.id, selected) + self.assertEqual(context['form'].fields['banner'].widget.choices.field.empty_label, 'Select') + self.assertNotIn(banner1, banner_field._queryset) + self.assertIn(banner2, banner_field._queryset) diff --git a/src/idea/tests/listview_tests.py b/src/idea/tests/listview_tests.py index 1c36d0a..6b4f011 100644 --- a/src/idea/tests/listview_tests.py +++ b/src/idea/tests/listview_tests.py @@ -13,8 +13,8 @@ def get_relative_date(delta_days=0): return datetime.date.today() + datetime.timedelta(days=delta_days) -def create_banner(title, delta_days=0): - banner = models.Banner(title=title, text=title+' Text', +def create_banner(title, delta_days=0, private=False): + banner = models.Banner(title=title, text=title+' Text', private=private, start_date=get_relative_date(-delta_days), end_date=get_relative_date(delta_days)) banner.save() @@ -268,6 +268,35 @@ def test_tags_exist(self, render): self.assertEqual(set(['bbb', 'ccc', 'ddd']), set([t.name for t in context['tags']])) + @patch('idea.views.render') + def test_exclude_private_tags(self, render): + """ + Check that the tag list does not include tags only used for + private banners + """ + user = random_user() + state = models.State.objects.get(name='Active') + state.save() + + pub_banner = create_banner('Public') + priv_banner = create_banner('Private', private=True) + + pub_idea = models.Idea(creator=user, title='AAAA', text='AAAA Text', + state=state, banner_id=pub_banner.id) + pub_idea.save() + priv_idea = models.Idea(creator=user, title='BBBB', text='BBBB Text', + state=state, banner_id=priv_banner.id) + priv_idea.save() + + pub_idea.tags.add('bbb', 'ccc', 'ddd') + priv_idea.tags.add('ddd', 'eee', 'fff') + views.list(mock_req()) + context = render.call_args[0][2] + self.assertTrue('tags' in context) + self.assertEqual(3, len(context['tags'])) + self.assertEqual(set(['bbb', 'ccc', 'ddd']), + set([t.name for t in context['tags']])) + @patch('idea.views.render') def test_tags_top_list(self, render): """ @@ -312,6 +341,19 @@ def test_tags_count(self, render): idea.save() idea.tags.add(tag) + # create some dummy tags for private banner ideas + # these tags should not be reflected in the tag count + banner = models.Banner(id=1, title="XXXX", text="text", private=True, + start_date=datetime.date.today()) + banner.save() + for count in range(5): + tag = str(count)*4 + for i in range(count+1): + idea = models.Idea(creator=user, title=str(i)*4+'2', + text=str(i)*42 +' Text', state=state, banner_id=1) + idea.save() + idea.tags.add(tag) + views.list(mock_req()) context = render.call_args[0][2] self.assertTrue('tags' in context) diff --git a/src/idea/tests/privatebannerview_tests.py b/src/idea/tests/privatebannerview_tests.py new file mode 100644 index 0000000..25cb262 --- /dev/null +++ b/src/idea/tests/privatebannerview_tests.py @@ -0,0 +1,365 @@ +import datetime +from django.contrib.auth import get_user_model +from django.utils.timezone import get_default_timezone +from django.test import TestCase +from idea import models, views +from idea.tests.utils import mock_req, random_user +from mock import patch +import string + +def get_relative_date(delta_days=0): + return datetime.date.today() + datetime.timedelta(days=delta_days) + +class PrivateBannerViewTest(TestCase): + """ + Tests for idea.views.room_detail + """ + fixtures = ['state'] + def _generate_data(self, paramfn=lambda x,y:None, postfn=lambda x,y:None, + entry_data=[(5, 'AAAA'), (9, 'BBBB'), (3, 'CCCC'), (7, 'DDDD'), + (1, 'EEEE'), (11, 'FFFF')]): + """ + Helper function to handle the idea (and related models) creation. + """ + user = get_user_model().objects.create_user('example') + state = models.State.objects.get(name='Active') + state.save() + + banner = models.Banner(id=1, title="XXXX", text="text", private=True, + start_date=datetime.date.today()) + banner.save() + + def make_idea(nonce, title, banner=banner): + kwargs = {'creator': user, 'title': title, 'banner': banner, + 'text': title + ' Text', 'state': state} + paramfn(kwargs, nonce) + idea = models.Idea(**kwargs) + idea.save() + postfn(idea, nonce) + return idea + + if entry_data: + ideas = [make_idea(pair[0], pair[1]) for pair in entry_data] + + extra_ideas = [make_idea(pair[0], pair[1], pair[2]) \ + for pair in [(24, 'ZZZZ', None), (25,'YYYY', None)]] + + def _verify_order(self, render): + """ + Given a patched render, verify the order of the ideas. + """ + context = render.call_args[0][2] + self.assertTrue('ideas' in context) + self.assertEqual(6, len(context['ideas'])) + self.assertEqual('FFFF', context['ideas'][0].title) + self.assertEqual('BBBB', context['ideas'][1].title) + self.assertEqual('DDDD', context['ideas'][2].title) + self.assertEqual('AAAA', context['ideas'][3].title) + self.assertEqual('CCCC', context['ideas'][4].title) + self.assertEqual('EEEE', context['ideas'][5].title) + + @patch('idea.views.render') + def test_private_sort(self, render): + """ + Verify sort order. + """ + def add_time(kwargs, nonce): + kwargs['time'] = datetime.datetime(2013, 1, nonce, tzinfo=get_default_timezone()) + self._generate_data(paramfn=add_time) + views.room_detail(mock_req(), slug='xxxx') + self._verify_order(render) + + @patch('idea.views.render') + def test_private_paging(self, render): + """ + Verify that paging works as we would expect. + """ + letters = string.uppercase + entry_data = [] + ## Count down from 12 to 1, so A has the most recent timestamp + for i in range(12, -1, -1): + entry_data.append((i+1, letters[i]*4)) + self._generate_data(entry_data=entry_data) + + views.room_detail(mock_req(), slug='xxxx') + context = render.call_args[0][2] + self.assertTrue('ideas' in context) + self.assertEqual(10, len(context['ideas'])) + self.assertEqual('AAAA', context['ideas'][0].title) + self.assertEqual('EEEE', context['ideas'][4].title) + + views.room_detail(mock_req('/?page_num=1'), slug='xxxx') + context = render.call_args[0][2] + self.assertTrue('ideas' in context) + self.assertEqual(10, len(context['ideas'])) + self.assertEqual('AAAA', context['ideas'][0].title) + self.assertEqual('EEEE', context['ideas'][4].title) + + views.room_detail(mock_req('/?page_num=sadasds'), slug='xxxx') + context = render.call_args[0][2] + self.assertTrue('ideas' in context) + self.assertEqual(10, len(context['ideas'])) + self.assertEqual('AAAA', context['ideas'][0].title) + self.assertEqual('EEEE', context['ideas'][4].title) + + views.room_detail(mock_req('/?page_num=2'), slug='xxxx') + context = render.call_args[0][2] + self.assertTrue('ideas' in context) + self.assertEqual(3, len(context['ideas'])) + self.assertEqual('KKKK', context['ideas'][0].title) + self.assertEqual('MMMM', context['ideas'][2].title) + + views.room_detail(mock_req('/?page_num=232432'), slug='xxxx') + context = render.call_args[0][2] + self.assertTrue('ideas' in context) + self.assertEqual(3, len(context['ideas'])) + self.assertEqual('KKKK', context['ideas'][0].title) + self.assertEqual('MMMM', context['ideas'][2].title) + + @patch('idea.views.render') + def test_private_idea_fields(self, render): + """ + Verify that the fields needed by the ui are available on all ideas. + """ + self._generate_data() + + views.room_detail(mock_req(), slug='xxxx') + context = render.call_args[0][2] + self.assertTrue('ideas' in context) + self.assertEqual(6, len(context['ideas'])) + for idea in context['ideas']: + self.assertTrue(hasattr(idea, 'title')) + self.assertTrue(hasattr(idea, 'url')) + self.assertTrue(hasattr(idea.creator, 'first_name')) + self.assertTrue(hasattr(idea.creator, 'last_name')) + #self.assertTrue(hasattr(idea.creator, 'photo')) + #self.assertTrue(hasattr(idea, 'comment_count')) + self.assertTrue(hasattr(idea, 'vote_count')) + self.assertTrue(hasattr(idea, 'time')) + + @patch('idea.views.render') + def test_private_tags_exist(self, render): + """ + Check that the tag list is populated with ONLY tags from private belonging ideas. + """ + user = random_user() + state = models.State.objects.get(name='Active') + state.save() + + self._generate_data(entry_data=None) + + # create some dummy tags that are not part of this banner's ideas + banner = models.Banner(id=2, title="XXXX", text="text", private=False, + start_date=datetime.date.today()) + banner.save() + for count in range(2): + tag = str(count)*4 + for i in range(count+1): + idea = models.Idea(creator=user, title=str(i)*4+'3', + text=str(i)*4 +'3 Text', state=state, banner_id=2) + idea.save() + idea.tags.add(tag) + banner = models.Banner(id=3, title="XXXX", text="text", private=True, + start_date=datetime.date.today()) + banner.save() + for count in range(2): + tag = str(count)*4 + for i in range(count+1): + idea = models.Idea(creator=user, title=str(i)*4+'4', + text=str(i)*4 +'4 Text', state=state, banner_id=3) + idea.save() + idea.tags.add(tag) + + idea = models.Idea(creator=user, title='AAAA', text='AAAA Text', + state=state, banner_id=1) + idea.save() + + idea.tags.add('bbb', 'ccc', 'ddd') + views.room_detail(mock_req(), slug='xxxx') + context = render.call_args[0][2] + self.assertTrue('tags' in context) + self.assertEqual(3, len(context['tags'])) + self.assertEqual(set(['bbb', 'ccc', 'ddd']), + set([t.name for t in context['tags']])) + + @patch('idea.views.render') + def test_private_tags_top_list(self, render): + """ + Tag list should be in proper order. + """ + user = random_user() + state = models.State.objects.get(name='Active') + state.save() + + # create a private and public banner + self._generate_data(entry_data=None) + + # Make 13 tags, and assign each to a set of ideas + for count in range(30): + tag = str(count)*4 + for i in range(count+1): + idea = models.Idea(creator=user, title=str(i)*4, + text=str(i)*4 +' Text', state=state, banner_id=1) + idea.save() + idea.tags.add(tag) + + views.room_detail(mock_req(), slug='xxxx') + context = render.call_args[0][2] + self.assertTrue('tags' in context) + self.assertEqual(25, len(context['tags'])) + # 29292929, 28282828, 27272727, ... + self.assertEqual([str(i)*4 for i in range(29,4,-1)], + [t.name for t in context['tags']]) + + @patch('idea.views.render') + def test_private_tags_count(self, render): + """ + Tag list should include tag count. + """ + user = random_user() + state = models.State.objects.get(name='Active') + state.save() + + self._generate_data(entry_data=None) + + # Make 13 tags, and assign each to a set of ideas + for count in range(13): + tag = str(count)*4 + for i in range(count+1): + idea = models.Idea(creator=user, title=str(i)*4, + text=str(i)*4 +' Text', state=state, banner_id=1) + idea.save() + idea.tags.add(tag) + + # create some dummy tags that are not part of this banner's ideas + for count in range(2): + tag = str(count)*4 + for i in range(count+1): + idea = models.Idea(creator=user, title=str(i)*4+'2', + text=str(i)*42 +' Text', state=state) + idea.save() + idea.tags.add(tag) + banner = models.Banner(id=2, title="XXXX", text="text", private=True, + start_date=datetime.date.today()) + banner.save() + for count in range(2): + tag = str(count)*4 + for i in range(count+1): + idea = models.Idea(creator=user, title=str(i)*4+'3', + text=str(i)*4 +'3 Text', state=state, banner_id=2) + idea.save() + idea.tags.add(tag) + banner = models.Banner(id=3, title="XXXX", text="text", private=True, + start_date=datetime.date.today()) + banner.save() + for count in range(2): + tag = str(count)*4 + for i in range(count+1): + idea = models.Idea(creator=user, title=str(i)*4+'4', + text=str(i)*4 +'4 Text', state=state, banner_id=3) + idea.save() + idea.tags.add(tag) + + views.room_detail(mock_req(), slug='xxxx') + context = render.call_args[0][2] + self.assertTrue('tags' in context) + + for i in range(12,2,-1): + tag = context['tags'][12-i] + self.assertTrue(hasattr(tag, 'count')) + self.assertEqual(i+1, tag.count) + + @patch('idea.views.render') + def test_private_tags_active(self, render): + """ + Tag list should include if tag was active in this search. + """ + def add_tag(idea, nonce): + tag = str(nonce % 3) + idea.tags.add(tag) + self._generate_data(postfn=add_tag) + + views.room_detail(mock_req(), slug='xxxx') + context = render.call_args[0][2] + self.assertTrue('tags' in context) + for tag in context['tags']: + self.assertFalse(tag.active) + + views.room_detail(mock_req('/?tags=0'), slug='xxxx') + context = render.call_args[0][2] + for tag in context['tags']: + self.assertEqual(tag.name == '0', tag.active) + + views.room_detail(mock_req('/?tags=1'), slug='xxxx') + context = render.call_args[0][2] + for tag in context['tags']: + self.assertEqual(tag.name == '1', tag.active) + + views.room_detail(mock_req('/?tags=1,2'), slug='xxxx') + context = render.call_args[0][2] + for tag in context['tags']: + self.assertEqual(tag.name in ['1','2'], tag.active) + + + @patch('idea.views.render') + def test_private_tag_filter(self, render): + """ + List of ideas should be filterable by tag. + """ + def add_tag(idea, nonce): + #entry_data=[(5, 'AAAA'), (9, 'BBBB'), (3, 'CCCC'), (7, 'DDDD'), + # (1, 'EEEE'), (11, 'FFFF')]): + tag = str(nonce % 3) # results: 2 0 0 1 1 2 + idea.tags.add(tag) + tag = str(nonce % 7) # results: 5 2 3 0 1 4 + idea.tags.add(tag) + self._generate_data(postfn=add_tag) + + views.room_detail(mock_req(), slug='xxxx') + context = render.call_args[0][2] + self.assertTrue('ideas' in context) + self.assertEqual(6, len(context['ideas'])) + self.assertEqual(6, len(context['tags'])) + self.assertEqual(set(['0','1','2','3','4','5']), + set([t.name for t in context['tags']])) + + views.room_detail(mock_req('/?tags=0'), slug='xxxx') + context = render.call_args[0][2] + self.assertEqual(3, len(context['ideas'])) + self.assertEqual(set(['BBBB', 'CCCC', 'DDDD']), + set([i.title for i in context['ideas']])) + self.assertEqual(4, len(context['tags'])) + self.assertEqual(set(['0','1','2','3']), + set([t.name for t in context['tags']])) + + views.room_detail(mock_req('/?tags=2'), slug='xxxx') + context = render.call_args[0][2] + self.assertEqual(3, len(context['ideas'])) + self.assertEqual(set(['AAAA', 'BBBB', 'FFFF']), + set([i.title for i in context['ideas']])) + self.assertEqual(4, len(context['tags'])) + self.assertEqual(set(['0','2','4','5']), + set([t.name for t in context['tags']])) + + views.room_detail(mock_req('/?tags=0,2'), slug='xxxx') + context = render.call_args[0][2] + self.assertEqual(1, len(context['ideas'])) + self.assertEqual(set(['BBBB']), + set([i.title for i in context['ideas']])) + self.assertEqual(2, len(context['tags'])) + self.assertEqual(set(['0','2']), + set([t.name for t in context['tags']])) + + @patch('idea.views.render') + def test_private_banner_is_not_current(self, render): + """ + Test boolean flag for banner status (active or not) + """ + banner = models.Banner(id=1, title="xxxx", text="text", private=True, + start_date=datetime.date.today()) + banner.save() + + views.room_detail(mock_req(), slug='xxxx') + context = render.call_args[0][2] + self.assertTrue('is_current_banner' in context) + self.assertFalse(context['is_current_banner']) diff --git a/src/idea/views.py b/src/idea/views.py index b941aad..b06a4cc 100644 --- a/src/idea/views.py +++ b/src/idea/views.py @@ -304,7 +304,7 @@ def add_idea(request, banner_id=None): if banner_id: banner = get_object_or_404(Banner, pk=int(banner_id)) if banner and banner.private: - form_initial['banner'] = banner + form_initial['banner'] = banner.id form = PrivateIdeaForm(initial=form_initial) form.fields["banner"].queryset = Banner.objects.filter(id=banner_id) elif current_banners.count() == 0: @@ -316,7 +316,7 @@ def add_idea(request, banner_id=None): if banner not in current_banners: banner = None else: - form_initial['banner'] = banner + form_initial['banner'] = banner.id form_initial['challenge-checkbox'] = "on" form = IdeaForm(initial=form_initial) From 5505cd88c98480c621363c27f61e97e84421e917 Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Wed, 10 Dec 2014 08:34:46 -0500 Subject: [PATCH 213/230] Use correct URL formatting in banner_detail template --- src/idea/templates/idea/banner_detail.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/idea/templates/idea/banner_detail.html b/src/idea/templates/idea/banner_detail.html index fc3faba..0733317 100644 --- a/src/idea/templates/idea/banner_detail.html +++ b/src/idea/templates/idea/banner_detail.html @@ -6,7 +6,7 @@
      {% else %} From c6ee78ed632ef2e3e44e4d31fa8759323b01d3dd Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Wed, 10 Dec 2014 09:32:41 -0500 Subject: [PATCH 214/230] Update return link on add_success page for private ideas --- src/idea/templates/idea/add_success.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/idea/templates/idea/add_success.html b/src/idea/templates/idea/add_success.html index e982f1f..71f2657 100644 --- a/src/idea/templates/idea/add_success.html +++ b/src/idea/templates/idea/add_success.html @@ -5,7 +5,11 @@
      From ad5c50b05f120a14ac61c3cb31c06ea709d1a8bd Mon Sep 17 00:00:00 2001 From: Mike Brown Date: Thu, 11 Dec 2014 11:56:58 -0500 Subject: [PATCH 215/230] Add anonymous ideas and comments, ability to hide voting --- src/idea/admin.py | 29 +++- src/idea/forms.py | 14 +- ..._add_field_banner_is_votes__add_field_i.py | 141 ++++++++++++++++++ src/idea/models.py | 22 ++- src/idea/search_indexes.py | 4 +- src/idea/static/idea/css/idea.css | 10 +- src/idea/static/idea/js/admin.js | 22 +++ src/idea/templates/idea/add.html | 8 +- src/idea/templates/idea/add_success.html | 4 +- src/idea/templates/idea/banner_detail.html | 12 +- src/idea/templates/idea/detail.html | 31 ++-- src/idea/templates/idea/edit.html | 4 + src/idea/templates/idea/show_likes.html | 4 +- src/idea/tests/addidea_tests.py | 2 +- src/idea/tests/editidea_tests.py | 2 +- src/idea/tests/listview_tests.py | 8 +- src/idea/tests/privatebannerview_tests.py | 12 +- src/idea/views.py | 43 +++--- 18 files changed, 303 insertions(+), 69 deletions(-) create mode 100644 src/idea/migrations/0022_auto__ren_field_banner_private__add_field_banner_is_votes__add_field_i.py create mode 100644 src/idea/static/idea/js/admin.js diff --git a/src/idea/admin.py b/src/idea/admin.py index 9b25ec9..5b326fe 100644 --- a/src/idea/admin.py +++ b/src/idea/admin.py @@ -10,17 +10,21 @@ idea_actions.append(export_as_csv_action("CSV Export", fields=['title', - 'text', 'creator', - 'banner'])) + 'is_anonymous', + 'banner', + 'summary', + 'text', + 'state'])) vote_actions.append(export_as_csv_action("CSV Export", fields=['creator', 'idea'])) banner_actions.append(export_as_csv_action("CSV Export", fields=['title', 'text', - 'private', 'slug', + 'is_private', + 'is_votes', 'start_date', 'end_date'])) except ImportError: @@ -33,7 +37,7 @@ class ConfigAdmin(admin.ModelAdmin): class IdeaAdmin(admin.ModelAdmin): list_display = ('title', 'creator', 'banner') search_fields = ['title', 'text'] - exclude = ('tags',) + fields = ('creator', ('banner', 'is_anonymous'), 'title', 'summary', 'text', 'state') actions = idea_actions class VoteAdmin(admin.ModelAdmin): @@ -42,12 +46,21 @@ class VoteAdmin(admin.ModelAdmin): actions = vote_actions class BannerAdmin(admin.ModelAdmin): - list_display = ('title', 'private', 'start_date', 'end_date') - fields = ('title', ('private', 'slug'), 'text', 'start_date', 'end_date') - readonly_fields = ('slug',) - search_fields = ['title', 'text'] + def room_link_clickable(self, obj): + return obj.room_link() + room_link_clickable.allow_tags = True + room_link_clickable.short_description = "room link" + readonly_fields = ('room_link_clickable',) + + list_display = ('title', 'is_private', 'is_votes', 'start_date', 'end_date') + fields = ('title', ('is_private', 'room_link_clickable', 'is_votes'), 'text', 'start_date', 'end_date') + + search_fields = ['title', 'summary'] actions = banner_actions + class Media: + js = ("idea/js/admin.js",) + admin.site.register(State) admin.site.register(Config, ConfigAdmin) admin.site.register(Idea, IdeaAdmin) diff --git a/src/idea/forms.py b/src/idea/forms.py index 87e4533..d34a38d 100644 --- a/src/idea/forms.py +++ b/src/idea/forms.py @@ -15,10 +15,13 @@ class Meta: exclude = ('creator', 'time', 'state', 'voters') def __init__(self, *args, **kwargs): + # override to hide the is_anonymous field + include_anonymous = kwargs.pop("include_anonymous", False) super(IdeaForm, self).__init__(*args, **kwargs) - self.fields['banner'].empty_label = "Select" + self.fields['banner'].empty_label = "Select" self.fields['title'].label = "What is your idea?" + self.fields['is_anonymous'].label = "I want my Idea to be anonymous" self.fields['banner'].label = None self.fields['summary'].label = "Pitch your idea" self.fields['tags'].label = "Tag it with keywords" @@ -39,13 +42,19 @@ def __init__(self, *args, **kwargs): self.fields.keyOrder = [ 'title', + 'is_anonymous', 'challenge-checkbox', 'banner', 'summary', 'tags', 'text'] - def save(self, commit=True): + # Do not allow anonymous comments for normal ideas + if not include_anonymous: + del self.fields['is_anonymous'] + + + def save(self): instance = super(IdeaForm, self).save(commit=False) # add tags separately tags = self.cleaned_data.get('tags', []) @@ -81,6 +90,7 @@ def clean_tags(self): class PrivateIdeaForm(IdeaForm): def __init__(self, *args, **kwargs): + kwargs['include_anonymous'] = True super(PrivateIdeaForm, self).__init__(*args, **kwargs) self.fields['challenge-checkbox'] = forms.BooleanField( diff --git a/src/idea/migrations/0022_auto__ren_field_banner_private__add_field_banner_is_votes__add_field_i.py b/src/idea/migrations/0022_auto__ren_field_banner_private__add_field_banner_is_votes__add_field_i.py new file mode 100644 index 0000000..a0036d4 --- /dev/null +++ b/src/idea/migrations/0022_auto__ren_field_banner_private__add_field_banner_is_votes__add_field_i.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Renaming field 'Banner.private' + db.rename_column(u'idea_banner', 'private', 'is_private') + + # Adding field 'Banner.is_votes' + db.add_column(u'idea_banner', 'is_votes', + self.gf('django.db.models.fields.BooleanField')(default=True), + keep_default=False) + + # Adding field 'Idea.is_anonymous' + db.add_column(u'idea_idea', 'is_anonymous', + self.gf('django.db.models.fields.BooleanField')(default=False), + keep_default=False) + + + def backwards(self, orm): + # Renaming field 'Banner.private' + db.rename_column(u'idea_banner', 'is_private', 'private') + + # Deleting field 'Banner.is_votes' + db.delete_column(u'idea_banner', 'is_votes') + + # Deleting field 'Idea.is_anonymous' + db.delete_column(u'idea_idea', 'is_anonymous') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'core.collabuser': { + 'Meta': {'object_name': 'CollabUser'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '254', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '75', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '75', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '75'}) + }, + u'idea.banner': { + 'Meta': {'object_name': 'Banner'}, + 'end_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_votes': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50', 'blank': 'True'}), + 'start_date': ('django.db.models.fields.DateField', [], {}), + 'text': ('django.db.models.fields.TextField', [], {'max_length': '2000'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'idea.config': { + 'Meta': {'object_name': 'Config'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50'}), + 'value': ('django.db.models.fields.TextField', [], {'max_length': '2000'}) + }, + u'idea.idea': { + 'Meta': {'object_name': 'Idea'}, + 'banner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['idea.Banner']", 'null': 'True', 'blank': 'True'}), + 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['core.CollabUser']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['idea.State']"}), + 'summary': ('django.db.models.fields.TextField', [], {'max_length': '200'}), + 'text': ('django.db.models.fields.TextField', [], {'max_length': '2000'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 12, 11, 0, 0)'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'voters': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'idea_vote_creator'", 'null': 'True', 'through': u"orm['idea.Vote']", 'to': u"orm['core.CollabUser']"}) + }, + u'idea.state': { + 'Meta': {'object_name': 'State'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'previous': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['idea.State']", 'unique': 'True', 'null': 'True', 'blank': 'True'}) + }, + u'idea.vote': { + 'Meta': {'object_name': 'Vote'}, + 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['core.CollabUser']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'idea': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['idea.Idea']"}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 12, 11, 0, 0)'}), + 'vote': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}) + }, + u'taggit.tag': { + 'Meta': {'object_name': 'Tag'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100'}) + }, + u'taggit.tagcategory': { + 'Meta': {'object_name': 'TagCategory'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}) + }, + u'taggit.taggeditem': { + 'Meta': {'object_name': 'TaggedItem'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_tagged_items'", 'to': u"orm['contenttypes.ContentType']"}), + 'create_timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_items'", 'to': u"orm['taggit.Tag']"}), + 'tag_category': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['taggit.TagCategory']", 'null': 'True'}), + 'tag_creator': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'taggit_taggeditem_related'", 'null': 'True', 'to': u"orm['core.CollabUser']"}) + } + } + + complete_apps = ['idea'] diff --git a/src/idea/models.py b/src/idea/models.py index c836b98..193743f 100644 --- a/src/idea/models.py +++ b/src/idea/models.py @@ -76,7 +76,8 @@ class Banner(models.Model): end_date = models.DateField(null=True, blank=True, help_text="Empty indicates that the banner " + "should be continued indefinitely. ") - private = models.BooleanField(default=False) + is_private = models.BooleanField('private room', default=False) + is_votes = models.BooleanField('voting enabled', default=True) def save(self, *args, **kwargs): unique_slug(self, 'title', 'slug') @@ -88,6 +89,12 @@ def __unicode__(self): else: return u'%s' % self.title + def room_url(self): + return reverse('idea:room_detail', args=(self.slug,)) + + def room_link(self): + return "%s" % (self.room_url(), self.title) + class State(models.Model): @@ -151,6 +158,8 @@ class Idea(UserTrackable): voters = models.ManyToManyField(settings.AUTH_USER_MODEL, through="Vote", null=True, related_name="idea_vote_creator") + is_anonymous = models.BooleanField("anonymous Idea", default=False, help_text=""" + Only enable anonymous if the Idea's challenge is private""") def __unicode__(self): return u'%s' % self.title @@ -176,16 +185,19 @@ def members(self): members.append(self.creator) for c in self.comments: - if c.user not in members: + if c.user not in members and not c.is_anonymous: members.append(c.user) return members def get_creator_profile(self): - try: - return self.creator.get_profile() - except (ObjectDoesNotExist, SiteProfileNotAvailable): + if self.is_anonymous: return None + else: + try: + return self.creator.get_profile() + except (ObjectDoesNotExist, SiteProfileNotAvailable): + return None objects = IdeaManager() diff --git a/src/idea/search_indexes.py b/src/idea/search_indexes.py index 103e000..d7bfa54 100644 --- a/src/idea/search_indexes.py +++ b/src/idea/search_indexes.py @@ -35,7 +35,7 @@ def get_model(self): def index_queryset(self, using=None): """Used when the entire index for model is updated.""" # State 2 = Archived - return self.get_model().objects.related_with_counts().exclude(state=2).exclude(banner__private=True) + return self.get_model().objects.related_with_counts().exclude(state=2).exclude(banner__is_private=True) class BannerIndex(indexes.SearchIndex, indexes.Indexable): @@ -69,4 +69,4 @@ def get_model(self): def index_queryset(self, using=None): """Used when the entire index for model is updated.""" - return self.get_model().objects.exclude(private=True) + return self.get_model().objects.exclude(is_private=True) diff --git a/src/idea/static/idea/css/idea.css b/src/idea/static/idea/css/idea.css index aea01b8..e16d0e4 100644 --- a/src/idea/static/idea/css/idea.css +++ b/src/idea/static/idea/css/idea.css @@ -317,11 +317,14 @@ ul.section-nav li.active a { .comment-form form { position: relative; } -.comment-form label { +.comment-form .comment-label { color: #666; position: absolute; left: .25em; } +.comment-form #id_is_anonymous { + vertical-align: middle; +} .comment-list { list-style-type: none; margin: 0; @@ -500,10 +503,15 @@ ul.section-nav li.active a { margin-bottom: 0; margin-right: .5em; } +.project-add .is_anonymous { + padding-bottom: 1em; +} +.project-add .is_anonymous label, .project-add .challenge-checkbox label { display: inline-block; vertical-align: middle; } +.project-add .is_anonymous input, .project-add .challenge-checkbox input { display: inline-block; vertical-align: middle; diff --git a/src/idea/static/idea/js/admin.js b/src/idea/static/idea/js/admin.js new file mode 100644 index 0000000..68841a2 --- /dev/null +++ b/src/idea/static/idea/js/admin.js @@ -0,0 +1,22 @@ +(function($) { + +// In Django Admin page for Idea Banner, if Private is set, display option to +// enable or disable votes. If Private is unchecked, for the voting to +// be set to true and hide the checkbox +function set_votes_visibility() { + if ($('.field-box.field-is_private > input').is(':checked')) { + $('.field-box.field-is_votes').show(); + $('.field-box.field-slug').show(); + } else { + $('.field-box.field-is_votes').hide(); + $('.field-box.field-slug').hide(); + $('.field-box.field-is_votes > input').attr('checked', 'checked'); + } +}; +$(document).ready(function() { + $('.field-box.field-is_private > input').click(set_votes_visibility); + + set_votes_visibility(); +}); + +})(django.jQuery); diff --git a/src/idea/templates/idea/add.html b/src/idea/templates/idea/add.html index 7e7a73b..5e3602b 100644 --- a/src/idea/templates/idea/add.html +++ b/src/idea/templates/idea/add.html @@ -12,11 +12,13 @@
      @@ -26,7 +28,7 @@

      Share your Idea

      {% csrf_token %} {% for field in form %}
      - {% if 'challenge-checkbox' == field.html_name %} + {% if 'challenge-checkbox' == field.html_name or 'is_anonymous' == field.html_name %} {{ field }}