kwipper 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.DS_Store +0 -0
  3. data/.gitignore +16 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +33 -0
  7. data/Rakefile +2 -0
  8. data/app/controllers/comments_controller.rb +32 -0
  9. data/app/controllers/post_favorites_controller.rb +17 -0
  10. data/app/controllers/posts_controller.rb +41 -0
  11. data/app/controllers/sessions_controller.rb +32 -0
  12. data/app/controllers/users_controller.rb +70 -0
  13. data/app/models/comment.rb +20 -0
  14. data/app/models/post.rb +31 -0
  15. data/app/models/post_favorite.rb +6 -0
  16. data/app/models/session.rb +12 -0
  17. data/app/models/user.rb +21 -0
  18. data/app/views/edit_user.erb +26 -0
  19. data/app/views/fave_button.erb +8 -0
  20. data/app/views/home.erb +126 -0
  21. data/app/views/layout.erb +110 -0
  22. data/app/views/login_user.erb +24 -0
  23. data/app/views/new_comment.erb +18 -0
  24. data/app/views/new_post.erb +11 -0
  25. data/app/views/new_user.erb +26 -0
  26. data/app/views/not_found.erb +11 -0
  27. data/app/views/pagination.erb +29 -0
  28. data/app/views/posts.erb +7 -0
  29. data/app/views/posts_list.erb +44 -0
  30. data/app/views/reply_button.erb +4 -0
  31. data/app/views/server_error.erb +17 -0
  32. data/app/views/show_post.erb +37 -0
  33. data/app/views/show_user.erb +11 -0
  34. data/app/views/users.erb +38 -0
  35. data/db/.DS_Store +0 -0
  36. data/kwipper.gemspec +27 -0
  37. data/lib/kwipper.rb +46 -0
  38. data/lib/kwipper/application.rb +87 -0
  39. data/lib/kwipper/controller.rb +36 -0
  40. data/lib/kwipper/controller_helpers.rb +22 -0
  41. data/lib/kwipper/errors.rb +5 -0
  42. data/lib/kwipper/http_parser.rb +51 -0
  43. data/lib/kwipper/http_server.rb +61 -0
  44. data/lib/kwipper/inflect.rb +19 -0
  45. data/lib/kwipper/model.rb +174 -0
  46. data/lib/kwipper/paginator.rb +71 -0
  47. data/lib/kwipper/renders_views.rb +18 -0
  48. data/lib/kwipper/request.rb +35 -0
  49. data/lib/kwipper/request_headers.rb +36 -0
  50. data/lib/kwipper/response.rb +85 -0
  51. data/lib/kwipper/version.rb +3 -0
  52. data/public/css/bootstrap-lumen.min.css +7 -0
  53. data/public/css/styles.css +48 -0
  54. data/public/fonts/glyphicons-halflings-regular.eot +0 -0
  55. data/public/fonts/glyphicons-halflings-regular.svg +229 -0
  56. data/public/fonts/glyphicons-halflings-regular.ttf +0 -0
  57. data/public/fonts/glyphicons-halflings-regular.woff +0 -0
  58. data/public/js/bootstrap.min.js +7 -0
  59. metadata +173 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e5274064f603e0f05b4a350a54d02ca0daffe742
4
+ data.tar.gz: 254cb60d948a74f41e1cbd58d5206f31ba1d4ef8
5
+ SHA512:
6
+ metadata.gz: ca2c1888f7ed78138981511db0853b1ca00b638e6070a7d27c80c8e3b2800f6d3fd1428edb94cb6ecdc448bffc0c8857eb2805aa7a55e9368aac6974ab9aad70
7
+ data.tar.gz: d094eba7c7f0e6b6a10dd1cc6830dd9251a9bde863cb25e31ff2259f7b4ff856f5c4c25fb58fd6dc40c5e17ca5c483fa3520e4d493bd2000c3c2df78deae1862
Binary file
@@ -0,0 +1,16 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ db/*.db
16
+ *.db
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in kwipper.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Diego Salazar
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,33 @@
1
+ # Kwipper
2
+
3
+ Kwipper is the reference example app for the Kwipper Programming Challenge. See more information on the live app [demo](#soon).
4
+
5
+ ## Installation
6
+
7
+ $ git clone git@github.com:DiegoSalazar/kwipper_challenge.git
8
+
9
+ ## Usage
10
+
11
+ Create yourself a user by starting the console:
12
+
13
+ $ irb -r ./lib/kwipper -r ./app/models/user
14
+
15
+ And running:
16
+
17
+ ```ruby
18
+ User.create 'username' => 'a username', 'email' => 'optional@mail.com', 'hashed_password' => '123'
19
+ ```
20
+
21
+ Start the server:
22
+
23
+ $ ruby lib/kwipper.rb
24
+
25
+ Open the app in a browser at [http://localhost:7335](http://localhost:7335).
26
+
27
+ ## Contributing
28
+
29
+ 1. Fork it ( https://github.com/DiegoSalazar/kwipper/fork )
30
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
31
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
32
+ 4. Push to the branch (`git push origin my-new-feature`)
33
+ 5. Create a new Pull Request
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,32 @@
1
+ module Kwipper
2
+ class CommentsController < Controller
3
+ add_routes({
4
+ [:GET, '/kwips/comments/new'] => :new,
5
+ [:POST, '/kwips/comments/create'] => :create
6
+ })
7
+
8
+ def new
9
+ require_login!
10
+ @post = Post.find params['id']
11
+ render :new_comment
12
+ end
13
+
14
+ def create
15
+ require_login!
16
+
17
+ post = Post.find params['id']
18
+ comment = Comment.new({
19
+ 'user_id' => current_user.id,
20
+ 'post_id' => post.id,
21
+ 'created_at' => Time.now.httpdate,
22
+ 'content' => params['content']
23
+ })
24
+
25
+ if comment.save
26
+ redirect "/kwips/show?id=#{post.id}"
27
+ else
28
+ redirect "/kwips/comments/new?id=#{post.id}", :bad_request
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,17 @@
1
+ module Kwipper
2
+ class PostFavoritesController < Controller
3
+ add_routes [:POST, '/favorites/create'] => :create
4
+
5
+ def create
6
+ require_login!
7
+
8
+ post = Post.find params['id']
9
+ fave = PostFavorite.create({
10
+ 'user_id' => current_user.id,
11
+ 'post_id' => post.id
12
+ })
13
+
14
+ redirect "/kwips/show?id=#{post.id}"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,41 @@
1
+ module Kwipper
2
+ class PostsController < Controller
3
+ add_routes({
4
+ [:GET, '/kwips'] => :posts,
5
+ [:GET, '/kwips/show'] => :show,
6
+ [:GET, '/kwips/new'] => :new,
7
+ [:POST, '/kwips/create'] => :create
8
+ })
9
+
10
+ def posts
11
+ @paginator = Paginator.new Post, page: params['page'], per: 10, path: '/kwips'
12
+ @posts = @paginator.get "SELECT * FROM posts ORDER BY created_at DESC"
13
+ render :posts
14
+ end
15
+
16
+ def show
17
+ @post = Post.find params['id']
18
+ @comments = @post.comments
19
+ render :show_post
20
+ end
21
+
22
+ def new
23
+ require_login!
24
+ render :new_post
25
+ end
26
+
27
+ def create
28
+ require_login!
29
+ post = Post.new params.merge({
30
+ 'user_id' => current_user.id,
31
+ 'created_at' => Time.now.httpdate
32
+ })
33
+
34
+ if post.save
35
+ redirect '/kwips'
36
+ else
37
+ redirect '/kwips/new', :bad_request
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,32 @@
1
+ module Kwipper
2
+ class SessionsController < Controller
3
+ add_routes({
4
+ [:POST, '/sessions/create'] => :create,
5
+ [:GET, '/logout'] => :destroy
6
+ })
7
+
8
+ def create
9
+ @user = User.authenticate params['username'], params['password']
10
+
11
+ if @user
12
+ Session.create({
13
+ 'id' => response.session_cookie_value,
14
+ 'user_id' => @user.id,
15
+ 'created_at' => Time.now.httpdate
16
+ })
17
+
18
+ redirect '/users'
19
+ else
20
+ redirect '/users/login', :bad_request
21
+ end
22
+ end
23
+
24
+ def destroy
25
+ if current_session
26
+ current_session.destroy
27
+ response.remove_session_cookie
28
+ end
29
+ redirect '/'
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,70 @@
1
+ module Kwipper
2
+ class UsersController < Controller
3
+ add_routes({
4
+ [:GET, '/users'] => :users,
5
+ [:GET, '/users/show'] => :show,
6
+ [:GET, '/users/new'] => :new,
7
+ [:POST, '/users/create'] => :create,
8
+ [:GET, '/users/edit'] => :edit,
9
+ [:POST, '/users/update'] => :update,
10
+ [:POST, '/users/destroy'] => :destroy,
11
+ [:GET, '/login'] => :login
12
+ })
13
+
14
+ def users
15
+ require_login!
16
+ @users = User.all
17
+ render :users
18
+ end
19
+
20
+ def show
21
+ @user = User.find params['id']
22
+ @posts = @user.posts
23
+ render :show_user
24
+ end
25
+
26
+ def new
27
+ require_login!
28
+ render :new_user
29
+ end
30
+
31
+ def create
32
+ require_login!
33
+ user = User.new params
34
+
35
+ if user.save
36
+ redirect '/users'
37
+ else
38
+ redirect '/users/new', :bad_request
39
+ end
40
+ end
41
+
42
+ def edit
43
+ require_login!
44
+ @user = User.find params['id']
45
+ render :edit_user
46
+ end
47
+
48
+ def update
49
+ require_login!
50
+ user = User.find params['id']
51
+
52
+ if user.update params
53
+ redirect '/users'
54
+ else
55
+ redirect '/users/new', :bad_request
56
+ end
57
+ end
58
+
59
+ def destroy
60
+ require_login!
61
+ user = User.find params['id']
62
+ user.destroy
63
+ redirect '/users'
64
+ end
65
+
66
+ def login
67
+ render :login_user
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,20 @@
1
+ module Kwipper
2
+ class Comment < Model
3
+ column 'user_id', :to_i
4
+ column 'post_id', :to_i
5
+ column 'content', :to_s
6
+ column 'created_at', :to_s
7
+
8
+ def user
9
+ @user ||= User.find user_id
10
+ end
11
+
12
+ def username
13
+ user.username
14
+ end
15
+
16
+ def post
17
+ @post ||= Post.find post_id
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,31 @@
1
+ module Kwipper
2
+ class Post < Model
3
+ column 'user_id', :to_i
4
+ column 'content', :to_s
5
+ column 'created_at', :to_s
6
+
7
+ def self.recent(statement = "SELECT * FROM posts")
8
+ all(statement).sort_by { |post| Time.parse post.created_at }.reverse
9
+ end
10
+
11
+ def user
12
+ @user ||= User.find user_id
13
+ end
14
+
15
+ def username
16
+ user.username
17
+ end
18
+
19
+ def comments
20
+ Comment.all "SELECT * FROM comments WHERE post_id = #{id}"
21
+ end
22
+
23
+ def comments_count
24
+ Model.count "SELECT COUNT(id) FROM comments WHERE post_id = #{id}"
25
+ end
26
+
27
+ def faves_count
28
+ Model.count "SELECT COUNT(id) FROM post_favorites WHERE post_id = #{id}"
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,6 @@
1
+ module Kwipper
2
+ class PostFavorite < Model
3
+ column 'user_id', :to_i
4
+ column 'post_id', :to_i
5
+ end
6
+ end
@@ -0,0 +1,12 @@
1
+ module Kwipper
2
+ class Session < Model
3
+ column 'id', :to_s
4
+ column 'user_id', :to_i
5
+ column 'created_at', :to_s
6
+
7
+
8
+ def destroy
9
+ super self.class.normalize_value_for_db(id, :to_s)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,21 @@
1
+ module Kwipper
2
+ class User < Model
3
+ column 'username', :to_s
4
+ column 'email', :to_s
5
+ column 'hashed_password', :to_s
6
+
7
+ def self.authenticate(username, password)
8
+ user = where(username: username).first
9
+ user && user.hashed_password == password && user
10
+ end
11
+
12
+ def favorite?(post)
13
+ result = sql("SELECT COUNT(id) FROM post_favorites WHERE user_id = #{id} AND post_id = #{post.id} LIMIT 1").first
14
+ result.first && result.first > 0
15
+ end
16
+
17
+ def posts
18
+ Post.recent "SELECT * FROM posts WHERE user_id = #{id}"
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ <h1>Edit User</h1>
2
+
3
+ <div class="row">
4
+ <div class="col-sm-12">
5
+ <form action="/users/update?id=<%= @user.id %>" method="post">
6
+ <div class="form-group">
7
+ <label>Username</label>
8
+ <input value="<%= @user.username %>" type="text" name="username" class="form-control">
9
+ </div>
10
+
11
+ <div class="form-group">
12
+ <label>Email</label>
13
+ <input value="<%= @user.email %>" type="text" name="email" class="form-control">
14
+ </div>
15
+
16
+ <div class="form-group">
17
+ <label>Password</label>
18
+ <input value="<%= @user.hashed_password %>" type="text" name="hashed_password" class="form-control">
19
+ </div>
20
+
21
+ <div class="form-group">
22
+ <input type="submit" value="Update" class="btn btn-primary">
23
+ </div>
24
+ </form>
25
+ </div>
26
+ </div>
@@ -0,0 +1,8 @@
1
+ <% faved = current_user && current_user.favorite?(@post) %>
2
+ <form action="/favorites/create?id=<%= @post.id %>" method="post" class="pull-left">
3
+ <span class="fa fa-thumb-o-up">&nbsp;</span>
4
+ <button type="submit" class="btn btn-xs btn-<%= faved ? 'success' : 'default' %>"<%= faved ? ' disabled="disabled"' : nil %>>
5
+ <span class="fa fa-thumbs-<%= faved ? 'up' : 'o-up' %>">&nbsp;</span>
6
+ <%= faved ? "Fave'd" : 'Fave' %>
7
+ </button
8
+ </form>
@@ -0,0 +1,126 @@
1
+ <div class="jumbotron">
2
+ <h1>Kwipper</h1>
3
+
4
+ <p>
5
+ This web app is a Twitter clone that was created as a reference example for a programming challenge designed to teach the fundamentals of web development in Ruby. Kwipper is a self contained app that uses no web application frameworks, ORMs or Active gems, and no third party server.
6
+ <a href="#rules">Read more</a> about the challenge below.
7
+ </p>
8
+ </div>
9
+
10
+ <p class="alert alert-info">
11
+ <span class="fa fa-github fa-lg">&nbsp;</span>
12
+ You can see the complete source code for this app on
13
+ <a href="https://github.com/DiegoSalazar/kwipper_challenge">Github</a>.
14
+ </p>
15
+
16
+ <h2>Components that were built from scratch:</h2>
17
+
18
+ <ul>
19
+ <li>[HTTP Server](https://github.com/DiegoSalazar/kwipper_challenge/blob/master/lib/kwipper/http_server.rb): using Ruby's TCPServer class</li>
20
+ <li>[HTTP Parser](https://github.com/DiegoSalazar/kwipper_challenge/blob/master/lib/kwipper/http_parser.rb): plain old Ruby</li>
21
+ <li>[Request class](https://github.com/DiegoSalazar/kwipper_challenge/blob/master/lib/kwipper/request.rb): represents the parsed HTTP request and headers</li>
22
+ <li>[Response class](https://github.com/DiegoSalazar/kwipper_challenge/blob/master/lib/kwipper/response.rb): holds response headers and renders HTTP responses to the socket. Sets the session.</li>
23
+ <li>[View Rendering module](https://github.com/DiegoSalazar/kwipper_challenge/blob/master/lib/kwipper/renders_views.rb): using ERB</li>
24
+ <li>[Model base class](https://github.com/DiegoSalazar/kwipper_challenge/blob/master/lib/kwipper/model.rb): The most complex class, it implements interaction with SQLite and defines ORM-like methods.</li>
25
+ <li>[Controller base class](https://github.com/DiegoSalazar/kwipper_challenge/blob/master/lib/kwipper/controller.rb): implements routing and authentication</li>
26
+ <li>[App micro framework](https://github.com/DiegoSalazar/kwipper_challenge/blob/master/lib/kwipper/application.rb): app directory with views, models and controllers that are automatically loaded on startup</li>
27
+ <li>[Paginator class](https://github.com/DiegoSalazar/kwipper_challenge/blob/master/lib/kwipper/paginator.rb): SQL based pagination</li>
28
+ </ul>
29
+
30
+ <hr>
31
+
32
+ <h2 id="rules">Challenge Rules</h2>
33
+
34
+ <div class="well">
35
+ <h3>Write a Twitter clone without using any frameworks or Active* gems. </h3>
36
+
37
+ <p>
38
+ The purpose of this challenge is to increase your knowledge of the fundamentals of web development and to understand what happens “under the hood” of the frameworks we use such as Rack and Rails. How you build the app is not important, just that it satisfies the Required Features and follows the Rules. This challenge was designed to encourage you to learn the fundamental concepts that a web developer should know. Once these ideas are grokked you will be closer to obtaining the skills necessary to freely move between web-based technologies.
39
+ </p>
40
+
41
+ <p>
42
+ Our trade evolves at an exponentially increasing rate. So too, must we constantly increase our understanding of our tools and techniques in order to keep up with the pace of advancement and innovation. By reaching for the forefront of our craft we can better hope to contribute our own innovations.
43
+ </p>
44
+
45
+ <p>The challenge will be divided into two phases, which are described in the phase sections below.</p>
46
+
47
+ <h3>Things To Learn:</h3>
48
+ <ul>
49
+ <li>Write a basic HTTP server (for example: using Ruby’s TCPServer class)</li>
50
+ <li>Parse the HTTP protocol (similar to how CSV is parsed but more complex)</li>
51
+ <li>Handle HTTP headers to set cookies and manage sessions</li>
52
+ <li>Handle GET and POST data in requests</li>
53
+ <li>Abstract away the HTTP layer so your app can focus on its own domain logic</li>
54
+ <li>Connect to a database without using ActiveRecord</li>
55
+ <li>Write SQL statements to perform CRUD operations</li>
56
+ <li>Manually validate and sanitize user input before inserting into the database</li>
57
+ <li>Generate HTML with dynamic content</li>
58
+ </ul>
59
+
60
+ <h2>Kwipper: A witty Twitter Clone</h2>
61
+
62
+ <h3>Required Features</h3>
63
+ <ul>
64
+ <li>User sign up, login, and logout using cookie based sessions</li>
65
+ <li>User can create, edit, delete their own text Posts</li>
66
+ <li>Index page displays timeline of Posts by all Users</li>
67
+ <li>Pagination of timeline</li>
68
+ <li>Server logs requests to STDOUT</li>
69
+ </ul>
70
+
71
+ <h3>Rules</h3>
72
+
73
+ <ul>
74
+ <li>The App must be run by executing `ruby app.rb`</li>
75
+ <li>The App logs requests to STDOUT (the console)</li>
76
+ <li>The App must be plain Ruby and HTML</li>
77
+ <li>Must not use any app frameworks (e.g. Rack, Rails, Sinatra)</li>
78
+ <li>Must not use any of the Active* gems (e.g. ActiveRecord, ActiveSupport)</li>
79
+ </ul>
80
+
81
+ <h3>Phase 1</h3>
82
+ <p>
83
+ Complete the HTTP server, database connection, and user signup, login and logout. These basic features form the foundation of every web application and require knowledge of HTTP, SQL, HTTP headers and the concept of session management.
84
+ </p>
85
+
86
+ <h3>Phase 2</h3>
87
+ <p>Complete the app to meet the required features.</p>
88
+
89
+ <h3>Bonus Features</h3>
90
+ <p>If you’re looking for more of a challenge you can optionally add:</p>
91
+
92
+ <ul>
93
+ <li>Users can comment on posts</li>
94
+ <li>Users can tag posts</li>
95
+ <li>Users can favorite posts</li>
96
+ <li>Add a tag cloud to the homepage</li>
97
+ <li>Make it look good on mobile</li>
98
+ </ul>
99
+
100
+ <h3>Deeper Concepts to Learn</h3>
101
+ <p>
102
+ Through completing the challenge you’ll maybe come to understand deeper concepts underlying what you’re building and the technology and protocols you’ll be using. Take these points as a source of motivation for further learning.
103
+ </p>
104
+
105
+ <p>In diving deeper into the ideas touched on by the points in the Things To Learn section:</p>
106
+
107
+ <ul>
108
+ <li>
109
+ The difference between TCP and HTTP and how they fit together. Why was a text protocol chosen for HTTP but a binary protocol for TCP? What exactly is TCP? What is HTTP?
110
+ </li>
111
+ <li>
112
+ What are sessions? What are cookies? Why are they necessary? How are they stored in memory on the server? How does the server know which users correspond with which sessions?
113
+ </li>
114
+ <li>
115
+ What are the differences between GET and POST data? When it is appropriate to use each and why do we get the “resubmit form data” warning in some apps?
116
+ </li>
117
+ <li>
118
+ What is the point of ActiveRecord? Why doesn’t it cover all use cases? What is the difference between modeling data, a data model, and a Model?
119
+ Where should validation be implemented? Why should we sanitize input? Why was this an important point?
120
+ </li>
121
+ <li>
122
+ How can we generate dynamic content without templating frameworks like ERB or HAML? At what point does a web page become a web app? What is HTML and what does it do?
123
+ </li>
124
+ </ul>
125
+
126
+ </div>