nitweb: move `web` group to `doc::api`
[nit.git] / src / doc / api / api_feedback.nit
diff --git a/src/doc/api/api_feedback.nit b/src/doc/api/api_feedback.nit
new file mode 100644 (file)
index 0000000..81faaeb
--- /dev/null
@@ -0,0 +1,287 @@
+# 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.
+
+# Feedback related features
+module api_feedback
+
+import api_base
+import popcorn::pop_auth
+
+redef class NitwebConfig
+
+       # MongoDB collection used to store stars.
+       var stars = new StarRatingRepo(db.collection("stars")) is lazy
+end
+
+redef class APIRouter
+
+       redef init do
+               super
+
+               use("/feedback/grades/most", new APIStarsMost(config))
+               use("/feedback/grades/best", new APIStarsBest(config))
+               use("/feedback/grades/worst", new APIStarsWorst(config))
+               use("/feedback/grades/users", new APIStarsUsers(config))
+
+               use("/feedback/user/stars", new APIUserStars(config))
+
+               use("/feedback/stars/:id", new APIStars(config))
+               use("/feedback/stars/:id/dimension/:dimension", new APIStarsDimension(config))
+       end
+end
+
+# Base handler for feedback features.
+abstract class APIFeedBack
+       super APIHandler
+
+       # Get the user logged in or null if no session
+       fun get_session_user(req: HttpRequest): nullable User do
+               var session = req.session
+               if session == null then return null
+               return session.user
+       end
+
+       # Get the login of the session user or null if no session
+       fun get_session_login(req: HttpRequest): nullable String do
+               var user = get_session_user(req)
+               if user == null then return null
+               return user.login
+       end
+end
+
+# Most rated entities
+class APIStarsMost
+       super APIFeedBack
+
+       redef fun get(req, res) do
+               res.api_json(req, new JsonArray.from(config.stars.most_rated))
+       end
+end
+
+# Best rated entities
+class APIStarsBest
+       super APIFeedBack
+
+       redef fun get(req, res) do
+               res.api_json(req, new JsonArray.from(config.stars.best_rated))
+       end
+end
+
+# Best rated entities
+class APIStarsWorst
+       super APIFeedBack
+
+       redef fun get(req, res) do
+               res.api_json(req, new JsonArray.from(config.stars.worst_rated))
+       end
+end
+
+# Best rated entities
+class APIStarsUsers
+       super APIFeedBack
+
+       redef fun get(req, res) do
+               res.api_json(req, new JsonArray.from(config.stars.users_ratings))
+       end
+end
+
+# Stars attributed to mentities by user
+class APIUserStars
+       super APIFeedBack
+
+       redef fun get(req, res) do
+               var user = get_session_user(req)
+               if user == null then return
+               res.api_json(req, new JsonArray.from(user.ratings(config)))
+       end
+end
+
+# Stars attributed to mentities
+class APIStars
+       super APIFeedBack
+
+       redef fun get(req, res) do
+               var login = get_session_login(req)
+               var mentity = mentity_from_uri(req, res)
+               if mentity == null then return
+               res.api_json(req, mentity.ratings(config, login))
+       end
+end
+
+# Stars attributed to mentities by dimension
+class APIStarsDimension
+       super APIFeedBack
+
+       redef fun get(req, res) do
+               var login = get_session_login(req)
+               var mentity = mentity_from_uri(req, res)
+               if mentity == null then return
+               var dimension = req.param("dimension")
+               if dimension == null then return
+               res.api_json(req, mentity.ratings_by_dimension(config, dimension, login))
+       end
+
+       redef fun post(req, res) do
+               var user = get_session_user(req)
+               var login = null
+               if user != null then login = user.login
+
+               var mentity = mentity_from_uri(req, res)
+               if mentity == null then return
+               var dimension = req.param("dimension")
+               if dimension == null then return
+
+               # Retrieve user previous rating
+               var previous = null
+               if user != null then
+                       previous = user.find_previous_rating(config, mentity, dimension)
+               end
+
+               var obj = req.body.parse_json
+               if not obj isa JsonObject then
+                       res.api_error(400, "Expected a JSON object")
+                       return
+               end
+               var rating = obj["rating"]
+               if not rating isa Int then
+                       res.api_error(400, "Expected a key `rating`")
+                       return
+               end
+
+               if previous != null then
+                       previous.rating = rating
+                       previous.timestamp = get_time
+                       config.stars.save previous
+               else
+                       config.stars.save new StarRating(login, mentity.full_name, dimension, rating)
+               end
+               res.api_json(req, mentity.ratings_by_dimension(config, dimension, login))
+       end
+end
+
+# Star ratings allow users to rate mentities with a 5-stars system.
+#
+# Each rating can consider only one `dimension` of the mentity.
+# Dimensions are arbitrary strings used to group ratings.
+class StarRating
+       super RepoObject
+       serialize
+
+       # The user login that made that rating (or null if anon)
+       var user: nullable String
+
+       # Rated `MEntity::full_name`
+       var mentity: String
+
+       # The dimension rated (arbritrary key)
+       var dimension: nullable String
+
+       # The rating (traditionally a score between 0 and 5)
+       var rating: Int is writable
+
+       # Timestamp when this rating was created
+       var timestamp = 0 is writable
+end
+
+redef class User
+
+       # Find a previous rating of `self` for `mentity` and `dimension`
+       fun find_previous_rating(config: NitwebConfig, mentity: MEntity, dimension: nullable String): nullable StarRating do
+               var match = new MongoMatch
+               match.eq("mentity", mentity.full_name)
+               match.eq("dimension", dimension)
+               match.eq("user", login)
+               return config.stars.find(match)
+       end
+
+       # Find all ratings by `self`
+       fun ratings(config: NitwebConfig): Array[StarRating] do
+               return config.stars.find_all((new MongoMatch).eq("user", login))
+       end
+end
+
+redef class MEntity
+
+       # Get the ratings of a `dimension`
+       fun ratings_by_dimension(config: NitwebConfig, dimension: String, user: nullable String): JsonObject do
+               var match = (new MongoMatch).eq("mentity", full_name).eq("dimension", dimension)
+               var pipeline = new MongoPipeline
+               pipeline.match(match)
+               pipeline.group((new MongoGroup("mean_group")).avg("mean", "$rating"))
+
+               var res = config.stars.collection.aggregate(pipeline)
+               var obj = new JsonObject
+               obj["mean"] = if res.is_empty then 0.0 else res.first["mean"]
+               obj["ratings"] = new JsonArray.from(config.stars.find_all(match))
+
+               if user != null then
+                       match["user"] = user
+                       obj["user"] = config.stars.find(match)
+               end
+               return obj
+       end
+
+       # Get the ratings of a `mentity`
+       fun ratings(config: NitwebConfig, user: nullable String): JsonObject do
+               var match = new JsonObject
+               match["mentity"] = full_name
+               match["ratings"] = new JsonArray.from(config.stars.find_all(match))
+               match["feature"] = ratings_by_dimension(config, "feature", user)
+               match["doc"] = ratings_by_dimension(config, "doc", user)
+               match["examples"] = ratings_by_dimension(config, "examples", user)
+               match["code"] = ratings_by_dimension(config, "code", user)
+               return match
+       end
+end
+
+# StarRating Mongo Repository
+class StarRatingRepo
+       super MongoRepository[StarRating]
+
+       # Find most rated mentities
+       fun most_rated: Array[JsonObject] do
+               var pipeline = new MongoPipeline
+               pipeline.group((new MongoGroup("$mentity")).sum("count", 1))
+               pipeline.sort((new MongoMatch).eq("count", -1))
+               pipeline.limit(10)
+               return collection.aggregate(pipeline)
+       end
+
+       # Find best rated mentities
+       fun best_rated: Array[JsonObject] do
+               var pipeline = new MongoPipeline
+               pipeline.group((new MongoGroup("$mentity")).avg("avg", "$rating"))
+               pipeline.sort((new MongoMatch).eq("avg", -1))
+               pipeline.limit(10)
+               return collection.aggregate(pipeline)
+       end
+
+       # Find worst rated mentities
+       fun worst_rated: Array[JsonObject] do
+               var pipeline = new MongoPipeline
+               pipeline.group((new MongoGroup("$mentity")).avg("avg", "$rating"))
+               pipeline.sort((new MongoMatch).eq("avg", 1))
+               pipeline.limit(10)
+               return collection.aggregate(pipeline)
+       end
+
+       # Find worst rated mentities
+       fun users_ratings: Array[JsonObject] do
+               var pipeline = new MongoPipeline
+               pipeline.group((new MongoGroup("$user")).sum("count", 1))
+               pipeline.sort((new MongoMatch).eq("count", -1))
+               pipeline.limit(10)
+               return collection.aggregate(pipeline)
+       end
+end