X-Git-Url: http://nitlanguage.org diff --git a/src/web/api_feedback.nit b/src/web/api_feedback.nit index 61ba756..6a356c3 100644 --- a/src/web/api_feedback.nit +++ b/src/web/api_feedback.nit @@ -16,135 +16,272 @@ module api_feedback import web_base -import mongodb +import popcorn::pop_auth -# Group all api handlers in one router -class APIFeedbackRouter - super Router +redef class NitwebConfig - # Model to pass to handlers - var model: Model + # MongoDB collection used to store stars. + var stars = new StarRatingRepo(db.collection("stars")) is lazy +end + +redef class APIRouter + + redef init do + super - # Mainmodule to pass to handlers - var mainmodule: MModule + 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)) - # Mongo collection used to store ratings - var collection: MongoCollection + use("/feedback/user/stars", new APIUserStars(config)) - init do - use("/stars/:id", new APIStars(model, mainmodule, collection)) + use("/feedback/stars/:id", new APIStars(config)) + use("/feedback/stars/:id/dimension/:dimension", new APIStarsDimension(config)) end end -# Stars attributed to mentities -class APIStars +# Base handler for feedback features. +abstract class APIFeedBack super APIHandler - # Collection used to store ratings - var collection: MongoCollection + # 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.json new JsonArray.from(config.stars.most_rated) + end +end + +# Best rated entities +class APIStarsBest + super APIFeedBack + + redef fun get(req, res) do + res.json new JsonArray.from(config.stars.best_rated) + end +end + +# Best rated entities +class APIStarsWorst + super APIFeedBack + + redef fun get(req, res) do + res.json new JsonArray.from(config.stars.worst_rated) + end +end + +# Best rated entities +class APIStarsUsers + super APIFeedBack redef fun get(req, res) do + res.json 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.json 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 - res.error 404 - return - end + if mentity == null then return + res.json mentity.ratings(config, login) + end +end - res.json mentity_ratings(mentity) +# 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.json 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 - res.error 404 - return + 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.error 400 + res.api_error(400, "Expected a JSON object") return end var rating = obj["rating"] if not rating isa Int then - res.error 400 + res.api_error(400, "Expected a key `rating`") return end - var val = new MEntityRating(mentity.full_name, rating, get_time) - collection.insert(val.json) - - res.json mentity_ratings(mentity) + 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.json mentity.ratings_by_dimension(config, dimension, login) end +end - # Get the ratings of a `mentity` - fun mentity_ratings(mentity: MEntity): MEntityRatings do - var ratings = new MEntityRatings(mentity) +# 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 - var req = new JsonObject - req["mentity"] = mentity.full_name - var rs = collection.find_all(req) - for r in rs do ratings.ratings.add new MEntityRating.from_json(r) - return ratings - end -end + # The user login that made that rating (or null if anon) + var user: nullable String -# Ratings representation for a mentity -class MEntityRatings - super Jsonable + # Rated `MEntity::full_name` + var mentity: String - # MEntity rated - var mentity: MEntity + # The dimension rated (arbritrary key) + var dimension: nullable String - # List of ratings - var ratings = new Array[MEntityRating] + # The rating (traditionally a score between 0 and 5) + var rating: Int is writable - # Mean of all ratings or 0 - fun mean: Float do - if ratings.is_empty then return 0.0 - var sum = 0.0 - for r in ratings do sum += r.rating.to_f - var res = sum / ratings.length.to_f - return res + # 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 - # Json representation of `self` - fun json: JsonObject do + # 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["mentity"] = mentity.full_name - obj["ratings"] = new JsonArray.from(ratings) - obj["mean"] = mean + 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 - redef fun to_json do return json.to_json + # 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 -# Rating value of a MEntity -class MEntityRating - super Jsonable - - # MEntity this rating is about - var mentity: String - - # Rating value (between 1 and 5) - var rating: Int +# StarRating Mongo Repository +class StarRatingRepo + super MongoRepository[StarRating] - # Timestamp of this rating - var timestamp: Int + # 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 - # Init this rating value from a JsonObject - init from_json(obj: JsonObject) do - init(obj["mentity"].as(String), obj["rating"].as(Int), obj["timestamp"].as(Int)) + # 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 - # Translate this rating value to a JsonObject - fun json: JsonObject do - var obj = new JsonObject - obj["mentity"] = mentity - obj["rating"] = rating - obj["timestamp"] = timestamp - return obj + # 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 - redef fun to_json do return json.to_json + # 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