Merge pull request #2641 from consul/homepage

Add customization of homepage from admin section
This commit is contained in:
Alberto
2018-06-01 19:18:44 +02:00
committed by GitHub
57 changed files with 1303 additions and 138 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -279,7 +279,7 @@ $sidebar-active: #f4fcd0;
}
.no-margin-bottom {
margin-bottom: 0 !important;
margin-bottom: 0 !important;
}
// 02. Sidebar

View File

@@ -23,6 +23,7 @@
// 21. Related content
// 22. Images
// 23. Maps
// 24. Homepage
//
// 01. Global styles
@@ -2224,17 +2225,17 @@ table {
// 19. Recommended Section Home
// ----------------------------
.home-page {
.push {
display: none;
}
}
.section-recommended {
padding: $line-height * 2 0;
background: #fafafa;
border-top: 1px solid $border;
margin-bottom: rem-calc(-48);
padding: $line-height 0;
h2 {
margin-bottom: 0;
}
p {
margin-bottom: $line-height * 2;
}
@@ -2258,12 +2259,12 @@ table {
.card {
.card-section {
padding: $line-height 0;
max-width: rem-calc(300);
margin: 0 auto;
max-width: none;
padding: 0;
p {
font-size: rem-calc(15);
font-size: $base-font-size;
text-align: left;
}
}
@@ -2293,18 +2294,6 @@ table {
width: 100%;
}
.debates-inner {
border-top: 4px solid $debates;
}
.proposals-inner {
border-top: 4px solid $proposals;
}
.budget-investments-inner {
border-top: 4px solid $budget;
}
.debates-inner,
.proposals-inner,
.budget-investments-inner {
@@ -2316,10 +2305,10 @@ table {
}
h4 {
margin-top: $line-height;
margin-bottom: 0;
margin-top: 0;
margin-bottom: $line-height;
font-size: rem-calc(18);
min-height: rem-calc(50);
min-height: 0;
}
h5 {
@@ -2338,9 +2327,21 @@ table {
}
}
.debates,
.proposals {
a {
display: block;
margin-top: $line-height;
}
}
.debates-inner,
.proposals-inner,
.budget-investments-inner {
border: 1px solid $border;
padding: $line-height;
margin-right: $line-height;
max-height: rem-calc(500);
@include breakpoint(small) {
@@ -2575,3 +2576,151 @@ table {
color: #525252 !important;
}
}
// 24. Homepage
// ------------
.home-page {
a {
p {
&.description {
color: $text;
}
}
}
a:hover {
h3 {
color: #fff;
}
}
.background-header {
clip-path: ellipse(60% 80% at 50% 0%);
height: $line-height * 2;
background: $highlight;
margin-top: rem-calc(-48);
}
.card {
border: 0;
}
h3 {
&.title {
display: inline-block;
border-top: 4px solid $brand;
min-width: rem-calc(240);
padding-top: $line-height / 2;
}
}
}
.feeds-list {
.proposal {
clear: both;
}
.debate {
margin: $line-height 0;
}
}
.feed-image {
display: inline-block;
height: rem-calc(120);
margin: $line-height / 2 0;
overflow: hidden;
img {
margin-left: rem-calc(-15);
max-width: none;
width: rem-calc(120);
}
}
.feed-description {
@include breakpoint(medium) {
padding-top: $line-height;
}
p {
font-size: $small-font-size;
}
}
.feed-content {
border-bottom: 1px solid $border;
display: inline-block;
a {
&.see-all {
font-size: $small-font-size;
margin: $line-height 0 $line-height / 2;
}
}
}
.figure-card {
display: flex;
margin: 0 0 $line-height;
position: relative;
@include breakpoint(medium) {
max-height: rem-calc(185);
overflow: hidden;
}
a {
h3,
.title {
color: #fff;
}
&:hover {
text-decoration: none;
}
}
img {
height: 100%;
width: 100%;
}
figcaption {
bottom: 0;
color: #fff;
font-size: rem-calc(24);
line-height: rem-calc(24);
text-transform: uppercase;
padding: $line-height / 4 $line-height / 2;
position: absolute;
width: 100%;
h3,
.title {
font-size: rem-calc(24);
line-height: rem-calc(24);
}
span {
background: #fff;
border-radius: rem-calc(4);
color: #000;
display: inline-block;
font-size: $small-font-size;
font-weight: bold;
margin-bottom: $line-height / 4;
padding: rem-calc(4) rem-calc(8);
}
}
}

View File

@@ -319,7 +319,8 @@
.legislation-process-new,
.legislation-process-edit,
.milestone-new,
.milestone-edit {
.milestone-edit,
.image-form {
@include direct-uploads;
}

View File

@@ -0,0 +1,28 @@
class Admin::HomepageController < Admin::BaseController
def show
load_header
load_feeds
load_recommendations
load_cards
end
private
def load_header
@header = ::Widget::Card.header
end
def load_recommendations
@recommendations = Setting.where(key: 'feature.user.recommendations').first
end
def load_cards
@cards = ::Widget::Card.body
end
def load_feeds
@feeds = Widget::Feed.order("created_at")
end
end

View File

@@ -11,7 +11,7 @@ class Admin::SettingsController < Admin::BaseController
def update
@setting = Setting.find(params[:id])
@setting.update(settings_params)
redirect_to admin_settings_path, notice: t("admin.settings.flash.updated")
redirect_to request.referer, notice: t("admin.settings.flash.updated")
end
def update_map

View File

@@ -0,0 +1,10 @@
class Admin::Widget::BaseController < Admin::BaseController
helper_method :namespace
private
def namespace
"admin"
end
end

View File

@@ -0,0 +1,51 @@
class Admin::Widget::CardsController < Admin::BaseController
def new
@card = ::Widget::Card.new(header: header_card?)
end
def create
@card = ::Widget::Card.new(card_params)
if @card.save
notice = "Success"
redirect_to admin_homepage_url, notice: notice
else
render :new
end
end
def edit
@card = ::Widget::Card.find(params[:id])
end
def update
@card = ::Widget::Card.find(params[:id])
if @card.update(card_params)
notice = "Updated"
redirect_to admin_homepage_url, notice: notice
else
render :edit
end
end
def destroy
@card = ::Widget::Card.find(params[:id])
@card.destroy
notice = "Removed"
redirect_to admin_homepage_url, notice: notice
end
private
def card_params
params.require(:widget_card).permit(:label, :title, :description, :link_text, :link_url,
:button_text, :button_url, :alignment, :header,
image_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy])
end
def header_card?
params[:header_card].present?
end
end

View File

@@ -0,0 +1,16 @@
class Admin::Widget::FeedsController < Admin::BaseController
def update
@feed = ::Widget::Feed.find(params[:id])
@feed.update(feed_params)
render nothing: true
end
private
def feed_params
params.require(:widget_feed).permit(:limit)
end
end

View File

@@ -5,6 +5,9 @@ class WelcomeController < ApplicationController
layout "devise", only: [:welcome, :verification]
def index
@header = Widget::Card.header.first
@feeds = Widget::Feed.active
@cards = Widget::Card.body
end
def welcome

View File

@@ -37,7 +37,11 @@ module AdminHelper
end
def menu_customization?
["pages", "images", "content_blocks"].include?(controller_name)
["pages", "images", "content_blocks"].include?(controller_name) || menu_homepage?
end
def menu_homepage?
["homepage", "cards"].include?(controller_name)
end
def official_level_options

View File

@@ -0,0 +1,19 @@
module FeedsHelper
def feed_debates?(feed)
feed.kind == "debates"
end
def feed_proposals?(feed)
feed.kind == "proposals"
end
def feed_processes?(feed)
feed.kind == "processes"
end
def feed_processes_enabled?
Setting['feature.homepage.widgets.feeds.processes'].present?
end
end

7
app/helpers/settings.rb Normal file
View File

@@ -0,0 +1,7 @@
module SettingsHelper
def setting_for_widget(widget)
Setting.where(key: 'feature.user.recommendations').first
end
end

View File

@@ -68,6 +68,8 @@ class Image < ActiveRecord::Base
def validate_image_dimensions
if attachment_of_valid_content_type?
return true if imageable_class == Widget::Card
dimensions = Paperclip::Geometry.from_file(attachment.queued_for_write[:original].path)
errors.add(:attachment, :min_image_width, required_min_width: MIN_SIZE) if dimensions.width < MIN_SIZE
errors.add(:attachment, :min_image_height, required_min_height: MIN_SIZE) if dimensions.height < MIN_SIZE

13
app/models/widget/card.rb Normal file
View File

@@ -0,0 +1,13 @@
class Widget::Card < ActiveRecord::Base
include Imageable
self.table_name = "widget_cards"
def self.header
where(header: true)
end
def self.body
where(header: false).order(:created_at)
end
end

37
app/models/widget/feed.rb Normal file
View File

@@ -0,0 +1,37 @@
class Widget::Feed < ActiveRecord::Base
self.table_name = "widget_feeds"
KINDS = %w(proposals debates processes)
def active?
setting.value.present?
end
def setting
Setting.where(key: "feature.homepage.widgets.feeds.#{kind}").first
end
def self.active
KINDS.collect do |kind|
feed = find_or_create_by(kind: kind)
feed if feed.active?
end.compact
end
def items
send(kind)
end
def proposals
Proposal.sort_by_hot_score.limit(limit)
end
def debates
Debate.sort_by_hot_score.limit(limit)
end
def processes
Legislation::Process.open.published.limit(limit)
end
end

View File

@@ -167,6 +167,10 @@
</a>
<ul <%= "class=is-active" if menu_customization? &&
controller.class.parent != Admin::Poll::Questions::Answers %>>
<li <%= "class=active" if menu_homepage? %>>
<%= link_to t("admin.menu.site_customization.homepage"), admin_homepage_path %>
</li>
<li <%= "class=active" if controller_name == "pages" %>>
<%= link_to t("admin.menu.site_customization.pages"), admin_site_customization_pages_path %>
</li>
@@ -179,6 +183,7 @@
<li <%= "class=active" if controller_name == "content_blocks" %>>
<%= link_to t("admin.menu.site_customization.content_blocks"), admin_site_customization_content_blocks_path%>
</li>
</ul>
</li>

View File

@@ -0,0 +1,30 @@
<tr id="<%= dom_id(card) %>" class="homepage-card">
<td>
<%= card.label %><br>
<%= card.title %>
</td>
<td><%= card.description %></td>
<td>
<%= card.link_text %><br>
<%= card.link_url %>
</td>
<!-- remove conditional once specs have image validations -->
<td>
<% if card.image.present? %>
<%= link_to t("admin.shared.show_image"), card.image_url(:large),
title: card.image.title, target: "_blank" %>
<% end %>
</td>
<td>
<%= link_to t("admin.actions.edit"),
edit_admin_widget_card_path(card),
class: "button hollow" %>
<%= link_to t("admin.actions.delete"),
admin_widget_card_path(card),
method: :delete,
data: { confirm: t('admin.actions.confirm') },
class: "button hollow alert" %>
</td>
</tr>

View File

@@ -0,0 +1,16 @@
<table>
<thead>
<tr>
<th><%= t("admin.homepage.cards.title") %></th>
<th class="small-4"><%= t("admin.homepage.cards.description") %></th>
<th><%= t("admin.homepage.cards.link_text") %> / <%= t("admin.homepage.cards.link_url") %></th>
<th><%= t("admin.shared.image") %></th>
<th class="small-2"><%= t("admin.shared.actions") %></th>
</tr>
</thead>
<tbody>
<% cards.each do |card| %>
<%= render "card", card: card %>
<% end %>
</tbody>
</table>

View File

@@ -0,0 +1,11 @@
<div id="<%= dom_id(feed) %>" class="small-12 medium-6 large-4 column end">
<div class="callout">
<h3><%= t("admin.homepage.feeds.#{feed.kind}") %></h3>
<%= render "setting", setting: feed.setting %>
<%= form_for [:admin, feed], remote: true do |f| %>
<%= f.select :limit, (1..10), {}, class: "js-submit-on-change" %>
<% end %>
</div>
</div>

View File

@@ -0,0 +1,11 @@
<div id="<%= dom_id(setting) %>">
<%= form_for(setting, url: admin_setting_path(setting), method: :put) do |f| %>
<%= f.hidden_field :value,
value: (setting.enabled? ? "" : "active") %>
<%= f.submit(t("admin.settings.index.features.#{setting.enabled? ? 'disable' : 'enable'}"),
class: "button #{setting.enabled? ? 'hollow alert' : 'success'}",
data: {confirm: t("admin.actions.confirm")}) %>
<% end %>
</div>

View File

@@ -0,0 +1,52 @@
<h2><%= t("admin.homepage.title") %></h2>
<p><%= t("admin.homepage.description") %></p>
<div id="header">
<h3 class="inline-block"><%= t("admin.homepage.header_title") %></h3>
<div class="float-right">
<%= link_to t("admin.homepage.create_header"), new_admin_widget_card_path(header_card: true), class: "button" %>
</div>
<% if @header.present? %>
<%= render "cards", cards: @header %>
<% else %>
<div class="callout primary clear">
<%= t("admin.homepage.no_header") %>
</div>
<% end %>
</div>
<hr>
<div id="cards">
<h3 class="inline-block"><%= t("admin.homepage.cards_title") %></h3>
<div class="float-right">
<%= link_to t("admin.homepage.create_card"), new_admin_widget_card_path, class: "button" %>
</div>
<% if @cards.present? %>
<%= render "cards", cards: @cards %>
<% else %>
<div class="callout primary clear">
<%= t("admin.homepage.no_cards") %>
</div>
<% end %>
</div>
<hr>
<% @feeds.each do |feed| %>
<%= render "feed", feed: feed %>
<% end %>
<div class="small-12 medium-6 large-4 column end">
<div class="callout">
<h3 class="inline-block"><%= t("settings.#{@recommendations.key}") %></h3>
<div class="float-right">
<%= render "setting", setting: @recommendations %>
</div>
</div>
</div>

View File

@@ -0,0 +1 @@
<%= render "admin/menu" %>

View File

@@ -0,0 +1,27 @@
<%= form_for [:admin, @card] do |f| %>
<div class="small-12 medium-6">
<%= f.text_field :label %>
</div>
<%= f.text_field :title %>
<%= f.text_area :description, rows: 5 %>
<div class="small-12 medium-6">
<%= f.text_field :link_text %>
</div>
<div class="small-12 medium-6">
<%= f.text_field :link_url %>
</div>
<%= f.hidden_field :header, value: @card.header? %>
<div class="image-form">
<div class="image small-12 column">
<%= render 'images/nested_image', imageable: @card, f: f %>
</div>
</div>
<%= f.submit(t("admin.homepage.#{action_name}.#{@card.header? ? 'submit_header' : 'submit_card'}"), class: "button success") %>
<% end %>

View File

@@ -0,0 +1,9 @@
<h2>
<% if @card.header? %>
<%= t("admin.homepage.edit.header_title") %>
<% else %>
<%= t("admin.homepage.edit.card_title") %>
<% end %>
</h2>
<%= render "form" %>

View File

@@ -0,0 +1,9 @@
<h2>
<% if @card.header? %>
<%= t("admin.homepage.new.header_title") %>
<% else %>
<%= t("admin.homepage.new.card_title") %>
<% end %>
</h2>
<%= render "form" %>

View File

@@ -16,7 +16,7 @@
<div class="top-bar-title">
<h1>
<%= link_to namespaced_root_path do %>
<%= link_to admin_root_path do %>
<%= setting['org_name'] %>
<br><small><%= namespaced_header_title %></small>
<% end %>

View File

@@ -0,0 +1,15 @@
<div id="<%= dom_id(card) %>" class="card small-12 medium-6 column margin-bottom end <%= 'large-4' unless feed_processes_enabled? %>">
<%= link_to card.link_url do %>
<figure class="figure-card">
<% if card.image.present? %>
<%= image_tag(card.image_url(:large), alt: card.image.title) %>
<% end %>
<figcaption>
<span><%= card.label %></span><br>
<h3><%= card.title %></h3>
</figcaption>
</figure>
<p class="description"><%= card.description %></p>
<p><%= card.link_text %></p>
<% end %>
</div>

View File

@@ -0,0 +1,7 @@
<h3 class="title"><%= t("welcome.cards.title") %></h3>
<div class="row">
<% @cards.all.each do |card| %>
<%= render "card", card: card %>
<% end %>
</div>

View File

@@ -0,0 +1,48 @@
<div class="row margin-bottom feeds-list">
<% @feeds.each do |feed| %>
<% if feed_proposals?(feed) %>
<div class="small-12 medium-8 column margin-top">
<div class="feed-content">
<h3 class="title"><%= t("welcome.feed.most_active.#{feed.kind}") %></h3>
<% feed.items.each do |item| %>
<div class="<%= item.class.to_s.parameterize('_') %> row">
<% if feature?(:allow_images) && item.image.present? %>
<div class="small-12 large-3 column">
<div class="feed-image">
<%= image_tag item.image_url(:thumb),
alt: item.image.title.unicode_normalize %>
</div>
</div>
<% end %>
<div class="feed-description small-12 column <%= 'large-9' if feature?(:allow_images) && item.image.present? %>">
<strong><%= link_to item.title, url_for(item) %></strong><br>
<p><%= item.summary %></p>
</div>
</div>
<% end %>
<%= link_to t("welcome.feed.see_all_proposals"), proposals_path, class: "float-right see-all" %>
</div>
</div>
<% end %>
<% if feed_debates?(feed) %>
<div class="small-12 medium-4 column margin-top">
<div class="feed-content">
<h3 class="title"><%= t("welcome.feed.most_active.#{feed.kind}") %></h3>
<% feed.items.each do |item| %>
<div class="<%= item.class.to_s.parameterize('_') %>">
<strong><%= link_to item.title, url_for(item) %></strong>
</div>
<% end %>
<%= link_to t("welcome.feed.see_all_debates"), debates_path, class: "float-right see-all" %>
</div>
</div>
<% end %>
<% end %>
</div>

View File

@@ -0,0 +1,23 @@
<% if header.present? %>
<div class="jumbo highlight">
<div class="row">
<div class="small-12 medium-6 column <%= 'medium-9 small-centered text-center' unless header.image.present? %>">
<span><%= header.label %></span>
<h1><%= header.title %></h1>
<p class="lead"><%= header.description %></p>
<div class="small-12 medium-6 large-4 <%= 'small-centered' unless header.image.present? %>">
<%= link_to header.link_text, header.link_url, class: "button expanded large" %>
</div>
</div>
<% if header.image.present? %>
<div class="small-12 medium-6 column">
<%= image_tag(header.image_url(:large),
class: "margin",
alt: header.image.title) %>
</div>
<% end %>
</div>
</div>
<div class="background-header"></div>
<% end %>

View File

@@ -0,0 +1,29 @@
<div class="feeds-list">
<% @feeds.each do |feed| %>
<% if feed_processes?(feed) %>
<div class="card small-12 column margin-bottom">
<div class="feed-content">
<h3 class="title"><%= t("welcome.feed.most_active.#{feed.kind}") %></h3>
<% feed.items.each do |item| %>
<%= link_to url_for(item) do %>
<figure class="figure-card <%= item.class.to_s.parameterize('_') %>">
<%= image_tag("welcome_process.png", alt: "") %>
<figcaption>
<span><%= t("welcome.feed.process_label") %></span><br>
<h3><%= item.title %></h3>
</figcaption>
</figure>
<p class="description small"><%= item.summary %></p>
<p class="small"><%= t("welcome.feed.see_process") %></p>
<% end %>
<% end %>
<%= link_to t("welcome.feed.see_all_processes"), legislation_processes_path, class: "float-right see-all" %>
</div>
</div>
<% end %>
<% end %>
</div>

View File

@@ -1,8 +1,12 @@
<div class="small-12 column section-recommended padding">
<div id="section_recommended" class="section-recommended">
<div class="row">
<h2 class="text-center"><%= t("welcome.recommended.title") %></h2>
<div class="small-12 column carousel-image">
<div class="small-12 column">
<h2><%= t("welcome.recommended.title") %></h2>
<p><%= t("welcome.recommended.help") %></p>
</div>
<div class="carousel-image">
<% if recommended_debates.any? %>
<% carousel_size = calculate_carousel_size(recommended_debates, recommended_proposals, true) %>
<%= render "recommended_carousel", recommendeds: recommended_debates,

View File

@@ -1,9 +1,9 @@
<div class="small-12 medium-4 large-4 <%= carousel_size %> column text-center <%= key %> ">
<div class="small-12 medium-6 column <%= key %> ">
<div class="card small-centered <%= key %>-inner">
<h3><%= t("welcome.recommended.#{key.underscore}.title") %></h3>
<div class="orbit" role="region" data-orbit data-use-m-u-i="false">
<div class="orbit" role="region" data-orbit data-use-m-u-i="false" data-timer-delay="4000">
<div class="orbit-wrapper">
<ul class="orbit-container no-bullet" tabindex="0" >
@@ -38,5 +38,5 @@
</div>
</div>
<%= link_to btn_text_link, btn_path_link, class: 'button hollow expanded' %>
<%= link_to btn_text_link, btn_path_link %>
</div>

View File

@@ -9,48 +9,25 @@
social_url: root_url %>
<% end %>
<div class="jumbo highlight">
<%= render "header", header: @header %>
<main>
<%= render "feeds" %>
<div class="row">
<div class="small-12 medium-9 small-centered column text-center">
<h1><%= t("layouts.header.open_city_title") %></h1>
<p class="lead">
<%= t("layouts.header.open_city_slogan_html") %>
</p>
<div class="small-12 medium-6 large-4 small-centered">
<%= link_to t("shared.more_info"), help_path, class: "button expanded large" %>
</div>
<div class="small-12 column <%= 'medium-8' if feed_processes_enabled? %>">
<%= render "cards" %>
</div>
<div class="small-12 medium-4 column">
<%= render "processes" %>
</div>
</div>
</div>
<% if feature?("user.recommendations") && (@recommended_debates.present? || @recommended_proposals.present?) %>
<%= render "recommended",
recommended_debates: @recommended_debates,
recommended_proposals: @recommended_proposals %>
<hr>
<% end %>
<% cache [locale_and_user_status, @featured_debates, @featured_proposals, 'featured'] do %>
<main>
<div class="small-12 column text-center">
<div class="row margin padding">
<div class="small-12 medium-3 column">
<h2><%= t("welcome.debates.title") %></h2>
<p><%= t("welcome.debates.description") %></p>
</div>
<div class="small-12 medium-3 column">
<h2><%= t("welcome.proposal.title") %></h2>
<p><%= t("welcome.proposal.description") %></p>
</div>
<div class="small-12 medium-3 column">
<h2><%= t("welcome.decide.title") %></h2>
<p><%= t("welcome.decide.description") %></p>
</div>
<div class="small-12 medium-3 column">
<h2><%= t("welcome.do.title") %></h2>
<p><%= t("welcome.do.description") %></p>
</div>
</div>
</div>
</main>
<% end %>
<% if feature?("user.recommendations") && (@recommended_debates.present? || @recommended_proposals.present?) %>
<%= render "recommended",
recommended_debates: @recommended_debates,
recommended_proposals: @recommended_proposals %>
<% end %>
</main>

View File

@@ -143,6 +143,7 @@ ignore_unused:
- 'admin.settings.index.features.*'
- 'admin.polls.*.submit_button'
- 'admin.booths.*.submit_button'
- 'admin.homepage.*'
- 'moderation.comments.index.filter*'
- 'moderation.comments.index.order*'
- 'moderation.debates.index.filter*'

View File

@@ -245,6 +245,14 @@ en:
subject: Subject
from: From
body: Email content
widget/card:
label: Label (optional)
title: Title
description: Description
link_text: Link text
link_url: Link URL
widget/feed:
limit: Number of items
errors:
models:
user:

View File

@@ -501,6 +501,7 @@ en:
stats: Statistics
signature_sheets: Signature Sheets
site_customization:
homepage: Homepage
pages: Custom Pages
images: Custom Images
content_blocks: Custom content blocks
@@ -962,6 +963,8 @@ en:
actions: Actions
title: Title
description: Description
image: Image
show_image: Show image
spending_proposals:
index:
geozone_filter_all: All zones
@@ -1216,3 +1219,31 @@ en:
status_draft: Draft
status_published: Published
locale: Language
homepage:
title: Homepage
description: The active modules appear in the homepage in the same order as here.
header_title: Header
no_header: There is no header.
create_header: Create header
cards_title: Cards
create_card: Create card
no_cards: There is no cards.
cards:
title: Title
description: Description
link_text: Link text
link_url: Link URL
feeds:
proposals: Proposals
debates: Debates
processes: Processes
new:
header_title: New header
submit_header: Create header
card_title: New card
submit_card: Create card
edit:
header_title: Edit header
submit_header: Save header
card_title: Edit card
submit_card: Save card

View File

@@ -235,8 +235,6 @@ en:
my_account_link: My account
my_activity_link: My activity
open: open
open_city_slogan_html: There are cities that are governed directly by their inhabitants, who <b>discuss</b> the topics they are concerned about, <b>propose</b> ideas to improve their lives and <b>decide</b> among themselves which ones will be carried out.
open_city_title: Love the city, and it will become a city you love
open_gov: Open government
proposals: Proposals
poll_questions: Voting
@@ -631,7 +629,6 @@ en:
previous_slide: Previous Slide
next_slide: Next Slide
documentation: Additional documentation
more_info: More information
view_mode:
title: View mode
cards: Cards
@@ -787,20 +784,21 @@ en:
one: "You can only support investment projects in %{count} district"
other: "You can only support investment projects in %{count} districts"
welcome:
debates:
description: For meeting, discussing and sharing the things that matter to us in our city.
title: Debates
decide:
description: The public decides if it accepts or rejects the most supported proposals.
title: You decide
do:
description: If the proposal is accepted by the majority, the City Council accepts it as its own and it gets done.
title: It gets done
proposal:
description: Open space for citizen proposals about the kind of city we want to live in.
title: You propose
feed:
most_active:
debates: "Most active debates"
proposals: "Most active proposals"
processes: "Open processes"
see_all_debates: See all debates
see_all_proposals: See all proposals
see_all_processes: See all processes
process_label: Process
see_process: See process
cards:
title: Featured
recommended:
title: Recommendations that may interest you
help: "These recommendations are generated by the tags of the debates and proposals you are following."
debates:
title: Recommended debates
btn_text_link: All recommended debates
@@ -845,3 +843,6 @@ en:
proposal: "Proposal"
debate: "Debate"
budget_investment: "Budget investment"
admin/widget:
header:
title: Administration

View File

@@ -41,7 +41,7 @@ en:
voting_allowed: Voting on investment projects
legislation: Legislation
user:
recommendations: Recommendeds
recommendations: Recommendations
skip_verification: Skip user verification
community: Community on proposals and investments
map: Proposals and budget investments geolocation

View File

@@ -241,6 +241,14 @@ es:
subject: Asunto
from: Enviado por
body: Contenido del email
widget/card:
label: Etiqueta (opcional)
title: Título
description: Descripción
link_text: Texto del enlace
link_url: URL del enlace
widget/feed:
limit: Número de elementos
errors:
models:
user:

View File

@@ -501,6 +501,7 @@ es:
stats: Estadísticas
signature_sheets: Hojas de firmas
site_customization:
homepage: Homepage
pages: Personalizar páginas
images: Personalizar imágenes
content_blocks: Personalizar bloques
@@ -962,6 +963,8 @@ es:
actions: Acciones
title: Título
description: Descripción
image: Imagen
show_image: Mostrar imagen
spending_proposals:
index:
geozone_filter_all: Todos los ámbitos de actuación
@@ -1216,3 +1219,31 @@ es:
status_draft: Borrador
status_published: Publicada
locale: Idioma
homepage:
title: Homepage
description: Los módulos activos aparecerán en la homepage en el mismo orden que aquí.
header_title: Encabezado
create_header: Crear encabezado
no_header: No hay encabezado.
cards_title: Tarjetas
create_card: Crear tarjeta
no_cards: No hay tarjetas.
cards:
title: Título
description: Descripción
link_text: Texto del enlace
link_url: URL del enlace
feeds:
proposals: Propuestas
debates: Debates
processes: Procesos
new:
header_title: Nuevo encabezado
submit_header: Crear encabezado
card_title: Nueva tarjeta
submit_card: Crear tarjeta
edit:
header_title: Editar encabezado
submit_header: Guardar encabezado
card_title: Editar tarjeta
submit_card: Guardar tarjeta

View File

@@ -235,8 +235,6 @@ es:
my_account_link: Mi cuenta
my_activity_link: Mi actividad
open: abierto
open_city_slogan_html: Existen ciudades gobernadas directamente por sus habitantes, que <strong>debaten</strong> sobre temas que les preocupan, <strong>proponen</strong> ideas para mejorar sus vidas y <strong>deciden</strong> entre todas y todos las que se llevan a cabo.
open_city_title: La ciudad que quieres será la ciudad que quieras
open_gov: Gobierno %{open}
proposals: Propuestas
poll_questions: Votaciones
@@ -630,7 +628,6 @@ es:
previous_slide: Imagen anterior
next_slide: Siguiente imagen
documentation: Documentación adicional
more_info: Más información
view_mode:
title: Modo de vista
cards: Tarjetas
@@ -786,20 +783,21 @@ es:
one: "Sólo puedes apoyar proyectos de gasto de %{count} distrito"
other: "Sólo puedes apoyar proyectos de gasto de %{count} distritos"
welcome:
debates:
description: Encontrarnos, debatir y compartir lo que nos parece importante en nuestra ciudad.
title: Debates
decide:
description: La ciudadanía decide si acepta o rechaza las propuestas más apoyadas.
title: Decides
do:
description: Si la propuesta es aceptada mayoritariamente, el Ayuntamiento la asume como propia y se hace.
title: Se hace
proposal:
description: Espacio abierto para propuestas ciudadanas sobre el tipo de ciudad en el que queremos vivir.
title: Propones
feed:
most_active:
debates: "Debates más activos"
proposals: "Propuestas más activas"
processes: "Procesos abiertos"
see_all_debates: Ver todos los debates
see_all_proposals: Ver todas las propuestas
see_all_processes: Ver todos los procesos
process_label: Proceso
see_process: Ver proceso
cards:
title: Destacados
recommended:
title: Recomendaciones que te pueden interesar
help: "Estas recomendaciones se generan por las etiquetas de los debates y propuestas que estás siguiendo."
debates:
title: Debates recomendados
btn_text_link: Todos los debates recomendados
@@ -844,3 +842,6 @@ es:
proposal: "Propuesta"
debate: "Debate"
budget_investment: "Proyecto de gasto"
admin/widget:
header:
title: Administración

View File

@@ -179,4 +179,11 @@ namespace :admin do
resources :images, only: [:index, :update, :destroy]
resources :content_blocks, except: [:show]
end
resource :homepage, controller: :homepage, only: [:show]
namespace :widget do
resources :cards
resources :feeds, only: [:update]
end
end

View File

@@ -63,4 +63,8 @@ section "Creating Settings" do
Setting.create(key: 'map_longitude', value: -3.7)
Setting.create(key: 'map_zoom', value: 10)
Setting.create(key: 'related_content_score_threshold', value: -0.3)
Setting['feature.homepage.widgets.feeds.proposals'] = true
Setting['feature.homepage.widgets.feeds.debates'] = true
Setting['feature.homepage.widgets.feeds.processes'] = true
end

View File

@@ -0,0 +1,13 @@
class CreateWidgetCards < ActiveRecord::Migration
def change
create_table :widget_cards do |t|
t.string :title
t.text :description
t.string :link_text
t.string :link_url
t.string :label
t.boolean :header, default: false
t.timestamps null: false
end
end
end

View File

@@ -0,0 +1,9 @@
class CreateWidgetFeeds < ActiveRecord::Migration
def change
create_table :widget_feeds do |t|
t.string :kind
t.integer :limit, default: 3
t.timestamps null: false
end
end
end

View File

@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180323190027) do
ActiveRecord::Schema.define(version: 20180519132610) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -1223,6 +1223,24 @@ ActiveRecord::Schema.define(version: 20180323190027) do
add_index "votes", ["votable_id", "votable_type", "vote_scope"], name: "index_votes_on_votable_id_and_votable_type_and_vote_scope", using: :btree
add_index "votes", ["voter_id", "voter_type", "vote_scope"], name: "index_votes_on_voter_id_and_voter_type_and_vote_scope", using: :btree
create_table "widget_cards", force: :cascade do |t|
t.string "title"
t.text "description"
t.string "link_text"
t.string "link_url"
t.string "label"
t.boolean "header", default: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "widget_feeds", force: :cascade do |t|
t.string "kind"
t.integer "limit", default: 3
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_foreign_key "administrators", "users"
add_foreign_key "annotations", "legacy_legislations"
add_foreign_key "annotations", "users"

View File

@@ -125,3 +125,8 @@ Setting['map_zoom'] = 10
Setting['related_content_score_threshold'] = -0.3
Setting["feature.user.skip_verification"] = 'true'
Setting['feature.homepage.widgets.feeds.proposals'] = true
Setting['feature.homepage.widgets.feeds.debates'] = true
Setting['feature.homepage.widgets.feeds.processes'] = true

14
lib/tasks/homepage.rake Normal file
View File

@@ -0,0 +1,14 @@
namespace :homepage do
desc "Initialize feeds available in homepage"
task create_feeds: :environment do
%w(proposals debates processes).each do |kind|
Widget::Feed.create(kind: kind)
Setting['feature.homepage.widgets.feeds.proposals'] = true
Setting['feature.homepage.widgets.feeds.debates'] = true
Setting['feature.homepage.widgets.feeds.processes'] = true
end
end
end

View File

@@ -814,10 +814,19 @@ FactoryBot.define do
result_publication_date Date.current + 5.days
end
trait :published do
published true
end
trait :not_published do
published false
end
trait :open do
start_date 1.week.ago
end_date 1.week.from_now
end
end
factory :legislation_draft_version, class: 'Legislation::DraftVersion' do
@@ -962,4 +971,25 @@ LOREM_IPSUM
sequence(:body) { |n| "Body #{n}" }
end
factory :widget_card, class: 'Widget::Card' do
sequence(:title) { |n| "Title #{n}" }
sequence(:description) { |n| "Description #{n}" }
sequence(:link_text) { |n| "Link text #{n}" }
sequence(:link_url) { |n| "Link url #{n}" }
trait :header do
header true
sequence(:button_text) { |n| "Button text #{n}" }
sequence(:button_url) { |n| "Button url #{n}" }
sequence(:alignment) { |n| "background" }
end
after :create do |widget_card|
create(:image, imageable: widget_card)
end
end
factory :widget_feed, class: 'Widget::Feed' do
end
end

View File

@@ -0,0 +1,131 @@
require 'rails_helper'
feature 'Homepage' do
background do
admin = create(:administrator).user
login_as(admin)
Setting['feature.homepage.widgets.feeds.proposals'] = false
Setting['feature.homepage.widgets.feeds.debates'] = false
Setting['feature.homepage.widgets.feeds.processes'] = false
Setting['feature.user.recommendations'] = false
end
let!(:proposals_feed) { create(:widget_feed, kind: "proposals") }
let!(:debates_feed) { create(:widget_feed, kind: "debates") }
let!(:processes_feed) { create(:widget_feed, kind: "processes") }
let(:user_recommendations) { Setting.where(key: 'feature.user.recommendations').first }
let(:user) { create(:user) }
scenario "Header" do
end
context "Feeds" do
scenario "Proposals", :js do
5.times { create(:proposal) }
visit admin_homepage_path
within("#widget_feed_#{proposals_feed.id}") do
select '1', from: 'widget_feed_limit'
accept_confirm { click_button "Enable" }
end
visit root_path
expect(page).to have_content "Most active proposals"
expect(page).to have_css(".proposal", count: 1)
end
scenario "Debates", :js do
5.times { create(:debate) }
visit admin_homepage_path
within("#widget_feed_#{debates_feed.id}") do
select '2', from: 'widget_feed_limit'
accept_confirm { click_button "Enable" }
end
visit root_path
expect(page).to have_content "Most active debates"
expect(page).to have_css(".debate", count: 2)
end
scenario "Processes", :js do
5.times { create(:legislation_process) }
visit admin_homepage_path
within("#widget_feed_#{processes_feed.id}") do
select '3', from: 'widget_feed_limit'
accept_confirm { click_button "Enable" }
end
visit root_path
expect(page).to have_content "Open processes"
expect(page).to have_css(".legislation_process", count: 3)
end
xscenario "Deactivate"
end
scenario "Cards" do
card1 = create(:widget_card, label: "Card1 label",
title: "Card1 text",
description: "Card1 description",
link_text: "Link1 text",
link_url: "consul1.dev")
card2 = create(:widget_card, label: "Card2 label",
title: "Card2 text",
description: "Card2 description",
link_text: "Link2 text",
link_url: "consul2.dev")
visit root_path
expect(page).to have_css(".card", count: 2)
within("#widget_card_#{card1.id}") do
expect(page).to have_content("Card1 label")
expect(page).to have_content("Card1 text")
expect(page).to have_content("Card1 description")
expect(page).to have_content("Link1 text")
expect(page).to have_link(href: "consul1.dev")
expect(page).to have_css("img[alt='#{card1.image.title}']")
end
within("#widget_card_#{card2.id}") do
expect(page).to have_content("Card2 label")
expect(page).to have_content("Card2 text")
expect(page).to have_content("Card2 description")
expect(page).to have_content("Link2 text")
expect(page).to have_link(href: "consul2.dev")
expect(page).to have_css("img[alt='#{card2.image.title}']")
end
end
scenario "Recomendations" do
proposal1 = create(:proposal, tag_list: "Sport")
proposal2 = create(:proposal, tag_list: "Sport")
create(:follow, followable: proposal1, user: user)
visit admin_homepage_path
within("#setting_#{user_recommendations.id}") do
click_button "Enable"
end
expect(page).to have_content "Value updated"
login_as(user)
visit root_path
expect(page).to have_content("Recommendations that may interest you")
end
end

View File

@@ -0,0 +1,138 @@
require 'rails_helper'
feature 'Cards' do
background do
admin = create(:administrator).user
login_as(admin)
end
scenario "Create", :js do
visit admin_homepage_path
click_link "Create card"
fill_in "widget_card_label", with: "Card label"
fill_in "widget_card_title", with: "Card text"
fill_in "widget_card_description", with: "Card description"
fill_in "widget_card_link_text", with: "Link text"
fill_in "widget_card_link_url", with: "consul.dev"
attach_image_to_card
click_button "Create card"
expect(page).to have_content "Success"
expect(page).to have_css(".homepage-card", count: 1)
card = Widget::Card.last
within("#widget_card_#{card.id}") do
expect(page).to have_content "Card label"
expect(page).to have_content "Card text"
expect(page).to have_content "Card description"
expect(page).to have_content "Link text"
expect(page).to have_content "consul.dev"
expect(page).to have_link("Show image", href: card.image_url(:large))
end
end
scenario "Index" do
3.times { create(:widget_card) }
visit admin_homepage_path
expect(page).to have_css(".homepage-card", count: 3)
cards = Widget::Card.all
cards.each do |card|
expect(page).to have_content card.title
expect(page).to have_content card.description
expect(page).to have_content card.link_text
expect(page).to have_content card.link_url
expect(page).to have_link("Show image", href: card.image_url(:large))
end
end
scenario "Edit" do
card = create(:widget_card)
visit admin_homepage_path
within("#widget_card_#{card.id}") do
click_link "Edit"
end
fill_in "widget_card_label", with: "Card label updated"
fill_in "widget_card_title", with: "Card text updated"
fill_in "widget_card_description", with: "Card description updated"
fill_in "widget_card_link_text", with: "Link text updated"
fill_in "widget_card_link_url", with: "consul.dev updated"
click_button "Save card"
expect(page).to have_content "Updated"
expect(page).to have_css(".homepage-card", count: 1)
within("#widget_card_#{Widget::Card.last.id}") do
expect(page).to have_content "Card label updated"
expect(page).to have_content "Card text updated"
expect(page).to have_content "Card description updated"
expect(page).to have_content "Link text updated"
expect(page).to have_content "consul.dev updated"
end
end
scenario "Remove", :js do
card = create(:widget_card)
visit admin_homepage_path
within("#widget_card_#{card.id}") do
accept_confirm do
click_link "Delete"
end
end
expect(page).to have_content "Removed"
expect(page).to have_css(".homepage-card", count: 0)
end
context "Header Card" do
scenario "Create" do
visit admin_homepage_path
click_link "Create header"
fill_in "widget_card_label", with: "Header label"
fill_in "widget_card_title", with: "Header text"
fill_in "widget_card_description", with: "Header description"
fill_in "widget_card_link_text", with: "Link text"
fill_in "widget_card_link_url", with: "consul.dev"
click_button "Create header"
expect(page).to have_content "Success"
within("#header") do
expect(page).to have_css(".homepage-card", count: 1)
expect(page).to have_content "Header label"
expect(page).to have_content "Header text"
expect(page).to have_content "Header description"
expect(page).to have_content "Link text"
expect(page).to have_content "consul.dev"
end
within("#cards") do
expect(page).to have_css(".homepage-card", count: 0)
end
end
end
pending "add image expectactions"
def attach_image_to_card
click_link "Add image"
image_input = all(".image").last.find("input[type=file]", visible: false)
attach_file(
image_input[:id],
Rails.root.join('spec/fixtures/files/clippy.jpg'),
make_visible: true)
expect(page).to have_field('widget_card_image_attributes_title', with: "clippy.jpg")
end
end

View File

@@ -2,12 +2,12 @@ require 'rails_helper'
feature "Home" do
feature "For not logged users" do
context "For not logged users" do
scenario 'Welcome message' do
visit root_path
expect(page).to have_content "Love the city, and it will become a city you love"
expect(page).to have_content "CONSUL"
end
scenario 'Not display recommended section' do
@@ -20,7 +20,7 @@ feature "Home" do
end
feature "For signed in users" do
context "For signed in users" do
feature "Recommended" do
@@ -95,7 +95,10 @@ feature "Home" do
debate = create(:debate, tag_list: "Sport")
visit root_path
click_on debate.title
within('#section_recommended') do
click_on debate.title
end
expect(page).to have_current_path(debate_path(debate))
end
@@ -104,29 +107,6 @@ feature "Home" do
visit root_path
expect(page).not_to have_content "Recommendations that may interest you"
end
feature 'Carousel size' do
scenario 'Display debates centered when there are no proposals' do
debate = create(:debate, tag_list: "Sport")
visit root_path
expect(page).to have_selector('.medium-centered.large-centered')
end
scenario 'Correct display debates and proposals' do
proposal = create(:proposal, tag_list: "Sport")
debates = create(:debate, tag_list: "Sport")
visit root_path
expect(page).to have_selector('.debates.medium-offset-2.large-offset-2')
expect(page).not_to have_selector('.proposals.medium-offset-2.large-offset-2')
expect(page).not_to have_selector('.debates.end')
expect(page).to have_selector('.proposals.end')
expect(page).not_to have_selector('.medium-centered.large-centered')
end
end
end
end

View File

@@ -3,10 +3,16 @@ require 'rails_helper'
feature 'Localization' do
scenario 'Wrong locale' do
card = create(:widget_card, title: 'Bienvenido a CONSUL',
description: 'Software libre para la participación ciudadana.',
link_text: 'Más información',
link_url: 'http://consulproject.org/',
header: true)
visit root_path(locale: :es)
visit root_path(locale: :klingon)
expect(page).to have_text('La ciudad que quieres será la ciudad que quieras')
expect(page).to have_text('Bienvenido a CONSUL')
end
scenario 'Available locales appear in the locale switcher' do

View File

@@ -0,0 +1,37 @@
require 'rails_helper'
describe Widget::Card do
let(:card) { build(:widget_card) }
context "validations" do
it "is valid" do
expect(card).to be_valid
end
end
describe "#header" do
it "returns the header card" do
header = create(:widget_card, header: true)
card = create(:widget_card, header: false)
expect(Widget::Card.header).to eq([header])
end
end
describe "#body" do
it "returns cards for the homepage body" do
header = create(:widget_card, header: true)
card1 = create(:widget_card, header: false, title: "Card 1")
card2 = create(:widget_card, header: false, title: "Card 2")
card3 = create(:widget_card, header: false, title: "Card 3")
expect(Widget::Card.body).to eq([card1, card2, card3])
end
end
end

View File

@@ -0,0 +1,78 @@
require 'rails_helper'
describe Widget::Feed do
let(:feed) { build(:widget_feed) }
context "validations" do
it "is valid" do
expect(feed).to be_valid
end
end
context "kinds" do
describe "#proposals" do
it "returns the most active proposals" do
best_proposal = create(:proposal, title: 'Best proposal')
best_proposal.update_column(:hot_score, 10)
worst_proposal = create(:proposal, title: 'Worst proposal')
worst_proposal.update_column(:hot_score, 2)
even_worst_proposal = create(:proposal, title: 'Worst proposal')
even_worst_proposal.update_column(:hot_score, 1)
medium_proposal = create(:proposal, title: 'Medium proposal')
medium_proposal.update_column(:hot_score, 5)
feed = build(:widget_feed, kind: "proposals")
expect(feed.proposals).to eq([best_proposal, medium_proposal, worst_proposal])
end
end
describe "#debates" do
it "returns the most active debates" do
best_debate = create(:debate, title: 'Best debate')
best_debate.update_column(:hot_score, 10)
worst_debate = create(:debate, title: 'Worst debate')
worst_debate.update_column(:hot_score, 2)
even_worst_debate = create(:debate, title: 'Worst debate')
even_worst_debate.update_column(:hot_score, 1)
medium_debate = create(:debate, title: 'Medium debate')
medium_debate.update_column(:hot_score, 5)
feed = build(:widget_feed, kind: "debates")
expect(feed.debates).to eq([best_debate, medium_debate, worst_debate])
end
end
describe "#processes" do
it "returns open and published processes" do
open_process1 = create(:legislation_process, :open, :published, title: "Open process 1")
open_process2 = create(:legislation_process, :open, :published, title: "Open process 2")
open_process3 = create(:legislation_process, :open, :published, title: "Open process 3")
open_process4 = create(:legislation_process, :open, :published, title: "Open process 4")
open_process5 = create(:legislation_process, :open, :not_published, title: "Open process 5")
past_process = create(:legislation_process, :past, title: "Past process")
feed = build(:widget_feed, kind: "processes")
expect(feed.processes).to eq([open_process4, open_process3, open_process2])
end
end
end
end