• R/O
  • HTTP
  • SSH
  • HTTPS

提交

标签
No Tags

Frequently used words (click to add to your profile)

javac++androidlinuxc#windowsobjective-ccocoa誰得qtpythonphprubygameguibathyscaphec計画中(planning stage)翻訳omegatframeworktwitterdomtestvb.netdirectxゲームエンジンbtronarduinopreviewer

Commit MetaInfo

修订版233ca9d157561caa2264cd7f1815b8c5e10e4d51 (tree)
时间2017-08-07 12:27:41
作者HMML <hmml3939@gmai...>
CommiterHMML

Log Message

Implement theme editor.

更改概述

差异

--- a/.gitignore
+++ b/.gitignore
@@ -22,6 +22,8 @@
2222
2323 /node_modules
2424 /yarn-error.log
25+/public/packs
26+/public/system
2527
2628 /.vagrant
2729 /erd.*
@@ -36,5 +38,3 @@
3638 *.orig
3739 *.tmp
3840 *.old
39-/public/packs
40-/node_modules
--- a/Gemfile
+++ b/Gemfile
@@ -42,6 +42,8 @@ gem 'kaminari-i18n'
4242 gem 'font-awesome-rails'
4343 gem 'bootstrap', '~> 4.0.0.alpha6'
4444 gem 'popper_js'
45+gem 'tether-rails'
46+gem 'i18n-js'
4547 gem 'enum_help'
4648 gem 'jquery-turbolinks'
4749 gem 'yaml_db'
@@ -53,6 +55,8 @@ gem 'devise'
5355 gem 'omniauth-twitter'
5456 gem 'daemons'
5557 gem 'exception_notification', group: :production
58+gem 'paperclip'
59+gem 'rqrcode', github: 'sugi/rqrcode', branch: 'inline-svg'
5660
5761 group :development, :test do
5862 # Call 'byebug' anywhere in the code to stop execution and get a debugger console
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -6,6 +6,14 @@ GIT
66 rb-inotify (0.9.7)
77 ffi (>= 0.5.0)
88
9+GIT
10+ remote: git://github.com/sugi/rqrcode.git
11+ revision: 74692545b8335b5ab9499f24a7e8cfbe4a01892f
12+ branch: inline-svg
13+ specs:
14+ rqrcode (0.10.1)
15+ chunky_png (~> 1.0)
16+
917 GEM
1018 remote: https://rubygems.org/
1119 specs:
@@ -86,6 +94,10 @@ GEM
8694 xpath (~> 2.0)
8795 childprocess (0.7.1)
8896 ffi (~> 1.0, >= 1.0.11)
97+ chunky_png (1.3.8)
98+ climate_control (0.1.0)
99+ cocaine (0.5.8)
100+ climate_control (>= 0.0.3, < 1.0)
89101 coderay (1.1.1)
90102 coffee-rails (4.2.2)
91103 coffee-script (>= 2.2.0)
@@ -170,6 +182,8 @@ GEM
170182 nokogiri (>= 1.6.0)
171183 ruby_parser (~> 3.5)
172184 i18n (0.8.6)
185+ i18n-js (3.0.0.rc16)
186+ i18n (~> 0.6, >= 0.6.6)
173187 jbuilder (2.7.0)
174188 activesupport (>= 4.2.0)
175189 multi_json (>= 1.2)
@@ -209,6 +223,7 @@ GEM
209223 mime-types (3.1)
210224 mime-types-data (~> 3.2015)
211225 mime-types-data (3.2016.0521)
226+ mimemagic (0.3.2)
212227 mini_portile2 (2.2.0)
213228 minitest (5.10.3)
214229 multi_json (1.12.1)
@@ -235,6 +250,12 @@ GEM
235250 omniauth-oauth (~> 1.1)
236251 rack
237252 orm_adapter (0.5.0)
253+ paperclip (5.1.0)
254+ activemodel (>= 4.2.0)
255+ activesupport (>= 4.2.0)
256+ cocaine (~> 0.5.5)
257+ mime-types
258+ mimemagic (~> 0.3.0)
238259 popper_js (1.10.8)
239260 pry (0.10.4)
240261 coderay (~> 1.1.0)
@@ -359,6 +380,8 @@ GEM
359380 sshkit-backend-docker (0.1.2)
360381 sshkit (>= 1.9.0)
361382 temple (0.8.0)
383+ tether-rails (1.4.0)
384+ rails (>= 3.1)
362385 therubyracer (0.12.3)
363386 libv8 (~> 3.16.14.15)
364387 ref
@@ -424,6 +447,7 @@ DEPENDENCIES
424447 guard-bundler
425448 guard-rspec
426449 haml-rails
450+ i18n-js
427451 jbuilder (~> 2.5)
428452 jquery-rails
429453 jquery-turbolinks
@@ -433,6 +457,7 @@ DEPENDENCIES
433457 mysql2
434458 nokogiri
435459 omniauth-twitter
460+ paperclip
436461 popper_js
437462 pry
438463 pry-byebug
@@ -444,6 +469,7 @@ DEPENDENCIES
444469 rails-i18n
445470 rb-inotify!
446471 responders
472+ rqrcode!
447473 rspec-rails
448474 sass-rails (~> 5.0)
449475 sdoc (~> 0.4.0)
@@ -453,6 +479,7 @@ DEPENDENCIES
453479 spring-commands-rspec
454480 spring-watcher-listen (~> 2.0.0)
455481 sqlite3
482+ tether-rails
456483 therubyracer
457484 turbolinks (~> 5)
458485 uglifier (>= 1.3.0)
Binary files /dev/null and b/app/assets/images/loading.gif differ
Binary files /dev/null and b/app/assets/images/preview-base.png differ
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -12,8 +12,10 @@
1212 //
1313 //= require jquery3
1414 //= require popper
15+//= require tether
1516 //= require jquery.turbolinks
1617 //= require jquery_ujs
1718 //= require bootstrap-sprockets
1819 //= require turbolinks
20+//= require i18n/translations
1921 //= require_tree .
--- /dev/null
+++ b/app/assets/javascripts/common.coffee
@@ -0,0 +1 @@
1+# console.log 'Hello, this is common coffee.'
--- /dev/null
+++ b/app/assets/javascripts/drafts.coffee
@@ -0,0 +1,3 @@
1+# Place all the behaviors and hooks related to the matching controller here.
2+# All this logic will automatically be available in application.js.
3+# You can use CoffeeScript in this file: http://coffeescript.org/
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -1,4 +1,10 @@
11 @import "bootstrap";
22 @import "font-awesome";
3+@import "tether";
4+@import "tether-theme-basic";
5+
6+@import "awesome_print";
37 @import "common";
8+
49 @import "dist_signals";
10+@import "drafts";
--- /dev/null
+++ b/app/assets/stylesheets/awesome_print.scss
@@ -0,0 +1,8 @@
1+.debug_dump {
2+ kbd {
3+ background-color: transparent;
4+ padding-left: 0;
5+ padding-right: 0;
6+ color: black;
7+ }
8+}
--- /dev/null
+++ b/app/assets/stylesheets/drafts.scss
@@ -0,0 +1,192 @@
1+.draft-editor {
2+ height: auto;
3+ .draft-editor-table {
4+ table-layout: fixed;
5+ width: 100%;
6+ table-collapse: collapse;
7+ th { text-align: center; }
8+ th, td { border: 1px solid #ccc; position: relative; }
9+ td {
10+ background-color: rgba(0, 0, 0, 0.5);
11+ color: white;
12+ }
13+ .corner {
14+ background-image: linear-gradient(35deg, transparent 49.5%, #ccc 49.5%, #ccc 50.5%, transparent 50.5%, transparent);
15+ .first { text-align: right }
16+ .second { text-align: left; }
17+ .first, .second { padding: 0 1rem; }
18+ }
19+ }
20+ .dropzone {
21+ border-width: 4px;
22+ border-color: #ddd;
23+ border-style: dashed;
24+ border-radius: 5px;
25+ width: 100%;
26+ height: 100%;
27+ position: relative;
28+ }
29+ .dropzone-active {
30+ border-style: solid;
31+ }
32+ .dropzone-accept {
33+ border-color: #6c6;
34+ border-style: solid;
35+ }
36+ .dropzone-reject {
37+ border-color: #c66;
38+ border-style: solid;
39+ }
40+ .user-weather-icon {
41+ height: 120px;
42+ width: 120px;
43+ margin: auto;
44+ position: relative;
45+ vertical-align: middle;
46+ text-align: center;
47+ line-height: 120px;
48+ background-image: linear-gradient(to bottom, rgba(34, 34, 34, 0.733), rgba(34, 34, 34, 0.866) 50%);
49+ border-radius: 8px;
50+ img {
51+ display: block;
52+ position: absolute;
53+ margin: auto;
54+ top: 0;
55+ bottom: 0;
56+ left: 0;
57+ right: 0;
58+ max-width: 97%;
59+ max-height: 97%;
60+ }
61+ }
62+ .weather-icon-progress {
63+ margin: auto;
64+ width: 32px;
65+ height: 32px;
66+ position: absolute;
67+ top: 0;
68+ bottom: 0;
69+ left: 0;
70+ right: 0;
71+ background-image: image-url('loading.gif');
72+ }
73+ .day-frame-white .user-weather-icon {
74+ background-image: linear-gradient(to bottom, rgba(255, 255, 255, 0.66666), rgba(255, 255, 255, 0.933333) 50%);
75+ }
76+ .weather-icon-missing {
77+ text-align: center;
78+ line-height: 1;
79+ .fa {
80+ font-size: 120px;
81+ color: #bbb;
82+ }
83+ }
84+}
85+@media (max-width: map-get($grid-breakpoints, lg)) {
86+ .draft-editor {
87+ .user-weather-icon {
88+ min-height: 80px;
89+ }
90+ .weather-icon-missing {
91+ .fa {
92+ font-size: 80px;
93+ }
94+ }
95+ }
96+}
97+@media (max-width: map-get($grid-breakpoints, md)) {
98+ .draft-editor {
99+ .user-weather-icon {
100+ min-height: 36px;
101+ }
102+ .weather-icon-missing {
103+ .fa {
104+ font-size: 36px;
105+ }
106+ }
107+ }
108+}
109+
110+.preview-info {
111+ .qr-code {
112+ display: inline-block;
113+ width: 300px;
114+ height: 300px;
115+ position: relative;
116+ svg { width: 100%; height: 100%; }
117+ }
118+}
119+
120+.preview-base {
121+ background-image: image-url('preview-base.png');
122+ background-size: cover;
123+ background-position: top left;
124+ background-repeat: no-repeat;
125+ height: 640px;
126+ width: 360px;
127+ position: relative;
128+ .widget-frame {
129+ position: relative;
130+ }
131+ .widget-frame1 {
132+ top: 88px;
133+ left: 11px;
134+ width: 338px;
135+ height: 154px;
136+ }
137+ .widget-frame2 {
138+ top: 103px;
139+ left: 11px;
140+ width: 338px;
141+ height: 238px;
142+ }
143+ .day-frame {
144+ position: absolute;
145+ top: 20px;
146+ border-radius: 3px;
147+ width: 164px;
148+ background-image: linear-gradient(to bottom, rgba(34, 34, 34, 0.733), rgba(34, 34, 34, 0.866) 50%);
149+ img {
150+ display: block;
151+ position: absolute;
152+ margin: auto;
153+ top: 0;
154+ bottom: 0;
155+ left: 0;
156+ right: 0;
157+ max-width: 97%;
158+ max-height: 97%;
159+ }
160+ }
161+ .widget-frame1 .day-frame {
162+ height: 111px;
163+ }
164+ .widget-frame2 .day-frame {
165+ height: 195px;
166+ }
167+ .day-frame1 {
168+ left: 2px;
169+ }
170+ .day-frame2 {
171+ left: 171.5px;
172+ }
173+ .frame-white .day-frame {
174+ background-image: linear-gradient(to bottom, rgba(255, 255, 255, 0.66666), rgba(255, 255, 255, 0.933333) 50%);
175+ }
176+ .weather-text {
177+ text-align: center;
178+ bottom: 3px;
179+ position: absolute;
180+ width: 100%;
181+ span {
182+ color: white;
183+ font-size: 12px;
184+ text-shadow: 0.5px 0.5px 2px #000;
185+ display: inline-block;
186+ background-color: rgba(0, 0, 0, 0.2);
187+ border-radius: 8px;
188+ font-weight: bold;
189+ padding: 1px 6px;
190+ }
191+ }
192+}
--- /dev/null
+++ b/app/controllers/drafts_controller.rb
@@ -0,0 +1,53 @@
1+class DraftsController < ApplicationController
2+ before_action :set_draft, only: [:show, :edit, :update, :destroy]
3+
4+ respond_to :html
5+
6+ def index
7+ if !signed_in?
8+ @drafts = []
9+ elsif current_user.admin?
10+ @drafts = Draft.all.page(params[:page])
11+ else # normal user
12+ @drafts = Draft.where(user_id: current_user.id).page(params[:page])
13+ end
14+ respond_with(@drafts)
15+ end
16+
17+ def show
18+ respond_with(@draft, layout: 'preview')
19+ end
20+
21+ def new
22+ @draft = Draft.create user: current_user
23+ redirect_to edit_draft_path(@draft)
24+ end
25+
26+ def edit
27+ end
28+
29+ def create
30+ @draft = Draft.new(draft_params)
31+ @draft.save
32+ respond_with(@draft)
33+ end
34+
35+ def update
36+ @draft.update(draft_params)
37+ respond_with(@draft)
38+ end
39+
40+ def destroy
41+ @draft.destroy
42+ respond_with(@draft)
43+ end
44+
45+ private
46+ def set_draft
47+ @draft = Draft.find_by!(key: params[:id])
48+ end
49+
50+ def draft_params
51+ params.require(:draft).permit(:title, :url, :day_frame, *Draft::ICONS.map{|i| "wi_#{i}"})
52+ end
53+end
--- /dev/null
+++ b/app/helpers/drafts_helper.rb
@@ -0,0 +1,2 @@
1+module DraftsHelper
2+end
--- /dev/null
+++ b/app/javascript/components/draft_editor.jsx
@@ -0,0 +1,192 @@
1+import React from 'react';
2+import PropTypes from 'prop-types';
3+import { Button } from 'reactstrap';
4+import Dropzone from 'react-dropzone'
5+
6+
7+class DraftImageDropper extends React.Component {
8+ static propTypes = {
9+ url: PropTypes.string.isRequired,
10+ wsym: PropTypes.string.isRequired,
11+ curImage: PropTypes.string,
12+ updateImage: PropTypes.func.isRequired,
13+ }
14+
15+ constructor (props) {
16+ super(props);
17+ this.state = {
18+ //draft: new DraftStore(props.draft),
19+ uploading: false,
20+ };
21+ }
22+
23+ render() {
24+ let on_drop = (okFiles, ngFiles) => {
25+ let remove_preview = (file) => window.URL.revokeObjectURL(file.preview);
26+ let wsym = this.props.wsym;
27+
28+ if (ngFiles)
29+ ngFiles.forEach(remove_preview);
30+ if (!okFiles)
31+ return;
32+ okFiles.forEach(remove_preview);
33+
34+ let data = new FormData()
35+ data.set(`draft[wi_${wsym}]`, okFiles[0]);
36+
37+ this.setState({uploading: true});
38+ jQuery.ajax({
39+ url: this.props.url,
40+ method: 'PATCH',
41+ contentType: false,
42+ processData: false,
43+ data: data,
44+ success: (data, status, xhr) => {
45+ this.props.updateImage(wsym, data[`wi_${wsym}_url`]);
46+ this.setState({uploading: false});
47+ },
48+ error: () => alert(I18n.t('upload_err')),
49+ });
50+ }
51+
52+ if (this.state.uploading) {
53+ return(
54+ <div className="theme-image">
55+ <div className="weather-icon-progress" />
56+ </div>
57+ );
58+ }
59+
60+ var content = <div className="weather-icon-missing">
61+ <span className="fa fa-plus-circle" />
62+ </div>;
63+ if (this.props.curImage) {
64+ content = <div className="user-weather-icon">
65+ <img src={this.props.curImage} />
66+ </div>;
67+ }
68+
69+ return (
70+ <div className="theme-image">
71+ <Dropzone accept="image/*" minSize={128}
72+ className="dropzone"
73+ activeClassName="dropzone dropzone-active"
74+ acceptClassName="dropzone dropzone-accept"
75+ rejectClassName="dropzone dropzone-reject"
76+ onDrop={on_drop}
77+ >
78+ {content}
79+ <div className="clearfix"/>
80+ </Dropzone>
81+ </div>
82+ );
83+ }
84+}
85+
86+class DraftEditor extends React.Component {
87+ static propTypes = {
88+ draft: PropTypes.object.isRequired,
89+ wsyms: PropTypes.array.isRequired,
90+ wsym_rejects: PropTypes.array,
91+ }
92+
93+ constructor (props) {
94+ super(props);
95+ this.state = {
96+ //draft: new DraftStore(props.draft),
97+ draft: Object.assign({}, props.draft),
98+ };
99+ }
100+
101+ updateImage (sym, imageUrl) {
102+ let d = Object.assign({}, this.state.draft)
103+ d[`wi_${sym}_url`] = imageUrl;
104+ this.setState({draft: d})
105+ }
106+
107+ updateFrame (value) {
108+ let d = Object.assign({}, this.state.draft)
109+ d["day_frame"] = value;
110+ this.setState({draft: d})
111+ jQuery.ajax({
112+ url: d.path,
113+ method: 'PATCH',
114+ data: {'draft[day_frame]': value}
115+ });
116+ }
117+
118+ render() {
119+ let d = this.state.draft;
120+
121+ let header_cols = []
122+ let wsyms = this.props.wsyms
123+ wsyms.forEach((ws) => {
124+ header_cols.push(<th key={`w-h-${ws}`}>
125+ {I18n.t('weather_symbols.'+ws)}
126+ </th>);
127+ });
128+ let body_rows = [
129+ <tr key="w-r-none">
130+ <th key="w-ch-none">{I18n.t('none')}</th>
131+ {wsyms.map((ws) =>
132+ <td key={`w-c-none-${ws}`}>
133+ <DraftImageDropper key={`i-${ws}`} wsym={ws} url={d.path}
134+ curImage={d[`wi_${ws}_url`]} updateImage={this.updateImage.bind(this)} />
135+ </td>)}
136+ </tr>
137+ ];
138+ wsyms.forEach((ws1) => {
139+ let cols = [<th key={`w-ch-${ws1}`}>{I18n.t(`weather_symbols.${ws1}`)}</th>];
140+ wsyms.forEach((ws2) => {
141+ var content = null;
142+ if (ws1 != ws2 && this.props.wsym_rejects && this.props.wsym_rejects.indexOf(ws1+ws2) === -1) {
143+ content = <DraftImageDropper key={'i-'+ws1+ws2} wsym={ws1+ws2} url={d.path}
144+ curImage={d[`wi_${ws1}${ws2}_url`]} updateImage={this.updateImage.bind(this)} />
145+ }
146+ cols.push(
147+ <td key={`w-c-${ws1}-${ws2}`}>
148+ {content}
149+ </td>);
150+ });
151+ body_rows.push(<tr key={`w-r-${ws1}`}>{cols}</tr>);
152+ });
153+
154+ return(
155+ <div className="draft-editor">
156+ <div>
157+ <b>{I18n.t('day_frame_type')}</b>:
158+ <div className="form-check form-check-inline">
159+ <label className="form-check-label">
160+ <input className="form-check-input" type="radio" name="frame"
161+ checked={d.day_frame == null || d.day_frame == "black"}
162+ onClick={() => this.updateFrame('black')}/>
163+ {I18n.t('day_frames.black')}
164+ </label>
165+ </div>
166+ <div className="form-check form-check-inline">
167+ <label className="form-check-label">
168+ <input className="form-check-input" type="radio" name="frame"
169+ checked={d.day_frame == "white"}
170+ onClick={(e) => this.updateFrame('white')}/>
171+ {I18n.t('day_frames.white')}
172+ </label>
173+ </div>
174+ </div>
175+ <table className={`draft-editor-table day-frame-${d.day_frame}`}>
176+ <thead>
177+ <tr>
178+ <th className="corner">
179+ <div className="first">{I18n.t('editor_table_header.first_weather')}</div>
180+ <div className="second">{I18n.t('editor_table_header.second_weather')}</div>
181+ </th>
182+ {header_cols}
183+ </tr>
184+ </thead>
185+ <tbody>{body_rows}</tbody>
186+ </table>
187+ </div>
188+ );
189+ }
190+}
191+
192+export default DraftEditor;
--- /dev/null
+++ b/app/javascript/components/hello.jsx
@@ -0,0 +1,15 @@
1+import React from 'react';
2+import { Button } from 'reactstrap';
3+
4+class Hello extends React.Component {
5+ render() {
6+ return(
7+ <div>
8+ <Button color="danger">ReactStrap!</Button>
9+ Hello, {this.props.name}!
10+ </div>
11+ );
12+ }
13+}
14+
15+export default Hello;
--- /dev/null
+++ b/app/javascript/packs/main.js
@@ -0,0 +1,11 @@
1+import WebpackerReact from 'webpacker-react';
2+import Hello from '../components/hello';
3+import DraftEditor from '../components/draft_editor';
4+
5+if (typeof(window.Turbolinks) != "undefined")
6+ Turbolinks.start();
7+
8+WebpackerReact.setup({Hello, DraftEditor});
9+
10+class H2 {
11+}
--- /dev/null
+++ b/app/models/draft.rb
@@ -0,0 +1,78 @@
1+class Draft < ApplicationRecord
2+ EXPIRES_IN = 3.days
3+ KEY_MAX_LEN = 12
4+
5+ icons = [
6+ Forecast::WEATHER_SYM_CLEAR_N,
7+ Forecast::WEATHER_SYM_CLEAR_D,
8+ Forecast::WEATHER_SYM_RAIN,
9+ Forecast::WEATHER_SYM_SNOW,
10+ Forecast::WEATHER_SYM_CLOUD,
11+ Forecast::WEATHER_SYM_THUNDER,
12+ ]
13+ icons += icons.map {|i1| icons.map {|i2| i1 == i2 ? nil : i1+i2 }}.flatten.compact
14+ icons -= [
15+ Forecast::WEATHER_SYM_CLEAR_N+Forecast::WEATHER_SYM_CLEAR_D,
16+ Forecast::WEATHER_SYM_CLEAR_D+Forecast::WEATHER_SYM_CLEAR_N,
17+ ]
18+ ICONS = icons.freeze
19+
20+ FILENAME_MAP = {
21+ Forecast::WEATHER_SYM_CLEAR_N => 'ClearN',
22+ Forecast::WEATHER_SYM_CLEAR_D => 'ClearD',
23+ Forecast::WEATHER_SYM_RAIN => 'Rain',
24+ Forecast::WEATHER_SYM_SNOW => 'Snow',
25+ Forecast::WEATHER_SYM_CLOUD => 'Cloud',
26+ Forecast::WEATHER_SYM_THUNDER => 'Thunder',
27+ }
28+ FILENAME_MAP.freeze
29+
30+ enum day_frame: {white: '@builtin/white', black: '@builtin/black'}, _prefix: 'day_frame'
31+ belongs_to :user, required: false
32+ before_save :renew_expire
33+ after_initialize :set_key
34+ validates :title, :url, length: { maximum: 255 }
35+ validates :url, format: { with: %r{\Ahttps?://[a-z0-9-]+(\.[a-z0-9-]+){1,}/}i }, allow_blank: true
36+ validates :key, uniqueness: true
37+
38+ icon_styles = {}
39+ [500, 250, 120, 64].each do |size|
40+ icon_styles["webp#{size}"] = {geometry: "#{size}x#{size}>", format: "webp", convert_options: "-quality 75"}
41+ icon_styles["png#{size}"] = {geometry: "#{size}x#{size}>", format: "png"}
42+ end
43+
44+ ICONS.each do |i|
45+ has_attached_file "wi_#{i}", styles: icon_styles.symbolize_keys
46+ validates_attachment "wi_#{i}",
47+ content_type: {content_type: /\Aimage\/.*\z/},
48+ size: { less_than: 20.megabyte }
49+ end
50+
51+ def renew_expire
52+ self.expires_at = Time.now + EXPIRES_IN
53+ end
54+
55+ def set_key(force = false)
56+ return true if key.present? && !force
57+ loop do
58+ self.key = (SecureRandom.random_number(('z'*KEY_MAX_LEN).to_i(36)) + 1).to_s(36).scan(/.{1,4}/).join("-")
59+ check_cond = {key: self.key}
60+ persisted? and check_cond[:id] = self.id
61+ break unless Draft.where(check_cond).exists?
62+ end
63+ end
64+
65+ def to_param
66+ key
67+ end
68+
69+ def as_json(opts = {})
70+ ret = super(opts)
71+ ret["path"] = Rails.application.routes.url_helpers.draft_path(self, format: :json)
72+ ICONS.each do |i|
73+ public_send("wi_#{i}?") or next
74+ ret["wi_#{i}_url"] = public_send("wi_#{i}").url(opts[:weather_image_style] || :png120)
75+ end
76+ ret
77+ end
78+end
--- a/app/models/forecast.rb
+++ b/app/models/forecast.rb
@@ -3,10 +3,10 @@ class Forecast < ApplicationRecord
33
44 WEATHER_SYM_CLEAR_D = "d";
55 WEATHER_SYM_CLEAR_N = "n";
6- WEATHER_SYM_THUNDER = "t";
7- WEATHER_SYM_SNOW = "s";
8- WEATHER_SYM_RAIN = "r";
96 WEATHER_SYM_CLOUD = "c";
7+ WEATHER_SYM_RAIN = "r";
8+ WEATHER_SYM_SNOW = "s";
9+ WEATHER_SYM_THUNDER = "t";
1010
1111 JMA_LABEL_TO_SYMBOL = {
1212 }
--- /dev/null
+++ b/app/views/drafts/_draft.json.jbuilder
@@ -0,0 +1,2 @@
1+json.extract! draft, :id, :key, :title, :url, :day_frame, :expires_at, :user_id, :created_at, :updated_at
2+json.url draft_url(draft, format: :json)
--- /dev/null
+++ b/app/views/drafts/_form.html.haml
@@ -0,0 +1,8 @@
1+= simple_form_for(@draft, remote: true) do |f|
2+ .form-inputs
3+ = f.input :key
4+ = f.input :title
5+ = f.input :url
6+
7+ .form-actions
8+ = f.button :submit
--- /dev/null
+++ b/app/views/drafts/edit.html.haml
@@ -0,0 +1,50 @@
1+- content_for(:title) { t('.title') }
2+
3+.actions.pull-right
4+ .btn.btn-secondary.btn-lg{data: {toggle: 'modal', target: '.preview-info-modal'}}
5+ = fa_icon :mobile
6+ = t 'device_preview'
7+ .btn.btn-primary.btn-lg{data: {toggle: 'modal', target: '.produce-modal'}}
8+ = fa_icon 'chevron-circle-right'
9+ = t 'finish_edit'
10+
11+%h1= t '.title'
12+
13+.clearfix
14+
15+- wsyms = Forecast.constants.grep(/^WEATHER_SYM_/).map {|n| Forecast.const_get(n)}
16+
17+= react_component 'DraftEditor', draft: @draft, wsyms: wsyms,
18+ wsym_rejects: [Forecast::WEATHER_SYM_CLEAR_D + Forecast::WEATHER_SYM_CLEAR_N, Forecast::WEATHER_SYM_CLEAR_N + Forecast::WEATHER_SYM_CLEAR_D]
19+
20+.preview-info-modal.modal.fade
21+ .modal-dialog.modal-lg
22+ .modal-content
23+ .modal-header
24+ %h5.modal-title= t 'to_display_preview_title'
25+ %button.close{type: 'button', data: {dismiss: 'modal'}}
26+ %span &times;
27+ .modal-body
28+ .preview-info
29+ .qr-code.pull-right
30+ = raw RQRCode::QRCode.new(draft_url(@draft)).as_svg(xml_decleration: false)
31+ %p= t 'to_display_preview_desc'
32+ %br
33+ %b= draft_url(@draft)
34+
35+.produce-modal.modal.fade
36+ .modal-dialog.modal-lg
37+ .modal-content
38+ .modal-header
39+ %h5.modal-title= t 'to_produce_title'
40+ %button.close{type: 'button', data: {dismiss: 'modal'}}
41+ %span &times;
42+ .modal-body
43+ .produce
44+ %p= t 'to_produce_desc'
45+ = render 'form'
46+
47+:coffee
48+ jQuery('.edit_draft').on 'ajax:success', (xhr, data, status) ->
49+ jQuery(this).html(jQuery(data).find('.edit_draft').html())
50+
--- /dev/null
+++ b/app/views/drafts/index.html.haml
@@ -0,0 +1,31 @@
1+%h1 Listing drafts
2+
3+%table
4+ %thead
5+ %tr
6+ %th Key
7+ %th Title
8+ %th Url
9+ %th Day frame
10+ %th Expires at
11+ %th User
12+ %th
13+ %th
14+ %th
15+
16+ %tbody
17+ - @drafts.each do |draft|
18+ %tr
19+ %td= draft.key
20+ %td= draft.title
21+ %td= draft.url
22+ %td= draft.day_frame
23+ %td= draft.expires_at
24+ %td= draft.user
25+ %td= link_to 'Show', draft
26+ %td= link_to 'Edit', edit_draft_path(draft)
27+ %td= link_to 'Destroy', draft, method: :delete, data: { confirm: 'Are you sure?' }
28+
29+%br
30+
31+= link_to 'New Draft', new_draft_path
--- /dev/null
+++ b/app/views/drafts/index.json.jbuilder
@@ -0,0 +1 @@
1+json.array! @drafts, partial: 'drafts/draft', as: :draft
--- /dev/null
+++ b/app/views/drafts/new.html.haml
@@ -0,0 +1,5 @@
1+%h1 New draft
2+
3+= render 'form'
4+
5+= link_to 'Back', drafts_path
--- /dev/null
+++ b/app/views/drafts/show.html.haml
@@ -0,0 +1,43 @@
1+- content_for(:title) { t('.title')}
2+
3+.alert.alert-info
4+ .btn.btn-primary.pull-right.btn-sm{onClick: 'location.reload()'}
5+ = fa_icon(:refresh)
6+ = t 'reload'
7+ = t('preview_desc')
8+ .clearfix
9+
10+:ruby
11+ imgs = {}
12+ @draft.as_json(weather_image_style: request.user_agent.to_s =~ /chrome/i ? :webp500 : :png500).each do |key, val|
13+ key =~ /^wi_(.*)_url/ or next
14+ imgs[$1] = val
15+ end
16+ img1_key = imgs.keys[rand(imgs.keys.length)]
17+ img2_key = imgs.keys[rand(imgs.keys.length)]
18+ img3_key = imgs.keys[rand(imgs.keys.length)]
19+ img4_key = imgs.keys[rand(imgs.keys.length)]
20+
21+- if imgs.empty?
22+ .alert.alert-danger= t 'draft_has_no_image'
23+- else
24+ .preview-base
25+ .widget-frame.widget-frame1{class: "frame-#{@draft.day_frame}"}
26+ .day-frame.day-frame1
27+ = image_tag imgs[img1_key]
28+ .weather-text
29+ %span= t("wheater_label_samples.#{img1_key}")
30+ .day-frame.day-frame2
31+ = image_tag imgs[img2_key]
32+ .weather-text
33+ %span= t("wheater_label_samples.#{img2_key}")
34+ .widget-frame.widget-frame2{class: "frame-#{@draft.day_frame}"}
35+ .day-frame.day-frame1
36+ = image_tag imgs[img3_key]
37+ .weather-text
38+ %span= t("wheater_label_samples.#{img3_key}")
39+ .day-frame.day-frame2
40+ = image_tag imgs[img4_key]
41+ .weather-text
42+ %span= t("wheater_label_samples.#{img4_key}")
43+
--- /dev/null
+++ b/app/views/drafts/show.json.jbuilder
@@ -0,0 +1 @@
1+json.partial! "drafts/draft", draft: @draft
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -7,6 +7,7 @@
77 %meta{content: content_for?(:description) ? yield(:description) : t('description'), :name => "description"}
88 = stylesheet_link_tag :application, :media => "all"
99 = javascript_include_tag :application
10+ = javascript_pack_tag 'main'
1011 = csrf_meta_tags
1112 = yield(:head)
1213 %body
--- /dev/null
+++ b/app/views/layouts/preview.html.haml
@@ -0,0 +1,14 @@
1+!!!
2+%html#preview{lang: I18n.locale}
3+ %head
4+ %meta{name: "viewport", content: "width=360, user-scalable=no"}
5+ - page_title = content_for?(:title) ? yield(:title) : t("#{controller_name}.#{action_name}.title", default: "")
6+ %title= page_title.present? ? "#{page_title} - #{t('site_title')}" : t('site_title')
7+ %meta{content: content_for?(:description) ? yield(:description) : t('description'), :name => "description"}
8+ = stylesheet_link_tag :application, :media => "all"
9+ = javascript_include_tag :application
10+ = javascript_pack_tag 'main'
11+ = csrf_meta_tags
12+ = yield(:head)
13+ %body#preview
14+ = yield
--- a/config/application.rb
+++ b/config/application.rb
@@ -29,9 +29,9 @@ module MmwPsh
2929 g.fixture_replacement :factory_girl, dir: "spec/factories"
3030 end
3131
32- #config.i18n.default_locale = :en
33- #config.i18n.available_locales = [:en, :ja]
34- #config.i18n.enforce_available_locales = true
32+ config.i18n.default_locale = :en
33+ config.i18n.available_locales = [:en, :ja]
34+ config.i18n.enforce_available_locales = true
3535 config.encoding = "utf-8"
3636
3737 config.hub_secret_default = ''
--- a/config/deploy/docker.rb
+++ b/config/deploy/docker.rb
@@ -1,6 +1,6 @@
11 set :sshkit_backend, SSHKit::Backend::Docker
22 set :stage, :production
3-set :branch, 'master'
3+set :branch, 'production'
44 set :deploy_to, '/app'
55 set :repo_url, '/src'
66 fetch(:default_env).merge!(rails_env: :production,
--- a/config/initializers/simple_form_bootstrap.rb
+++ b/config/initializers/simple_form_bootstrap.rb
@@ -4,7 +4,7 @@ SimpleForm.setup do |config|
44 config.button_class = 'btn btn-primary'
55 config.boolean_label_class = nil
66
7- config.wrappers :vertical_form, tag: 'div', class: 'form-group', error_class: 'has-error' do |b|
7+ config.wrappers :vertical_form, tag: 'div', class: 'form-group', error_class: 'has-danger' do |b|
88 b.use :html5
99 b.use :placeholder
1010 b.optional :maxlength
@@ -12,27 +12,27 @@ SimpleForm.setup do |config|
1212 b.optional :pattern
1313 b.optional :min_max
1414 b.optional :readonly
15- b.use :label, class: 'control-label'
15+ b.use :label, class: 'form-control-label'
1616
1717 b.use :input, class: 'form-control'
18- b.use :error, wrap_with: { tag: 'span', class: 'help-block' }
19- b.use :hint, wrap_with: { tag: 'p', class: 'help-block' }
18+ b.use :error, wrap_with: { tag: 'span', class: 'form-control-feedback' }
19+ b.use :hint, wrap_with: { tag: 'p', class: 'form-text text-muted' }
2020 end
2121
22- config.wrappers :vertical_file_input, tag: 'div', class: 'form-group', error_class: 'has-error' do |b|
22+ config.wrappers :vertical_file_input, tag: 'div', class: 'form-group', error_class: 'has-danger' do |b|
2323 b.use :html5
2424 b.use :placeholder
2525 b.optional :maxlength
2626 b.optional :minlength
2727 b.optional :readonly
28- b.use :label, class: 'control-label'
28+ b.use :label, class: 'form-control-label'
2929
3030 b.use :input
31- b.use :error, wrap_with: { tag: 'span', class: 'help-block' }
32- b.use :hint, wrap_with: { tag: 'p', class: 'help-block' }
31+ b.use :error, wrap_with: { tag: 'span', class: 'form-control-feedback' }
32+ b.use :hint, wrap_with: { tag: 'p', class: 'form-text text-muted' }
3333 end
3434
35- config.wrappers :vertical_boolean, tag: 'div', class: 'form-group', error_class: 'has-error' do |b|
35+ config.wrappers :vertical_boolean, tag: 'div', class: 'form-group', error_class: 'has-danger' do |b|
3636 b.use :html5
3737 b.optional :readonly
3838
@@ -40,20 +40,20 @@ SimpleForm.setup do |config|
4040 ba.use :label_input
4141 end
4242
43- b.use :error, wrap_with: { tag: 'span', class: 'help-block' }
44- b.use :hint, wrap_with: { tag: 'p', class: 'help-block' }
43+ b.use :error, wrap_with: { tag: 'span', class: 'form-control-feedback' }
44+ b.use :hint, wrap_with: { tag: 'p', class: 'form-text text-muted' }
4545 end
4646
47- config.wrappers :vertical_radio_and_checkboxes, tag: 'div', class: 'form-group', error_class: 'has-error' do |b|
47+ config.wrappers :vertical_radio_and_checkboxes, tag: 'div', class: 'form-group', error_class: 'has-danger' do |b|
4848 b.use :html5
4949 b.optional :readonly
50- b.use :label, class: 'control-label'
50+ b.use :label, class: 'form-control-label'
5151 b.use :input
52- b.use :error, wrap_with: { tag: 'span', class: 'help-block' }
53- b.use :hint, wrap_with: { tag: 'p', class: 'help-block' }
52+ b.use :error, wrap_with: { tag: 'span', class: 'form-control-feedback' }
53+ b.use :hint, wrap_with: { tag: 'p', class: 'form-text text-muted' }
5454 end
5555
56- config.wrappers :horizontal_form, tag: 'div', class: 'form-group', error_class: 'has-error' do |b|
56+ config.wrappers :horizontal_form, tag: 'div', class: 'form-group row', error_class: 'has-danger' do |b|
5757 b.use :html5
5858 b.use :placeholder
5959 b.optional :maxlength
@@ -61,31 +61,31 @@ SimpleForm.setup do |config|
6161 b.optional :pattern
6262 b.optional :min_max
6363 b.optional :readonly
64- b.use :label, class: 'col-3 control-label'
64+ b.use :label, class: 'col-3 form-control-label'
6565
6666 b.wrapper tag: 'div', class: 'col-9' do |ba|
6767 ba.use :input, class: 'form-control'
68- ba.use :error, wrap_with: { tag: 'span', class: 'help-block' }
69- ba.use :hint, wrap_with: { tag: 'p', class: 'help-block' }
68+ ba.use :error, wrap_with: { tag: 'span', class: 'form-control-feedback' }
69+ ba.use :hint, wrap_with: { tag: 'p', class: 'form-text text-muted' }
7070 end
7171 end
7272
73- config.wrappers :horizontal_file_input, tag: 'div', class: 'form-group', error_class: 'has-error' do |b|
73+ config.wrappers :horizontal_file_input, tag: 'div', class: 'form-group row', error_class: 'has-danger' do |b|
7474 b.use :html5
7575 b.use :placeholder
7676 b.optional :maxlength
7777 b.optional :minlength
7878 b.optional :readonly
79- b.use :label, class: 'col-3 control-label'
79+ b.use :label, class: 'col-3 form-control-label'
8080
8181 b.wrapper tag: 'div', class: 'col-9' do |ba|
8282 ba.use :input
83- ba.use :error, wrap_with: { tag: 'span', class: 'help-block' }
84- ba.use :hint, wrap_with: { tag: 'p', class: 'help-block' }
83+ ba.use :error, wrap_with: { tag: 'span', class: 'form-control-feedback' }
84+ ba.use :hint, wrap_with: { tag: 'p', class: 'form-text text-muted' }
8585 end
8686 end
8787
88- config.wrappers :horizontal_boolean, tag: 'div', class: 'form-group', error_class: 'has-error' do |b|
88+ config.wrappers :horizontal_boolean, tag: 'div', class: 'form-group row', error_class: 'has-danger' do |b|
8989 b.use :html5
9090 b.optional :readonly
9191
@@ -94,25 +94,25 @@ SimpleForm.setup do |config|
9494 ba.use :label_input
9595 end
9696
97- wr.use :error, wrap_with: { tag: 'span', class: 'help-block' }
98- wr.use :hint, wrap_with: { tag: 'p', class: 'help-block' }
97+ wr.use :error, wrap_with: { tag: 'span', class: 'form-control-feedback' }
98+ wr.use :hint, wrap_with: { tag: 'p', class: 'form-text text-muted' }
9999 end
100100 end
101101
102- config.wrappers :horizontal_radio_and_checkboxes, tag: 'div', class: 'form-group', error_class: 'has-error' do |b|
102+ config.wrappers :horizontal_radio_and_checkboxes, tag: 'div', class: 'form-group row', error_class: 'has-danger' do |b|
103103 b.use :html5
104104 b.optional :readonly
105105
106- b.use :label, class: 'col-3 control-label'
106+ b.use :label, class: 'col-3 form-control-label'
107107
108108 b.wrapper tag: 'div', class: 'col-9' do |ba|
109109 ba.use :input
110- ba.use :error, wrap_with: { tag: 'span', class: 'help-block' }
111- ba.use :hint, wrap_with: { tag: 'p', class: 'help-block' }
110+ ba.use :error, wrap_with: { tag: 'span', class: 'form-control-feedback' }
111+ ba.use :hint, wrap_with: { tag: 'p', class: 'form-text text-muted' }
112112 end
113113 end
114114
115- config.wrappers :inline_form, tag: 'div', class: 'form-group', error_class: 'has-error' do |b|
115+ config.wrappers :inline_form, tag: 'div', class: 'form-group', error_class: 'has-danger' do |b|
116116 b.use :html5
117117 b.use :placeholder
118118 b.optional :maxlength
@@ -123,32 +123,47 @@ SimpleForm.setup do |config|
123123 b.use :label, class: 'sr-only'
124124
125125 b.use :input, class: 'form-control'
126- b.use :error, wrap_with: { tag: 'span', class: 'help-block' }
127- b.use :hint, wrap_with: { tag: 'p', class: 'help-block' }
126+ b.use :error, wrap_with: { tag: 'span', class: 'form-control-feedback' }
127+ b.use :hint, wrap_with: { tag: 'p', class: 'form-text text-muted' }
128128 end
129129
130- config.wrappers :multi_select, tag: 'div', class: 'form-group', error_class: 'has-error' do |b|
130+ config.wrappers :multi_select, tag: 'div', class: 'form-group', error_class: 'has-danger' do |b|
131131 b.use :html5
132132 b.optional :readonly
133- b.use :label, class: 'control-label'
133+ b.use :label, class: 'form-control-label'
134134 b.wrapper tag: 'div', class: 'form-inline' do |ba|
135135 ba.use :input, class: 'form-control'
136- ba.use :error, wrap_with: { tag: 'span', class: 'help-block' }
137- ba.use :hint, wrap_with: { tag: 'p', class: 'help-block' }
136+ ba.use :error, wrap_with: { tag: 'span', class: 'form-control-feedback' }
137+ ba.use :hint, wrap_with: { tag: 'p', class: 'form-text text-muted' }
138138 end
139139 end
140+
141+ config.wrappers :horizontal_multi_select, tag: 'div', class: 'form-group row', error_class: 'has-danger' do |b|
142+ b.use :html5
143+ b.optional :readonly
144+ b.use :label, class: 'col-3 form-control-label'
145+
146+ b.wrapper tag: 'div', class: 'col-9' do |ba|
147+ ba.wrapper tag: 'div', class: 'form-inline' do |bb|
148+ bb.use :input, class: 'form-control'
149+ bb.use :error, wrap_with: { tag: 'span', class: 'form-control-feedback' }
150+ bb.use :hint, wrap_with: { tag: 'p', class: 'form-text text-muted' }
151+ end
152+ end
153+ end
154+
140155 # Wrappers for forms and inputs using the Bootstrap toolkit.
141156 # Check the Bootstrap docs (http://getbootstrap.com)
142157 # to learn about the different styles for forms and inputs,
143158 # buttons and other elements.
144- config.default_wrapper = :vertical_form
159+ config.default_wrapper = :horizontal_form
145160 config.wrapper_mappings = {
146161 check_boxes: :vertical_radio_and_checkboxes,
147162 radio_buttons: :vertical_radio_and_checkboxes,
148163 file: :vertical_file_input,
149164 boolean: :vertical_boolean,
150- datetime: :multi_select,
151- date: :multi_select,
152- time: :multi_select
165+ datetime: :horizontal_multi_select,
166+ date: :horizontal_multi_select,
167+ time: :horizontal_multi_select
153168 }
154169 end
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1,5 +1,76 @@
11 en:
2+ activerecord:
3+ models:
4+ attributes:
5+
6+ drafts:
7+ show:
8+ title: Theme preview
9+ edit:
10+ title: Theme editor
11+
212 site_title: "MikuMikuWeather"
313 login_with_: "Login with %{provider}"
414 providers:
15+ twitter: Twitter
16+ google: Google
17+ facebook: Facebook
518 line: LINE
19+ upload_err: "[Upload failed] Error on server. Please contact to administrator if you try again after some while."
20+ weather_symbols:
21+ d: Clear(Day)
22+ n: Clear(Night)
23+ c: Clouds
24+ r: Rain
25+ s: Snow
26+ t: Thunderstorm
27+ wheater_label_samples:
28+ d: Clear
29+ n: Clear
30+ c: Clouds
31+ r: Rain
32+ s: Snow
33+ t: Thunderstorm
34+ nr: Clear - Rain
35+ ns: Clear - Snow
36+ nc: Clear - Clouds
37+ nt: Clear - Thunderstorm
38+ dr: Clear, Rain
39+ ds: Clear, Snow
40+ dc: Clear, Clouds
41+ dt: Clear, Thunderstorm
42+ rn: Rain, Clear
43+ rd: Rain, Clear
44+ rs: Rain, Snow
45+ rc: Rain, Clouds
46+ rt: Rain, Thunderstorm
47+ sn: Snow, Clear
48+ sd: Snow, Clear
49+ sr: Snow, Rain
50+ sc: Snow, Clouds
51+ st: Snow, Thunderstorm
52+ cn: Clouds, Clear
53+ cd: Clouds, Clear
54+ cr: Clouds, Rain
55+ cs: Clouds, Snow
56+ ct: Clouds, Thunderstorm
57+ tn: Thunderstorm, Clear
58+ td: Thunderstorm, Clear
59+ tr: Thunderstorm, Rain
60+ ts: Thunderstorm, Snow
61+ tc: Thunderstorm, Clouds
62+ editor_table_header:
63+ first_weather: First
64+ second_weather: Second
65+ none: (None)
66+ day_frame_type: Frame type
67+ day_frames:
68+ white: White
69+ black: Black
70+ preview_desc: Please reload to change icons.
71+ draft_has_no_image: This theme draft has no image. Please upload your image before preview!
72+ reload: Reload
73+ to_display_preview_desc: To show widget preview with your uploading fiels, please open following URL or scan QR Code by your Android device.
74+ to_display_preview_title: Preview on your device
75+ device_preview: Device preview
76+ finish_edit: Finish edit
\ No newline at end of file
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -9,6 +9,7 @@ Rails.application.routes.draw do
99
1010 get 'sub', to: 'subscribe#verify'
1111 post 'sub', to: 'subscribe#distribute'
12+ resources :drafts, except: %i(destroy)
1213 resources :users, only: %i(index show)
1314 resources :forecasts, only: %i(index show)
1415 resources :areas, only: %i(index show)
@@ -20,4 +21,6 @@ Rails.application.routes.draw do
2021 get 'logout', to: 'welcome#logout'
2122
2223 root 'welcome#index'
24+
25+ mount ActionCable.server => '/cable'
2326 end
--- /dev/null
+++ b/db/migrate/20170804181345_create_drafts.rb
@@ -0,0 +1,16 @@
1+class CreateDrafts < ActiveRecord::Migration[5.1]
2+ def change
3+ create_table :drafts do |t|
4+ t.string :key, null: false
5+ t.string :title, null: false, default: ''
6+ t.string :url, null: false, default: ''
7+ t.string :day_frame
8+ t.datetime :expires_at, null: false
9+ t.references :user
10+
11+ t.timestamps
12+
13+ t.index :key, unique: true
14+ end
15+ end
16+end
--- /dev/null
+++ b/db/migrate/20170804183731_add_attachment_wi_images_to_drafts.rb
@@ -0,0 +1,77 @@
1+class AddAttachmentWiImagesToDrafts < ActiveRecord::Migration[5.1]
2+ def self.up
3+ change_table :drafts do |t|
4+ t.attachment :wi_n
5+ t.attachment :wi_d
6+ t.attachment :wi_r
7+ t.attachment :wi_s
8+ t.attachment :wi_c
9+ t.attachment :wi_t
10+ t.attachment :wi_nr
11+ t.attachment :wi_ns
12+ t.attachment :wi_nc
13+ t.attachment :wi_nt
14+ t.attachment :wi_dr
15+ t.attachment :wi_ds
16+ t.attachment :wi_dc
17+ t.attachment :wi_dt
18+ t.attachment :wi_rn
19+ t.attachment :wi_rd
20+ t.attachment :wi_rs
21+ t.attachment :wi_rc
22+ t.attachment :wi_rt
23+ t.attachment :wi_sn
24+ t.attachment :wi_sd
25+ t.attachment :wi_sr
26+ t.attachment :wi_sc
27+ t.attachment :wi_st
28+ t.attachment :wi_cn
29+ t.attachment :wi_cd
30+ t.attachment :wi_cr
31+ t.attachment :wi_cs
32+ t.attachment :wi_ct
33+ t.attachment :wi_tn
34+ t.attachment :wi_td
35+ t.attachment :wi_tr
36+ t.attachment :wi_ts
37+ t.attachment :wi_tc
38+ end
39+ end
40+
41+ def self.down
42+ remove_attachment :drafts, :wi_n
43+ remove_attachment :drafts, :wi_d
44+ remove_attachment :drafts, :wi_r
45+ remove_attachment :drafts, :wi_s
46+ remove_attachment :drafts, :wi_c
47+ remove_attachment :drafts, :wi_t
48+ remove_attachment :drafts, :wi_nr
49+ remove_attachment :drafts, :wi_ns
50+ remove_attachment :drafts, :wi_nc
51+ remove_attachment :drafts, :wi_nt
52+ remove_attachment :drafts, :wi_dr
53+ remove_attachment :drafts, :wi_ds
54+ remove_attachment :drafts, :wi_dc
55+ remove_attachment :drafts, :wi_dt
56+ remove_attachment :drafts, :wi_rn
57+ remove_attachment :drafts, :wi_rd
58+ remove_attachment :drafts, :wi_rs
59+ remove_attachment :drafts, :wi_rc
60+ remove_attachment :drafts, :wi_rt
61+ remove_attachment :drafts, :wi_sn
62+ remove_attachment :drafts, :wi_sd
63+ remove_attachment :drafts, :wi_sr
64+ remove_attachment :drafts, :wi_sc
65+ remove_attachment :drafts, :wi_st
66+ remove_attachment :drafts, :wi_cn
67+ remove_attachment :drafts, :wi_cd
68+ remove_attachment :drafts, :wi_cr
69+ remove_attachment :drafts, :wi_cs
70+ remove_attachment :drafts, :wi_ct
71+ remove_attachment :drafts, :wi_tn
72+ remove_attachment :drafts, :wi_td
73+ remove_attachment :drafts, :wi_tr
74+ remove_attachment :drafts, :wi_ts
75+ remove_attachment :drafts, :wi_tc
76+ end
77+end
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
1010 #
1111 # It's strongly recommended that you check this file into your version control system.
1212
13-ActiveRecord::Schema.define(version: 20170801155404) do
13+ActiveRecord::Schema.define(version: 20170804183731) do
1414
1515 create_table "areas", force: :cascade do |t|
1616 t.string "ns"
@@ -50,6 +50,155 @@ ActiveRecord::Schema.define(version: 20170801155404) do
5050 t.index ["status"], name: "index_dist_signals_on_status"
5151 end
5252
53+ create_table "drafts", force: :cascade do |t|
54+ t.string "key", null: false
55+ t.string "title", default: "", null: false
56+ t.string "url", default: "", null: false
57+ t.string "day_frame"
58+ t.datetime "expires_at", null: false
59+ t.integer "user_id"
60+ t.datetime "created_at", null: false
61+ t.datetime "updated_at", null: false
62+ t.string "wi_n_file_name"
63+ t.string "wi_n_content_type"
64+ t.integer "wi_n_file_size"
65+ t.datetime "wi_n_updated_at"
66+ t.string "wi_d_file_name"
67+ t.string "wi_d_content_type"
68+ t.integer "wi_d_file_size"
69+ t.datetime "wi_d_updated_at"
70+ t.string "wi_r_file_name"
71+ t.string "wi_r_content_type"
72+ t.integer "wi_r_file_size"
73+ t.datetime "wi_r_updated_at"
74+ t.string "wi_s_file_name"
75+ t.string "wi_s_content_type"
76+ t.integer "wi_s_file_size"
77+ t.datetime "wi_s_updated_at"
78+ t.string "wi_c_file_name"
79+ t.string "wi_c_content_type"
80+ t.integer "wi_c_file_size"
81+ t.datetime "wi_c_updated_at"
82+ t.string "wi_t_file_name"
83+ t.string "wi_t_content_type"
84+ t.integer "wi_t_file_size"
85+ t.datetime "wi_t_updated_at"
86+ t.string "wi_nr_file_name"
87+ t.string "wi_nr_content_type"
88+ t.integer "wi_nr_file_size"
89+ t.datetime "wi_nr_updated_at"
90+ t.string "wi_ns_file_name"
91+ t.string "wi_ns_content_type"
92+ t.integer "wi_ns_file_size"
93+ t.datetime "wi_ns_updated_at"
94+ t.string "wi_nc_file_name"
95+ t.string "wi_nc_content_type"
96+ t.integer "wi_nc_file_size"
97+ t.datetime "wi_nc_updated_at"
98+ t.string "wi_nt_file_name"
99+ t.string "wi_nt_content_type"
100+ t.integer "wi_nt_file_size"
101+ t.datetime "wi_nt_updated_at"
102+ t.string "wi_dr_file_name"
103+ t.string "wi_dr_content_type"
104+ t.integer "wi_dr_file_size"
105+ t.datetime "wi_dr_updated_at"
106+ t.string "wi_ds_file_name"
107+ t.string "wi_ds_content_type"
108+ t.integer "wi_ds_file_size"
109+ t.datetime "wi_ds_updated_at"
110+ t.string "wi_dc_file_name"
111+ t.string "wi_dc_content_type"
112+ t.integer "wi_dc_file_size"
113+ t.datetime "wi_dc_updated_at"
114+ t.string "wi_dt_file_name"
115+ t.string "wi_dt_content_type"
116+ t.integer "wi_dt_file_size"
117+ t.datetime "wi_dt_updated_at"
118+ t.string "wi_rn_file_name"
119+ t.string "wi_rn_content_type"
120+ t.integer "wi_rn_file_size"
121+ t.datetime "wi_rn_updated_at"
122+ t.string "wi_rd_file_name"
123+ t.string "wi_rd_content_type"
124+ t.integer "wi_rd_file_size"
125+ t.datetime "wi_rd_updated_at"
126+ t.string "wi_rs_file_name"
127+ t.string "wi_rs_content_type"
128+ t.integer "wi_rs_file_size"
129+ t.datetime "wi_rs_updated_at"
130+ t.string "wi_rc_file_name"
131+ t.string "wi_rc_content_type"
132+ t.integer "wi_rc_file_size"
133+ t.datetime "wi_rc_updated_at"
134+ t.string "wi_rt_file_name"
135+ t.string "wi_rt_content_type"
136+ t.integer "wi_rt_file_size"
137+ t.datetime "wi_rt_updated_at"
138+ t.string "wi_sn_file_name"
139+ t.string "wi_sn_content_type"
140+ t.integer "wi_sn_file_size"
141+ t.datetime "wi_sn_updated_at"
142+ t.string "wi_sd_file_name"
143+ t.string "wi_sd_content_type"
144+ t.integer "wi_sd_file_size"
145+ t.datetime "wi_sd_updated_at"
146+ t.string "wi_sr_file_name"
147+ t.string "wi_sr_content_type"
148+ t.integer "wi_sr_file_size"
149+ t.datetime "wi_sr_updated_at"
150+ t.string "wi_sc_file_name"
151+ t.string "wi_sc_content_type"
152+ t.integer "wi_sc_file_size"
153+ t.datetime "wi_sc_updated_at"
154+ t.string "wi_st_file_name"
155+ t.string "wi_st_content_type"
156+ t.integer "wi_st_file_size"
157+ t.datetime "wi_st_updated_at"
158+ t.string "wi_cn_file_name"
159+ t.string "wi_cn_content_type"
160+ t.integer "wi_cn_file_size"
161+ t.datetime "wi_cn_updated_at"
162+ t.string "wi_cd_file_name"
163+ t.string "wi_cd_content_type"
164+ t.integer "wi_cd_file_size"
165+ t.datetime "wi_cd_updated_at"
166+ t.string "wi_cr_file_name"
167+ t.string "wi_cr_content_type"
168+ t.integer "wi_cr_file_size"
169+ t.datetime "wi_cr_updated_at"
170+ t.string "wi_cs_file_name"
171+ t.string "wi_cs_content_type"
172+ t.integer "wi_cs_file_size"
173+ t.datetime "wi_cs_updated_at"
174+ t.string "wi_ct_file_name"
175+ t.string "wi_ct_content_type"
176+ t.integer "wi_ct_file_size"
177+ t.datetime "wi_ct_updated_at"
178+ t.string "wi_tn_file_name"
179+ t.string "wi_tn_content_type"
180+ t.integer "wi_tn_file_size"
181+ t.datetime "wi_tn_updated_at"
182+ t.string "wi_td_file_name"
183+ t.string "wi_td_content_type"
184+ t.integer "wi_td_file_size"
185+ t.datetime "wi_td_updated_at"
186+ t.string "wi_tr_file_name"
187+ t.string "wi_tr_content_type"
188+ t.integer "wi_tr_file_size"
189+ t.datetime "wi_tr_updated_at"
190+ t.string "wi_ts_file_name"
191+ t.string "wi_ts_content_type"
192+ t.integer "wi_ts_file_size"
193+ t.datetime "wi_ts_updated_at"
194+ t.string "wi_tc_file_name"
195+ t.string "wi_tc_content_type"
196+ t.integer "wi_tc_file_size"
197+ t.datetime "wi_tc_updated_at"
198+ t.index ["key"], name: "index_drafts_on_key", unique: true
199+ t.index ["user_id"], name: "index_drafts_on_user_id"
200+ end
201+
53202 create_table "forecasts", force: :cascade do |t|
54203 t.integer "area_id"
55204 t.string "weather_symbol"
--- a/lib/application_responder.rb
+++ b/lib/application_responder.rb
@@ -5,4 +5,14 @@ class ApplicationResponder < ActionController::Responder
55 # Redirects resources to the collection path (index action) instead
66 # of the resource path (show action) for POST/PUT/DELETE requests.
77 # include Responders::CollectionResponder
8+
9+ def api_behavior
10+ raise MissingRenderer.new(format) unless has_renderer?
11+
12+ if put? || patch?
13+ display resource, :status => :ok
14+ else
15+ super
16+ end
17+ end
818 end
--- a/package.json
+++ b/package.json
@@ -30,14 +30,15 @@
3030 "react-addons-css-transition-group": "^15.6.0",
3131 "react-addons-transition-group": "^15.6.0",
3232 "react-dom": "^15.6.1",
33+ "react-dropzone": "^4.0.0",
3334 "reactstrap": "^4.8.0",
3435 "resolve-url-loader": "^2.1.0",
3536 "sass-loader": "^6.0.6",
3637 "style-loader": "^0.18.2",
37- "turbolinks": "^5.0.3",
3838 "webpack": "^3.3.0",
3939 "webpack-manifest-plugin": "^1.1.2",
40- "webpack-merge": "^4.1.0"
40+ "webpack-merge": "^4.1.0",
41+ "webpacker-react": "^0.3.0"
4142 },
4243 "devDependencies": {
4344 "webpack-dev-server": "^2.5.1"
--- /dev/null
+++ b/spec/controllers/drafts_controller_spec.rb
@@ -0,0 +1,141 @@
1+require 'rails_helper'
2+
3+# This spec was generated by rspec-rails when you ran the scaffold generator.
4+# It demonstrates how one might use RSpec to specify the controller code that
5+# was generated by Rails when you ran the scaffold generator.
6+#
7+# It assumes that the implementation code is generated by the rails scaffold
8+# generator. If you are using any extension libraries to generate different
9+# controller code, this generated spec may or may not pass.
10+#
11+# It only uses APIs available in rails and/or rspec-rails. There are a number
12+# of tools you can use to make these specs even more expressive, but we're
13+# sticking to rails and rspec-rails APIs to keep things simple and stable.
14+#
15+# Compared to earlier versions of this generator, there is very limited use of
16+# stubs and message expectations in this spec. Stubs are only used when there
17+# is no simpler way to get a handle on the object needed for the example.
18+# Message expectations are only used when there is no simpler way to specify
19+# that an instance is receiving a specific message.
20+#
21+# Also compared to earlier versions of this generator, there are no longer any
22+# expectations of assigns and templates rendered. These features have been
23+# removed from Rails core in Rails 5, but can be added back in via the
24+# `rails-controller-testing` gem.
25+
26+RSpec.describe DraftsController, type: :controller do
27+
28+ # This should return the minimal set of attributes required to create a valid
29+ # Draft. As you add validations to Draft, be sure to
30+ # adjust the attributes here as well.
31+ let(:valid_attributes) {
32+ skip("Add a hash of attributes valid for your model")
33+ }
34+
35+ let(:invalid_attributes) {
36+ skip("Add a hash of attributes invalid for your model")
37+ }
38+
39+ # This should return the minimal set of values that should be in the session
40+ # in order to pass any filters (e.g. authentication) defined in
41+ # DraftsController. Be sure to keep this updated too.
42+ let(:valid_session) { {} }
43+
44+ describe "GET #index" do
45+ it "returns a success response" do
46+ draft = Draft.create! valid_attributes
47+ get :index, params: {}, session: valid_session
48+ expect(response).to be_success
49+ end
50+ end
51+
52+ describe "GET #show" do
53+ it "returns a success response" do
54+ draft = Draft.create! valid_attributes
55+ get :show, params: {id: draft.to_param}, session: valid_session
56+ expect(response).to be_success
57+ end
58+ end
59+
60+ describe "GET #new" do
61+ it "returns a success response" do
62+ get :new, params: {}, session: valid_session
63+ expect(response).to be_success
64+ end
65+ end
66+
67+ describe "GET #edit" do
68+ it "returns a success response" do
69+ draft = Draft.create! valid_attributes
70+ get :edit, params: {id: draft.to_param}, session: valid_session
71+ expect(response).to be_success
72+ end
73+ end
74+
75+ describe "POST #create" do
76+ context "with valid params" do
77+ it "creates a new Draft" do
78+ expect {
79+ post :create, params: {draft: valid_attributes}, session: valid_session
80+ }.to change(Draft, :count).by(1)
81+ end
82+
83+ it "redirects to the created draft" do
84+ post :create, params: {draft: valid_attributes}, session: valid_session
85+ expect(response).to redirect_to(Draft.last)
86+ end
87+ end
88+
89+ context "with invalid params" do
90+ it "returns a success response (i.e. to display the 'new' template)" do
91+ post :create, params: {draft: invalid_attributes}, session: valid_session
92+ expect(response).to be_success
93+ end
94+ end
95+ end
96+
97+ describe "PUT #update" do
98+ context "with valid params" do
99+ let(:new_attributes) {
100+ skip("Add a hash of attributes valid for your model")
101+ }
102+
103+ it "updates the requested draft" do
104+ draft = Draft.create! valid_attributes
105+ put :update, params: {id: draft.to_param, draft: new_attributes}, session: valid_session
106+ draft.reload
107+ skip("Add assertions for updated state")
108+ end
109+
110+ it "redirects to the draft" do
111+ draft = Draft.create! valid_attributes
112+ put :update, params: {id: draft.to_param, draft: valid_attributes}, session: valid_session
113+ expect(response).to redirect_to(draft)
114+ end
115+ end
116+
117+ context "with invalid params" do
118+ it "returns a success response (i.e. to display the 'edit' template)" do
119+ draft = Draft.create! valid_attributes
120+ put :update, params: {id: draft.to_param, draft: invalid_attributes}, session: valid_session
121+ expect(response).to be_success
122+ end
123+ end
124+ end
125+
126+ describe "DELETE #destroy" do
127+ it "destroys the requested draft" do
128+ draft = Draft.create! valid_attributes
129+ expect {
130+ delete :destroy, params: {id: draft.to_param}, session: valid_session
131+ }.to change(Draft, :count).by(-1)
132+ end
133+
134+ it "redirects to the drafts list" do
135+ draft = Draft.create! valid_attributes
136+ delete :destroy, params: {id: draft.to_param}, session: valid_session
137+ expect(response).to redirect_to(drafts_url)
138+ end
139+ end
140+
141+end
--- /dev/null
+++ b/spec/factories/drafts.rb
@@ -0,0 +1,10 @@
1+FactoryGirl.define do
2+ factory :draft do
3+ key "MyString"
4+ title "MyString"
5+ url "MyString"
6+ day_frame "MyString"
7+ expires_at "2017-08-05 03:13:45"
8+ user nil
9+ end
10+end
--- /dev/null
+++ b/spec/models/draft_spec.rb
@@ -0,0 +1,5 @@
1+require 'rails_helper'
2+
3+RSpec.describe Draft, type: :model do
4+ pending "add some examples to (or delete) #{__FILE__}"
5+end
--- /dev/null
+++ b/spec/routing/drafts_routing_spec.rb
@@ -0,0 +1,35 @@
1+require "rails_helper"
2+
3+RSpec.describe DraftsController, type: :routing do
4+ describe "routing" do
5+
6+ it "routes to #index" do
7+ expect(:get => "/drafts").to route_to("drafts#index")
8+ end
9+
10+ it "routes to #new" do
11+ expect(:get => "/drafts/new").to route_to("drafts#new")
12+ end
13+
14+ it "routes to #show" do
15+ expect(:get => "/drafts/1").to route_to("drafts#show", :id => "1")
16+ end
17+
18+ it "routes to #edit" do
19+ expect(:get => "/drafts/1/edit").to route_to("drafts#edit", :id => "1")
20+ end
21+
22+ it "routes to #create" do
23+ expect(:post => "/drafts").to route_to("drafts#create")
24+ end
25+
26+ it "routes to #update via PUT" do
27+ expect(:put => "/drafts/1").to route_to("drafts#update", :id => "1")
28+ end
29+
30+ it "routes to #update via PATCH" do
31+ expect(:patch => "/drafts/1").to route_to("drafts#update", :id => "1")
32+ end
33+
34+ end
35+end
--- /dev/null
+++ b/test/application_system_test_case.rb
@@ -0,0 +1,5 @@
1+require "test_helper"
2+
3+class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
4+ driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
5+end
--- /dev/null
+++ b/test/system/drafts_test.rb
@@ -0,0 +1,9 @@
1+require "application_system_test_case"
2+
3+class DraftsTest < ApplicationSystemTestCase
4+ # test "visiting the index" do
5+ # visit drafts_url
6+ #
7+ # assert_selector "h1", text: "Draft"
8+ # end
9+end
--- a/yarn.lock
+++ b/yarn.lock
@@ -223,6 +223,10 @@ atob@~1.1.0:
223223 version "1.1.3"
224224 resolved "https://registry.yarnpkg.com/atob/-/atob-1.1.3.tgz#95f13629b12c3a51a5d215abdce2aa9f32f80773"
225225
226+attr-accept@^1.0.3:
227+ version "1.1.0"
228+ resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-1.1.0.tgz#b5cd35227f163935a8f1de10ed3eba16941f6be6"
229+
226230 autoprefixer@^6.3.1:
227231 version "6.7.7"
228232 resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.7.7.tgz#1dbd1c835658e35ce3f9984099db00585c782014"
@@ -3946,7 +3950,7 @@ promise@^7.1.1:
39463950 dependencies:
39473951 asap "~2.0.3"
39483952
3949-prop-types@^15.5.10, prop-types@^15.5.6, prop-types@^15.5.8:
3953+prop-types@^15.5.10, prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.5.8:
39503954 version "15.5.10"
39513955 resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.10.tgz#2797dfc3126182e3a95e3dfbb2e893ddd7456154"
39523956 dependencies:
@@ -4071,6 +4075,13 @@ react-dom@^15.6.1:
40714075 object-assign "^4.1.0"
40724076 prop-types "^15.5.10"
40734077
4078+react-dropzone@^4.0.0:
4079+ version "4.0.0"
4080+ resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-4.0.0.tgz#3ff7fab769ad4fb6206ca4a16fe35eab34297053"
4081+ dependencies:
4082+ attr-accept "^1.0.3"
4083+ prop-types "^15.5.7"
4084+
40744085 react-transition-group@^1.2.0:
40754086 version "1.2.0"
40764087 resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-1.2.0.tgz#b51fc921b0c3835a7ef7c571c79fc82c73e9204f"
@@ -4796,10 +4807,6 @@ tunnel-agent@^0.6.0:
47964807 dependencies:
47974808 safe-buffer "^5.0.1"
47984809
4799-turbolinks@^5.0.3:
4800- version "5.0.3"
4801- resolved "https://registry.yarnpkg.com/turbolinks/-/turbolinks-5.0.3.tgz#c8ce128cfc9be1d691a1e41ffa858a5a5ee86a02"
4802-
48034810 tweetnacl@^0.14.3, tweetnacl@~0.14.0:
48044811 version "0.14.5"
48054812 resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
@@ -5038,6 +5045,10 @@ webpack@^3.3.0:
50385045 webpack-sources "^1.0.1"
50395046 yargs "^6.0.0"
50405047
5048+webpacker-react@^0.3.0:
5049+ version "0.3.0"
5050+ resolved "https://registry.yarnpkg.com/webpacker-react/-/webpacker-react-0.3.0.tgz#176d4537d997772f66641d565b796f33dfbb0272"
5051+
50415052 websocket-driver@>=0.5.1:
50425053 version "0.6.5"
50435054 resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.6.5.tgz#5cb2556ceb85f4373c6d8238aa691c8454e13a36"