lib & contrib: update imports
[nit.git] / contrib / tnitter / src / action.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2014 Alexis Laferrière <alexis.laf@xymus.net>
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 # View and controller of Tnitter
18 module action
19
20 import nitcorn
21 import json
22
23 import model
24 import database
25
26 # Path to the Sqlite3 database
27 fun tnitter_db_path: String do return "tnitter.db"
28
29 redef class Session
30 # User logged in
31 var user: nullable String = null
32 end
33
34 # Main Tnitter Action
35 class TnitterWeb
36 super Action
37
38 # Header on pages served by this `Action`
39 #
40 # Keywords to `Text::replace`:
41 # * `%app_path%` is the main URL to reach this `Action`
42 # * `%nav_right%` is the pulled right part of the header, used for login form
43 var header = """
44 <nav class="navbar navbar-default" role="navigation">
45 <div class="container-fluid">
46 <div class="navbar-header">
47 <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
48 <span class="sr-only">Toggle navigation</span>
49 <span class="icon-bar"></span>
50 <span class="icon-bar"></span>
51 <span class="icon-bar"></span>
52 </button>
53 <a class="navbar-brand" href="%app_path%">Tnitter</a>
54 </div>
55
56 <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
57 <ul class="nav navbar-nav">
58 <li><a href="https://github.com/nitlang/nit/">Nit repository</a></li>
59 </ul>
60
61 <ul class="nav navbar-nav pull-right">
62 %header_right%
63 </ul>
64 </div>
65 </div>
66 </nav>"""
67
68 # Template of the pages served by this `Action`
69 #
70 # Keywords to `Text::replace`:
71 # * The `%header%`, first thing in the `<body>`
72 # * The main page `%content%` within a `<div class="container">`
73 var template = """
74 <!DOCTYPE html>
75 <html>
76 <head>
77 <title>Tnitter</title>
78 <meta charset="utf-8">
79 <meta http-equiv="X-UA-Compatible" content="IE=edge">
80 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
81
82 <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
83 <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
84 <script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
85 <script>
86 {{{javascript_header or else ""}}}
87 </script>
88 </head>
89 <body>
90
91 %header%
92
93 <div class="container">
94 %content%
95 </div>
96 </body>
97 </html>"""
98
99 # Custom JavaScript code added within a `<script>` block to each page
100 var javascript_header: nullable Writable = null is writable
101
102 redef fun answer(request, turi)
103 do
104 # Get existing session
105 var session = request.session
106
107 # Error to display on page as a dismissable panel
108 var error = null
109
110 var db = new DB.open(tnitter_db_path)
111
112 # Login/logout
113 if turi == "/login" and request.post_args.keys.has("user") and
114 request.post_args.keys.has("pass") then
115
116 var user = request.post_args["user"].trim
117 var pass = request.post_args["pass"]
118
119 var original_user = db.check_login(user, pass)
120 if original_user != null then
121 # Log in successful
122 if session == null then session = new Session
123 session.user = original_user
124 else
125 # Check for basic requirements
126 if user.is_empty then
127 error = "Username must have at least 1 character"
128 else if user.chars.has(' ') or user.chars.has('\n') then
129 error = "Username cannot contain white spaces"
130 else if db.sign_up(user, pass) then
131 # Sign up successful
132 if session == null then session = new Session
133 session.user = user
134 else
135 # Invalid user/pass
136 error = "Invalid combination of username and password"
137 session = null
138 end
139 end
140 else if turi == "/logout" then
141 # Logging out
142 session = null
143 else if turi == "/post" and request.post_args.keys.has("text") and session != null then
144 var user = session.user
145 var text = request.post_args["text"].trim
146 if user != null and not text.is_empty then
147 # Post a Tnit!
148 db.post(user, text)
149 db.close
150
151 # Redirect the user to avoid double posting
152 var response = new HttpResponse(303)
153 response.header["Location"] = request.uri
154 response.session = session
155 return response
156 end
157 end
158
159 var login_or_out
160 var content
161 if session == null or session.user == null then
162 # Log in form in the navbar
163 login_or_out = """
164 <li>
165 <form class="navbar-form" role="form" action="login" method="POST">
166 <div class="form-group">
167 <input type="text" placeholder="Username" class="form-control" name="user">
168 </div>
169 <div class="form-group">
170 <input type="password" placeholder="Password" class="form-control" name="pass">
171 </div>
172 <button type="submit" class="btn btn-default">Log in (or sign up)</button>
173 </form>
174 </li>
175 """
176
177 # Cannot post when not logged in
178 content = ""
179 else
180 # Log out form in the navbar
181 login_or_out = """
182 <li><p class="navbar-text">Signed in as @{{{session.user.html_escape}}}</p></li>
183 <li>
184 <form class="navbar-form" role="form" action="logout" method="POST">
185 <button type="submit" class="btn btn-default">Log out</button>
186 </form>
187 </li>
188 """
189
190 # Post form
191 content = """
192 <form class="form" role="form" action="post" method="POST">
193 <div class="form-group">
194 <div class="input-group">
195 <div class="input-group-addon">Share your thoughts</div>
196 <input class="form-control" type="text" placeholder="..." name="text">
197 <span class="input-group-btn">
198 <button class="btn btn-default" type="submit">Tnit!</button>
199 </span>
200 </div><!-- /input-group -->
201 </div>
202 </form>
203 """
204 end
205
206 # Show error if any
207 var error_html
208 if error != null then
209 error_html = """
210 <div class="alert alert-danger alert-dismissible" role="alert">
211 <button type="button" class="close" data-dismiss="alert"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
212 {{{error}}}
213 </div>
214 """
215 else error_html = ""
216
217 # Load the last 16 Tnits
218 var posts = db.list_posts(0, 16)
219 db.close
220
221 var html_posts = new Array[String]
222 for post in posts do
223 html_posts.add "<tr><td>@{post.user.html_escape}</td><td>{post.text.html_escape}</td></tr>"
224 end
225
226 content += """
227 <div class="panel panel-default">
228 <div class="panel-heading">Latest Tnits</div>
229 <table class="table table-striped">
230 {{{html_posts.join("\n")}}}
231 </table>
232 </div>
233 </div>
234 """
235
236 # Get page from template, we replace the header first so we can replace
237 # everything on the same body afterwards
238 var body = template.
239 replace("%header%", header).
240 replace("%app_path%", request.uri.strip_extension(turi) + "/").
241 replace("%header_right%", login_or_out).
242 replace("%content%", error_html + content)
243
244 # Build response
245 var response = new HttpResponse(200)
246 response.body = body
247 response.session = session
248 return response
249 end
250 end
251
252 # Tnitter RESTful interface
253 class TnitterREST
254 super Action
255
256 redef fun answer(request, turi)
257 do
258 if turi == "/list" then
259 # list?from=1&count=2 -> Error | Array[Post]
260
261 var from = request.int_arg("from") or else 0
262 var count = request.int_arg("count") or else 8
263
264 var db = new DB.open(tnitter_db_path)
265 var posts = db.list_posts(from, count)
266 db.close
267
268 var response = new HttpResponse(200)
269 response.body = posts.serialize_to_json
270 return response
271 end
272
273 # Format not recognized
274 var error = new Error("Bad Request")
275 var response = new HttpResponse(400)
276 response.body = error.serialize_to_json
277 return response
278 end
279 end