diff --git a/Gemfile b/Gemfile
index 9f03d3da7..5430c9c41 100644
--- a/Gemfile
+++ b/Gemfile
@@ -24,7 +24,7 @@ gem 'devise'
gem 'omniauth'
gem 'omniauth-twitter'
gem 'omniauth-facebook', '~> 3.0.0'
-gem 'omniauth-google-oauth2', '~> 0.2.10'
+gem 'omniauth-google-oauth2', '~> 0.3.0'
gem 'kaminari'
gem 'ancestry'
diff --git a/Gemfile.lock b/Gemfile.lock
index f0f5a9dbe..4756e3aed 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -67,9 +67,9 @@ GEM
arel (6.0.3)
ast (2.2.0)
bcrypt (3.1.10)
- browser (1.0.1)
+ browser (1.1.0)
builder (3.2.2)
- bullet (4.14.10)
+ bullet (5.0.0)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.9.0)
byebug (8.2.1)
@@ -96,7 +96,7 @@ GEM
rack-test (>= 0.5.4)
xpath (~> 2.0)
chronic (0.10.2)
- ckeditor (4.1.5)
+ ckeditor (4.1.6)
cocaine
orm_adapter (~> 0.5.0)
climate_control (0.0.3)
@@ -173,7 +173,7 @@ GEM
fuubar (2.0.0)
rspec (~> 3.0)
ruby-progressbar (~> 1.4)
- geocoder (1.2.13)
+ geocoder (1.2.14)
globalid (0.3.6)
activesupport (>= 4.1.0)
groupdate (2.5.0)
@@ -229,7 +229,7 @@ GEM
multipart-post (2.0.0)
net-scp (1.2.1)
net-ssh (>= 2.6.5)
- net-ssh (3.0.1)
+ net-ssh (3.0.2)
netrc (0.11.0)
newrelic_rpm (3.14.1.311)
nokogiri (1.6.7.1)
@@ -247,23 +247,23 @@ GEM
rack (>= 1.0, < 3)
omniauth-facebook (3.0.0)
omniauth-oauth2 (~> 1.2)
- omniauth-google-oauth2 (0.2.10)
+ omniauth-google-oauth2 (0.3.0)
addressable (~> 2.3)
jwt (~> 1.0)
multi_json (~> 1.3)
omniauth (>= 1.1.1)
- omniauth-oauth2 (~> 1.3.1)
+ omniauth-oauth2 (>= 1.3.1)
omniauth-oauth (1.1.0)
oauth
omniauth (~> 1.0)
- omniauth-oauth2 (1.3.1)
+ omniauth-oauth2 (1.4.0)
oauth2 (~> 1.0)
omniauth (~> 1.2)
omniauth-twitter (1.2.1)
json (~> 1.3)
omniauth-oauth (~> 1.1)
orm_adapter (0.5.0)
- paranoia (2.1.4)
+ paranoia (2.1.5)
activerecord (~> 4.0)
parser (2.2.3.0)
ast (>= 1.1, < 3.0)
@@ -310,9 +310,9 @@ GEM
thor (>= 0.18.1, < 2.0)
raindrops (0.15.0)
rake (10.4.2)
- redcarpet (3.3.3)
+ redcarpet (3.3.4)
referer-parser (0.3.0)
- request_store (1.2.1)
+ request_store (1.3.0)
responders (2.1.1)
railties (>= 4.2.0, < 5.1)
rest-client (1.8.0)
@@ -331,7 +331,7 @@ GEM
rspec-expectations (3.4.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.4.0)
- rspec-mocks (3.4.0)
+ rspec-mocks (3.4.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.4.0)
rspec-rails (3.4.0)
@@ -344,14 +344,14 @@ GEM
rspec-support (~> 3.4.0)
rspec-support (3.4.1)
ruby-progressbar (1.7.5)
- sass (3.4.20)
+ sass (3.4.21)
sass-rails (5.0.4)
railties (>= 4.0.0, < 5.0)
sass (~> 3.1)
sprockets (>= 2.8, < 4.0)
sprockets-rails (>= 2.0, < 4.0)
tilt (>= 1.1, < 3)
- sassc (1.8.2)
+ sassc (1.8.3)
bundler
ffi (~> 1.9.6)
sass (>= 3.3.0)
@@ -369,14 +369,14 @@ GEM
nokogiri (>= 1.4.0)
nori (~> 2.4)
wasabi (~> 3.4)
- simple_captcha2 (0.3.4)
+ simple_captcha2 (0.4.0)
rails (>= 4.1)
simplecov (0.11.1)
docile (~> 1.1.0)
json (~> 1.8)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.0)
- spring (1.6.1)
+ spring (1.6.2)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
sprockets (3.5.2)
@@ -395,11 +395,11 @@ GEM
thor (0.19.1)
thread (0.2.2)
thread_safe (0.3.5)
- tilt (2.0.1)
+ tilt (2.0.2)
tins (1.6.0)
turbolinks (2.5.3)
coffee-rails
- turnout (2.2.0)
+ turnout (2.2.1)
rack (~> 1.3)
rack-accept (~> 0.4)
tzinfo (1.2.2)
@@ -477,7 +477,7 @@ DEPENDENCIES
newrelic_rpm (~> 3.14)
omniauth
omniauth-facebook (~> 3.0.0)
- omniauth-google-oauth2 (~> 0.2.10)
+ omniauth-google-oauth2 (~> 0.3.0)
omniauth-twitter
paranoia
pg
@@ -504,4 +504,4 @@ DEPENDENCIES
whenever
BUNDLED WITH
- 1.10.6
+ 1.11.2
diff --git a/app/assets/fonts/icons.eot b/app/assets/fonts/icons.eot
index 3046c50cc..ced98a9b4 100644
Binary files a/app/assets/fonts/icons.eot and b/app/assets/fonts/icons.eot differ
diff --git a/app/assets/fonts/icons.svg b/app/assets/fonts/icons.svg
index c107e740d..d92b9a44a 100644
--- a/app/assets/fonts/icons.svg
+++ b/app/assets/fonts/icons.svg
@@ -19,7 +19,6 @@
-
@@ -30,12 +29,10 @@
-
-
@@ -45,4 +42,8 @@
+
+
+
+
diff --git a/app/assets/fonts/icons.ttf b/app/assets/fonts/icons.ttf
index b933f1c3f..f3486cf96 100644
Binary files a/app/assets/fonts/icons.ttf and b/app/assets/fonts/icons.ttf differ
diff --git a/app/assets/fonts/icons.woff b/app/assets/fonts/icons.woff
index feb047bd0..9045aa563 100644
Binary files a/app/assets/fonts/icons.woff and b/app/assets/fonts/icons.woff differ
diff --git a/app/assets/images/featured_debates.jpg b/app/assets/images/featured_debates.jpg
deleted file mode 100644
index f1daa4c6e..000000000
Binary files a/app/assets/images/featured_debates.jpg and /dev/null differ
diff --git a/app/assets/images/featured_proposals.jpg b/app/assets/images/featured_proposals.jpg
deleted file mode 100644
index 303c21747..000000000
Binary files a/app/assets/images/featured_proposals.jpg and /dev/null differ
diff --git a/app/assets/stylesheets/admin.scss b/app/assets/stylesheets/admin.scss
index 5d3418748..470885665 100644
--- a/app/assets/stylesheets/admin.scss
+++ b/app/assets/stylesheets/admin.scss
@@ -70,10 +70,6 @@ body.admin {
.admin-content {
margin-top: rem-calc(24);
- .filters h2 {
- margin-top: 0;
- }
-
.proposal-new, .proposal-edit {
padding-top: 0;
}
diff --git a/app/assets/stylesheets/icons.scss b/app/assets/stylesheets/icons.scss
index eda8b979d..46e24e4cf 100644
--- a/app/assets/stylesheets/icons.scss
+++ b/app/assets/stylesheets/icons.scss
@@ -38,116 +38,119 @@
}
.icon-angle-down:before {
- content: "a";
+ content: "\61";
}
.icon-angle-left:before {
- content: "b";
+ content: "\62";
}
.icon-angle-right:before {
- content: "c";
+ content: "\63";
}
.icon-angle-up:before {
- content: "d";
+ content: "\64";
}
.icon-comments:before {
- content: "e";
+ content: "\65";
}
.icon-twitter:before {
- content: "f";
+ content: "\66";
}
.icon-calendar:before {
- content: "g";
+ content: "\67";
}
.icon-debates:before {
- content: "i";
+ content: "\69";
}
.icon-unlike:before {
- content: "j";
+ content: "\6a";
}
.icon-like:before {
- content: "k";
+ content: "\6b";
}
.icon-check:before {
- content: "l";
+ content: "\6c";
}
.icon-edit:before {
- content: "m";
-}
-.icon-star:before {
- content: "n";
+ content: "\6d";
}
.icon-user:before {
- content: "o";
+ content: "\6f";
}
.icon-settings:before {
- content: "q";
+ content: "\71";
}
.icon-stats:before {
- content: "r";
+ content: "\72";
}
.icon-proposals:before {
- content: "h";
+ content: "\68";
}
.icon-organizations:before {
- content: "s";
+ content: "\73";
}
.icon-deleted:before {
- content: "t";
+ content: "\74";
}
.icon-tag:before {
- content: "u";
+ content: "\75";
}
.icon-eye:before {
- content: "p";
+ content: "\70";
}
.icon-x:before {
- content: "v";
+ content: "\76";
}
.icon-flag:before {
- content: "w";
-}
-.icon-notification:before {
- content: "x";
+ content: "\77";
}
.icon-comment:before {
- content: "y";
+ content: "\79";
}
.icon-reply:before {
- content: "z";
+ content: "\7a";
}
.icon-facebook:before {
- content: "A";
+ content: "\41";
}
.icon-google-plus:before {
- content: "B";
-}
-.icon-language:before {
- content: "C";
+ content: "\42";
}
.icon-search:before {
- content: "E";
+ content: "\45";
}
.icon-external:before {
- content: "F";
+ content: "\46";
}
.icon-video:before {
- content: "D";
+ content: "\44";
}
.icon-document:before {
- content: "G";
+ content: "\47";
}
.icon-print:before {
- content: "H";
+ content: "\48";
}
.icon-blog:before {
- content: "J";
+ content: "\4a";
}
.icon-box:before {
- content: "I";
+ content: "\49";
}
.icon-youtube:before {
- content: "K";
+ content: "\4b";
}
.icon-letter:before {
- content: "L";
-}
\ No newline at end of file
+ content: "\4c";
+}
+.icon-no-notification:before {
+ content: "\78";
+}
+.icon-notification:before {
+ content: "\6e";
+}
+.icon-circle:before {
+ content: "\43";
+}
+.icon-circle-o:before {
+ content: "\4d";
+}
diff --git a/app/assets/stylesheets/layout.scss b/app/assets/stylesheets/layout.scss
index 62226e967..d7cf4d4d1 100644
--- a/app/assets/stylesheets/layout.scss
+++ b/app/assets/stylesheets/layout.scss
@@ -8,7 +8,7 @@
// 06. Forms
// 07. Alerts
// 08. User account
-// 09. Filters & search
+// 09. Search
// 10. Official levels
// 11. Pagination
// 12. Tables
@@ -220,6 +220,16 @@ a {
}
}
+.sub-nav {
+ border-bottom: 1px solid $border;
+
+ dd.active {
+ border-bottom: 2px solid $brand;
+ color: $brand;
+ padding-bottom: $line-height/4;
+ }
+}
+
// 02. Header
// - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -396,7 +406,6 @@ header {
&:hover {
background: none;
color: white;
- text-decoration: underline;
transition: text-decoration 275ms;
}
@@ -420,6 +429,7 @@ header {
&:hover, &:focus {
background-color: #007095 !important;
+ text-decoration: underline;
}
}
@@ -629,11 +639,7 @@ footer {
background-repeat: no-repeat;
background-size: cover;
- @media (min-width: $small-breakpoint) {
- padding-top: $line-height*2;
- }
-
- h1 {
+ h1:not(.logo) {
@include logo;
a {
@@ -651,25 +657,18 @@ footer {
}
.auth {
- min-height: $line-height*20;
.back, .icon-angle-left {
@include back;
}
- p, a {
- font-family: $font-sans;
+ p, a, .checkbox {
font-size: $small-font-size;
}
.panel {
background: white;
border: 0;
-
- h1 {
- font-size: rem-calc(30);
- font-weight: bolder;
- }
}
}
@@ -947,60 +946,80 @@ img.avatar, img.admin-avatar, img.moderator-avatar, img.initialjs-avatar {
}
}
-// 09. Filters & search
-// - - - - - - - - - - - - - - - - - - - - - - - - -
+.notifications {
+ position: relative;
-.filters, .search-results {
-
- h2 {
- display: inline-block;
- font-size: rem-calc(24);
- margin: $line-height/2 0;
-
- @media (min-width: $small-breakpoint) {
- margin: $line-height 0;
- }
+ &:hover {
+ text-decoration: none;
}
- select {
- background-color: #DEE0E2;
- height: auto;
- margin-bottom: $line-height;
- min-width: $line-height*7.5;
- outline: 0;
- padding: $line-height/2;
- width: auto;
-
- @media (min-width: $small-breakpoint) {
- margin: 0 0 $line-height $line-height/2;
- }
-
- optgroup {
- font-size: $small-font-size;
- }
-
- option {
-
- &:after {
- content: "a";
- font-family: "icons";
- }
- }
+ [class^="icon-"] {
+ font-size: $h4-font-size;
+ vertical-align: middle;
}
- .debates-order {
- text-align: left;
-
- @media (min-width: $small-breakpoint) {
- text-align: right;
- }
-
- select {
- margin-left: 0;
- }
+ .icon-circle {
+ color: #ecf00b;
+ font-size: $tiny-font-size;
+ position: absolute;
+ right: 4px;
+ top: -6px;
}
}
+.notifications-list:before {
+ background: $border;
+ content: '';
+ height: 100%;
+ left: 28px;
+ position: absolute;
+ top: 84px;
+ width: 2px;
+}
+
+.notification {
+ display: block;
+ padding: $line-height/2 0 $line-height/2 $line-height*1.5;
+ position: relative;
+
+ &:hover {
+
+ a {
+ text-decoration: none;
+ }
+
+ p:not(.time) {
+ color: $link;
+ }
+
+ &:before {
+ content: "\43";
+ }
+ }
+
+ &:before {
+ background: white;
+ color: $brand;
+ content: "\4d";
+ font-family: "icons" !important;
+ left: 6px;
+ position: absolute;
+ }
+
+ p {
+ color: $text;
+ margin-bottom: 0;
+ }
+
+ .time {
+ font-size: $small-font-size;
+ color: $text-medium;
+ }
+}
+
+// 09. Search
+// - - - - - - - - - - - - - - - - - - - - - - - - -
+
.search-form {
h3 {
@@ -1574,7 +1593,6 @@ table {
font-family: $font-sans;
font-size: $small-font-size;
line-height: $line-height;
- margin: rem-calc(10) $line-height/2 $line-height/4 0;
a {
color: $text-light;
@@ -1595,15 +1613,12 @@ table {
.comment-body {
margin-left: rem-calc(42);
- p {
- font-size: $small-font-size;
- }
-
.reply {
background: white;
border: 1px solid $border;
- font-family: $font-sans;
- font-size: rem-calc(12);
+ border-left: 0;
+ border-right: 0;
+ font-size: $small-font-size;
margin: rem-calc(6) 0;
padding: rem-calc(6);
@@ -1735,17 +1750,6 @@ table {
.activity {
margin-bottom: $line-height*2;
- margin-top: $line-height;
-
- .sub-nav {
- border-bottom: 1px solid $border;
-
- dd.active {
- border-bottom: 2px solid $brand;
- color: $brand;
- padding-bottom: $line-height/4;
- }
- }
table {
border: 0;
diff --git a/app/assets/stylesheets/participation.scss b/app/assets/stylesheets/participation.scss
index c7d296744..ba61be077 100644
--- a/app/assets/stylesheets/participation.scss
+++ b/app/assets/stylesheets/participation.scss
@@ -789,8 +789,12 @@
// 05. Featured
// - - - - - - - - - - - - - - - - - - - - - - - - -
-.featured-debates-container {
- background: $votes-bg;
+.featured-debates, .featured-proposals {
+ padding: $line-height/2 0;
+
+ @media (min-width: $small-breakpoint) {
+ margin-top: $line-height !important;
+ }
@media (min-width: $medium-breakpoint) {
margin-left: 0 !important;
@@ -798,134 +802,40 @@
}
h2 {
- color: white;
- }
-
- .debates-list {
- margin-bottom: 0;
- }
-}
-
-.featured-proposals-container {
- background: #FED900;
-
- @media (min-width: $medium-breakpoint) {
- margin-left: 0 !important;
- margin-right: rem-calc(-15) !important;
- }
-
- h2 {
- color: $text;
-
- @media (min-width: $medium-breakpoint) {
- margin-bottom: rem-calc(27);
- }
- }
-}
-
-.featured-debates-container, .featured-proposals-container {
- margin: $line-height 0 !important;
-
- h2 {
- font-size: rem-calc(24);
- margin: rem-calc(12) 0 0;
- position: relative;
+ font-size: $small-font-size;
+ line-height: $line-height;
text-transform: uppercase;
+ }
- .icon-proposals {
- font-size: rem-calc(20);
- position: absolute;
- right: -26px;
- top: -5px;
+ h3 {
+ margin-bottom: 0;
+
+ a {
+ color: $text;
+ font-size: $h3-font-size;
}
}
- .debates-list {
-
- @media (min-width: $medium-breakpoint) {
- border-left: 1px solid $votes-border;
- }
-
- &:after {
-
- @media (min-width: $medium-breakpoint) {
- content: none;
- }
- }
+ a, .info {
+ color: lighten($text, 15%);
+ font-size: $small-font-size;
}
- .proposals-list {
-
- @media (min-width: $medium-breakpoint) {
- border-left: 1px solid #D2B400;
- }
-
- &:after {
- border-color: #C9AF00 transparent transparent transparent;
-
- @media (min-width: $medium-breakpoint) {
- content: " ";
- }
- }
- }
-
- .debates-list, .proposals-list {
- margin: rem-calc(6) 0;
-
- &:after {
- content: none;
- position: absolute;
- display: block;
- border-style: solid;
- bottom: rem-calc(-20);
- border-left-width: 0;
- border-right-color: transparent;
- right: rem-calc(1);
- border-width: rem-calc(14) rem-calc(14) 0 0;
- }
- }
}
-.debate-featured, .proposal-featured {
- margin-bottom: 0;
- margin-top: 0;
- padding-left: 0;
+.featured-debates {
+ background: $highlight;
+}
- .panel {
- background: none;
- border: 0;
- margin-bottom: 0;
- padding: 0 rem-calc(12);
-
- h3 {
- font-weight: bold;
- margin: rem-calc(8) 0 0 0;
- min-height: auto;
-
- a {
- clear: both;
- display: block;
- font-size: rem-calc(14);
- line-height: $line-height;
- }
- }
- }
-
- .progress {
- display: none !important;
- }
-
- .content {
- overflow: hidden;
- }
+.featured-proposals {
+ background: $featured;
.supports {
@include supports;
background: none;
border: 0;
- padding: 0;
- padding-left: rem-calc(13);
- margin-right: rem-calc(-26);
+ padding-bottom: 0;
+ padding-top: 0;
&:after {
content: none;
@@ -935,8 +845,12 @@
display: none;
}
+ .button-support {
+ margin-top: 0;
+ }
+
.not-logged, .organizations-votes {
- background: #FED900;
+ background: $featured;
color: $warning-color;
font-size: $small-font-size;
line-height: rem-calc(24);
@@ -949,14 +863,12 @@
}
.anonymous-votes {
- background: #FED900;
- padding: 0;
- padding-top: rem-calc(6);
+ background: $featured;
p {
- font-size: rem-calc(12);
- line-height: rem-calc(20);
- margin: 0;
+ color: $text;
+ font-size: $small-font-size;
+ line-height: $line-height;
}
}
@@ -967,10 +879,6 @@
text-align: center;
}
- .button-support {
- margin-top: rem-calc(4);
- }
-
.supported {
margin-top: 0;
}
@@ -981,53 +889,12 @@
.social-share-button-facebook,
.social-share-button-google_plus {
height: rem-calc(33);
- }
- .social-share-button-twitter:before,
- .social-share-button-facebook:before,
- .social-share-button-google_plus:before {
- font-size: rem-calc(18);
- line-height: rem-calc(33);
+ &:before {
+ font-size: rem-calc(18);
+ line-height: rem-calc(33);
+ }
}
}
}
}
-
-.debate-featured {
- background: $votes-bg;
- margin-bottom: rem-calc(12) !important;
-
- .panel h3 a {
- color: #0D3F54;
- font-size: $h3-font-size;
- margin: 0;
- }
-
- .info, .info a {
- color: white !important;
- font-family: $font-sans;
- font-size: $small-font-size;
- }
-
- .content {
- height: auto;
- }
-}
-
-.proposal-featured {
- background: #FED900;
-
- .panel h3 a {
- color: $text;
- font-size: $base-font-size;
- }
-
- .info {
- color: #806C00 !important;
- font-size: $small-font-size;
- }
-
- .content {
- height: rem-calc(60);
- }
-}
\ No newline at end of file
diff --git a/app/assets/stylesheets/variables.scss b/app/assets/stylesheets/variables.scss
index d9da9c580..3cb09718c 100644
--- a/app/assets/stylesheets/variables.scss
+++ b/app/assets/stylesheets/variables.scss
@@ -39,6 +39,7 @@ $h6-font-size: rem-calc(13);
$h6-line-height: rem-calc(17);
$small-font-size: rem-calc(14);
+$tiny-font-size: rem-calc(10);
$line-height: rem-calc(24);
// 02. Colors
@@ -73,6 +74,7 @@ $proposals: #FFA42D;
$proposals-border: #CC8425;
$highlight: #E7F2FC;
+$featured: #FED900;
$footer-bg: #DEE0E2;
$footer-color: #171819;
diff --git a/app/controllers/admin/spending_proposals_controller.rb b/app/controllers/admin/spending_proposals_controller.rb
new file mode 100644
index 000000000..817de285d
--- /dev/null
+++ b/app/controllers/admin/spending_proposals_controller.rb
@@ -0,0 +1,23 @@
+class Admin::SpendingProposalsController < Admin::BaseController
+ has_filters %w{unresolved accepted rejected}, only: :index
+
+ load_and_authorize_resource
+
+ def index
+ @spending_proposals = @spending_proposals.includes([:geozone]).send(@current_filter).order(created_at: :desc).page(params[:page])
+ end
+
+ def show
+ end
+
+ def accept
+ @spending_proposal.accept
+ redirect_to request.query_parameters.merge(action: :index)
+ end
+
+ def reject
+ @spending_proposal.reject
+ redirect_to request.query_parameters.merge(action: :index)
+ end
+
+end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 841b32498..5150f97ec 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -108,4 +108,5 @@ class ApplicationController < ActionController::Base
store_location_for(:user, request.path)
end
end
+
end
diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb
index 35406faf0..df928aa02 100644
--- a/app/controllers/comments_controller.rb
+++ b/app/controllers/comments_controller.rb
@@ -1,5 +1,5 @@
class CommentsController < ApplicationController
- before_action :authenticate_user!
+ before_action :authenticate_user!, only: :create
before_action :load_commentable, only: :create
before_action :build_comment, only: :create
@@ -9,11 +9,17 @@ class CommentsController < ApplicationController
def create
if @comment.save
CommentNotifier.new(comment: @comment).process
+ add_notification @comment
else
render :new
end
end
+ def show
+ @comment = Comment.find(params[:id])
+ set_comment_flags(@comment.subtree)
+ end
+
def vote
@comment.vote_by(voter: current_user, vote: params[:value])
respond_with @comment
@@ -62,4 +68,13 @@ class CommentsController < ApplicationController
["1", true].include?(comment_params[:as_moderator]) && can?(:comment_as_moderator, @commentable)
end
+ def add_notification(comment)
+ if comment.reply?
+ notifiable = comment.parent
+ else
+ notifiable = comment.commentable
+ end
+ Notification.add(notifiable.author_id, notifiable) unless comment.author_id == notifiable.author_id
+ end
+
end
diff --git a/app/controllers/moderation/users_controller.rb b/app/controllers/moderation/users_controller.rb
index 6ff22f94a..3255bb6cc 100644
--- a/app/controllers/moderation/users_controller.rb
+++ b/app/controllers/moderation/users_controller.rb
@@ -30,4 +30,4 @@ class Moderation::UsersController < Moderation::BaseController
Activity.log(current_user, :block, @user)
end
-end
+end
\ No newline at end of file
diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb
new file mode 100644
index 000000000..a4ec31b50
--- /dev/null
+++ b/app/controllers/notifications_controller.rb
@@ -0,0 +1,26 @@
+class NotificationsController < ApplicationController
+ before_action :authenticate_user!
+ after_action :mark_as_read, only: :show
+ skip_authorization_check
+
+ def index
+ @notifications = current_user.notifications.unread.recent.for_render
+ end
+
+ def show
+ @notification = current_user.notifications.find(params[:id])
+ redirect_to url_for(@notification.notifiable)
+ end
+
+ def mark_all_as_read
+ current_user.notifications.each { |notification| notification.mark_as_read }
+ redirect_to notifications_path
+ end
+
+ private
+
+ def mark_as_read
+ @notification.mark_as_read
+ end
+
+end
diff --git a/app/controllers/spending_proposals_controller.rb b/app/controllers/spending_proposals_controller.rb
new file mode 100644
index 000000000..a7871549f
--- /dev/null
+++ b/app/controllers/spending_proposals_controller.rb
@@ -0,0 +1,32 @@
+class SpendingProposalsController < ApplicationController
+ before_action :authenticate_user!, except: [:index]
+
+ load_and_authorize_resource
+
+ def index
+ end
+
+ def new
+ @spending_proposal = SpendingProposal.new
+ @featured_tags = ActsAsTaggableOn::Tag.where(featured: true)
+ end
+
+ def create
+ @spending_proposal = SpendingProposal.new(spending_proposal_params)
+ @spending_proposal.author = current_user
+
+ if @spending_proposal.save_with_captcha
+ redirect_to spending_proposals_path, notice: t('flash.actions.create.notice', resource_name: t("activerecord.models.spending_proposal", count: 1))
+ else
+ @featured_tags = ActsAsTaggableOn::Tag.where(featured: true)
+ render :new
+ end
+ end
+
+ private
+
+ def spending_proposal_params
+ params.require(:spending_proposal).permit(:title, :description, :external_url, :geozone_id, :terms_of_service, :captcha, :captcha_key)
+ end
+
+end
\ No newline at end of file
diff --git a/app/helpers/comments_helper.rb b/app/helpers/comments_helper.rb
index 9ac29e4da..9d9d08075 100644
--- a/app/helpers/comments_helper.rb
+++ b/app/helpers/comments_helper.rb
@@ -13,8 +13,11 @@ module CommentsHelper
end
def child_comments_of(parent)
- return [] unless @comment_tree
- @comment_tree.children_of(parent)
+ if @comment_tree.present?
+ @comment_tree.ordered_children_of(parent)
+ else
+ parent.children
+ end
end
def user_level_class(comment)
diff --git a/app/helpers/geozones_helper.rb b/app/helpers/geozones_helper.rb
new file mode 100644
index 000000000..bfc5f9105
--- /dev/null
+++ b/app/helpers/geozones_helper.rb
@@ -0,0 +1,11 @@
+module GeozonesHelper
+
+ def geozone_name(geozonable)
+ geozonable.geozone ? geozonable.geozone.name : t("geozones.none")
+ end
+
+ def geozone_select_options
+ Geozone.all.order(name: :asc).collect { |g| [ g.name, g.id ] }
+ end
+
+end
diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
new file mode 100644
index 000000000..281163380
--- /dev/null
+++ b/app/helpers/notifications_helper.rb
@@ -0,0 +1,6 @@
+module NotificationsHelper
+
+ def notification_action(notification)
+ notification.notifiable_type == "Comment" ? "replies_to" : "comments_on"
+ end
+end
diff --git a/app/models/abilities/administrator.rb b/app/models/abilities/administrator.rb
index 4af32a75c..6ba99e4fa 100644
--- a/app/models/abilities/administrator.rb
+++ b/app/models/abilities/administrator.rb
@@ -34,6 +34,8 @@ module Abilities
can [:search, :create, :index, :destroy], ::Moderator
can :manage, Annotation
+
+ can :manage, SpendingProposal
end
end
end
diff --git a/app/models/abilities/common.rb b/app/models/abilities/common.rb
index 181238d2c..4d74ce375 100644
--- a/app/models/abilities/common.rb
+++ b/app/models/abilities/common.rb
@@ -17,6 +17,8 @@ module Abilities
proposal.editable_by?(user)
end
+ can :read, SpendingProposal
+
can :create, Comment
can :create, Debate
can :create, Proposal
@@ -38,6 +40,7 @@ module Abilities
if user.level_two_or_three_verified?
can :vote, Proposal
can :vote_featured, Proposal
+ can :create, SpendingProposal
end
can :create, Annotation
diff --git a/app/models/abilities/everyone.rb b/app/models/abilities/everyone.rb
index d6b2a6d57..377ef7440 100644
--- a/app/models/abilities/everyone.rb
+++ b/app/models/abilities/everyone.rb
@@ -5,6 +5,8 @@ module Abilities
def initialize(user)
can :read, Debate
can :read, Proposal
+ can :read, Comment
+ can :read, SpendingProposal
can :read, Legislation
can :read, User
can [:search, :read], Annotation
diff --git a/app/models/activity.rb b/app/models/activity.rb
index 977204669..047ccb7dd 100644
--- a/app/models/activity.rb
+++ b/app/models/activity.rb
@@ -1,5 +1,4 @@
class Activity < ActiveRecord::Base
-
belongs_to :actionable, -> { with_hidden }, polymorphic: true
belongs_to :user, -> { with_hidden }
@@ -24,5 +23,4 @@ class Activity < ActiveRecord::Base
def self.by(user)
where(user: user)
end
-
end
diff --git a/app/models/concerns/sanitizable.rb b/app/models/concerns/sanitizable.rb
index 65c8431ef..261eccbf2 100644
--- a/app/models/concerns/sanitizable.rb
+++ b/app/models/concerns/sanitizable.rb
@@ -13,7 +13,7 @@ module Sanitizable
end
def sanitize_tag_list
- self.tag_list = TagSanitizer.new.sanitize_tag_list(self.tag_list)
+ self.tag_list = TagSanitizer.new.sanitize_tag_list(self.tag_list) if self.class.taggable?
end
end
diff --git a/app/models/geozone.rb b/app/models/geozone.rb
new file mode 100644
index 000000000..303671493
--- /dev/null
+++ b/app/models/geozone.rb
@@ -0,0 +1,3 @@
+class Geozone < ActiveRecord::Base
+ validates :name, presence: true
+end
diff --git a/app/models/notification.rb b/app/models/notification.rb
new file mode 100644
index 000000000..1cb500ccf
--- /dev/null
+++ b/app/models/notification.rb
@@ -0,0 +1,24 @@
+class Notification < ActiveRecord::Base
+ belongs_to :user, counter_cache: true
+ belongs_to :notifiable, polymorphic: true
+
+ scope :unread, -> { all }
+ scope :recent, -> { order(id: :desc) }
+ scope :for_render, -> { includes(:notifiable) }
+
+ def timestamp
+ notifiable.created_at
+ end
+
+ def mark_as_read
+ self.destroy
+ end
+
+ def self.add(user_id, notifiable)
+ if notification = Notification.find_by(user_id: user_id, notifiable: notifiable)
+ Notification.increment_counter(:counter, notification.id)
+ else
+ Notification.create!(user_id: user_id, notifiable: notifiable)
+ end
+ end
+end
\ No newline at end of file
diff --git a/app/models/spending_proposal.rb b/app/models/spending_proposal.rb
new file mode 100644
index 000000000..157b5f586
--- /dev/null
+++ b/app/models/spending_proposal.rb
@@ -0,0 +1,44 @@
+class SpendingProposal < ActiveRecord::Base
+ include Measurable
+ include Sanitizable
+
+ apply_simple_captcha
+
+ RESOLUTIONS = ["accepted", "rejected"]
+
+ belongs_to :author, -> { with_hidden }, class_name: 'User', foreign_key: 'author_id'
+ belongs_to :geozone
+
+ validates :title, presence: true
+ validates :author, presence: true
+ validates :description, presence: true
+
+ validates :title, length: { in: 4..SpendingProposal.title_max_length }
+ validates :description, length: { maximum: SpendingProposal.description_max_length }
+ validates :resolution, inclusion: { in: RESOLUTIONS, allow_nil: true }
+ validates :terms_of_service, acceptance: { allow_nil: false }, on: :create
+
+ scope :accepted, -> { where(resolution: "accepted") }
+ scope :rejected, -> { where(resolution: "rejected") }
+ scope :unresolved, -> { where(resolution: nil) }
+
+ def accept
+ update_attribute(:resolution, "accepted")
+ end
+
+ def reject
+ update_attribute(:resolution, "rejected")
+ end
+
+ def accepted?
+ resolution == "accepted"
+ end
+
+ def rejected?
+ resolution == "rejected"
+ end
+
+ def unresolved?
+ resolution.blank?
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index dd2bc5cf0..276598bf2 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -22,6 +22,7 @@ class User < ActiveRecord::Base
has_many :proposals, -> { with_hidden }, foreign_key: :author_id
has_many :comments, -> { with_hidden }
has_many :failed_census_calls
+ has_many :notifications
validates :username, presence: true, if: :username_required?
validates :username, uniqueness: true, if: :username_required?
@@ -199,7 +200,7 @@ class User < ActiveRecord::Base
def email_required?
!erased?
end
-
+
def has_official_email?
domain = Setting.value_for 'email_domain_for_officials'
!email.blank? && ( (email.end_with? "@#{domain}") || (email.end_with? ".#{domain}") )
diff --git a/app/views/admin/_menu.html.erb b/app/views/admin/_menu.html.erb
index 5f45e96c8..b27a8a73b 100644
--- a/app/views/admin/_menu.html.erb
+++ b/app/views/admin/_menu.html.erb
@@ -32,6 +32,13 @@
<% end %>
+
>
+ <%= link_to admin_spending_proposals_path do %>
+
+ <%= t("admin.menu.spending_proposals") %>
+ <% end %>
+
+
>
<%= link_to admin_users_path do %>
diff --git a/app/views/admin/spending_proposals/index.html.erb b/app/views/admin/spending_proposals/index.html.erb
new file mode 100644
index 000000000..0f381bb1f
--- /dev/null
+++ b/app/views/admin/spending_proposals/index.html.erb
@@ -0,0 +1,36 @@
+<%= t("admin.spending_proposals.index.title") %>
+
+<%= render 'shared/filter_subnav', i18n_namespace: "admin.spending_proposals.index" %>
+
+<%= page_entries_info @spending_proposals %>
+
+
+ <% @spending_proposals.each do |spending_proposal| %>
+
+ |
+ <%= link_to spending_proposal.title, admin_spending_proposal_path(spending_proposal) %>
+ |
+
+ <%= geozone_name(spending_proposal) %>
+ |
+
+ <% unless spending_proposal.accepted? %>
+ <%= link_to t("admin.spending_proposals.actions.accept"),
+ accept_admin_spending_proposal_path(spending_proposal, request.query_parameters),
+ method: :put,
+ data: { confirm: t("admin.actions.confirm") },
+ class: "button radius tiny success no-margin" %>
+ <% end %>
+ <% unless spending_proposal.rejected? %>
+ <%= link_to t("admin.spending_proposals.actions.reject"),
+ reject_admin_spending_proposal_path(spending_proposal, request.query_parameters),
+ method: :put,
+ data: { confirm: t("admin.actions.confirm") },
+ class: "button radius tiny warning right" %>
+ <% end %>
+ |
+
+ <% end %>
+
+
+<%= paginate @spending_proposals %>
diff --git a/app/views/admin/spending_proposals/show.html.erb b/app/views/admin/spending_proposals/show.html.erb
new file mode 100644
index 000000000..121f28940
--- /dev/null
+++ b/app/views/admin/spending_proposals/show.html.erb
@@ -0,0 +1,28 @@
+<%= @spending_proposal.title %>
+
+<%= safe_html_with_links @spending_proposal.description.html_safe %>
+
+<% if @spending_proposal.external_url.present? %>
+ <%= text_with_links @spending_proposal.external_url %>
+<% end %>
+
+<%= t("admin.spending_proposals.show.by") %>: <%= link_to @spending_proposal.author.name, admin_user_path(@spending_proposal.author) %>
+<%= t("admin.spending_proposals.show.geozone") %>: <%= geozone_name(@spending_proposal) %>
+<%= l @spending_proposal.created_at, format: :datetime %>
+
+
+ <% unless @spending_proposal.accepted? %>
+ <%= link_to t("admin.spending_proposals.actions.accept"),
+ accept_admin_spending_proposal_path(@spending_proposal),
+ method: :put,
+ data: { confirm: t("admin.actions.confirm") },
+ class: "button radius tiny success no-margin" %>
+ <% end %>
+ <% unless @spending_proposal.rejected? %>
+ <%= link_to t("admin.spending_proposals.actions.reject"),
+ reject_admin_spending_proposal_path(@spending_proposal),
+ method: :put,
+ data: { confirm: t("admin.actions.confirm") },
+ class: "button radius tiny warning" %>
+ <% end %>
+
diff --git a/app/views/comments/_comment.html.erb b/app/views/comments/_comment.html.erb
index 9178f0b9b..e8c49636b 100644
--- a/app/views/comments/_comment.html.erb
+++ b/app/views/comments/_comment.html.erb
@@ -1,9 +1,9 @@
-<% cache [locale_and_user_status(comment), comment, commentable_cache_key(@commentable), comment.author, (@comment_flags[comment.id] if @comment_flags)] do %>
+<% cache [locale_and_user_status(comment), comment, commentable_cache_key(comment.commentable), comment.author, (@comment_flags[comment.id] if @comment_flags)] do %>
<%= t("comments.comment.deleted") %>
- <%= t("debates.index.featured_debates_html") %> -
- - <%= image_tag("featured_debates.jpg", size: "210x135") %> - +<%= t("debates.index.featured_debates") %>
- <%= link_to "#Túpreguntas: Manuela Carmena, Alcaldesa de Madrid.", "https://decide.madrid.es/debates/4320" %>
-- <%= link_to "¿Y si cualquiera pudiera entrevistar a Manuela Carmena o a los otros concejales?", "https://decide.madrid.es/debates/4299" %>
-<%= link_to "#Túpreguntas: Manuela Carmena, Alcaldesa de Madrid.", "https://decide.madrid.es/debates/4320" %>
+ <%= link_to "Manuela Carmena ", "https://decide.madrid.es/users/21452"%><%= link_to "¿Y si pudieras entrevistar a Manuela Carmena o a los otros concejales?", "https://decide.madrid.es/debates/4299" %>
+ <%= link_to "ParticipaciónYTransparenciaAytoMadrid", "https://decide.madrid.es/users/52055"%> +- <%= page_entries_info @debates %> - <%= t("debates.index.search_results", count: @debates.size, search_term: @search_terms) %> -
- <% elsif @tag_filter %> -- <%= page_entries_info @debates %> - <%= t("debates.index.filter_topic", count: @debates.size, topic: @tag_filter) %> -
- <% end %> -+ <%= page_entries_info @debates %> + <%= t("debates.index.search_results", count: @debates.size, search_term: @search_terms) %> +
+ <% elsif @tag_filter %> ++ <%= page_entries_info @debates %> + <%= t("debates.index.filter_topic", count: @debates.size, topic: @tag_filter) %> +
<% end %> - - <%= render 'shared/order_links', i18n_namespace: "debates.index" %> - -<% if user_signed_in? %> +-
+ <%= link_to notifications_path, class: "notifications" do %>
+ <% if current_user.notifications_count > 0 %>
+
+
+
+ <% else %>
+
+ <% end %>
+ <% end %>
+
-
<%= link_to(t("layouts.header.my_activity_link"), user_path(current_user)) %>
diff --git a/app/views/layouts/_header.html.erb b/app/views/layouts/_header.html.erb
index 78899aba5..a48ff8dbd 100644
--- a/app/views/layouts/_header.html.erb
+++ b/app/views/layouts/_header.html.erb
@@ -33,14 +33,16 @@
+
- <%= link_to t("layouts.header.debates"), debates_path, class: ("active" if current_page?(controller: "/debates")) %>
- <%= link_to t("layouts.header.proposals"), proposals_path, class: ("active" if current_page?(controller: "/proposals")) %>
- <%= link_to t("layouts.header.more_information"), page_path('more_information'), class: ("active" if current_page?("/more_information")) %>
- <%= link_to t("layouts.header.external_link_blog_url"), target: "_blank" do %>
- <%= t("layouts.header.external_link_blog") %>
-
- <% end %>
+ <%= link_to t("layouts.header.debates"), debates_path, class: ("active" if controller_name == "debates") %>
+ <%= link_to t("layouts.header.proposals"), proposals_path, class: ("active" if controller_name == "proposals") %>
+ <%= link_to t("layouts.header.spending_proposals"), spending_proposals_path, class: ("active" if controller_name == "spending_proposals") %>
+ <%= link_to t("layouts.header.more_information"), page_path('more_information'), class: ("active" if current_page?("/more_information")) %>
+ <%= link_to t("layouts.header.external_link_blog_url"), target: "_blank" do %>
+ <%= t("layouts.header.external_link_blog") %>
+
+ <% end %>
<%= yield :header_addon %>
diff --git a/app/views/layouts/devise.html.erb b/app/views/layouts/devise.html.erb
index 3593cf4c3..0a6f0ecc6 100644
--- a/app/views/layouts/devise.html.erb
+++ b/app/views/layouts/devise.html.erb
@@ -13,9 +13,9 @@
-
+
-
+
-
+ <%= link_to notification do %>
+
\ No newline at end of file
diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb
new file mode 100644
index 000000000..4f57ed5d9
--- /dev/null
+++ b/app/views/notifications/index.html.erb
@@ -0,0 +1,21 @@
+
+
+
+
+ <%= t("notifications.index.empty_notifications") %>
+
+ <% else %>
+
+ <%= link_to t("notifications.index.mark_all_as_read"),
+ mark_all_as_read_notifications_path, method: :put %>
+
+
+
+
diff --git a/app/views/pages/more_information.html.erb b/app/views/pages/more_information.html.erb
index cdc4afc05..4d8586107 100644
--- a/app/views/pages/more_information.html.erb
+++ b/app/views/pages/more_information.html.erb
@@ -29,6 +29,12 @@
<% end %>
-
+ <%= link_to page_path('spending_proposals_info') do %>
+ <%= t('pages.more_information.titles.spending_proposals_info') %>
+ <%= t('pages.more_information.description.spending_proposals_info') %>
+ <% end %>
+
+ -
<%= link_to page_path('participation_world') do %>
<%= t('pages.more_information.titles.participation_world') %>
<%= t('pages.more_information.description.participation_world') %>
diff --git a/app/views/pages/spending_proposals_info.html.erb b/app/views/pages/spending_proposals_info.html.erb
new file mode 100644
index 000000000..bb02f0286
--- /dev/null
+++ b/app/views/pages/spending_proposals_info.html.erb
@@ -0,0 +1,20 @@
+
+
+
+
+ <%= link_to t("spending_proposals.new.back_link"), "/more_information", class: 'left back' %>
+
+
+
+
+
\ No newline at end of file
diff --git a/app/views/proposals/_featured_proposal.html.erb b/app/views/proposals/_featured_proposal.html.erb
index f67a46721..623932c38 100644
--- a/app/views/proposals/_featured_proposal.html.erb
+++ b/app/views/proposals/_featured_proposal.html.erb
@@ -1,22 +1,18 @@
-
-
-
-
-
- <% if proposal.author.hidden? || proposal.author.erased? %>
- <%= t("proposals.show.author_deleted") %>
- <% else %>
- <%= proposal.author.name %>
- <% end %>
-
- •
- <%= t("proposals.proposal.supports", count: proposal.total_votes) %>
-
-
-
-
- <%= render 'featured_votes', proposal: proposal %>
+
+
+
+ <% if proposal.author.hidden? || proposal.author.erased? %>
+ <%= t("proposals.show.author_deleted") %>
+ <% else %>
+ <%= proposal.author.name %>
+ <% end %>
+ •
+ <%= t("proposals.proposal.supports", count: proposal.total_votes) %>
+
+
+ <%= render 'featured_votes', proposal: proposal %>
+
diff --git a/app/views/proposals/_form.html.erb b/app/views/proposals/_form.html.erb
index 65722fbba..ad4554599 100644
--- a/app/views/proposals/_form.html.erb
+++ b/app/views/proposals/_form.html.erb
@@ -86,5 +86,3 @@
<% end %>
-
-
diff --git a/app/views/proposals/index.html.erb b/app/views/proposals/index.html.erb
index d7fdadee9..fc20bc3af 100644
--- a/app/views/proposals/index.html.erb
+++ b/app/views/proposals/index.html.erb
@@ -9,48 +9,41 @@
-
-
- <% if @search_terms %>
-
diff --git a/app/views/shared/_errors.html.erb b/app/views/shared/_errors.html.erb
index 3ced01dbf..ac8abe7e2 100644
--- a/app/views/shared/_errors.html.erb
+++ b/app/views/shared/_errors.html.erb
@@ -8,7 +8,7 @@
<% if local_assigns[:message].present? %>
<%= message %>
<% else %>
- <%= t("form.not_saved", resource: t("form.#{resource.class.to_s.downcase}")) %>
+ <%= t("form.not_saved", resource: t("form.#{resource.class.to_s.underscore}")) %>
<% end %>
diff --git a/app/views/shared/_filter_subnav.html.erb b/app/views/shared/_filter_subnav.html.erb
index 0bf9f773c..3607290af 100644
--- a/app/views/shared/_filter_subnav.html.erb
+++ b/app/views/shared/_filter_subnav.html.erb
@@ -4,7 +4,7 @@
%>
<%= link_to root_path do %> <%= image_tag('header_logo_madrid.png', class: 'show-for-medium-up left', size: '96x96', alt: t("layouts.header.logo")) %> diff --git a/app/views/notifications/_notification.html.erb b/app/views/notifications/_notification.html.erb new file mode 100644 index 000000000..28def42f7 --- /dev/null +++ b/app/views/notifications/_notification.html.erb @@ -0,0 +1,9 @@ +
+ <%= t("notifications.index.#{notification_action(notification)}", count: notification.counter) %> + <%= notification.notifiable.is_a?(Comment) ? notification.notifiable.commentable.title : notification.notifiable.title %> +
+<%= l notification.timestamp, format: :datetime %>
+ <% end %> +<%= t("notifications.index.title") %>
+ + <% if @notifications.empty? %> ++ <%= render @notifications %> +
+ <% end %> ++-
+ Explicación detallada del proceso
+
+
+¿Cómo funcionan los presupuestos ciudadanos?
+ +Explicación detallada del proceso
+Próximamente se podrá encontrar aquí una descripción del proceso de participación ciudadana en los presupuestos.
+ +<%= link_to proposal.title, proposal %>
-<%= link_to proposal.title, proposal %>
+- <%= page_entries_info @proposals %> - <%= t("proposals.index.search_results", count: @proposals.size, search_term: @search_terms) %> -
- <% elsif @tag_filter %> -- <%= page_entries_info @proposals %> - <%= t("proposals.index.filter_topic", count: @proposals.size, topic: @tag_filter) %> +
+ <% if @search_terms %>
+
+
+ <% if @featured_proposals.present? %>
+
+
+
+ <% @featured_proposals.each do |featured_proposal| %>
+ <%= render "featured_proposal", proposal: featured_proposal %>
<% end %>
+ <% end %>
- <% if @featured_proposals.present? %>
-
-
-
-
- <% @featured_proposals.each do |featured_proposal| %>
- <%= render "featured_proposal", proposal: featured_proposal %>
- <% end %>
-
-
- <% end %>
+ <%= render 'shared/order_links', i18n_namespace: "proposals.index" %>
- <%= render 'shared/order_links', i18n_namespace: "proposals.index" %>
-
-
- <%= link_to t("proposals.index.start_proposal"), new_proposal_path, class: 'button radius expand' %>
-
-
- <%= render partial: 'proposals/proposal', collection: @proposals %>
- <%= paginate @proposals %>
+
+ <%= link_to t("proposals.index.start_proposal"), new_proposal_path, class: 'button radius expand' %>
+
+ <%= render partial: 'proposals/proposal', collection: @proposals %>
+ <%= paginate @proposals %>
+ <%= page_entries_info @proposals %> + <%= t("proposals.index.search_results", count: @proposals.size, search_term: @search_terms) %> +
+ <% elsif @tag_filter %> ++ <%= page_entries_info @proposals %> + <%= t("proposals.index.filter_topic", count: @proposals.size, topic: @tag_filter) %> +
+ <% end %> ++ <%= t("proposals.index.featured_proposals") %>
+- <%= t("proposals.index.featured_proposals_html") %> -
- - <%= image_tag("featured_proposals.jpg", size: "210x135") %> - --- <%= t("#{i18n_namespace}.filter") %>:
+ - <%= t("#{i18n_namespace}.filter") %>:
<% @valid_filters.each do |filter| %>
<% if @current_filter == filter %>
diff --git a/app/views/spending_proposals/_form.html.erb b/app/views/spending_proposals/_form.html.erb
new file mode 100644
index 000000000..4805b1bb4
--- /dev/null
+++ b/app/views/spending_proposals/_form.html.erb
@@ -0,0 +1,46 @@
+<%= form_for(@spending_proposal, url: form_url) do |f| %>
+ <%= render 'shared/errors', resource: @spending_proposal %>
+
+
+
+ <%= f.label :title, t("spending_proposals.form.title") %>
+ <%= f.text_field :title, maxlength: SpendingProposal.title_max_length, placeholder: t("spending_proposals.form.title"), label: false %>
+
+
+
+ <%= f.label :description, t("spending_proposals.form.description") %>
+ <%= f.cktext_area :description, maxlength: SpendingProposal.description_max_length, ckeditor: { language: I18n.locale }, label: false %>
+
+
+
+ <%= f.label :external_url, t("spending_proposals.form.external_url") %>
+ <%= f.text_field :external_url, placeholder: t("spending_proposals.form.external_url"), label: false %>
+
+
+
+ <%= f.label :geozone_id, t("spending_proposals.form.geozone") %>
+ <%= f.select :geozone_id, geozone_select_options, {include_blank: t("geozones.none"), label: false} %>
+
+
+
+ <% if @spending_proposal.new_record? %>
+ <%= f.label :terms_of_service do %>
+ <%= f.check_box :terms_of_service, label: false %>
+
+ <%= t("form.accept_terms",
+ policy: link_to(t("form.policy"), "/privacy", target: "blank"),
+ conditions: link_to(t("form.conditions"), "/conditions", target: "blank")).html_safe %>
+
+ <% end %>
+ <% end %>
+
+
+
+ <%= f.simple_captcha input_html: { required: false } %>
+
+
+
+ <%= f.submit(class: "button radius", value: t("spending_proposals.form.submit_buttons.#{action_name}")) %>
+
+
+<% end %>
\ No newline at end of file
diff --git a/app/views/spending_proposals/index.html.erb b/app/views/spending_proposals/index.html.erb
new file mode 100644
index 000000000..0bf276894
--- /dev/null
+++ b/app/views/spending_proposals/index.html.erb
@@ -0,0 +1,16 @@
+<% provide :title do %><%= t('spending_proposals.index.title') %><% end %>
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/views/spending_proposals/new.html.erb b/app/views/spending_proposals/new.html.erb
new file mode 100644
index 000000000..c761487ff
--- /dev/null
+++ b/app/views/spending_proposals/new.html.erb
@@ -0,0 +1,27 @@
+
+
+
+ <%= link_to spending_proposals_path, class: "left back" do %>
+
+ <%= t("spending_proposals.new.back_link") %>
+ <% end %>
+
+ <%= link_to "/spending_proposals_info", target: "_blank" do %>
+ <%= t("spending_proposals.new.more_info")%>
+ <% end %>
+
+ <%= render "spending_proposals/form", form_url: spending_proposals_url %>
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb
index afa133b98..1b82049f8 100644
--- a/app/views/users/show.html.erb
+++ b/app/views/users/show.html.erb
@@ -1,5 +1,5 @@
-
+
<%= t('spending_proposals.index.title') %>
+ +<%= t('spending_proposals.index.text') %>
+ + <% if can? :create, SpendingProposal %> + <%= link_to t('spending_proposals.index.create_link'), new_spending_proposal_path, class: 'button radius' %> + <% else %> +<%= t('spending_proposals.index.verified_only', verify_account: link_to(t('spending_proposals.index.verify_account'), verification_path)).html_safe %>
+ <% end %> +<%= t("spending_proposals.new.start_new") %>
+<%= t("spending_proposals.new.recommendations_title") %>
++- <%= t("spending_proposals.new.recommendation_one") %>
+ - <%= t("spending_proposals.new.recommendation_two") %>
+ - <%= t("spending_proposals.new.recommendation_three") %>
+
+diff --git a/app/views/welcome/highlights.html.erb b/app/views/welcome/highlights.html.erb index f0b4e446e..66d335448 100644 --- a/app/views/welcome/highlights.html.erb +++ b/app/views/welcome/highlights.html.erb @@ -4,19 +4,16 @@
-
-
-
-
-
- <%= render @list %>
- <%= paginate @paginator %>
+
+
+
+ <%= render @list %>
+ <%= paginate @paginator %>
diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml
index 561ad3347..9941114f8 100644
--- a/config/i18n-tasks.yml
+++ b/config/i18n-tasks.yml
@@ -109,6 +109,7 @@ ignore_unused:
- 'admin.comments.index.filter*'
- 'admin.debates.index.filter*'
- 'admin.proposals.index.filter*'
+ - 'admin.spending_proposals.index.filter*'
- 'admin.organizations.index.filter*'
- 'admin.users.index.filter*'
- 'admin.activity.show.filter*'
@@ -127,6 +128,8 @@ ignore_unused:
- 'proposals.index.select_order'
- 'proposals.index.orders.*'
- 'proposals.index.search_form.*'
+ - 'notifications.index.comments_on*'
+ - 'notifications.index.replies_to*'
- 'helpers.page_entries_info.*' # kaminari
- 'views.pagination.*' # kaminari
# - '{devise,kaminari,will_paginate}.*'
diff --git a/config/locales/activerecord.en.yml b/config/locales/activerecord.en.yml
index 4e9cd5931..42c29fed8 100644
--- a/config/locales/activerecord.en.yml
+++ b/config/locales/activerecord.en.yml
@@ -37,6 +37,9 @@ en:
proposal:
one: "Citizen proposal"
other: "Citizen proposals"
+ spending_proposal:
+ one: "Spending proposal"
+ other: "Spending proposals"
attributes:
comment:
body: "Comment"
diff --git a/config/locales/activerecord.es.yml b/config/locales/activerecord.es.yml
index 998bc4fb6..780595562 100644
--- a/config/locales/activerecord.es.yml
+++ b/config/locales/activerecord.es.yml
@@ -37,6 +37,9 @@ es:
proposal:
one: "Propuesta ciudadana"
other: "Propuestas ciudadanas"
+ spending_proposal:
+ one: "Propuesta de gasto"
+ other: "Propuestas de gasto"
attributes:
comment:
body: "Comentario"
diff --git a/config/locales/admin.en.yml b/config/locales/admin.en.yml
index 22bd6d97b..e511d8db1 100755
--- a/config/locales/admin.en.yml
+++ b/config/locales/admin.en.yml
@@ -16,6 +16,7 @@ en:
hidden_debates: "Hidden debates"
hidden_comments: "Hidden comments"
hidden_users: "Hidden users"
+ spending_proposals: "Spending proposals"
incomplete_verifications: "Incomplete verifications"
organizations: "Organisations"
officials: "Officials"
@@ -95,6 +96,20 @@ en:
all: "All"
with_confirmed_hide: "Confirmed"
without_confirmed_hide: "Pending"
+ spending_proposals:
+ actions:
+ accept: Accept
+ reject: Reject
+ index:
+ title: "Spending proposals for participatory budgeting"
+ filter: "Filter"
+ filters:
+ unresolved: "Unresolved"
+ accepted: "Accepted"
+ rejected: "Rejected"
+ show:
+ geozone: "Scope"
+ by: "Sent by"
users:
index:
title: "Hidden users"
diff --git a/config/locales/admin.es.yml b/config/locales/admin.es.yml
index a1b9395a1..9c7ed27a1 100644
--- a/config/locales/admin.es.yml
+++ b/config/locales/admin.es.yml
@@ -16,6 +16,7 @@ es:
hidden_debates: "Debates ocultos"
hidden_comments: "Comentarios ocultos"
hidden_users: "Usuarios bloqueados"
+ spending_proposals: "Propuestas de gasto"
incomplete_verifications: "Verificaciones incompletas"
organizations: "Organizaciones"
officials: "Cargos públicos"
@@ -95,6 +96,20 @@ es:
all: "Todas"
with_confirmed_hide: "Confirmadas"
without_confirmed_hide: "Pendientes"
+ spending_proposals:
+ actions:
+ accept: Aceptar
+ reject: Rechazar
+ index:
+ title: "Propuestas de gasto para presupuestos participativos"
+ filter: "Filtro"
+ filters:
+ unresolved: "Sin resolver"
+ accepted: "Aceptadas"
+ rejected: "Rechazadas"
+ show:
+ geozone: "Ámbito"
+ by: "Enviada por"
users:
index:
title: "Usuarios bloqueados"
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 60c22b775..7b2361b4e 100755
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -29,6 +29,11 @@ en:
more_information: "More information"
debates: "Debates"
proposals: "Proposals"
+ spending_proposals: "Spending proposals"
+ new_notifications:
+ one: "You have a new notification"
+ other: "You have %{count} new notifications"
+ no_notifications: "You don't have new notifications"
footer:
description: "This portal uses the %{consul} which is %{open_source}. From Madrid out into the world."
open_source: "open-source software"
@@ -71,6 +76,7 @@ en:
user: "Account"
debate: "Debate"
proposal: "Proposal"
+ spending_proposal: "Spending proposal"
verification::sms: "Telephone"
verification::letter: "the verification"
application:
@@ -79,7 +85,7 @@ en:
debates:
index:
title: "Debates"
- featured_debates_html: "Featured
<%= t('welcome.highlights') %>
- -- <%= t('welcome.signed_in_home_title') %> -
-<%= t('welcome.highlights') %>
+ ++ <%= t('welcome.signed_in_home_title') %> +
debates" + featured_debates: "Featured" start_debate: "Start a debate" select_order: "Order by" orders: @@ -156,7 +162,7 @@ en: proposals: index: title: "Proposals" - featured_proposals_html: "Featured
proposals" + featured_proposals: "Featured" start_proposal: "Create a proposal" select_order_long: "You are viewing proposals according to:" select_order: "Order by" @@ -244,7 +250,34 @@ en: update: form: submit_button: "Save changes" + spending_proposals: + index: + title: "Participatory budgeting" + text: "Here you can send spending proposals to be considered in the frame of the annual participatory budgeting." + create_link: "Create spending proposal" + verified_only: "Only verified users can create spending proposals, %{verify_account}." + verify_account: "verify your account" + new: + back_link: Back + start_new: "Create spending proposal" + more_info: "How do participatory budgeting works?" + recommendations_title: "Recommendations for creating a spending proposal" + recommendation_one: "It's mandatory that the proposal makes reference to a budgetable action." + recommendation_two: "Any proposal or comment suggesting illegal action will be deleted." + recommendation_three: "Try to go into details when describing your spending proposal so the reviewing team undertands your points." + form: + title: "Spending proposal title" + description: "Description" + external_url: "Link to additional documentation" + geozone: "Scope of operation" + submit_buttons: + new: Create + create: Create + geozones: + none: "All city" comments: + show: + return_to_commentable: "Go back to " select_order: "Sort by" orders: most_voted: "Most voted" @@ -311,6 +344,17 @@ en: user_permission_votes: "Participate on final voting" user_permission_verify: "To perform all the actions verify your account." user_permission_verify_info: "* Only for users on Madrid City Census." + notifications: + index: + title: "Notifications" + mark_all_as_read: "Mark all as read" + empty_notifications: "You don't have new notifications." + comments_on: + one: "Someone commented on" + other: "There are %{count} new comments on" + replies_to: + one: "Someone replied to your comment on" + other: "There are %{count} new replies to your comment on" simple_captcha: placeholder: "Enter the text from the image" label: "Enter the text from the image in the box below" @@ -319,6 +363,7 @@ en: user: "the secret code does not match the image" debate: "the secret code does not match the image" proposal: "the secret code does not match the image" + spendingproposal: "the secret code does not match the image" shared: author_info: author_deleted: "User deleted" diff --git a/config/locales/es.yml b/config/locales/es.yml index 246221c04..efd750781 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -29,6 +29,11 @@ es: more_information: "Más información" debates: "Debates" proposals: "Propuestas" + spending_proposals: "Presupuestos ciudadanos" + new_notifications: + one: "Tienes una nueva notificación" + other: "Tienes %{count} notificaciones nuevas" + no_notifications: "No tienes notificaciones nuevas" footer: description: "Este portal usa la %{consul} que es %{open_source}. De Madrid, para el mundo entero." open_source: "software libre" @@ -71,6 +76,7 @@ es: user: "la cuenta" debate: "el debate" proposal: "la propuesta" + spending_proposal: "la propuesta de gasto" verification::sms: "el teléfono" verification::letter: "la verificación" application: @@ -79,7 +85,7 @@ es: debates: index: title: "Debates" - featured_debates_html: "Debates
destacados" + featured_debates: "Destacados" start_debate: "Empieza un debate" select_order: "Ordenar por" orders: @@ -156,7 +162,7 @@ es: proposals: index: title: "Propuestas ciudadanas" - featured_proposals_html: "Propuestas
destacadas" + featured_proposals: "Destacadas" start_proposal: "Crea una propuesta" select_order_long: "Estas viendo las propuestas" select_order: "Ordenar por" @@ -205,7 +211,7 @@ es: proposal_responsible_name: "Nombre y apellidos de la persona que hace esta propuesta" proposal_responsible_name_note: "(individualmente o como representante de un colectivo; no se mostrará públicamente)" tags_label: "Temas" - tags_instructions: "Etiqueta esta propuesta. Puedes elegir entre Categorias y Distritos propuestos o introducir las que desees." + tags_instructions: "Etiqueta esta propuesta. Puedes elegir entre nuestras sugerencias o introducir las que desees." tags_placeholder: "Escribe las etiquetas que desees separadas por una coma (',')" tag_category_label: "Categorías" tag_district_label: "Distritos" @@ -244,7 +250,34 @@ es: update: form: submit_button: "Guardar cambios" + spending_proposals: + index: + title: "Presupuestos participativos" + text: "Desde esta sección podrás sugerir propuestas de gasto que irán asociadas a las partidas de presupuestos ciudadanos. El requisito principal es que sean propuestas presupuestables." + create_link: "Enviar propuesta de gasto" + verified_only: "Sólo los usuarios verificados pueden crear propuestas de gasto, %{verify_account}." + verify_account: "verifica tu cuenta" + new: + back_link: Volver + start_new: "Crear una propuesta de gasto" + more_info: "¿Cómo funcionan los presupuestos participativos?" + recommendations_title: "Recomendaciones para crear una propuesta de gasto" + recommendation_one: "Es fundamental que haga referencia a una actuación presupuestable." + recommendation_two: "Cualquier propuesta o comentario que implique acciones ilegales será eliminada." + recommendation_three: "Intenta detallar lo máximo posible la propuesta para que el equipo de gobierno encargado de estudiarla tenga las menor dudas posibles." + form: + title: "Título de la propuesta de gasto" + description: "Descripción detallada" + external_url: "Enlace a documentación adicional" + geozone: "Ámbito de actuación" + submit_buttons: + new: Crear + create: Crear + geozones: + none: "Toda la ciudad" comments: + show: + return_to_commentable: "Volver a " select_order: "Ordenar por" orders: most_voted: "Más votados" @@ -311,6 +344,17 @@ es: user_permission_votes: "Participar en las votaciones finales*" user_permission_verify: "Para poder realizar todas las acciones verifica tu cuenta." user_permission_verify_info: "* Sólo usuarios empadronados en el municipio de Madrid." + notifications: + index: + title: "Notificaciones" + mark_all_as_read: "Marcar todas como leídas" + empty_notifications: "No tienes notificaciones nuevas." + comments_on: + one: "Hay un nuevo comentario en" + other: "Hay %{count} comentarios nuevos en" + replies_to: + one: "Hay una respuesta nueva a tu comentario en" + other: "Hay %{count} nuevas respuestas a tu comentario en" simple_captcha: placeholder: "Introduce el texto de la imagen" label: "Introduce el texto de la imagen en la siguiente caja" @@ -319,6 +363,7 @@ es: user: "el código secreto no coincide con la imagen" debate: "el código secreto no coincide con la imagen" proposal: "el código secreto no coincide con la imagen" + spendingproposal: "el código secreto no coincide con la imagen" shared: author_info: author_deleted: Usuario eliminado diff --git a/config/locales/pages.en.yml b/config/locales/pages.en.yml index b69694d38..f34e7250a 100755 --- a/config/locales/pages.en.yml +++ b/config/locales/pages.en.yml @@ -35,6 +35,7 @@ en: how_to_use: "Use it in your local government" participation: "Madrid Participation and Transparency y Transparencia - coming news" proposals_info: "How does citizen proposals work?" + spending_proposals_info: "How does participatory budgeting work?" participation_world: "Direct citizen participation in the world" participation_facts: "Facts about citizen participation and direct democracy" faq: "Solution to tecnical problemas (FAQ)" @@ -44,6 +45,7 @@ en: how_to_use: "Use it freely or help us to improve it, it is free software" participation: "Citizen participation, transparency and open government" proposals_info: "Create your own proposals" + spending_proposals_info: "Create your own spending proposals" participation_world: "Systems of citizen participation that exist in the world" participation_facts: "To lose your fear" faq: "Frecuently asked question about tecnical problems" diff --git a/config/locales/pages.es.yml b/config/locales/pages.es.yml index 4e6877482..77a996ba5 100644 --- a/config/locales/pages.es.yml +++ b/config/locales/pages.es.yml @@ -35,6 +35,7 @@ es: how_to_use: "Utilízalo en tu municipio" participation: "Participación y Transparencia en Madrid - Próximas novedades" proposals_info: "¿Cómo funcionan las propuestas ciudadanas?" + spending_proposals_info: "¿Cómo funcionan los presupuestos participativos?" participation_world: "Participación ciudadana directa en el mundo" participation_facts: "Hechos sobre participación ciudadana y democracia directa" faq: "Soluciones a problemas técnicos (FAQ)" @@ -44,6 +45,7 @@ es: how_to_use: "Utilízalo libremente o ayúdanos a mejorarlo, es software libre" participation: "Participación Ciudadana, Transparencia y Gobierno Abierto" proposals_info: "Crea tus propias propuestas" + spending_proposals_info: "Envía tus propuestas de gasto" participation_world: "Sistemas de participación ciudadana que ya existen en el mundo" participation_facts: "Para perderle el miedo" faq: "Preguntas frecuentes sobre problemas técnicos" diff --git a/config/routes.rb b/config/routes.rb index 3472f1c4e..d10ffefd7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -53,7 +53,7 @@ Rails.application.routes.draw do end end - resources :comments, only: :create, shallow: true do + resources :comments, only: [:create, :show], shallow: true do member do post :vote put :flag @@ -61,6 +61,8 @@ Rails.application.routes.draw do end end + resources :spending_proposals, only: [:index, :new, :create] + resources :legislations, only: [:show] resources :annotations do @@ -74,6 +76,11 @@ Rails.application.routes.draw do resource :account, controller: "account", only: [:show, :update, :delete] do collection { get :erase } end + + resources :notifications, only: [:index, :show] do + collection { put :mark_all_as_read } + end + resource :verification, controller: "verification", only: [:show] scope module: :verification do @@ -115,6 +122,13 @@ Rails.application.routes.draw do end end + resources :spending_proposals, only: [:index, :show] do + member do + put :accept + put :reject + end + end + resources :comments, only: :index do member do put :restore diff --git a/db/dev_seeds.rb b/db/dev_seeds.rb index 8bd6f9876..47fc39300 100644 --- a/db/dev_seeds.rb +++ b/db/dev_seeds.rb @@ -14,6 +14,9 @@ Setting.create(key: 'max_votes_for_proposal_edit', value: '1000') Setting.create(key: 'proposal_code_prefix', value: 'MAD') Setting.create(key: 'votes_for_proposal_success', value: '100') +puts "Creating Geozones" +('A'..'Z').each{ |i| Geozone.create(name: "District #{i}") } + puts "Creating Users" def create_user(email, username = Faker::Name.name) @@ -180,6 +183,25 @@ end Flag.flag(flagger, proposal) end +puts "Creating Spending Proposals" + +resolutions = ["accepted", "rejected", nil] + +(1..30).each do |i| + geozone = Geozone.reorder("RANDOM()").first + author = User.reorder("RANDOM()").first + description = "
#{Faker::Lorem.paragraphs.join('
')}
" + spending_proposal = SpendingProposal.create!(author: author, + title: Faker::Lorem.sentence(3).truncate(60), + external_url: Faker::Internet.url, + description: description, + created_at: rand((Time.now - 1.week) .. Time.now), + resolution: resolutions.sample, + geozone: [geozone, nil].sample, + terms_of_service: "1") + puts " #{spending_proposal.title}" +end + puts "Creating Legislation" Legislation.create!(title: 'Participatory Democracy', body: 'In order to achieve...') diff --git a/db/migrate/20150928115005_create_notifications.rb b/db/migrate/20150928115005_create_notifications.rb new file mode 100644 index 000000000..42bbbf72e --- /dev/null +++ b/db/migrate/20150928115005_create_notifications.rb @@ -0,0 +1,9 @@ +class CreateNotifications < ActiveRecord::Migration + def change + create_table :notifications do |t| + t.belongs_to :user, index: true, foreign_key: true + t.belongs_to :activity, index: true, foreign_key: true + t.boolean :read, default: false + end + end +end diff --git a/db/migrate/20151218114205_create_spending_proposals.rb b/db/migrate/20151218114205_create_spending_proposals.rb new file mode 100644 index 000000000..5bcd3aba1 --- /dev/null +++ b/db/migrate/20151218114205_create_spending_proposals.rb @@ -0,0 +1,12 @@ +class CreateSpendingProposals < ActiveRecord::Migration + def change + create_table :spending_proposals do |t| + t.string :title + t.text :description + t.integer :author_id + t.string :external_url + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20160104203329_add_spending_proposals_counter_to_tags.rb b/db/migrate/20160104203329_add_spending_proposals_counter_to_tags.rb new file mode 100644 index 000000000..2aade49cb --- /dev/null +++ b/db/migrate/20160104203329_add_spending_proposals_counter_to_tags.rb @@ -0,0 +1,6 @@ +class AddSpendingProposalsCounterToTags < ActiveRecord::Migration + def change + add_column :tags, :spending_proposals_count, :integer, default: 0 + add_index :tags, :spending_proposals_count + end +end diff --git a/db/migrate/20160104203438_add_spending_proposals_indexes.rb b/db/migrate/20160104203438_add_spending_proposals_indexes.rb new file mode 100644 index 000000000..777b0f678 --- /dev/null +++ b/db/migrate/20160104203438_add_spending_proposals_indexes.rb @@ -0,0 +1,5 @@ +class AddSpendingProposalsIndexes < ActiveRecord::Migration + def change + add_index :spending_proposals, :author_id + end +end diff --git a/db/migrate/20160105121132_create_geozones.rb b/db/migrate/20160105121132_create_geozones.rb new file mode 100644 index 000000000..3e43339e9 --- /dev/null +++ b/db/migrate/20160105121132_create_geozones.rb @@ -0,0 +1,11 @@ +class CreateGeozones < ActiveRecord::Migration + def change + create_table :geozones do |t| + t.string :name + t.string :html_map_coordinates + t.string :external_code + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20160105170113_merge_activities_and_notifications.rb b/db/migrate/20160105170113_merge_activities_and_notifications.rb new file mode 100644 index 000000000..fac5b213d --- /dev/null +++ b/db/migrate/20160105170113_merge_activities_and_notifications.rb @@ -0,0 +1,10 @@ +class MergeActivitiesAndNotifications < ActiveRecord::Migration + def change + change_table :notifications do |t| + t.remove :read + t.remove :activity_id + t.references :notifiable, polymorphic: true + end + end + +end diff --git a/db/migrate/20160107114749_add_geozone_to_spending_proposal.rb b/db/migrate/20160107114749_add_geozone_to_spending_proposal.rb new file mode 100644 index 000000000..62130fd9b --- /dev/null +++ b/db/migrate/20160107114749_add_geozone_to_spending_proposal.rb @@ -0,0 +1,6 @@ +class AddGeozoneToSpendingProposal < ActiveRecord::Migration + def change + add_column :spending_proposals, :geozone_id, :integer, default: nil + add_index :spending_proposals, :geozone_id + end +end diff --git a/db/migrate/20160107132059_add_resolution_to_spending_proposals.rb b/db/migrate/20160107132059_add_resolution_to_spending_proposals.rb new file mode 100644 index 000000000..18365a715 --- /dev/null +++ b/db/migrate/20160107132059_add_resolution_to_spending_proposals.rb @@ -0,0 +1,6 @@ +class AddResolutionToSpendingProposals < ActiveRecord::Migration + def change + add_column :spending_proposals, :resolution, :string, default: nil + add_index :spending_proposals, :resolution + end +end diff --git a/db/migrate/20160108114750_add_counter_to_notifications.rb b/db/migrate/20160108114750_add_counter_to_notifications.rb new file mode 100644 index 000000000..7cf3c434c --- /dev/null +++ b/db/migrate/20160108114750_add_counter_to_notifications.rb @@ -0,0 +1,5 @@ +class AddCounterToNotifications < ActiveRecord::Migration + def change + add_column :notifications, :counter, :integer, default: 1 + end +end diff --git a/db/migrate/20160108133501_add_notifications_counter_cache_to_user.rb b/db/migrate/20160108133501_add_notifications_counter_cache_to_user.rb new file mode 100644 index 000000000..e106df6ae --- /dev/null +++ b/db/migrate/20160108133501_add_notifications_counter_cache_to_user.rb @@ -0,0 +1,5 @@ +class AddNotificationsCounterCacheToUser < ActiveRecord::Migration + def change + add_column :users, :notifications_count, :integer, default: 0 + end +end diff --git a/db/schema.rb b/db/schema.rb index 8136d925b..9578a4763 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,8 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160108101736) do + +ActiveRecord::Schema.define(version: 20160108133501) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -171,6 +172,14 @@ ActiveRecord::Schema.define(version: 20160108101736) do add_index "flags", ["user_id", "flaggable_type", "flaggable_id"], name: "access_inappropiate_flags", using: :btree add_index "flags", ["user_id"], name: "index_flags_on_user_id", using: :btree + create_table "geozones", force: :cascade do |t| + t.string "name" + t.string "html_map_coordinates" + t.string "external_code" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "identities", force: :cascade do |t| t.integer "user_id" t.string "provider" @@ -191,7 +200,7 @@ ActiveRecord::Schema.define(version: 20160108101736) do create_table "locks", force: :cascade do |t| t.integer "user_id" t.integer "tries", default: 0 - t.datetime "locked_until", default: '2000-01-01 07:01:01', null: false + t.datetime "locked_until", default: '2000-01-01 00:01:01', null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end @@ -204,6 +213,15 @@ ActiveRecord::Schema.define(version: 20160108101736) do add_index "moderators", ["user_id"], name: "index_moderators_on_user_id", using: :btree + create_table "notifications", force: :cascade do |t| + t.integer "user_id" + t.integer "notifiable_id" + t.string "notifiable_type" + t.integer "counter", default: 1 + end + + add_index "notifications", ["user_id"], name: "index_notifications_on_user_id", using: :btree + create_table "organizations", force: :cascade do |t| t.integer "user_id" t.string "name", limit: 60 @@ -264,6 +282,21 @@ ActiveRecord::Schema.define(version: 20160108101736) do add_index "simple_captcha_data", ["key"], name: "idx_key", using: :btree + create_table "spending_proposals", force: :cascade do |t| + t.string "title" + t.text "description" + t.integer "author_id" + t.string "external_url" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "geozone_id" + t.string "resolution" + end + + add_index "spending_proposals", ["author_id"], name: "index_spending_proposals_on_author_id", using: :btree + add_index "spending_proposals", ["geozone_id"], name: "index_spending_proposals_on_geozone_id", using: :btree + add_index "spending_proposals", ["resolution"], name: "index_spending_proposals_on_resolution", using: :btree + create_table "taggings", force: :cascade do |t| t.integer "tag_id" t.integer "taggable_id" @@ -284,11 +317,13 @@ ActiveRecord::Schema.define(version: 20160108101736) do t.integer "debates_count", default: 0 t.integer "proposals_count", default: 0 t.string "kind", limit: 40 + t.integer "spending_proposals_count", default: 0 end add_index "tags", ["debates_count"], name: "index_tags_on_debates_count", using: :btree add_index "tags", ["name"], name: "index_tags_on_name", unique: true, using: :btree add_index "tags", ["proposals_count"], name: "index_tags_on_proposals_count", using: :btree + add_index "tags", ["spending_proposals_count"], name: "index_tags_on_spending_proposals_count", using: :btree create_table "users", force: :cascade do |t| t.string "email", default: "" @@ -331,6 +366,7 @@ ActiveRecord::Schema.define(version: 20160108101736) do t.datetime "erased_at" t.boolean "public_activity", default: true t.boolean "newsletter", default: false + t.integer "notifications_count", default: 0 end add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree @@ -405,5 +441,6 @@ ActiveRecord::Schema.define(version: 20160108101736) do add_foreign_key "identities", "users" add_foreign_key "locks", "users" add_foreign_key "moderators", "users" + add_foreign_key "notifications", "users" add_foreign_key "organizations", "users" end diff --git a/lib/comment_tree.rb b/lib/comment_tree.rb index 8d0e072dd..f2eadb8ab 100644 --- a/lib/comment_tree.rb +++ b/lib/comment_tree.rb @@ -2,24 +2,34 @@ class CommentTree ROOT_COMMENTS_PER_PAGE = 10 - attr_accessor :root_comments, :comments + attr_accessor :root_comments, :comments, :commentable, :page, :order def initialize(commentable, page, order = 'confidence_score') - @root_comments = commentable.comments.roots.send("sort_by_#{order}").page(page).per(ROOT_COMMENTS_PER_PAGE).for_render - - root_descendants = @root_comments.each_with_object([]) do |root, col| - col.concat(Comment.descendants_of(root).send("sort_descendants_by_#{order}").for_render.to_a) - end + @commentable = commentable + @page = page + @order = order @comments = root_comments + root_descendants + end - @comments_by_parent_id = @comments.each_with_object({}) do |comment, col| - (col[comment.parent_id] ||= []) << comment + def root_comments + commentable.comments.roots.send("sort_by_#{order}").page(page).per(ROOT_COMMENTS_PER_PAGE).for_render + end + + def root_descendants + root_comments.each_with_object([]) do |root, array| + array.concat(Comment.descendants_of(root).send("sort_descendants_by_#{order}").for_render.to_a) end end - def children_of(parent) - @comments_by_parent_id[parent.id] || [] + def ordered_children_of(parent) + comments_by_parent_id[parent.id] || [] + end + + def comments_by_parent_id + comments.each_with_object({}) do |comment, array| + (array[comment.parent_id] ||= []) << comment + end end def comment_authors diff --git a/spec/factories.rb b/spec/factories.rb index fc71e241c..1791b2b6e 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -180,6 +180,14 @@ FactoryGirl.define do end end + factory :spending_proposal do + sequence(:title) { |n| "Spending Proposal #{n} title" } + description 'Spend money on this' + external_url 'http://external_documention.org' + terms_of_service '1' + association :author, factory: :user + end + factory :vote do association :votable, factory: :debate association :voter, factory: :user @@ -291,4 +299,12 @@ FactoryGirl.define do sequence(:track_id) { |n| "#{n}" } end + factory :notification do + user + association :notifiable, factory: :proposal + end + + factory :geozone do + sequence(:name) { |n| "District #{n}" } + end end diff --git a/spec/features/admin/spending_proposals_spec.rb b/spec/features/admin/spending_proposals_spec.rb new file mode 100644 index 000000000..a355c13ec --- /dev/null +++ b/spec/features/admin/spending_proposals_spec.rb @@ -0,0 +1,140 @@ +require 'rails_helper' + +feature 'Admin spending proposals' do + + background do + admin = create(:administrator) + login_as(admin.user) + end + + scenario 'Index shows spending proposals' do + spending_proposal = create(:spending_proposal) + visit admin_spending_proposals_path + + expect(page).to have_content(spending_proposal.title) + end + + scenario 'Accept from index' do + spending_proposal = create(:spending_proposal) + visit admin_spending_proposals_path + + click_link 'Accept' + + expect(page).to_not have_content(spending_proposal.title) + + click_link 'Accepted' + expect(page).to have_content(spending_proposal.title) + + expect(spending_proposal.reload).to be_accepted + end + + scenario 'Reject from index' do + spending_proposal = create(:spending_proposal) + visit admin_spending_proposals_path + + click_link 'Reject' + + expect(page).to_not have_content(spending_proposal.title) + + click_link('Rejected') + expect(page).to have_content(spending_proposal.title) + + expect(spending_proposal.reload).to be_rejected + end + + scenario "Current filter is properly highlighted" do + visit admin_spending_proposals_path + expect(page).to_not have_link('Unresolved') + expect(page).to have_link('Accepted') + expect(page).to have_link('Rejected') + + visit admin_spending_proposals_path(filter: 'unresolved') + expect(page).to_not have_link('Unresolved') + expect(page).to have_link('Accepted') + expect(page).to have_link('Rejected') + + visit admin_spending_proposals_path(filter: 'accepted') + expect(page).to have_link('Unresolved') + expect(page).to_not have_link('Accepted') + expect(page).to have_link('Rejected') + + visit admin_spending_proposals_path(filter: 'rejected') + expect(page).to have_link('Accepted') + expect(page).to have_link('Unresolved') + expect(page).to_not have_link('Rejected') + end + + scenario "Filtering proposals" do + create(:spending_proposal, title: "Recent spending proposal") + create(:spending_proposal, title: "Good spending proposal", resolution: "accepted") + create(:spending_proposal, title: "Bad spending proposal", resolution: "rejected") + + visit admin_spending_proposals_path(filter: 'unresolved') + expect(page).to have_content('Recent spending proposal') + expect(page).to_not have_content('Good spending proposal') + expect(page).to_not have_content('Bad spending proposal') + + visit admin_spending_proposals_path(filter: 'accepted') + expect(page).to have_content('Good spending proposal') + expect(page).to_not have_content('Recent spending proposal') + expect(page).to_not have_content('Bad spending proposal') + + visit admin_spending_proposals_path(filter: 'rejected') + expect(page).to have_content('Bad spending proposal') + expect(page).to_not have_content('Good spending proposal') + expect(page).to_not have_content('Recent spending proposal') + end + + scenario "Action links remember the pagination setting and the filter" do + per_page = Kaminari.config.default_per_page + (per_page + 2).times { create(:spending_proposal, resolution: "accepted") } + + visit admin_spending_proposals_path(filter: 'accepted', page: 2) + + click_on('Reject', match: :first, exact: true) + + expect(current_url).to include('filter=accepted') + expect(current_url).to include('page=2') + end + + scenario 'Show' do + spending_proposal = create(:spending_proposal, geozone: create(:geozone)) + visit admin_spending_proposals_path + + click_link spending_proposal.title + + expect(page).to have_content(spending_proposal.title) + expect(page).to have_content(spending_proposal.description) + expect(page).to have_content(spending_proposal.author.name) + expect(page).to have_content(spending_proposal.geozone.name) + end + + scenario 'Accept from show' do + spending_proposal = create(:spending_proposal) + visit admin_spending_proposal_path(spending_proposal) + + click_link 'Accept' + + expect(page).to_not have_content(spending_proposal.title) + + click_link 'Accepted' + expect(page).to have_content(spending_proposal.title) + + expect(spending_proposal.reload).to be_accepted + end + + scenario 'Reject from show' do + spending_proposal = create(:spending_proposal) + visit admin_spending_proposal_path(spending_proposal) + + click_link 'Reject' + + expect(page).to_not have_content(spending_proposal.title) + + click_link('Rejected') + expect(page).to have_content(spending_proposal.title) + + expect(spending_proposal.reload).to be_rejected + end + +end diff --git a/spec/features/comments/debates_spec.rb b/spec/features/comments/debates_spec.rb index 5adfa218c..7d807a6c0 100644 --- a/spec/features/comments/debates_spec.rb +++ b/spec/features/comments/debates_spec.rb @@ -20,6 +20,21 @@ feature 'Commenting debates' do end end + scenario 'Show' do + parent_comment = create(:comment, commentable: debate) + first_child = create(:comment, commentable: debate, parent: parent_comment) + second_child = create(:comment, commentable: debate, parent: parent_comment) + + visit comment_path(parent_comment) + + expect(page).to have_css(".comment", count: 3) + expect(page).to have_content parent_comment.body + expect(page).to have_content first_child.body + expect(page).to have_content second_child.body + + expect(page).to have_link "Go back to #{debate.title}", debate_path(debate) + end + scenario 'Comment order' do c1 = create(:comment, :with_confidence_score, commentable: debate, cached_votes_up: 100, cached_votes_total: 120, created_at: Time.now - 2) c2 = create(:comment, :with_confidence_score, commentable: debate, cached_votes_up: 10, cached_votes_total: 12, created_at: Time.now - 1) @@ -254,7 +269,7 @@ feature 'Commenting debates' do fill_in "comment-body-debate_#{debate.id}", with: 'Testing submit button!' click_button 'Publish comment' - + # The button's text should now be "..." # This should be checked before the Ajax request is finished expect(page).to_not have_button 'Publish comment' diff --git a/spec/features/comments/proposals_spec.rb b/spec/features/comments/proposals_spec.rb index 1c277cf4d..d919f0b51 100644 --- a/spec/features/comments/proposals_spec.rb +++ b/spec/features/comments/proposals_spec.rb @@ -20,6 +20,21 @@ feature 'Commenting proposals' do end end + scenario 'Show' do + parent_comment = create(:comment, commentable: proposal) + first_child = create(:comment, commentable: proposal, parent: parent_comment) + second_child = create(:comment, commentable: proposal, parent: parent_comment) + + visit comment_path(parent_comment) + + expect(page).to have_css(".comment", count: 3) + expect(page).to have_content parent_comment.body + expect(page).to have_content first_child.body + expect(page).to have_content second_child.body + + expect(page).to have_link "Go back to #{proposal.title}", proposal_path(proposal) + end + scenario 'Comment order' do c1 = create(:comment, :with_confidence_score, commentable: proposal, cached_votes_up: 100, cached_votes_total: 120, created_at: Time.now - 2) c2 = create(:comment, :with_confidence_score, commentable: proposal, cached_votes_up: 10, cached_votes_total: 12, created_at: Time.now - 1) diff --git a/spec/features/notifications_spec.rb b/spec/features/notifications_spec.rb new file mode 100644 index 000000000..1ffc6a854 --- /dev/null +++ b/spec/features/notifications_spec.rb @@ -0,0 +1,192 @@ +require 'rails_helper' + +feature "Notifications" do + let(:author) { create :user } + let(:user) { create :user } + let(:debate) { create :debate, author: author } + let(:proposal) { create :proposal, author: author } + + scenario "User commented on my debate", :js do + login_as user + visit debate_path debate + + fill_in "comment-body-debate_#{debate.id}", with: "I commented on your debate" + click_button "Publish comment" + within "#comments" do + expect(page).to have_content "I commented on your debate" + end + + logout + login_as author + visit root_path + + find(".icon-notification").click + + expect(page).to have_css ".notification", count: 1 + + expect(page).to have_content "Someone commented on" + expect(page).to have_xpath "//a[@href='#{notification_path(Notification.last)}']" + end + + scenario "Multiple comments on my proposal", :js do + login_as user + visit proposal_path proposal + + fill_in "comment-body-proposal_#{proposal.id}", with: "I agree" + click_button "Publish comment" + within "#comments" do + expect(page).to have_content "I agree" + end + + logout + login_as create(:user) + visit proposal_path proposal + + fill_in "comment-body-proposal_#{proposal.id}", with: "I disagree" + click_button "Publish comment" + within "#comments" do + expect(page).to have_content "I disagree" + end + + logout + login_as author + visit root_path + + find(".icon-notification").click + + expect(page).to have_css ".notification", count: 1 + + expect(page).to have_content "There are 2 new comments on" + expect(page).to have_xpath "//a[@href='#{notification_path(Notification.last)}']" + end + + scenario "User replied to my comment", :js do + comment = create :comment, commentable: debate, user: author + login_as user + visit debate_path debate + + click_link "Reply" + within "#js-comment-form-comment_#{comment.id}" do + fill_in "comment-body-comment_#{comment.id}", with: "I replied to your comment" + click_button "Publish reply" + end + + within "#comment_#{comment.id}" do + expect(page).to have_content "I replied to your comment" + end + + logout + login_as author + visit root_path + + find(".icon-notification").click + + expect(page).to have_css ".notification", count: 1 + expect(page).to have_content "Someone replied to your comment on" + expect(page).to have_xpath "//a[@href='#{notification_path(Notification.last)}']" + end + + scenario "Multiple replies to my comment", :js do + comment = create :comment, commentable: debate, user: author + 3.times do |n| + login_as create(:user) + visit debate_path debate + + within("#comment_#{comment.id}_reply") { click_link "Reply" } + within "#js-comment-form-comment_#{comment.id}" do + fill_in "comment-body-comment_#{comment.id}", with: "Reply number #{n}" + click_button "Publish reply" + end + + within "#comment_#{comment.id}" do + expect(page).to have_content "Reply number #{n}" + end + logout + end + + login_as author + visit root_path + + find(".icon-notification").click + + expect(page).to have_css ".notification", count: 1 + expect(page).to have_content "There are 3 new replies to your comment on" + expect(page).to have_xpath "//a[@href='#{notification_path(Notification.last)}']" + end + + scenario "Author commented on his own debate", :js do + login_as author + visit debate_path debate + + fill_in "comment-body-debate_#{debate.id}", with: "I commented on my own debate" + click_button "Publish comment" + within "#comments" do + expect(page).to have_content "I commented on my own debate" + end + + find(".icon-no-notification").click + expect(page).to have_css ".notification", count: 0 + end + + scenario "Author replied to his own comment", :js do + comment = create :comment, commentable: debate, user: author + login_as author + visit debate_path debate + + click_link "Reply" + within "#js-comment-form-comment_#{comment.id}" do + fill_in "comment-body-comment_#{comment.id}", with: "I replied to my own comment" + click_button "Publish reply" + end + + within "#comment_#{comment.id}" do + expect(page).to have_content "I replied to my own comment" + end + + find(".icon-no-notification") + + visit notifications_path + expect(page).to have_css ".notification", count: 0 + end + + context "mark as read" do + + scenario "mark a single notification as read" do + user = create :user + notification = create :notification, user: user + + login_as user + visit notifications_path + + expect(page).to have_css ".notification", count: 1 + + first(".notification a").click + visit notifications_path + + expect(page).to have_css ".notification", count: 0 + end + + scenario "mark all notifications as read" do + user = create :user + 2.times { create :notification, user: user } + + login_as user + visit notifications_path + + expect(page).to have_css ".notification", count: 2 + click_link "Mark all as read" + + expect(page).to have_css ".notification", count: 0 + expect(current_path).to eq(notifications_path) + end + + end + + scenario "no notifications" do + login_as user + visit notifications_path + + expect(page).to have_content "You don't have new notifications" + end + +end diff --git a/spec/features/spending_proposals_spec.rb b/spec/features/spending_proposals_spec.rb new file mode 100644 index 000000000..6b76b02e0 --- /dev/null +++ b/spec/features/spending_proposals_spec.rb @@ -0,0 +1,66 @@ +require 'rails_helper' + +feature 'Spending proposals' do + + let(:author) { create(:user, :level_two) } + + scenario 'Index' do + visit spending_proposals_path + + expect(page).to_not have_link('Create spending proposal', href: new_spending_proposal_path) + expect(page).to have_link('verify your account') + + login_as(author) + + visit spending_proposals_path + + expect(page).to have_link('Create spending proposal', href: new_spending_proposal_path) + expect(page).to_not have_link('verify your account') + end + + scenario 'Create' do + login_as(author) + + visit new_spending_proposal_path + fill_in 'spending_proposal_title', with: 'Build a skyscraper' + fill_in 'spending_proposal_description', with: 'I want to live in a high tower over the clouds' + fill_in 'spending_proposal_external_url', with: 'http://http://skyscraperpage.com/' + fill_in 'spending_proposal_captcha', with: correct_captcha_text + select 'All city', from: 'spending_proposal_geozone_id' + check 'spending_proposal_terms_of_service' + + click_button 'Create' + + expect(page).to have_content 'Spending proposal created successfully' + end + + scenario 'Captcha is required for proposal creation' do + login_as(author) + + visit new_spending_proposal_path + fill_in 'spending_proposal_title', with: 'Build a skyscraper' + fill_in 'spending_proposal_description', with: 'I want to live in a high tower over the clouds' + fill_in 'spending_proposal_external_url', with: 'http://http://skyscraperpage.com/' + fill_in 'spending_proposal_captcha', with: 'wrongText' + check 'spending_proposal_terms_of_service' + + click_button 'Create' + + expect(page).to_not have_content 'Spending proposal created successfully' + expect(page).to have_content '1 error' + + fill_in 'spending_proposal_captcha', with: correct_captcha_text + click_button 'Create' + + expect(page).to have_content 'Spending proposal created successfully' + end + + scenario 'Errors on create' do + login_as(author) + + visit new_spending_proposal_path + click_button 'Create' + expect(page).to have_content error_message + end + +end diff --git a/spec/helpers/geozones_helper_spec.rb b/spec/helpers/geozones_helper_spec.rb new file mode 100644 index 000000000..d85ff8753 --- /dev/null +++ b/spec/helpers/geozones_helper_spec.rb @@ -0,0 +1,35 @@ +require 'rails_helper' + +describe GeozonesHelper do + + describe "#geozones_name" do + let(:geozone) { create :geozone } + + + it "returns geozone name if present" do + spending_proposal = create(:spending_proposal, geozone: geozone) + expect(geozone_name(spending_proposal)).to eq geozone.name + end + + it "returns default string for no geozone if geozone is blank" do + spending_proposal = create(:spending_proposal, geozone: nil) + expect(geozone_name(spending_proposal)).to eq "All city" + end + end + + describe "#geozone_select_options" do + it "returns array of ids and names ordered by name" do + g1 = create(:geozone, name: "AAA") + g3 = create(:geozone, name: "CCC") + g2 = create(:geozone, name: "BBB") + + select_options = geozone_select_options + + expect(select_options.size).to eq 3 + expect(select_options.first).to eq [g1.name, g1.id] + expect(select_options[1]).to eq [g2.name, g2.id] + expect(select_options.last).to eq [g3.name, g3.id] + end + end + +end diff --git a/spec/helpers/notifications_helper_spec.rb b/spec/helpers/notifications_helper_spec.rb new file mode 100644 index 000000000..ccb3b3a9d --- /dev/null +++ b/spec/helpers/notifications_helper_spec.rb @@ -0,0 +1,25 @@ +require 'rails_helper' + +describe NotificationsHelper do + + describe "#notification_action" do + let(:debate) { create :debate } + let(:debate_comment) { create :comment, commentable: debate } + + context "when action was comment on a debate" do + it "returns correct text when someone comments on your debate" do + notification = create :notification, notifiable: debate + expect(notification_action(notification)).to eq "comments_on" + end + end + + context "when action was comment on a debate" do + it "returns correct text when someone replies to your comment" do + notification = create :notification, notifiable: debate_comment + expect(notification_action(notification)).to eq "replies_to" + end + end + end + + +end diff --git a/spec/models/abilities/administrator_spec.rb b/spec/models/abilities/administrator_spec.rb index 66757ddc0..8df88a46f 100644 --- a/spec/models/abilities/administrator_spec.rb +++ b/spec/models/abilities/administrator_spec.rb @@ -51,4 +51,6 @@ describe "Abilities::Administrator" do it { should_not be_able_to(:comment_as_moderator, proposal) } it { should be_able_to(:manage, Annotation) } + + it { should be_able_to(:manage, SpendingProposal) } end diff --git a/spec/models/abilities/common_spec.rb b/spec/models/abilities/common_spec.rb index 4775cd1ee..d942e2d18 100644 --- a/spec/models/abilities/common_spec.rb +++ b/spec/models/abilities/common_spec.rb @@ -28,6 +28,9 @@ describe "Abilities::Common" do it { should_not be_able_to(:vote, Proposal) } it { should_not be_able_to(:vote_featured, Proposal) } + it { should be_able_to(:index, SpendingProposal) } + it { should_not be_able_to(:create, SpendingProposal) } + it { should_not be_able_to(:comment_as_administrator, debate) } it { should_not be_able_to(:comment_as_moderator, debate) } it { should_not be_able_to(:comment_as_administrator, proposal) } @@ -84,6 +87,8 @@ describe "Abilities::Common" do it { should be_able_to(:vote, Proposal) } it { should be_able_to(:vote_featured, Proposal) } + + it { should be_able_to(:create, SpendingProposal) } end describe "when level 3 verified" do @@ -91,5 +96,7 @@ describe "Abilities::Common" do it { should be_able_to(:vote, Proposal) } it { should be_able_to(:vote_featured, Proposal) } + + it { should be_able_to(:create, SpendingProposal) } end end diff --git a/spec/models/abilities/everyone_spec.rb b/spec/models/abilities/everyone_spec.rb index 4c532b7a4..3f1e57278 100644 --- a/spec/models/abilities/everyone_spec.rb +++ b/spec/models/abilities/everyone_spec.rb @@ -21,4 +21,9 @@ describe "Abilities::Everyone" do it { should_not be_able_to(:vote, Proposal) } it { should_not be_able_to(:flag, Proposal) } it { should_not be_able_to(:unflag, Proposal) } + + it { should be_able_to(:show, Comment) } + + it { should be_able_to(:index, SpendingProposal) } + it { should_not be_able_to(:create, SpendingProposal) } end diff --git a/spec/models/comment_spec.rb b/spec/models/comment_spec.rb index 11a02400a..73b70fb42 100644 --- a/spec/models/comment_spec.rb +++ b/spec/models/comment_spec.rb @@ -128,4 +128,5 @@ describe Comment do expect(Comment.not_as_admin_or_moderator.first).to eq(comment1) end end + end diff --git a/spec/models/geozone_spec.rb b/spec/models/geozone_spec.rb new file mode 100644 index 000000000..a29c4c918 --- /dev/null +++ b/spec/models/geozone_spec.rb @@ -0,0 +1,14 @@ +require 'rails_helper' + +RSpec.describe Geozone, type: :model do + let(:geozone) { build(:geozone) } + + it "should be valid" do + expect(geozone).to be_valid + end + + it "should not be valid without a name" do + geozone.name = nil + expect(geozone).to_not be_valid + end +end diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb new file mode 100644 index 000000000..361211a51 --- /dev/null +++ b/spec/models/notification_spec.rb @@ -0,0 +1,50 @@ +require 'rails_helper' + +describe Notification do + + describe "#unread (scope)" do + it "returns only unread notifications" do + 2.times { create :notification } + expect(Notification.unread.size).to be 2 + end + end + + describe "#recent (scope)" do + it "returns notifications sorted by id descendant" do + old_notification = create :notification + new_notification = create :notification + + sorted_notifications = Notification.recent + expect(sorted_notifications.size).to be 2 + expect(sorted_notifications.first).to eq new_notification + expect(sorted_notifications.last).to eq old_notification + end + end + + describe "#for_render (scope)" do + it "returns notifications including notifiable and user" do + expect(Notification).to receive(:includes).with(:notifiable).exactly(:once) + Notification.for_render + end + end + + describe "#timestamp" do + it "returns the timestamp of the trackable object" do + comment = create :comment + notification = create :notification, notifiable: comment + + expect(notification.timestamp).to eq comment.created_at + end + end + + describe "#mark_as_read" do + it "destroys notification" do + notification = create :notification + expect(Notification.unread.size).to eq 1 + + notification.mark_as_read + expect(Notification.unread.size).to eq 0 + end + end + +end diff --git a/spec/models/spending_proposal_spec.rb b/spec/models/spending_proposal_spec.rb new file mode 100644 index 000000000..25bccbfc6 --- /dev/null +++ b/spec/models/spending_proposal_spec.rb @@ -0,0 +1,140 @@ +require 'rails_helper' + +describe SpendingProposal do + let(:spending_proposal) { build(:spending_proposal) } + + it "should be valid" do + expect(spending_proposal).to be_valid + end + + it "should not be valid without an author" do + spending_proposal.author = nil + expect(spending_proposal).to_not be_valid + end + + describe "#title" do + it "should not be valid without a title" do + spending_proposal.title = nil + expect(spending_proposal).to_not be_valid + end + + it "should not be valid when very short" do + spending_proposal.title = "abc" + expect(spending_proposal).to_not be_valid + end + + it "should not be valid when very long" do + spending_proposal.title = "a" * 81 + expect(spending_proposal).to_not be_valid + end + end + + describe "#description" do + it "should be sanitized" do + spending_proposal.description = "" + spending_proposal.valid? + expect(spending_proposal.description).to eq("alert('danger');") + end + + it "should not be valid when very long" do + spending_proposal.description = "a" * 6001 + expect(spending_proposal).to_not be_valid + end + end + + describe "resolution status" do + it "should be valid" do + spending_proposal.resolution = "accepted" + expect(spending_proposal).to be_valid + spending_proposal.resolution = "rejected" + expect(spending_proposal).to be_valid + spending_proposal.resolution = "wrong" + expect(spending_proposal).to_not be_valid + end + + it "can be accepted" do + spending_proposal.accept + expect(spending_proposal.reload.resolution).to eq("accepted") + end + + it "can be rejected" do + spending_proposal.reject + expect(spending_proposal.reload.resolution).to eq("rejected") + end + + describe "#accepted?" do + it "should be true if resolution equals 'accepted'" do + spending_proposal.resolution = "accepted" + expect(spending_proposal.accepted?).to eq true + end + + it "should be false otherwise" do + spending_proposal.resolution = "rejected" + expect(spending_proposal.accepted?).to eq false + spending_proposal.resolution = nil + expect(spending_proposal.accepted?).to eq false + end + end + + describe "#rejected?" do + it "should be true if resolution equals 'rejected'" do + spending_proposal.resolution = "rejected" + expect(spending_proposal.rejected?).to eq true + end + + it "should be false otherwise" do + spending_proposal.resolution = "accepted" + expect(spending_proposal.rejected?).to eq false + spending_proposal.resolution = nil + expect(spending_proposal.rejected?).to eq false + end + end + + describe "#unresolved?" do + it "should be true if resolution is blank" do + spending_proposal.resolution = nil + expect(spending_proposal.unresolved?).to eq true + end + + it "should be false otherwise" do + spending_proposal.resolution = "accepted" + expect(spending_proposal.unresolved?).to eq false + spending_proposal.resolution = "rejected" + expect(spending_proposal.unresolved?).to eq false + end + end + end + + describe "scopes" do + before(:each) do + 2.times { create(:spending_proposal, resolution: "accepted") } + 2.times { create(:spending_proposal, resolution: "rejected") } + 2.times { create(:spending_proposal, resolution: nil) } + end + + describe "unresolved" do + it "should return all spending proposals without resolution" do + unresolved = SpendingProposal.all.unresolved + expect(unresolved.size).to eq(2) + unresolved.each {|u| expect(u.resolution).to be_nil} + end + end + + describe "accepted" do + it "should return all accepted spending proposals" do + accepted = SpendingProposal.all.accepted + expect(accepted.size).to eq(2) + accepted.each {|a| expect(a.resolution).to eq("accepted")} + end + end + + describe "rejected" do + it "should return all rejected spending proposals" do + rejected = SpendingProposal.all.rejected + expect(rejected.size).to eq(2) + rejected.each {|r| expect(r.resolution).to eq("rejected")} + end + end + end + +end