contrib: Intro Opportunity, a meetup planner in Nit
authorLucas Bajolet <r4pass@hotmail.com>
Mon, 29 Sep 2014 21:19:49 +0000 (17:19 -0400)
committerLucas Bajolet <r4pass@hotmail.com>
Wed, 8 Oct 2014 14:59:42 +0000 (10:59 -0400)
Signed-off-by: Lucas Bajolet <r4pass@hotmail.com>

contrib/opportunity/src/opportunity_controller.nit [new file with mode: 0644]
contrib/opportunity/src/opportunity_model.nit [new file with mode: 0644]
contrib/opportunity/src/opportunity_web.nit [new file with mode: 0644]
contrib/opportunity/src/templates/boilerplate.nit [new file with mode: 0644]
contrib/opportunity/src/templates/meetup.nit [new file with mode: 0644]
contrib/opportunity/src/templates/meetup_confirmation.nit [new file with mode: 0644]
contrib/opportunity/src/templates/meetup_creation.nit [new file with mode: 0644]
contrib/opportunity/src/templates/templates.nit [new file with mode: 0644]
contrib/opportunity/src/templates/welcome.nit [new file with mode: 0644]

diff --git a/contrib/opportunity/src/opportunity_controller.nit b/contrib/opportunity/src/opportunity_controller.nit
new file mode 100644 (file)
index 0000000..032b7d3
--- /dev/null
@@ -0,0 +1,205 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License
+
+# Actions for Opportunity web application
+module opportunity_controller
+
+import nitcorn
+import sha1
+import templates
+import opportunity_model
+
+# Any kind of opportunity `Action` (serves a request)
+abstract class OpportunityAction
+       super Action
+
+       # Path to db
+       var db_path = "opportunity"
+
+       # Returns a bad request with an error code 400
+       #
+       # TODO: Add a specific body to the bad request page.
+       fun bad_req: HttpResponse do
+               var rsp = new HttpResponse(400)
+               rsp.body = (new OpportunityHomePage).write_to_string
+               return rsp
+       end
+end
+
+# Welcome page for Opportunity
+class OpportunityWelcome
+       super OpportunityAction
+
+       redef fun answer(request, url) do
+               print "Received request for {url}"
+               var get = request.get_args
+               var rq = url.split("/")
+               if rq.has("meetup_create") then
+                       var mname = request.string_arg("meetup_name")
+                       var mdate = request.string_arg("meetup_date")
+                       var mplace = request.string_arg("meetup_place")
+                       if mname == null or mdate == null or mplace == null then return bad_req
+                       var db = new OpportunityDB.open(db_path)
+                       var meet = new Meetup(mname, mdate, mplace)
+                       if meet == null then
+                               db.close
+                               return bad_req
+                       end
+                       meet.commit(db)
+                       var ans_tmp = "answer_"
+                       var cnt = 1
+                       loop
+                               var anss = request.string_arg(ans_tmp + cnt.to_s)
+                               if anss == null then break
+                               var ans = new Answer(anss)
+                               ans.meetup = meet
+                               ans.commit(db)
+                               cnt += 1
+                       end
+                       db.close
+                       var rsp = new HttpResponse(200)
+                       if meet.id == "" then
+                               rsp.body = (new MeetupCreationPage).write_to_string
+                       else
+                               rsp.body = (new MeetupConfirmation(meet)).write_to_string
+                       end
+                       return rsp
+               end
+               if rq.has("new_meetup") then
+                       var rsp = new HttpResponse(200)
+                       var page = new MeetupCreationPage
+                       rsp.body = page.write_to_string
+                       return rsp
+               end
+               if get.has_key("meetup_id") then
+                       var rsp = new HttpResponse(200)
+                       rsp.body = (new OpportunityMeetupPage.from_id(get["meetup_id"])).write_to_string
+                       return rsp
+               end
+               var rsp = new HttpResponse(200)
+               rsp.body = (new OpportunityHomePage).write_to_string
+               return rsp
+       end
+
+end
+
+# Any kind of REST request to Opportunity
+class OpportunityRESTAction
+       super OpportunityAction
+
+       redef fun answer(request, uri) do
+               print "Received REST request from {uri}"
+               var get = request.get_args
+               var req = uri.split("/")
+               if req.has("people") then
+                       return (new OpportunityPeopleREST).answer(request, uri)
+               else if req.has("answer") then
+                       return (new OpportunityAnswerREST).answer(request, uri)
+               else if req.has("meetup") then
+                       return (new OpportunityMeetupREST).answer(request, uri)
+               else
+                       return new HttpResponse(400)
+               end
+       end
+
+end
+
+# REST Actions working on People
+class OpportunityPeopleREST
+       super OpportunityAction
+
+       redef fun answer(request, uri) do
+               # Should be DELETE for true REST API
+               # TODO : change method to DELETE once supported by Nitcorn
+               if request.method == "POST" then
+                       var meth = request.string_arg("method")
+                       if meth == null then return bad_req
+                       if meth != "DELETE" then return bad_req
+                       var pid = request.int_arg("p_id")
+                       if pid == null then return bad_req
+                       var db = new OpportunityDB.open(db_path)
+                       db.remove_people_by_id(pid)
+                       db.close
+                       return new HttpResponse(200)
+               end
+               return new HttpResponse(400)
+       end
+
+end
+
+# REST Actions working on Answers
+class OpportunityAnswerREST
+       super OpportunityAction
+
+       redef fun answer(request, uri) do
+               var persid = request.int_arg("pers_id")
+               var ansid = request.int_arg("answer_id")
+               var ans = request.bool_arg("answer")
+               if persid == null or ansid == null or ans == null then return bad_req
+               var db = new OpportunityDB.open(db_path)
+               db.change_answer(ansid, persid, ans)
+               db.close
+               return new HttpResponse(200)
+       end
+end
+
+# REST Actions working on Meetups
+class OpportunityMeetupREST
+       super OpportunityAction
+
+       redef fun answer(request, uri) do
+               var args = uri.split("/")
+               if args.has("new_pers") then
+                       var name = request.string_arg("persname")
+                       var m_id = request.string_arg("meetup_id")
+                       var ans = request.string_arg("answers").split("&")
+                       if name == null or m_id == null then return bad_req
+                       print ans
+                       var ansmap = new HashMap[Int, Bool]
+                       for i in ans do
+                               var mp = i.split("=")
+                               var b = false
+                               if mp.last == "true" then b = true
+                               var id = mp.first.split("_").last
+                               if not id.is_numeric then continue
+                               ansmap[id.to_i] = b
+                       end
+                       var db = new OpportunityDB.open(db_path)
+                       var m = db.find_meetup_by_id(m_id)
+                       var sublen = name.index_of(' ')
+                       var rname = ""
+                       var rsurname = ""
+                       if sublen == -1 then
+                               rsurname = name
+                       else
+                               rsurname = name.substring(0, sublen)
+                               rname = name.substring_from(sublen + 1)
+                       end
+                       var p = new People(rname, rsurname)
+                       for i in m.answers(db) do
+                               if not ansmap.has_key(i.id) then
+                                       p.answers[i] = false
+                               else
+                                       p.answers[i] = ansmap[i.id]
+                               end
+                       end
+                       p.commit(db)
+                       db.close
+                       var rsp = new HttpResponse(200)
+                       rsp.body = p.id.to_s
+                       return rsp
+               end
+               return new HttpResponse(400)
+       end
+end
diff --git a/contrib/opportunity/src/opportunity_model.nit b/contrib/opportunity/src/opportunity_model.nit
new file mode 100644 (file)
index 0000000..fa9e674
--- /dev/null
@@ -0,0 +1,301 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License
+
+# Model for the data of Opportunity
+module opportunity_model
+
+import sqlite3
+import sha1
+import serialization
+
+# A SQLiteDB object for `Opportunity`
+class OpportunityDB
+       super Sqlite3DB
+
+       init open(x) do
+               super
+
+               create_db
+       end
+
+       # Creates the tables and triggers for Opportunity (SQLite3 DB)
+       fun create_db do
+               assert create_table("IF NOT EXISTS meetups (id CHAR(40) PRIMARY KEY, name TEXT, date TEXT, place TEXT);") else
+                       print error or else "?"
+               end
+               assert create_table("IF NOT EXISTS people(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, surname TEXT);") else
+                       print error or else "?"
+               end
+               assert create_table("IF NOT EXISTS answers(id INTEGER PRIMARY KEY AUTOINCREMENT, meetup_id CHAR(40), name TEXT, FOREIGN KEY(meetup_id) REFERENCES meetups(id));") else
+                       print error or else "?"
+               end
+               assert create_table("IF NOT EXISTS part_answers(id_part INTEGER, id_ans INTEGER, value INTEGER, FOREIGN KEY(id_part) REFERENCES people(id), FOREIGN KEY(id_ans) REFERENCES answers(id));") else
+                       print error or else "?"
+               end
+               #NOTE: The following triggers could be replaced by ON DELETE CASCADE clauses
+               # Thing is, SQLite does not seem to support those operations (well, not by default, it seems
+               # we must re-compile the lib to support it. So, well, let's just create triggers heh.
+               assert execute("CREATE TRIGGER IF NOT EXISTS answers_clean AFTER DELETE ON meetups BEGIN DELETE FROM answers WHERE answers.meetup_id=OLD.id;END;") else
+                       print error or else "?"
+               end
+               assert execute("CREATE TRIGGER IF NOT EXISTS ans_clean AFTER DELETE ON answers BEGIN DELETE FROM part_answers WHERE OLD.id=part_answers.id_ans;END;") else
+                       print error or else "?"
+               end
+               assert execute("CREATE TRIGGER IF NOT EXISTS ppl_clean AFTER DELETE ON people BEGIN DELETE FROM part_answers WHERE OLD.id=part_answers.id_part;END;")
+       end
+
+       # Find a `People` by its id, or `null` if it could not be found
+       fun find_people_by_id(id: Int): nullable People do
+               var req = select("* from people where id={id};")
+               for i in req do
+                       return new People.from_db(i[0].to_i, i[1].to_s, i[2].to_s)
+               end
+               return null
+       end
+
+       # Find a `Meetup` by its id or `null` if it could not be found
+       fun find_meetup_by_id(id: String): nullable Meetup do
+               var req = select("* FROM meetups where id={id.to_sql_string};")
+               for i in req do
+                       return new Meetup.from_db(i[0].to_s, i[1].to_s, i[2].to_s, i[3].to_s)
+               end
+               return null
+       end
+
+       # Change an Answer `ansid` for someone with an id `pid` to `resp`
+       #
+       # Returns `true` if the request was sucessful, false otherwise
+       fun change_answer(pid: Int, ansid: Int, resp: Bool): Bool do
+               var rsp = 0
+               if resp then rsp = 1
+               var rq = execute("INSERT OR REPLACE INTO part_answers(id_part, id_ans, value) VALUES({pid},{ansid},{rsp});")
+               if not rq then
+                       print "Error while updating answer {ansid}:{pid}"
+                       print error or else "Unknown error"
+                       return false
+               end
+               return true
+       end
+
+       # Removes a person in the Database by its `id`
+       #
+       # Returns true if sucessful, false otherwise
+       fun remove_people_by_id(id: Int): Bool do
+               var rq = execute("DELETE FROM people WHERE id = {id};")
+               if not rq then
+                       print "Cannot delete people {id}"
+                       print error or else "Unknown error"
+                       return false
+               end
+               return true
+       end
+end
+
+# Any kind of Database Object that can be persisted to the database
+abstract class DBObject
+
+       # Commits the modifications done to the Object in the database
+       fun commit(db: OpportunityDB): Bool is abstract
+end
+
+# A Meetup participant, linked to the DB
+class People
+       super DBObject
+
+       # ID in the Database, -1 if not set
+       var id: Int = -1
+       # Name of the participant
+       var name: String
+       # Surname of the participant
+       var surname: String
+       # Map of the answers of a Meetup and the answers of the participant
+       var answers: Map[Answer, Bool] = new HashMap[Answer, Bool]
+
+       # To be used internally when fetching the `People` in Database
+       private init from_db(id: Int, name, surname: String) do
+               init(name, surname)
+               self.id = id
+       end
+
+       # Changes an answer `ans` (or adds it)
+       fun answer=(ans: Answer, resp: Bool) do
+               answers[ans] = resp
+       end
+
+       # Loads the answers for a Meetup
+       #
+       # NOTE: If `self` does not exist in the Database, no answers will be fetched
+       fun load_answers(db: OpportunityDB, meetup: Meetup) do
+               self.answers = new HashMap[Answer, Bool]
+               var req = db.select("answers.id, answers.name, part_answers.value FROM part_answers, answers WHERE part_answers.id_part={id} AND answers.id=part_answers.id_ans AND answers.meetup_id={meetup.id.to_sql_string} GROUP BY answers.id;")
+               for i in req do
+                       var ans = new Answer.from_db(i[0].to_i, i[1].to_s)
+                       answers[ans] = false
+                       if i[2].to_i == 1 then answers[ans] = true
+               end
+       end
+
+       redef fun to_s do return "{surname.capitalized} {name.capitalized}"
+
+       redef fun commit(db) do
+               if id == -1 then
+                       if not db.execute("INSERT INTO people (name,surname) VALUES ({name.to_sql_string}, {surname.to_sql_string});") then
+                               print "Error while adding people {self}"
+                               print db.error or else "Unknown error"
+                               return false
+                       end
+                       id = db.last_insert_rowid
+               else
+                       if not db.execute("UPDATE people SET name={name.to_sql_string}, surname={surname.to_sql_string} WHERE ID={id};") then
+                               print "Error while updating people {self}"
+                               print db.error or else "Unknown error"
+                               return false
+                       end
+               end
+               for i,j in answers do
+                       if i.id == -1 then i.commit(db)
+                       var val = 0
+                       if j then val = 1
+                       if not db.execute("INSERT OR REPLACE INTO part_answers(id_part, id_ans, value) VALUES ({id},{i.id},{val});") then
+                               print("Error while adding/replacing part_answers {id}|{i.id}|{j}")
+                               print db.error or else "Unknown error"
+                               return false
+                       end
+               end
+               return true
+       end
+end
+
+# A `Meetup` is an opportunity of meeting, linked to the database
+class Meetup
+       super DBObject
+
+       # ID of the meetup, SHA-1 of the informations that are contained
+       var id: String = ""
+       # Name for the meetup
+       var name: String
+       # SQLite-formatted date : YYYY:DD:MM HH:MM:SS
+       var date: String
+       # Place of the meetup
+       var place: String
+
+       # Builds the object with all the informations found in the database
+       private init from_db(id, name, date, place: String) do
+               self.id = id
+               self.name = name
+               self.date = date
+               self.place = place
+       end
+
+       # Gets the answers bound to the current `Meetup`
+       fun answers(db: OpportunityDB): Array[Answer] do
+               if id == "" then
+                       return new Array[Answer]
+               end
+               var res = db.select("id, name FROM answers WHERE meetup_id={id.to_sql_string}")
+               var ans = new Array[Answer]
+               for i in res do
+                       ans.add new Answer.from_db(i[0].to_i, i[1].to_s)
+               end
+               return ans
+       end
+
+       # Gets the list of the participants of a `Meetup`
+       fun participants(db: OpportunityDB): Array[People] do
+               var resp = db.select("people.* FROM people, meetups, answers, part_answers WHERE meetups.id={id.to_sql_string} AND answers.meetup_id={id.to_sql_string} AND part_answers.id_ans=answers.id AND people.id=part_answers.id_part GROUP BY people.id;")
+               var arr = new Array[People]
+               for i in resp do
+                       arr.add (new People.from_db(i[0].to_i, i[1].to_s, i[2].to_s))
+               end
+               return arr
+       end
+
+       redef fun commit(db) do
+               if id == "" then
+                       var tmpid = (name + date + place).sha1_to_s
+                       if not db.execute("INSERT INTO meetups (id, name, date, place) VALUES({tmpid.to_sql_string}, {name.to_sql_string}, {date.to_sql_string}, {place.to_sql_string});") then
+                               print "Error recording entry Meetup {self}"
+                               print db.error or else "Null error"
+                               return false
+                       end
+                       id = tmpid
+                       return true
+               else
+                       return db.execute("UPDATE meetups (name, date, place) VALUES({name.to_sql_string}, {date.to_sql_string}, {place.to_sql_string}) WHERE ID={id.to_sql_string};")
+               end
+       end
+
+       redef fun to_s do
+               return "Event : {name}\nWhen : {date}\nWhere : {place}"
+       end
+end
+
+# An answer linked to a Meetup in the database
+class Answer
+       super DBObject
+
+       # Name of the answer (title)
+       var name: String
+       # Id in the database, -1 if not set
+       var id: Int = -1
+       # Meetup the answer is linked to (null while it is not added in the database or set via API)
+       var meetup: nullable Meetup = null is writable
+
+       # To be used internally when fetching the object from Database
+       private init from_db(id: Int, name: String) do
+               init name
+               self.id = id
+       end
+
+       # Loads the Meetup associated to `self`
+       #
+       # REQUIRE: is loaded in database
+       fun load_meetup(db: OpportunityDB): Meetup do
+               assert id != null
+               var res = db.select("meetups.* FROM meetups, answers WHERE answers.id={id} AND answers.meetup_id=meetups.id;")
+               for i in res do
+                       return new Meetup.from_db(i[0].to_s, i[1].to_s, i[2].to_s, i[3].to_s)
+               end
+               # If no Meetup could be loaded, the contract was not respected
+               abort
+       end
+
+       redef fun commit(db) do
+               var m = meetup
+               if m == null then return false
+               if m.id == "" then
+                       if not m.commit(db) then
+                               print "Error when creating meetup {m}"
+                               return false
+                       end
+               end
+               if id == -1 then
+                       if not db.execute("INSERT INTO answers (name, meetup_id) VALUES({name.to_sql_string}, {m.id.to_sql_string});") then
+                               print "Cannot create {self} in database"
+                               print db.error or else "Unknown error"
+                               return false
+                       end
+                       id = db.last_insert_rowid
+               else
+                       if not db.execute("UPDATE answers (name) VALUES ({name.to_sql_string}) WHERE meetup_id={m.id.to_sql_string};") then
+                               print "Error updating {self} in database"
+                               print db.error or else "Unknown error"
+                               return false
+                       end
+               end
+               return true
+       end
+
+       redef fun to_s do return name
+end
diff --git a/contrib/opportunity/src/opportunity_web.nit b/contrib/opportunity/src/opportunity_web.nit
new file mode 100644 (file)
index 0000000..73a3b39
--- /dev/null
@@ -0,0 +1,35 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License
+
+# Web server for Opportunity : A meetup scheduler for real-world encounters !
+module opportunity_web
+
+import opportunity_controller
+import opts
+import privileges
+
+# Avoids a crash when running automatic tests
+if "NIT_TESTING".environ == "true" then exit 0
+
+var iface = "localhost:80"
+
+var vh = new VirtualHost(iface)
+vh.routes.add new Route("/rest/", new OpportunityRESTAction)
+vh.routes.add new Route(null, new OpportunityWelcome)
+
+var fac = new HttpFactory.and_libevent
+fac.config.virtual_hosts.add vh
+
+print "Launching server on http://{iface}/"
+fac.run
diff --git a/contrib/opportunity/src/templates/boilerplate.nit b/contrib/opportunity/src/templates/boilerplate.nit
new file mode 100644 (file)
index 0000000..cb2b11c
--- /dev/null
@@ -0,0 +1,124 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License
+
+# Contains the main components of a webpage for Opportunity
+module boilerplate
+
+import template
+
+# Header for a Opportunity page
+class OpportunityHeader
+       super Template
+
+       # Javascript code that is included in the `OpportunityPage`
+       var page_js = "" is writable
+
+       redef fun rendering do
+               add """
+<!DOCTYPE html>
+<html>
+<head>
+       <title>Opportunity - The meetup planner</title>
+       <meta charset="utf-8">
+       <meta name="viewport" content="width=device-width, initial-scale=1">
+       <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
+       <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
+       <script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
+       <script>
+               {{{page_js}}}
+       </script>
+       <style>
+               .menu {
+                       background-color: #0d8921;
+                       background-repeat: repeat-x;
+                       color: white;
+               }
+               .navbar-brand{
+                       color: white;
+               }
+               .navbar-brand:hover{
+                       color: #EEEEEE;
+               }
+               .navbar-nav{
+                       color: white;
+               }
+               .navbar-nav: hover{
+                       background-color: #0d8921;
+                       color: white;
+               }
+               .answer:hover {
+                       cursor:pointer;
+                       background-color:#0d8921;
+                       color:white;
+               }
+               .opportunity-action:hover {
+                       cursor:pointer;
+               }
+       </style>
+</head>
+<body>
+<nav class="menu" role="navigation">
+       <div class="container">
+               <div class="navbar-header">
+                       <a class="navbar-brand" href="/" >Opportunity</a>
+               </div>
+       </div>
+</nav>
+<div class="container-fluid">
+"""
+       end
+
+end
+
+# Footer for a Opportunity page
+class OpportunityFooter
+       super Template
+
+       redef fun rendering do
+               add """
+</div>
+</body>
+<div class="footer">
+       <div class="well well-sm">
+               <p class="text-muted text-center">
+                       Opportunity, the meetup planner.
+               </p>
+               <p class="text-muted text-center">
+                       Proudly powered by <a href="http://nitlanguage.org/">Nit</a>!
+               </p>
+       </div>
+</div>
+</html>
+"""
+       end
+
+end
+
+# Any Opportunity page that contains the header, body and footer.
+class OpportunityPage
+       super Template
+
+       var header = new OpportunityHeader
+
+       var body: Streamable = "" is writable
+
+       var footer = new OpportunityFooter
+
+       redef fun rendering do
+               add header
+               add body
+               add footer
+       end
+
+end
diff --git a/contrib/opportunity/src/templates/meetup.nit b/contrib/opportunity/src/templates/meetup.nit
new file mode 100644 (file)
index 0000000..d9af51d
--- /dev/null
@@ -0,0 +1,193 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License
+
+# Shows a meetup and allows to modify its participants
+module meetup
+
+import opportunity_model
+import boilerplate
+import welcome
+import template
+
+# Shows a meetup and allows to modify its participants
+class OpportunityMeetupPage
+       super OpportunityPage
+
+       # Meetup the page is supposed to show
+       var meetup: nullable Meetup = null
+
+       init from_id(id: String) do
+               var db = new OpportunityDB.open("opportunity")
+               meetup = db.find_meetup_by_id(id)
+               db.close
+       end
+
+       init do
+               header.page_js = """
+               function change_answer(ele){
+                       var e = document.getElementById(ele.id);
+                       var i = e.innerHTML;
+                       var ans = true;
+                       if(i === "<center>✔</center>"){
+                               ans = false;
+                               e.innerHTML = "<center>✘</center>"
+                               e.style.color = "red";
+                       }else{
+                               e.innerHTML = "<center>✔</center>";
+                               e.style.color = "green";
+                       }
+                       var a = ele.id.split('_')
+                       var pid = a[1]
+                       var aid = a[2]
+                       $.ajax({
+                               type: "POST",
+                               url: "/rest/answer",
+                               data: {
+                                       answer_id: aid,
+                                       pers_id: pid,
+                                       answer: ans
+                               }
+                       });
+               }
+               function change_temp_answer(ele){
+                       var e = document.getElementById(ele.id);
+                       var i = e.innerHTML;
+                       var ans = true;
+                       if(i === "<center>✔</center>"){
+                               ans = false;
+                               e.innerHTML = "<center>✘</center>";
+                               e.style.color = "red";
+                       }else{
+                               e.innerHTML = "<center>✔</center>";
+                               e.style.color = "green";
+                       }
+               }
+               function add_part(ele){
+                       var e = document.getElementById(ele.id);
+                       var pname = document.getElementById("new_name").value;
+                       var arr = e.id.split("_");
+                       var mid = arr[1];
+                       var ans = $('#' + ele.id).parent().parent().parent().children(".answer");
+                       ansmap = {};
+                       for(i=0;i<ans.length;i++){
+                               var curr = ans.eq(i)
+                               if(curr[0].innerHTML === "✔"){
+                                       ansmap[curr.attr('id')] = true
+                               }else{
+                                       ansmap[curr.attr('id')] = false
+                               }
+                       }
+                       $.ajax({
+                               type: "POST",
+                               url: "/rest/meetup/new_pers",
+                               data: {
+                                       meetup_id: mid,
+                                       persname: pname,
+                                       answers: $.param(ansmap)
+                               }
+                               })
+                               .done(function(data){
+                                       location.reload();
+                               })
+                               .fail(function(data){
+                                       //TODO: Notify of failure
+                               });
+               }
+               function remove_people(ele){
+                       var arr = ele.id.split("_")
+                       var pid = arr[1]
+                       $('#' + ele.id).parent().remove();
+                       $.ajax({
+                               type: "POST",
+                               url: "/rest/people",
+                               data: {
+                                       method: "DELETE",
+                                       p_id: pid
+                               }
+                       });
+               }
+               """
+       end
+
+
+       redef fun rendering do
+               if meetup == null then
+                       add((new OpportunityHomePage).write_to_string)
+                       return
+               end
+               add header
+               var db = new OpportunityDB.open("opportunity")
+               add meetup.to_html(db)
+               db.close
+               add footer
+       end
+end
+
+redef class Meetup
+       # Build the HTML for `self`
+       fun to_html(db: OpportunityDB): Streamable do
+               var t = new Template
+               t.add """
+<div class="page-header">
+       <center><h1>{{{name}}}</h1></center>
+       <center><h4>When : {{{date}}}</h4></center>
+       <center><h4>Where : {{{place}}}</h4></center>
+</div>
+<table class="table">
+"""
+               t.add "<th></th>"
+               t.add "<th>Participating</th>"
+               for i in answers(db) do
+                       t.add "<th>"
+                       t.add i.to_s
+                       t.add "</th>"
+               end
+               t.add "</tr>"
+               for i in participants(db) do
+                       t.add "<tr>"
+                       t.add """<td class="opportunity-action" style="color: red;" onclick="remove_people(this)" id="remove_{{{i.id}}}"><center>❌</center></td>"""
+                       i.load_answers(db, self)
+                       t.add "<td>"
+                       t.add i.to_s
+                       t.add "</td>"
+                       for j,k in i.answers do
+                               t.add """<td class="answer" onclick="change_answer(this)" id="answer_{{{j.id}}}_{{{i.id}}}""""
+                               if k then
+                                       t.add " style=\"color:green;\""
+                               else
+                                       t.add " style=\"color:red;\""
+                               end
+                               t.add"><center>"
+                               if k then
+                                       t.add "✔"
+                               else
+                                       t.add "✘"
+                               end
+                               t.add "</center></td>"
+                       end
+                       t.add "</tr>"
+               end
+               t.add """
+<tr id="newrow">
+<td><center><span id="add_{{{id}}}" onclick="add_part(this)" style="color:green;" class="action"><strong>+</strong></span></center></td>
+       <td><input id="new_name" type="text" placeholder="Your name" class="input-large"></td>
+               """
+               for i in answers(db) do
+                       t.add "<td class=\"answer\" id=\"newans_{i.id}\" onclick=\"change_temp_answer(this)\" style=\"color:red;\"><center>✘</center></td>"
+               end
+               t.add "</tr>"
+               t.add "</table>"
+               return t
+       end
+end
diff --git a/contrib/opportunity/src/templates/meetup_confirmation.nit b/contrib/opportunity/src/templates/meetup_confirmation.nit
new file mode 100644 (file)
index 0000000..0752a37
--- /dev/null
@@ -0,0 +1,44 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License
+
+# Page to show when the creation of a Meetup is successful
+module meetup_confirmation
+
+import boilerplate
+import opportunity_model
+
+# Show this page when a `Meetup` is sucessfully created.
+class MeetupConfirmation
+       super OpportunityPage
+
+       var meetup: Meetup
+
+       init do
+               body = """
+               <div class="page-header">
+                       <center><h1>Congratulations !</h1></center>
+               </div>
+               <p class="text-center">
+                       <h2> Your meetup was successfully created. </h2>
+                       <p>
+                       You can invite people to participate to your event by sharing them this link : <a href="/?meetup_id={{{meetup.id}}}">{{{meetup.name}}}</a>
+                       </p>
+                       <p>
+                       See you soon for more Opportunities !
+                       </p>
+               </p>
+               """
+       end
+
+end
diff --git a/contrib/opportunity/src/templates/meetup_creation.nit b/contrib/opportunity/src/templates/meetup_creation.nit
new file mode 100644 (file)
index 0000000..8cd364a
--- /dev/null
@@ -0,0 +1,66 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License
+
+module meetup_creation
+
+import boilerplate
+
+class MeetupCreationPage
+       super OpportunityPage
+
+       init do
+               header.page_js = """
+                       function new_answer(sender){
+                               var ansdiv = $('#answers');
+                               var nb = ansdiv.children()
+                               var s = nb.last();
+                               var ss = s.attr("id").split('_');
+                               var l = ss[ss.length - 1];
+                               nb = parseInt(l) + 1;
+                               var ch = ansdiv.children();
+                               ch.last().after('<input name="answer_' + nb + '" id="answer_' + nb + '" class="form-control" type="text" placeholder="Answer">')
+                               ch.last().after('<label for="answer_' + nb + '">' + nb + '</label>');
+                       }
+               """
+               body = """
+               <div class="page-header">
+                       <center><h1>New meetup</h1></center>
+               </div>
+               <center>
+               <form action="meetup_create" method="POST" role="form">
+                       <div class = "form-group">
+                               <label for="meetup_name">Meetup Name : </label>
+                               <input name="meetup_name" id="meetup_name" type="text" class="form-control" placeholder="Meetup Name"/>
+                               <label for="meetup_date">When ? </label>
+                               <input name="meetup_date" id="meetup_date" type="text" class="form-control" placeholder="Time of the event">
+                               <label for="meetup=place">Where ? </label>
+                               <input name="meetup_place" id="meetup_place" type="text" class="form-control" placeholder="Place of the event">
+                       </div>
+                       <div id="answers" class="form-group">
+                               <h2>Answers</h2>
+                               <label for="answer_1">1</label>
+                               <input name="answer_1" id="answer_1" type="text" class="form-control" placeholder="Answer">
+                       </div>
+                       <div class="form-group">
+                               <button type="button" class="btn btn-lg" onclick="new_answer(this)">Add answer</button>
+                       </div>
+                       <div class="form-group">
+                               <button type="submit" class="btn btn-lg btn-success">Finish</button>
+                       </div>
+               </form>
+               </center>
+"""
+       end
+
+end
diff --git a/contrib/opportunity/src/templates/templates.nit b/contrib/opportunity/src/templates/templates.nit
new file mode 100644 (file)
index 0000000..d2dfd9c
--- /dev/null
@@ -0,0 +1,22 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License
+
+# All the templates used for the view are to be declared here
+module templates
+
+import boilerplate
+import welcome
+import meetup_creation
+import meetup
+import meetup_confirmation
diff --git a/contrib/opportunity/src/templates/welcome.nit b/contrib/opportunity/src/templates/welcome.nit
new file mode 100644 (file)
index 0000000..eaae527
--- /dev/null
@@ -0,0 +1,41 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License
+
+# Welcome page for Opportunity
+module welcome
+
+import boilerplate
+
+# Welcome page for Opportunity
+class OpportunityHomePage
+       super OpportunityPage
+
+       init do
+               body = """
+               <div class="page-header">
+               <h1 class="text-center">Welcome to opportunity !</h1>
+                       </div>
+                       <p class="text-center">
+                               <p class="text-center">Opportunity is a free (as in free software), easy-to-use, meetup planifier.</p>
+                               <p class="text-center">You can start using it right now by creating a new Meetup and sharing it with your friends!</p>
+                               <p class="text-center">
+                               <form action="new_meetup">
+                               <button type="submit" class="btn btn-lg center-block btn-success">Create a Meetup</button>
+                               </form>
+                               </p>
+                       </p>
+"""
+       end
+
+end