Skip to content
This repository has been archived by the owner on Oct 24, 2022. It is now read-only.

Commit

Permalink
Adds topic selections for subscribers
Browse files Browse the repository at this point in the history
Fixes #151
Fixes #93
  • Loading branch information
Richard Rodgers committed Apr 3, 2015
1 parent 2c89e4f commit 834d587
Show file tree
Hide file tree
Showing 13 changed files with 534 additions and 6 deletions.
14 changes: 14 additions & 0 deletions app/controllers/Application.scala
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,20 @@ object Application extends Controller {
).getOrElse(NotFound(views.html.static.trouble("No such hold: " + id)))
}

def pickBrowse(id: Int, page: Int) = Action { implicit request =>
Subscriber.findById(id).map( sub =>
Ok(views.html.topic_pick.browse(sub.id, sub.picks(page), page, sub.pickCount))
).getOrElse(NotFound(views.html.static.trouble("No such subscriber: " + id)))
}

def resolvePick(id: Int, accept: Boolean) = Action { implicit request =>
TopicPick.findById(id).map( pick => {
conveyor ! (pick, accept)
Redirect(routes.Application.pickBrowse(1, 0))
}
).getOrElse(NotFound(views.html.static.trouble("No such topic pick: " + id)))
}

val modelForm = Form(
single(
"model" -> nonEmptyText
Expand Down
71 changes: 71 additions & 0 deletions app/models/Agent.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Copyright (c) 2015 MIT Libraries
* Licensed under: http://www.apache.org/licenses/LICENSE-2.0
*/
package models

import play.api.db.DB
import play.api._
import play.api.Play.current

import anorm.SqlParser._
import anorm.~
import anorm.SQL

/** An agent is a software service that can perform operations for
* benefit of subscribers, such as suggesting topics to subscribe to,
* automatically subscribing or unsubscribing, etc
*
* @author richardrodgers
*/

case class Agent(id: Int,
tag: String,
label: String,
description: String,
code: String,
params: String,
icon: Option[String])

object Agent {

val agent = {
get[Int]("id") ~ get[String]("tag") ~ get[String]("label") ~ get[String]("description") ~
get[String]("code") ~ get[String]("params") ~ get[Option[String]]("icon") map {
case id ~ tag ~ label ~ description ~ code ~ params ~ icon => Agent(id, tag, label, description, code, params, icon)
}
}

def create(tag: String, label: String, description: String, code: String, params: String, icon: Option[String]) = {
DB.withConnection { implicit c =>
SQL("insert into agent (tag, label, description, code, params, icon) values ({tag}, {label}, {description}, {code}, {params}, {icon})")
.on('tag -> tag, 'label -> label, 'description -> description, 'code -> code, 'params -> params, 'icon -> icon).executeInsert()
}
}

def make(tag: String, label: String, description: String, code: String, params: String, icon: Option[String]) = {
findById(create(tag, label, description, code, params, icon).get.toInt).get
}

def all: List[Agent] = {
DB.withConnection { implicit c =>
SQL("select * from agent").as(agent *)
}
}

def findById(id: Int): Option[Agent] = {
DB.withConnection { implicit c =>
SQL("select * from agent where id = {id}").on('id -> id).as(agent.singleOpt)
}
}

def findByTag(tag: String): Option[Agent] = {
DB.withConnection { implicit c =>
SQL("select * from agent where tag = {tag}").on('tag -> tag).as(agent.singleOpt)
}
}

def mapView: Map[String, String] = {
all map (cp => cp.id.toString -> cp.tag) toMap
}
}
27 changes: 27 additions & 0 deletions app/models/Subscriber.scala
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,33 @@ case class Subscriber(id: Int, // DB key
}
}

def pickCount = {
DB.withConnection { implicit c =>
val count = SQL("select count(*) as c from topic_pick where subscriber_id = {id}").on('id -> id).apply.head
count[Long]("c")
}
}

def picked(topicId: Int): Option[TopicPick] = {
DB.withConnection { implicit c =>
SQL("select * from topic_pick where topic_id = {topic_id} and subscriber_id = {id}")
.on('topic_id -> topicId, 'id -> id).as(TopicPick.pick.singleOpt)
}
}

def picks(page: Int) = {
val offset = page * 10
DB.withConnection { implicit c =>
SQL(
"""
select * from topic_pick where subscriber_id = {sub_id}
order by created desc
limit 10 offset {offset}
"""
).on('sub_id -> id, 'offset -> offset).as(TopicPick.pick *)
}
}

def channels: List[Channel] = {
DB.withConnection { implicit c =>
SQL("select * from channel where subscriber_id = {id}").on('id -> id).as(Channel.channel *)
Expand Down
78 changes: 78 additions & 0 deletions app/models/TopicPick.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* Copyright (c) 2015 MIT Libraries
* Licensed under: http://www.apache.org/licenses/LICENSE-2.0
*/
package models

import java.util.Date

import play.api.db.DB
import play.api._
import play.api.Play.current

import anorm.SqlParser._
import anorm.~
import anorm.SQL
import anorm.Row

/** TopicPick is a topic suggestion (potential subscription) for a subscriber
* Following a review, the pick is resolved to a subscription, or discarded
*
* @author richardrodgers
*/

case class TopicPick(id: Int, // DB key
subscriberId: Int, // DB key of subscriber
topicId: Int, // DB key of selected topic
agentId: Int, // DB key of agent that created it
created: Date, // when pick created
resolved: Date) { // when pick resolved

def topic = {
DB.withConnection { implicit c =>
SQL("select * from topic where id = {topic_id}").on('topic_id -> topicId).as(Topic.topic.singleOpt).get
}
}

def resolve(accept: Boolean) = {
// not currently remembering state, so just delete
DB.withConnection { implicit c =>
SQL("delete from topic_pick where id = {id}").on('id -> id).executeUpdate()
}
}
}

object TopicPick {

val pick = {
get[Int]("id") ~ get[Int]("subscriber_id") ~ get[Int]("topic_id") ~ get[Int]("agent_id") ~
get[Date]("created") ~ get[Date]("resolved") map {
case id ~ subscriberId ~ topicId ~ agentId ~ created ~ resolved =>
TopicPick(id, subscriberId, topicId, agentId, created, resolved)
}
}

def picked(topicId: Int, subscriberId: Int) = {
DB.withConnection { implicit c =>
SQL("select * from topic_pick where topic_id = {topic_id} and subscriber_id = {subscriber_id}")
.on('topic_id -> topicId, 'subscriber_id -> subscriberId).as(pick.singleOpt).isDefined
}
}

def findById(id: Int): Option[TopicPick] = {
DB.withConnection { implicit c =>
SQL("select * from topic_pick where id = {id}").on('id -> id).as(pick.singleOpt)
}
}

def create(subscriberId: Int, topicId: Int, agentId: Int) = {
DB.withConnection { implicit c =>
SQL("insert into topic_pick (subscriber_id, topic_id, agent_id, created, resolved) values ({subscriber_id}, {topic_id}, {agent_id}, {created}, {resolved})")
.on('subscriber_id -> subscriberId, 'topic_id -> topicId, 'agent_id -> agentId, 'created -> new Date, 'resolved -> new Date).executeInsert()
}
}

def make(subscriberId: Int, topicId: Int, agentId: Int) = {
findById(create(subscriberId, topicId, agentId).get.toInt).get
}
}
8 changes: 7 additions & 1 deletion app/views/subscriber/dashboard.scala.html
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,13 @@ <h6>Note: deliveries and rejections can be seen as you click through the links b
</ul>

<h4>Suggested Subscriptions</h4>
<ul class="list-group"><li class="list-group-item">Coming (much) Later</li></ul>
<ul class="list-group">
<li class="list-group-item">
<a href="@routes.Application.pickBrowse(1)">Total to review:
<span class="badge">@Subscriber.findById(1).get.pickCount</span>
</a>
</li>
</ul>

<h4>Destinations</h4>
<ul class="list-group">
Expand Down
3 changes: 3 additions & 0 deletions app/views/topic/show.scala.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ <h3><a href="@routes.Application.scheme(topic.scheme.get.id)">@topic.scheme.get.
@if(sub.get.hasInterest(topic.scheme_id)) {
<span><a rel="tooltip" title="Subscribe to this topic" href="@routes.Application.topicSubscribe(topic.id)" class="btn btn-primary btn-large">Subscribe <span class="badge">@sub.get.newItemCountFor(topic.id)</span> &raquo;</a></span>
}
@if(sub.get.picked(topic.id).isDefined) {
<span><a rel="tooltip" title="Forget this topic" href="@routes.Application.resolvePick(sub.get.picked(topic.id).get.id, false)" class="btn btn-primary btn-large pull-right">Meh &raquo;</a></span>
}
}
}
</div>
Expand Down
17 changes: 17 additions & 0 deletions app/views/topic_pick/browse.scala.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@*****************************************************************************
* Entry page for topic pick browsing (Review) *
* Copyright (c) 2015 MIT Libraries *
*****************************************************************************@
@(id: Int, picks: List[TopicPick], page: Int, total: Long)@*(implicit hubContext: HubContext)*@
@layout.main("Topic Picks Browse - TopicHub") {
<div class="page-header">
<h2>Review Topic Picks</h2>
@tags.summary(page, 10, total, picks.length)
</div>
<ul>
@picks.map { pick =>
<li><a href="@routes.Application.topic(pick.topicId)">@pick.topic.tag</a> @pick.topic.name</li>
}
</ul>
@tags.pagination(page, 10, total, routes.Application.pickBrowse(id).toString, picks.length)
}
15 changes: 14 additions & 1 deletion app/workers/Conveyor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import play.api.libs.ws._
import play.api.mvc._
import play.api.http.HeaderNames._

import models.{Channel, Hold, Item, Scheme, Subscriber, Subscription, Topic, Transfer}
import models.{Channel, Hold, Item, Scheme, Subscriber, Subscription, Topic, TopicPick, Transfer}
import services.Emailer

/** Conveyor is the worker responsible for transmitting notifications
Expand All @@ -29,6 +29,7 @@ class ConveyorWorker extends Actor {
case sub: Subscription => Conveyor.fulfill(sub)
case (item: Item, subscr: Subscriber) => Conveyor.transferItem(item, subscr)
case (hold: Hold, accept: Boolean) => Conveyor.resolveHold(hold, accept)
case (pick: TopicPick, accept: Boolean) => Conveyor.resolvePick(pick, accept)
case _ => println("I'm lost")
}
}
Expand Down Expand Up @@ -74,6 +75,18 @@ object Conveyor {
hold.resolve(accept)
}

def resolvePick(pick: TopicPick, accept: Boolean) = {
// keep a record as a cancelled subscription? TODO
if (accept) {
val sub = Subscriber.findById(pick.subscriberId).get
val topic = Topic.findById(pick.topicId).get
// TODO derive action from topic's scheme plan
fulfill(Subscription.make(pick.subscriberId, pick.topicId, "review", sub.created, new Date))
}
// clean up pick in any case
pick.resolve(accept)
}

private def processSubs(item: Item, subMap: Map[String, List[Subscription]]) = {
// process in confidence order - deliver, review, notify
subMap.get("deliver").getOrElse(List()).foreach(convey(item,_))
Expand Down
40 changes: 40 additions & 0 deletions conf/evolutions/default/6.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# core model types
# --- !Ups

-- Fixes #93 tag field too short: varchar(255)
ALTER TABLE topic ALTER tag TYPE varchar;

CREATE SEQUENCE agent_id_seq;
CREATE TABLE agent (
id integer NOT NULL DEFAULT nextval('agent_id_seq'),
tag varchar UNIQUE,
label varchar,
description varchar,
code varchar,
params varchar,
icon varchar,
PRIMARY KEY(id)
);

CREATE SEQUENCE topic_pick_id_seq;
CREATE TABLE topic_pick (
id integer NOT NULL DEFAULT nextval('topic_pick_id_seq'),
subscriber_id integer,
topic_id integer,
agent_id integer,
created timestamp,
resolved timestamp,
FOREIGN KEY(subscriber_id) REFERENCES subscriber(id),
FOREIGN KEY(topic_id) REFERENCES topic(id),
FOREIGN KEY(agent_id) REFERENCES agent(id),
PRIMARY KEY(id)
);

# --- !Downs

ALTER TABLE topic ALTER tag TYPE varchar(255);

DROP TABLE topic_pick;
DROP SEQUENCE topic_pick_id_seq;
DROP TABLE agent;
DROP SEQUENCE agent_id_seq;
4 changes: 4 additions & 0 deletions conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ GET /subscriptions/browse controllers.Application.subscriptionBrowse(f
GET /holds/browse controllers.Application.holdBrowse(id: Int, page: Int ?= 0)
GET /hold/:id/resolve controllers.Application.resolveHold(id: Int, accept: Boolean)

# Topic Pick pages
GET /picks/browse controllers.Application.pickBrowse(id: Int, page: Int ?= 0)
GET /pick/:id/resolve controllers.Application.resolvePick(id: Int, accept: Boolean)

# Search pages
GET /search controllers.Search.index
GET /search/results controllers.Search.results(q: String, target: String ?="topic", page: Int ?=0, perpage: Int ?=25)
Expand Down
Loading

0 comments on commit 834d587

Please sign in to comment.