scala-news-reader
rss/atom news reader in scala
git clone https://9o.is/git/scala-news-reader.git
commit 4bb6c0af03145bdac77a495a91911703381a87f3 parent fc3c63640903bf4f625f19c2bdc9bf3d1d317387 Author: Jul <jul@9o.is> Date: Sat, 10 Aug 2013 18:40:58 -0400 BIG Change: Modularized model package and articles. Diffstat:
48 files changed, 1149 insertions(+), 1278 deletions(-)
diff --git a/src/main/less/styles.less b/src/main/less/styles.less @@ -904,8 +904,11 @@ body { overflow: hidden; text-overflow: ellipsis; - .article-sharedby { color: @grayLight } - .article-sharedby-img { .border-radius(50%); width: 25px; height: 25px; } + .article-sharedby { + a { color: @grayLight } + img { .border-radius(50%); width: 25px; height: 25px; } + } + .right-sub-header * { margin-left: 15px; float:right; diff --git a/src/main/scala/com/joereader/actor/EntriesEngine.scala b/src/main/scala/com/joereader/actor/EntriesEngine.scala @@ -0,0 +1,30 @@ +package com.joereader.actor + +import com.joereader._ +import model._ +import lib.rss._ + +import net.liftweb.common._ + +import dispatch._ +import Defaults._ + +/** + * Maintains blog entries in memory. + * Fetch entries from here. + */ +object EntriesEngine extends Logger { + + def entries(blog: Blog): List[FeedEntry] = + blog.urlRss.get.head.entries.present + + def find(blog: Box[Blog], guid: String): Box[FeedEntry] = + blog.map(_.urlRss.get.head.entry(guid)() match { + case Left(msg) => + warn(msg) + Empty + case Right(entry) => + entry + }) openOr (Empty ?~ "Blog is missing") + +} +\ No newline at end of file diff --git a/src/main/scala/com/joereader/lib/Helper.scala b/src/main/scala/com/joereader/lib/Helper.scala @@ -2,7 +2,7 @@ package com.joereader.lib object Helper { - implicit class PatternImplicitStringHelper(str: String) { + implicit class ImplicitStringHelper(str: String) { val Email = """(^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$)""".r /* Checks if string is an email. */ @@ -11,6 +11,15 @@ object Helper { case _ => false } + /* Converts a string to a byte array. */ + def str2bytes: Array[Byte] = str.toCharArray.map(_.toByte) + + /* Converts a hex string to an array of bytes. */ + def hex2bytes: Array[Byte] = { + str.replaceAll("[^0-9A-Fa-f]", "").sliding(2, 2). + toArray.map(Integer.parseInt(_, 16).toByte) + } + /* Converts all single quotes to double quotes. */ def singleQuoteToDouble: String = { str.toList.map(_.toString).map(ch => @@ -23,11 +32,19 @@ object Helper { str.toList. map(_.toString). map(ch => - if (meta_regex.exists(c => - ch.exists(_ == c))) "\\" + ch - else ch). + if (meta_regex.exists(c => + ch.exists(_ == c))) "\\" + ch + else ch). mkString } } + implicit class ImplicitByteArrayHelper(bytes: Array[Byte]) { + + /* Converts a byte array to a string of hex. */ + def bytes2hex: String = bytes.map("%02x" format _).mkString + + /* Converts a byte array to its original string. */ + def bytes2str: String = new String(bytes.map(_.toChar)) + } } diff --git a/src/main/scala/com/joereader/lib/api/rest/oauth/AccessToken.scala b/src/main/scala/com/joereader/lib/api/rest/oauth/AccessToken.scala @@ -1,11 +1,12 @@ package com.joereader.lib.api.rest.oauth import net.liftweb._ -import net.liftweb.http._ -import net.liftweb.common._ +import http._ +import common._ +import util._ case class AccessToken(access_token: String) abstract class OauthAccessToken(token: Box[AccessToken]) extends SessionVar[Box[AccessToken]](token) { - override def __nameSalt = hashCode.toString + override def __nameSalt = Helpers.nextFuncName } \ No newline at end of file diff --git a/src/main/scala/com/joereader/lib/rss/Feed.scala b/src/main/scala/com/joereader/lib/rss/Feed.scala @@ -98,7 +98,7 @@ object Feed { case class FeedImage(src: String) -case class FeedAuthor(name: String, imgUrl: Option[String]) +case class FeedAuthor(name: String, imgUrl: Option[String] = None) private object FeedAuthorOrdering extends Ordering[FeedAuthor] { def compare(a: FeedAuthor, b: FeedAuthor) = a.name compare b.name diff --git a/src/main/scala/com/joereader/lib/rss/FeedEntry.scala b/src/main/scala/com/joereader/lib/rss/FeedEntry.scala @@ -23,7 +23,7 @@ import net.liftweb.common._ /* An RSS Feed Entry/Item. */ case class FeedEntry( - id: String, + guid: String, title: String, link: String, date: Date, @@ -31,7 +31,8 @@ case class FeedEntry( images: List[FeedImage], content: NodeSeq) { - val df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") + private def df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") + def dateFormatted = df.format(date) def nonAuthorImages = images.filterNot(i => author.imgUrl.exists(_ == i.src)) } @@ -61,6 +62,8 @@ object FeedEntry extends Logger { builtMediaImages, builtContent) } + + def empty = FeedEntry("","","", new Date(), FeedAuthor(""), Nil, NodeSeq.Empty) def build(entries: List[SyndEntry]): List[FeedEntry] = entries.map(build) diff --git a/src/main/scala/com/joereader/lib/rss/RSSHtmlResponse.scala b/src/main/scala/com/joereader/lib/rss/RSSHtmlResponse.scala @@ -0,0 +1,3 @@ +package com.joereader.lib.rss + +case class RSSHtmlResponse(loc: String, content: String, redirectTo: String) +\ No newline at end of file diff --git a/src/main/scala/com/joereader/lib/rss/package.scala b/src/main/scala/com/joereader/lib/rss/package.scala @@ -14,33 +14,44 @@ import net.liftweb._ import common._ import util.Helpers._ -case class RSSHtmlResponse(loc: String, content: String, redirectTo: String) - /** * */ package object rss extends Logger { + type FutureFeedEntry = Future[Either[String, Box[FeedEntry]]] + type FutureFeedEntries = Future[Either[String, List[FeedEntry]]] + type FutureRSSHtmlResponse = Future[Either[String, RSSHtmlResponse]] + implicit class RssImplicitString(str: String) { /** + * Returns the one entry matching the requested guid. + */ + def entry(guid: String): FutureFeedEntry = { + for (entries <- entries.right) + yield Box(entries.find(_.guid == guid)) ?~ + s"RSS entry could not be found -> guid: $guid" + } + + /** * Grabs all feed entries given a url to a rss feed. */ - def entries: Future[Either[String, List[FeedEntry]]] = { + def entries: FutureFeedEntries = { val response = catchThrowable(requestLink(str)) - - for(res <- extractFeed(response).right) - yield Feed.build(res, List(str)).entries + + for (res <- extractFeed(response).right) + yield Feed.build(res, List(str)).entries } /** * Returns html given a url to a web page */ - def response: Future[Either[String, RSSHtmlResponse]] = { + def response: FutureRSSHtmlResponse = { val req = RSSHtmlResponse(str, "", str) Future { requestLink(Some(req)). - toRight("Failed to get html response") + toRight(s"Failed to get html response -> $str") } } @@ -64,16 +75,29 @@ package object rss extends Logger { map { r => val (response, link) = r - for (syndFeed <- extractFeed(response).right) - yield Feed.build(syndFeed, List(link)) - }.map( - _() match { - case Left(msg) => - warn(msg) - Feed.empty - case Right(feed) => - feed - })) + (extractFeed(response).right.map( + syndFeed => Feed.build(syndFeed, List(link))), + link) + }. + map { + r => + val (response, link) = r + response() match { + case Left(msg) => + warn(s"$msg -> $link") + Feed.empty + case Right(feed) => + feed + } + }) + } + + implicit class FutureFeedEntriesHelper(entries: FutureFeedEntries) { + def present: List[FeedEntry] = + entries() match { + case Left(msg) => warn(msg); Nil + case Right(entries) => entries + } } /** @@ -81,7 +105,7 @@ package object rss extends Logger { * @param link the web link to the page. * @return either a failure with a message or successful content as a string */ - def requestLink(link: String): Future[Either[Throwable, String]] = { + protected[rss] def requestLink(link: String): Future[Either[Throwable, String]] = { val request = url(URLFormatter(link).toString) Http.configure(_. @@ -94,7 +118,7 @@ package object rss extends Logger { * Note: Redirects can be handled by dispatch with setFollowRedirects as true, * but we want to retrieve the redirected url. Limit is 3 redirects. */ - def requestLink(optreq: Option[RSSHtmlResponse], redirectCount: Int = 0): Option[RSSHtmlResponse] = { + protected[rss] def requestLink(optreq: Option[RSSHtmlResponse], redirectCount: Int = 0): Option[RSSHtmlResponse] = { if (optreq.exists(_.redirectTo == null) || redirectCount > 2) return optreq @@ -115,17 +139,14 @@ package object rss extends Logger { } } - private def catchThrowable(res: Future[Either[Throwable, String]]): Future[Either[String, String]] = - for (e <- res.left) yield { - error("Cannot connect to request RSS: " + e.getMessage) - e.getMessage - } + protected[rss] def catchThrowable(res: Future[Either[Throwable, String]]): Future[Either[String, String]] = + for (e <- res.left) yield e.getMessage /** * Finds link(s) to RSS feeds in HTML elements. * @param html html in plain text. */ - def findRssLinks(html: String): List[String] = { + protected[rss] def findRssLinks(html: String): List[String] = { val document = parseHtml(html) @@ -153,7 +174,7 @@ package object rss extends Logger { filterByMimeType(headElements(document)))) } - def findRssLinks(html: Option[String]): List[String] = + protected[rss] def findRssLinks(html: Option[String]): List[String] = html map findRssLinks getOrElse Nil /** @@ -161,22 +182,22 @@ package object rss extends Logger { * @param xml xml as string. * @return a syndicated feed if xml if correctly parsed. */ - def extractFeed(xml: String): Either[String, SyndFeed] = + protected[rss] def extractFeed(xml: String): Either[String, SyndFeed] = tryo { val bytes = new ByteArrayInputStream(xml.getBytes("UTF-8")) val reader = new XmlReader(bytes) new SyndFeedInput().build(reader) - } toRight "Unable to extract feed." + } toRight "Unable to extract feed" - def extractFeed(content: Future[Either[String, String]]): Future[Either[String, SyndFeed]] = + protected[rss] def extractFeed(content: Future[Either[String, String]]): Future[Either[String, SyndFeed]] = for (either <- content) yield for { feedStr <- either.right out <- extractFeed(feedStr).right } yield out - def parseHtml(html: String): Document = Jsoup.parse(html) + protected[rss] def parseHtml(html: String): Document = Jsoup.parse(html) - def headElements(doc: Document): List[Element] = + protected[rss] def headElements(doc: Document): List[Element] = doc.head.children.toList } \ No newline at end of file diff --git a/src/main/scala/com/joereader/model/Article.scala b/src/main/scala/com/joereader/model/Article.scala @@ -0,0 +1,96 @@ +package com.joereader.model + +import com.joereader._ +import actor._ +import lib._ +import Helper._ +import rss._ + +import net.liftweb._ +import util._ +import http._ +import SHtml._ +import js._ +import JsCmds._ + +import scala.xml._ +import java.util.Date + +import dispatch._, Defaults._ + +trait ArticleTrait { + def partDivider = "-" + + def createId(blog: Blog, entry: FeedEntry) = + blog.id.get + partDivider + entry.guid.str2bytes.bytes2hex + +} + +object Article extends ArticleTrait { + + def sort(articles: List[Article]) = + articles.sortWith((x, y) => x.date.getTime > y.date.getTime) + + def fromString(s: String): Article = + s.split(partDivider).toList match { + case blogId :: guidHex :: Nil => + val guid = guidHex.hex2bytes.bytes2str + val blog = Blog.findByStringId(blogId) + val entry = EntriesEngine.find(blog, guid) + val bwu = BlogWriterUser.fromBlogFeedEntry(blog, entry) + + entry.map(new Article(bwu, _)) openOr Article.empty + case a => + error("Invalid article in data store -> " + + a.mkString(partDivider)) + Article.empty + } + + def empty = new Article(BlogWriterUser.empty, FeedEntry.empty) +} + +/** + * The representation of an article at the data store level. + * Article entry content is not saved in data store. Only + * guid. + */ +class Article(_bwu: BlogWriterUser, _entry: FeedEntry) { + + /* date is used to sort articles. */ + def date: Date = entry.date + def entry = _entry + def bwu = _bwu + + def sharedBy: Option[BlogWriterUser] = None + + def saved: Boolean = User.currentUser.exists( + _.saved.get.exists(_.toString == toString)) + + def shared: Boolean = User.currentUser.exists( + _.shared.get.exists(_.toString == toString)) + + def save = + for (u <- User.currentUser) + u.saved.add(this).update + + def unSave = + for (u <- User.currentUser) + u.saved.remove(this).update + + def share = + for (u <- User.currentUser) { + val bwu1 = new BlogWriterUser(u) + val sa = new ArticleShared(bwu, entry, new Date, bwu1) + u.shared.add(sa).update + } + + def unShare = + for (u <- User.currentUser) + u.shared.remove(asInstanceOf[ArticleShared]).update + + override def toString: String = { + for (blog <- bwu.blog) + yield Article.createId(blog, entry) + } getOrElse "" + +} +\ No newline at end of file diff --git a/src/main/scala/com/joereader/model/ArticleShared.scala b/src/main/scala/com/joereader/model/ArticleShared.scala @@ -0,0 +1,53 @@ +package com.joereader.model + +import com.joereader._ +import actor._ +import lib._ +import Helper._ +import rss._ +import java.util.Date + +/** + * Like an article but with a sharedBy value that points + * to the user that shared this article and a date to know + * when it was shared. + */ +object ArticleShared extends ArticleTrait { + def createId(blog: Blog, entry: FeedEntry, date: Date) = + super.createId(blog, entry) + partDivider + date.getTime + + def fromString(s: String): ArticleShared = + s.split(partDivider).toList match { + case blogId :: guidHex :: dateLong :: Nil => + val guid = guidHex.hex2bytes.bytes2str + val blog = Blog.findByStringId(blogId) + val entry = EntriesEngine.find(blog, guid) + val date = new Date(dateLong.toLong) + val bwu = BlogWriterUser.fromBlogFeedEntry(blog, entry) + + entry.map (new ArticleShared(bwu, _, date)) openOr + ArticleShared.empty + case a => + error("Invalid shared article in data store -> " + + a.mkString(partDivider)) + ArticleShared.empty + } + + def empty = new ArticleShared( + BlogWriterUser.empty, FeedEntry.empty) +} + +class ArticleShared( + bwu: BlogWriterUser, + entry: FeedEntry, + sharedDate: Date = new Date, + _sharedBy: BlogWriterUser = BlogWriterUser.empty) extends Article(bwu, entry) { + + override def date = sharedDate + override def sharedBy = Some(_sharedBy) + + override def toString = { + for (blog <- bwu.blog) + yield ArticleShared.createId(blog, entry, date) + } getOrElse "" +} +\ No newline at end of file diff --git a/src/main/scala/com/joereader/model/Blog.scala b/src/main/scala/com/joereader/model/Blog.scala @@ -7,12 +7,15 @@ import mongodb._ import mongodb.record._ import mongodb.record.field._ import mongodb.BsonDSL._ -import com.joereader.lib._ +import com.joereader._ +import model.field._ +import lib._ -import org.bson.types._ +import dispatch._ +import org.bson.types._ -class Blog private() extends MongoRecord[Blog] with ObjectIdPk[Blog] { +class Blog private () extends MongoRecord[Blog] with ObjectIdPk[Blog] { def meta = Blog object name extends StringField(this, 64) @@ -28,7 +31,6 @@ class Blog private() extends MongoRecord[Blog] with ObjectIdPk[Blog] { /* html web page url of this blog */ object urlHtml extends StringField(this, 500) { def format(s: ValueType) = URLFormatter(s).hostAndPath - override def setFilter = format _ :: super.setFilter } @@ -38,76 +40,36 @@ class Blog private() extends MongoRecord[Blog] with ObjectIdPk[Blog] { } /* owner is the writer that has control to edit blog */ - object owner extends ObjectIdRefField(this, User) { - override def defaultValue = new ObjectId("0" * 24) - } - - def isOwner(bu: Box[User]): Boolean = bu.exists(isOwner) + object owner extends ObjectIdRefField(this, User) with ObjectIdRefFieldExtra[Blog, User] - def isOwner(u: User): Boolean = owner.get == u.id.get + /* other blog writers */ + object writers extends BsonRecordListField(this, BlogWriter) with MongoListFieldExtra[Blog, BlogWriter] { - def nonOwner(u: User): Boolean = !isOwner(u) + def find(user: User): Box[BlogWriter] = + Box(writers.get.find(_.user.get == user.id.get)) - def hasOwner: Boolean = owner.get != owner.defaultValue + /* Blog writers that have registered with the website. */ + def registered: List[BlogWriter] = + writers.get.filterNot(bu => bu.user.get == bu.user.defaultValue) - /* other blog writers */ - object writers extends BsonRecordListField(this, BlogWriter) + /* Blog writers that haven't registered with the website. */ + def unregistered: List[BlogWriter] = + writers.get diff registered - def addWriter(bw: BlogWriter) = - if (writers.get.exists(_.name.get == bw.name.get)) this - else writers(bw :: writers.get) + def names: List[String] = writers.get.map(_.name.get) + def registeredNames = registered.map(_.name.get) + def unregisteredNames = unregistered.map(_.name.get) - /* + /* * If writer exists, only update user id and image. * (everything except name [because it doesn't change] and followers) */ - def addWriterSafely(bw: BlogWriter) = { - val existing = writer(bw.name.get). - map(_.user(bw.user.get).img(bw.img.get)) - - if (existing.isEmpty) addWriter(bw) - else this - } - - def removeWriter(bw: BlogWriter) = - writers(writers.get.filterNot(_.name.get == bw.name.get)) - - def removeWriter(n: String) = - writers(writers.get.filterNot(_.name.get == n)) - - /* List of names of all writers. */ - def writersNames: List[String] = writers.get.map(_.name.get) - - /* Blog writers that have registered with the website. */ - def registeredWriters: List[BlogWriter] = - writers.get.filterNot(u => u.user.get == u.user.defaultValue) - - /* Blog writers that haven't registered with the website. */ - def unregisteredWriters: List[BlogWriter] = - writers.get diff registeredWriters - - def registeredWritersNames = registeredWriters.map(_.name.get) - - def unregisteredWritersNames = unregisteredWriters.map(_.name.get) - - /* Finds blog writer by name. */ - def writer(name: String): Box[BlogWriter] = - writers.get.find(_.name.get == name) match { - case Some(x) => Full(x) - case None => Empty + def addSafely(bw: BlogWriter): Blog = { + val existing = findStr(bw.name.get). + map(_.user(bw.user.get).img(bw.img.get)) + if (existing.isEmpty) add(bw) else owner } - - /* Finds blog writer by user id. */ - def writer(user: User): Box[BlogWriter] = - writers.get.find(_.user.get == user.id.get) match { - case Some(x) => Full(x) - case None => Empty - } - - /* Checks if writer under the name of _ exists. */ - def writerExists(name: String): Boolean = - writers.get.exists(_.name.get == name) - + } } object Blog extends Blog with MongoMetaRecord[Blog] { @@ -120,10 +82,8 @@ object Blog extends Blog with MongoMetaRecord[Blog] { ensureIndex(blogname.name -> 1, unique = true) ensureIndex(urlHtml.name -> 1, unique = true) - def defaultId = new ObjectId("0" * 24) - def findByStringId(id: String): Box[Blog] = - if (ObjectId.isValid(id) && id != defaultId.toString) + if (ObjectId.isValid(id) && id != "0" * 24) find(new ObjectId(id)) else Empty diff --git a/src/main/scala/com/joereader/model/BlogWriter.scala b/src/main/scala/com/joereader/model/BlogWriter.scala @@ -1,13 +1,14 @@ package com.joereader.model -import net.liftweb._ -import record.field._ -import mongodb.record._, field._ +import net.liftweb.record.field._ +import net.liftweb.mongodb.record._ +import net.liftweb.mongodb.record.field._ +import com.joereader.model.field._ import org.bson.types.ObjectId /** - * BlogWriter is a writer of a blog. Users literally follow this instead of + * BlogWriter is a writer of a blog. Users follow this instead of * another user because not all blog writers are registered. */ class BlogWriter private() extends BsonRecord[BlogWriter] { @@ -26,40 +27,20 @@ class BlogWriter private() extends BsonRecord[BlogWriter] { } // the user representing this blog writer - object user extends ObjectIdRefField(this, User) { - override def defaultValue = new ObjectId("0" * 24) - } + object user extends ObjectIdRefField(this, User) with ObjectIdRefFieldExtra[BlogWriter, User] - object followers extends ObjectIdRefListField(this, User) + object followers extends ObjectIdRefListField(this, User) with ObjectIdRefListFieldExtra[BlogWriter, User] - object categories extends MongoListField[BlogWriter, String](this) + object categories extends MongoListField[BlogWriter, String](this) with MongoListFieldExtra[BlogWriter, String] // url to image found in rss feed of blog writer object img extends StringField(this, 255) - - def isUser(u: User) = user.get == u.id.get - - def nonUser(u: User) = !isUser(u) - - def hasUser: Boolean = user.get != user.defaultValue - - def removeUser() { - user(user.defaultValue) - } - - def addFollower(user: User) = followers(user.id.get :: followers.get) - - def removeFollower(user: User) = - followers(followers.get.filterNot(_ == user.id.get)) - - def addCategory(category: String) = - if (categories.get.exists(_ == category)) this - else categories(category :: categories.get) - - def removeCategory(category: String) = - categories(categories.get.filter(_ != category)) - - def categoryExists(s: String) = categories.get.exists(_ == s) + + /* + * Have override so MongoListFieldExtra can use name to + * compare with other blog writers. + */ + override def toString = name.get } object BlogWriter extends BlogWriter with BsonMetaRecord[BlogWriter] diff --git a/src/main/scala/com/joereader/model/BlogWriterUser.scala b/src/main/scala/com/joereader/model/BlogWriterUser.scala @@ -1,35 +1,59 @@ package com.joereader.model import com.joereader._ +import lib._ +import Helper._ +import rss._ import config._, S3Config._ import net.liftmodules.extras.Gravatar -import net.liftweb.common._ +import net.liftweb._ +import util._ +import common._ + +import dispatch._ +import Defaults._ /** - * Things get a tad ugly. A user is allowed to follow another user's blog, but + * A container of a user, blog, blog writer. + * A user is allowed to follow another user's blog, but * for flexibility, we allow user's to follow BlogWriter's that haven't * registered. So what type are we following? User or BlogWriter? * BlogWriterUser solves that problem. */ -case class BlogWriterUser( - user: Option[User], - blog: Option[Blog] = None, - blogWriter: Option[BlogWriter] = None) { +class BlogWriterUser( + _user: Option[User], + _blog: Option[Blog] = None, + _blogWriter: Option[BlogWriter] = None) { + + def user = _user + def blog = _blog + def blogWriter = _blogWriter + + def this(user: User) = + this(Some(user)) + + def this(blog: Blog, blogWriter: BlogWriter) = + this(None, Some(blog), Some(blogWriter)) + + def this(user: User, blog: Blog, blogWriter: BlogWriter) = + this(Some(user), Some(blog), Some(blogWriter)) def gravatarSize = 300 - private def defaultColor: String = BlogWriter.createRecord.color.defaultValue + private def defaultColor: String = + BlogWriter.createRecord.color.defaultValue /* * Creates a link that points to the user's page * if present else the blog writer's page. */ - val link: String = - user.map(Site.userProfileLoc.calcHref) getOrElse - (for{ + def link: String = + user.map(Site.userProfileLoc.calcHref) orElse { + for { blogWriter <- blogWriter blog <- blog - } yield Site.blogWriterProfileLoc.calcHref(this)).getOrElse("/") + } yield Site.blogWriterProfileLoc.calcHref(this) + } getOrElse "/" /* * We try everything possible to display the writer's image. @@ -43,43 +67,57 @@ case class BlogWriterUser( private def userImage: Option[String] = { val img = user.map(_.img.get) getOrElse "" - if (img.isEmpty) None else Some(s3.fileUrl(img)) + if (img.isEmpty) None + else Some(s3.fileUrl(img)) } private def userEmailImage: Option[String] = { val email = user.map(_.email.get) getOrElse "" - if (email.isEmpty) None else Some(Gravatar.imageUrl(email, gravatarSize)) + if (email.isEmpty) None + else Some(Gravatar.imageUrl(email, gravatarSize)) } private def blogWriterImage: Option[String] = { val img = blogWriter.map(_.img.get) getOrElse "" - if (img.isEmpty) None else Some(img) + if (img.isEmpty) None + else Some(img) } private def blogWriterEmailImage: Option[String] = { val email = blogWriter.map(_.email.get) getOrElse "" - if (email.isEmpty) None else Some(Gravatar.imageUrl(email, gravatarSize)) + if (email.isEmpty) None + else Some(Gravatar.imageUrl(email, gravatarSize)) } - def image: String = List(userImage, userEmailImage, blogWriterImage, - blogWriterEmailImage, Some(s3.fileUrl("mr_noman"))).filter(_.isDefined).head.get - + def image: String = + userImage orElse + userEmailImage orElse + blogWriterImage orElse + blogWriterEmailImage getOrElse + s3.fileUrl("mr_noman") - def name: String = user.map(_.name.get). - getOrElse(blogWriter.map(_.name.get).getOrElse("")) + def name: String = + user.map(_.name.get) orElse + blogWriter.map(_.name.get) getOrElse + "" def dashName = name.split(" ").mkString("-") def categories = - blogWriter.map(_.categories.get.mkString(", ")).getOrElse("") + blogWriter.map(_.categories.get.mkString(", ")). + getOrElse("") def color = blogWriter.map(_.color.get).getOrElse(defaultColor) def url = blog.map(_.urlHtml.get).getOrElse("") - val id: String = user.map(_.id.get.toString).getOrElse( - blogWriter.map(bw => blog.map(BlogWriterUser.create(bw, _))).flatten. - getOrElse("")).split(" ").mkString("-") + override def toString = + user.map(BlogWriterUser.create) orElse { + for { + blogWriter <- blogWriter + blog <- blog + } yield BlogWriterUser.create(blogWriter, blog) + } getOrElse (Helpers.nextFuncName).split(" ").mkString("-") } @@ -87,18 +125,44 @@ object BlogWriterUser { import collection.breakOut - lazy val separator: String = "~" + def partDivider = "~" + + /* + * Indicator that this BlogWriterUser only has user, no blog + * or BlogWriter. This must be used to view the user's shared + * articles, hence the abbreviation "sa" + */ + def sa = "sa" + + def empty = new BlogWriterUser(User.createRecord) /* Creates string format given the type */ def create(writer: BlogWriter, blog: Blog): String = - blog.id.get + separator + writer.name.is + blog.id.get + partDivider + writer.name.get.str2bytes.bytes2hex /* Creates string format given the type */ - def create(user: User): String = user.id.get + separator + "sa" + def create(user: User): String = + user.id.get + partDivider + sa.str2bytes.bytes2hex + + def fromString(s: String): BlogWriterUser = s.split(partDivider).toList match { + case id :: nameHex :: Nil => + val name = nameHex.hex2bytes.bytes2str + val blog = if (name != sa) Blog.findByStringId(id) else Empty + val user = if (name == sa) User.findByStringId(id) else Empty + val blogWriter = blog.flatMap(_.writers.findStr(name)) + val blogUser = blogWriter.flatMap(_.user.obj) + new BlogWriterUser(user or blogUser, blog, blogWriter) + case bwu => + error("Invalid BlogWriterUser in data store -> " + bwu.mkString(partDivider)) + BlogWriterUser.empty + } /* * Removes repeats by grouping user id if user is available, else by * grouping the string format of blog-writer to blog + * This is needed in case we're following a user's multiple blogs + * so the user may appear multiple times in our list of following. + * This function assures the user is not repeated. */ def uniqueOnly(l: Seq[BlogWriterUser]): List[BlogWriterUser] = l.groupBy { @@ -106,24 +170,33 @@ object BlogWriterUser { val userId = bwu.user.map(_.id.get) val writer = for (bw <- bwu.blogWriter; blog <- bwu.blog) - yield create(bw, blog) + yield create(bw, blog) userId.getOrElse(writer getOrElse "") }.map { _._2.head }(breakOut) + def fromBlogFeedEntry(blog: Box[Blog], entry: Box[FeedEntry]): BlogWriterUser = { + val writer = for { + blog <- blog + entry <- entry + blogWriter <- blog.writers.findStr(entry.author.name) + } yield blogWriter + val user = writer.flatMap(_.user.obj) + new BlogWriterUser(user, blog, writer) + } + // parse and encode functions are used by SiteMap def parse(path: List[String]): Box[BlogWriterUser] = { val blog = Blog.findByBlogName(path(0)) - val blogWriter = blog.map(_.writer(path(1).replace('+', ' '))).openOr(Empty) + val blogWriter = blog.map(_.writers.findStr(path(1).replace('+', ' '))).openOr(Empty) for (b <- blog; bw <- blogWriter) - yield BlogWriterUser(None, blog, blogWriter) + yield new BlogWriterUser(b, bw) } def encode(bwu: BlogWriterUser): List[String] = List( bwu.blog.map(_.blogname.get), - bwu.blogWriter.map(_.name.get) - ).flatten + bwu.blogWriter.map(_.name.get)).flatten } \ No newline at end of file diff --git a/src/main/scala/com/joereader/model/Category.scala b/src/main/scala/com/joereader/model/Category.scala @@ -1,5 +1,6 @@ package com.joereader.model +import field._ import net.liftweb._ import common._ import record.field._ @@ -19,57 +20,14 @@ class Category private () extends MongoRecord[Category] { override def shouldDisplay_? = false override def setFilter = trim _ :: toLower _ :: super.setFilter } - + // Users that fall under this category - object writers extends MongoListField[Category, String](this) { - - import BlogWriterUser._ - - /* Given an index, gets a BlogWriterUser */ - def writer(index: Int): Box[BlogWriterUser] = { - val id = get(index).split(separator).head - val who = get(index).split(separator).tail.mkString - - (for { - blog <- Blog.findByStringId(id) - blogWriter <- blog.writer(who) - } yield { - val user = blogWriter.user.obj - - // remove blogWriter if it's not attached to a user - if (user.isEmpty) { - removeWriter(blog, blogWriter).update - Empty - } - else user.map(user => - BlogWriterUser(Some(user), Some(blog), Some(blogWriter))) - - }) openOr Empty + object writers extends BlogWriterUserListField(this) { + // remove category record if no users are listed + override def remove(bwu: BlogWriterUser): Category = { + super.remove(bwu) + if(get.isEmpty) {owner.delete_!; owner} else owner } - - def randomWriters(n: Int): List[BlogWriterUser] = - uniqueOnly { - if (get.size > 0) { - val random = - Seq.fill(n)(scala.util.Random.nextInt(get.size)).distinct - random.map(writer).flatten - } - else Nil - } - - } - - def addWriter(blog: Blog, blogWriter: BlogWriter) = { - val addMe = BlogWriterUser.create(blogWriter, blog) - if(writers.get.exists(_ == addMe)) this - else writers(addMe :: writers.get) - } - - def removeWriter(blog: Blog, blogWriter: BlogWriter) = { - val removeMe = BlogWriterUser.create(blogWriter, blog) - writers(writers.get.filter(_ != removeMe)) - if(writers.is.isEmpty) delete_! - this } override def update: Category = super.update diff --git a/src/main/scala/com/joereader/model/InviteToken.scala b/src/main/scala/com/joereader/model/InviteToken.scala @@ -1,18 +1,14 @@ package com.joereader.model -import net.liftmodules.mongoauth.{MongoAuth, field} -import field.ExpiresField - import org.joda.time.{Days, ReadablePeriod, Hours} - import net.liftweb._ import common._ import http.{S, StringField => _, BooleanField => _, _} import mongodb.record._ import mongodb.record.field._ import net.liftweb.record.field._ - import org.bson.types.ObjectId +import net.liftmodules.mongoauth.field.ExpiresField /** * This is a token to automatically invite a user if a blog owner claims him as writer. diff --git a/src/main/scala/com/joereader/model/User.scala b/src/main/scala/com/joereader/model/User.scala @@ -7,18 +7,19 @@ import net.liftweb._ import util._ import common._ import http.{ StringField => _, BooleanField => _, _ } -import mongodb.record._, field._ -import record.field._ -import net.liftmodules.mongoauth._, model._ +import net.liftweb.record._ +import net.liftweb.record.field._ +import net.liftweb.mongodb.record._ +import net.liftweb.mongodb.record.field._ +import net.liftmodules.mongoauth._ +import net.liftmodules.mongoauth.model._ import com.joereader._ +import com.joereader.model.field._ import config._ -import snippet._ import lib.rss._ -import net.liftweb.common.Full - class User private () extends ProtoAuthUser[User] with ObjectIdPk[User] { def meta = User @@ -36,148 +37,24 @@ class User private () extends ProtoAuthUser[User] with ObjectIdPk[User] { object introVid extends StringField(this, 20) - object otherVid extends MongoListField[User, String](this) - - def addVideo(v: String) = - if (otherVid.get.exists(_ == v)) this - else otherVid(v :: otherVid.get) - - def removeVideo(v: String) = - otherVid(otherVid.get.filter(_ != v)) + object otherVid extends MongoListField[User, String](this) with MongoListFieldExtra[User, String] - object blogs extends ObjectIdRefListField(this, Blog) + object blogs extends ObjectIdRefListField(this, Blog) with ObjectIdRefListFieldExtra[User, Blog] def isWriter: Boolean = !blogs.get.isEmpty - def addBlog(blog: Blog) = - if (blogs.get.exists(_ == blog.id.get)) this - else blogs(blog.id.get :: blogs.get) - - def removeBlog(blog: Blog) = - blogs(blogs.get.filterNot(_ == blog.id.get)) - - /* Contains a list of blog id to writer name separated by separator. */ - object following extends MongoListField[User, String](this) { - - import BlogWriterUser._ - - /* Given an index, gets a BlogWriterUser */ - def user(index: Int): Box[BlogWriterUser] = { - val id = get(index).split(separator).head - val who = get(index).split(separator).tail.mkString - - if (who == "sa") - for (user <- User.findByStringId(id)) - yield BlogWriterUser(Some(user)) - - else - for { - blog <- Blog.findByStringId(id) - blogWriter <- blog.writer(who) - } yield { - val user = blogWriter.user.obj - BlogWriterUser(user, Some(blog), Some(blogWriter)) - } - } - - def randomUsers(n: Int): List[BlogWriterUser] = - uniqueOnly { - if (get.size > 0) { - val random = - Seq.fill(n)(scala.util.Random.nextInt(get.size)).distinct - random.map(user).flatten - } else Nil - } - - def allUsers: List[BlogWriterUser] = - uniqueOnly((0 until get.size).map(user).flatten) - - def usersSharedArticles: List[User] = - (0 until get.size).map { i => - val who = get(i).split(separator).tail.mkString - if(who == "sa") user(i).map(_.user) else Empty - }.flatten.flatten.toList - - def exists(bw: BlogWriter, blog: Blog): Boolean = - get.exists(_ == create(bw, blog)) - - def exists(u: User): Boolean = - get.exists(_ == create(u)) - } - - def follow(writer: BlogWriter, blog: Blog) = { - val addMe = BlogWriterUser.create(writer, blog) - - if (!blog.writerExists(writer.name.is) || - following.get.exists(_ == addMe)) - this - else - following(addMe :: following.get) - } - - def follow(user: User) = { - val addMe = BlogWriterUser.create(user) - if (following.get.exists(_ == addMe)) this - else following(addMe :: following.get) - } - - def unFollow(writer: BlogWriter, blog: Blog) = - following(following.get. - filterNot(_ == BlogWriterUser.create(writer, blog))) - - def unFollow(user: User) = - following(following.get. - filterNot(_ == BlogWriterUser.create(user))) + /* List of blog writer users this user follows. */ + object following extends BlogWriterUserListField(this) /* * Follow this user to view his shared articles * (only if this user is a writer) */ - object followers extends ObjectIdRefListField(this, User) - - def addFollower(user: User) = - followers(user.id.get :: followers.get) - - def removeFollower(user: User) = - followers(followers.get.filterNot(_ == user.id.get)) - - /* - * Get a set of user id's following this user - * (that should be a writer) - */ - def followersIds: List[String] = { - val blogFollowers: List[String] = - (for { - blogs <- blogs.get - blog <- Blog.find(blogs) - writer <- blog.writer(this) - } yield writer.followers.get.map(_.toString)).flatten - - val sharedArticlesFollowers: List[String] = - followers.get.map(_.toString) - - (blogFollowers ::: sharedArticlesFollowers).distinct - } + object followers extends ObjectIdRefListField(this, User) with ObjectIdRefListFieldExtra[User, User] - object saved extends MongoListField[User, String](this) - - def saveArticle(article: Article) = { - if (saved.get.exists(_ == article.id)) this - else saved(article.id :: saved.get) - } - - def unSaveArticle(article: Article) = - saved(saved.get.filterNot(_ == article.id)) - - object shared extends MongoListField[User, String](this) - - def shareArticle(article: SharedArticle) = { - if (shared.get.exists(_ == article.id)) this - else shared(article.id :: shared.get) - } + object saved extends ArticleListField(this) - def unShareArticle(article: SharedArticle) = - shared(shared.get.filterNot(_ == article.id)) + object shared extends ArticleSharedListField(this, this) } object User extends User with ProtoAuthUserMeta[User] with Loggable { @@ -263,7 +140,7 @@ object User extends User with ProtoAuthUserMeta[User] with Loggable { def sendLoginToken(user: User) { import net.liftweb.util.Mailer._ - val token = LoginToken.createForUserId(user.id.is) + val token = LoginToken.createForUserId(user.id.get) val msgTxt = """ @@ -313,8 +190,8 @@ object User extends User with ProtoAuthUserMeta[User] with Loggable { val blogWriter = BlogWriter.createRecord.user(user.id.get).name(at.name.get) - blog.addWriterSafely(blogWriter).save - user.addBlog(blog).verified(true).save + blog.writers.addSafely(blogWriter).save + user.blogs.add(blog).verified(true).save if (userBoxed.isDefined) resp = RedirectWithState(Site.categoriesLoc.calcHref(blog), @@ -325,8 +202,8 @@ object User extends User with ProtoAuthUserMeta[User] with Loggable { else resp = RedirectWithState(Site.signUp3.url, RedirectState(() => { - BlogIdVar(at.blogId.get.toString) - VerifiedVar(true) + snippet.BlogIdVar(at.blogId.get.toString) + snippet.VerifiedVar(true) S.notice("Congratulations! You are verified with " + blog.name.get + "!") })) diff --git a/src/main/scala/com/joereader/model/field/ArticleListField.scala b/src/main/scala/com/joereader/model/field/ArticleListField.scala @@ -0,0 +1,31 @@ +package com.joereader.model.field + +import com.mongodb._ +import net.liftweb._ +import common._ +import mongodb.record._ +import field._ + +import com.joereader.model.Article + +/** + * Mongo Field to store Articles. + * Does not save article's content, just the guid. + */ +class ArticleListField[OwnerType <: BsonRecord[OwnerType]](rec: OwnerType) + extends MongoListField[OwnerType, Article](rec) + with MongoListFieldExtra[OwnerType, Article] { + + import scala.collection.JavaConversions._ + + override def asDBObject: DBObject = { + val dbl = new BasicDBList + value.foreach { v => dbl.add(v.toString) } + dbl + } + + override def setFromDBObject(dbo: DBObject): Box[List[Article]] = + setBox(Full(dbo.keySet.toList.map(k => { + Article.fromString(dbo.get(k.toString).asInstanceOf[String]) + }))) +} +\ No newline at end of file diff --git a/src/main/scala/com/joereader/model/field/ArticleSharedListField.scala b/src/main/scala/com/joereader/model/field/ArticleSharedListField.scala @@ -0,0 +1,35 @@ +package com.joereader.model.field + +import com.mongodb._ +import net.liftweb._ +import common._ +import mongodb.record._ +import field._ +import net.liftmodules.mongoauth._ + +import com.joereader.model._ + +/** + * Mongo Field to store shared articles. + * Does not save article's content, just the guid. + */ +class ArticleSharedListField[ + OwnerType <: BsonRecord[OwnerType], + OwnerTypeUser <: User](rec: OwnerType, recUser: OwnerTypeUser) + extends MongoListField[OwnerType, ArticleShared](rec) + with MongoListFieldExtra[OwnerType, ArticleShared] { + + import scala.collection.JavaConversions._ + + override def asDBObject: DBObject = { + val dbl = new BasicDBList + value.foreach { v => dbl.add(v.toString) } + dbl + } + + override def setFromDBObject(dbo: DBObject): Box[List[ArticleShared]] = + setBox(Full(dbo.keySet.toList.map(k => { + val a = ArticleShared.fromString(dbo.get(k.toString).asInstanceOf[String]) + new ArticleShared(a.bwu, a.entry, a.date, new BlogWriterUser(recUser)) + }))) +} +\ No newline at end of file diff --git a/src/main/scala/com/joereader/model/field/BlogWriterUserListField.scala b/src/main/scala/com/joereader/model/field/BlogWriterUserListField.scala @@ -0,0 +1,66 @@ +package com.joereader.model.field + +import com.mongodb._ +import net.liftweb._ +import common._ +import mongodb.record._ +import field._ + +import com.joereader.model._ + +class BlogWriterUserListField[OwnerType <: BsonRecord[OwnerType]](rec: OwnerType) + extends MongoListField[OwnerType, BlogWriterUser](rec) + with MongoListFieldExtra[OwnerType, BlogWriterUser] { + + import scala.collection.JavaConversions._ + import BlogWriterUser._ + + override def asDBObject: DBObject = { + val dbl = new BasicDBList + value.foreach { v => dbl.add(v.toString) } + dbl + } + + override def setFromDBObject(dbo: DBObject): Box[List[BlogWriterUser]] = + setBox(Full(dbo.keySet.toList.map(k => { + BlogWriterUser.fromString(dbo.get(k.toString).asInstanceOf[String]) + }))) + + /* Random amount of unique blog writer users. */ + def random(n: Int): List[BlogWriterUser] = + uniqueOnly { + if (get.size > 0) { + val random = + Seq.fill(n)(scala.util.Random.nextInt(get.size)).distinct + random.map(get) + } else Nil + } + + /* All blog writer users. Uniqueness assured. */ + def allUsers: List[BlogWriterUser] = + uniqueOnly((0 until get.size).map(get)) + + /* + * All Users that are followed by their shared articles (sa). + * We determine this by checking if user is the only defined + * value in BlogWriterUser container. + */ + def usersSharedArticles: List[User] = + (0 until get.size).map { i => + val bwu = get(i) + if (bwu.user.isDefined && + bwu.blog.isEmpty && + bwu.blogWriter.isEmpty) + Box(get(i).user) + else + Empty + }.flatten.toList + + /* Is the BlogWriter in this list? */ + def exists(bw: BlogWriter, blog: Blog): Boolean = + get.exists(_.toString == create(bw, blog)) + + /* Is the User in this list? */ + def exists(u: User): Boolean = + get.exists(_.toString == create(u)) +} +\ No newline at end of file diff --git a/src/main/scala/com/joereader/model/field/MongoListFieldExtra.scala b/src/main/scala/com/joereader/model/field/MongoListFieldExtra.scala @@ -0,0 +1,33 @@ +package com.joereader.model.field + +import com.mongodb._ +import net.liftweb._ +import common._ +import mongodb.record._ +import field._ + +trait MongoListFieldExtra[OwnerType <: BsonRecord[OwnerType], ListType] + extends MongoListField[OwnerType, ListType] { + + def add(a: ListType): OwnerType = + if (exists(a)) owner + else this(a :: get) + + def remove(a: ListType): OwnerType = + removeStr(a.toString) + + def removeStr(s: String): OwnerType = + this(get.filterNot(_.toString == s)) + + def exists(a: ListType): Boolean = + existsStr(a.toString) + + def existsStr(s: String): Boolean = + get.exists(_.toString == s) + + def find(a: ListType): Box[ListType] = + findStr(a.toString) + + def findStr(s: String): Box[ListType] = + get.find(_.toString == s) +} +\ No newline at end of file diff --git a/src/main/scala/com/joereader/model/field/ObjectIdRefFieldExtra.scala b/src/main/scala/com/joereader/model/field/ObjectIdRefFieldExtra.scala @@ -0,0 +1,24 @@ +package com.joereader.model.field + +import org.bson.types._ +import com.mongodb._ +import net.liftweb._ +import common._ +import mongodb.record._ +import field._ + +trait ObjectIdRefFieldExtra[ + OwnerType <: BsonRecord[OwnerType], + RefType <: MongoRecord[RefType] with ObjectIdPk[RefType]] + extends ObjectIdRefField[OwnerType, RefType] { + + override def defaultValue = new ObjectId("0" * 24) + + def isEmpty: Boolean = get == defaultValue + def isDefined: Boolean = !isEmpty + def remove: OwnerType = this(defaultValue) + + def is(a: RefType): Boolean = get == a.id.get + def is(a: Box[RefType]): Boolean = a.exists(is) + def isNot(a: RefType): Boolean = !is(a) +} +\ No newline at end of file diff --git a/src/main/scala/com/joereader/model/field/ObjectIdRefListFieldExtra.scala b/src/main/scala/com/joereader/model/field/ObjectIdRefListFieldExtra.scala @@ -0,0 +1,21 @@ +package com.joereader.model.field + +import com.mongodb._ +import net.liftweb._ +import common._ +import mongodb.record._ +import field._ + + +trait ObjectIdRefListFieldExtra[ + OwnerType <: BsonRecord[OwnerType], + RefType <: MongoRecord[RefType] with ObjectIdPk[RefType]] + extends ObjectIdRefListField[OwnerType, RefType] { + + def add(a: RefType): OwnerType = + if (get.exists(_ == a.id.get)) owner + else this(a.id.get :: get) + + def remove(blog: RefType) = + this(get.filterNot(_ == blog.id.get)) +} +\ No newline at end of file diff --git a/src/main/scala/com/joereader/snippet/ArticleSnip.scala b/src/main/scala/com/joereader/snippet/ArticleSnip.scala @@ -4,37 +4,31 @@ import net.liftweb._ import common._ import http._ import SHtml._ -import js._, JsCmds._ +import js._ +import JsCmds._ import util._ import Helpers._ +import sitemap.Loc import com.joereader._ +import com.joereader.actor.EntriesEngine._ import config._ import lib.rss._ import model._ +import Article._ import dispatch._ import Defaults._ - import xml._ import java.util.Date +import net.liftmodules.extras.SnippetHelper + /** * Mix this in to list articles. */ trait ArticleSnip { - def sort(entries: List[Article]) = - entries.sortWith((x, y) => x.date.getTime > y.date.getTime) - - def parse(articles: Future[Either[String, CssSel]]) = - articles() match { - case Left(msg) => - warn(msg) - noArticles - case Right(a) => a - } - def noArticles: CssSel = ".reader-nav [class+]" #> "hide" & ".article *" #> @@ -42,74 +36,86 @@ trait ArticleSnip { No articles to display here </p> - def articles(articles: Future[Either[String, List[Article]]]) = - for (articles <- articles.right) - yield if (articles.isEmpty) noArticles + def articles(articles: List[Article]): CssSel = + if (articles.isEmpty) noArticles else ".article *" #> sort(articles).map { article => def bwu = article.bwu + def sharedBy = article.sharedBy def entry = article.entry - def shareId = article.id.hashCode + "-share" - def saveId = article.id.hashCode + "-save" + def shareId = article.toString + "-share" + def saveId = article.toString + "-save" def color = "color:" + bwu.color - def saved: Boolean = User.currentUser.exists( - _.saved.get.exists(_ == article.id)) - - def shared: Boolean = User.currentUser.exists( - _.shared.get.exists(_ == article.id)) - - def saveLink: NodeSeq = - a(() => save, Text("Save"), "id" -> saveId, "style" -> color) - - def unSaveLink: NodeSeq = - a(() => unSave, Text("Undo Save"), "id" -> saveId, "style" -> color) - - def shareLink: NodeSeq = - a(() => share, Text("Share"), "id" -> shareId, "style" -> color) - - def unShareLink: NodeSeq = - a(() => unShare, Text("Undo Share"), "id" -> shareId, "style" -> color) - - def save: JsCmd = { - for (u <- User.currentUser) - u.saveArticle(article).update - Replace(saveId, unSaveLink) - } - - def unSave: JsCmd = { - for (u <- User.currentUser) - u.unSaveArticle(article).update - Replace(saveId, saveLink) - } - - def share: JsCmd = { - for (u <- User.currentUser) { - val sa = new SharedArticle(bwu, entry, u, new Date) - u.shareArticle(sa).update - } - Replace(shareId, unShareLink) - } - - def unShare: JsCmd = { - for (u <- User.currentUser) - u.unShareArticle(article.asInstanceOf[SharedArticle]).update - Replace(shareId, shareLink) - } - - ".article-save" #> (if (User.isLoggedIn) { if (saved) unSaveLink else saveLink } else NodeSeq.Empty) & - ".article-share" #> (if (User.isLoggedIn) { if (shared) unShareLink else shareLink } else NodeSeq.Empty) & + def saveLink: NodeSeq = a( + () => { + article.save + Replace(saveId, unSaveLink) + }: JsCmd, + Text("Save"), + "id" -> saveId, + "style" -> color) + + def unSaveLink: NodeSeq = a( + () => { + article.unSave + Replace(saveId, saveLink) + }: JsCmd, + Text("Undo Save"), + "id" -> saveId, + "style" -> color) + + def shareLink: NodeSeq = a( + () => { + article.share + Replace(shareId, unShareLink) + }: JsCmd, + Text("Share"), + "id" -> shareId, + "style" -> color) + + def unShareLink: NodeSeq = a( + () => { + article.unShare + Replace(shareId, shareLink) + }: JsCmd, + Text("Undo Share"), + "id" -> shareId, + "style" -> color) + + def articleSave = + if (User.isLoggedIn) + if (article.saved) + unSaveLink + else + saveLink + else + NodeSeq.Empty + + def articleShare = + if (User.isLoggedIn) + if (article.shared) + unShareLink + else + shareLink + else + NodeSeq.Empty + + ".article-save" #> articleSave & + ".article-share" #> articleShare & ".article-inner [style]" #> ("border-right:3px solid " + bwu.color) & - ".left-sub-header-inner [class+]" #> (if (article.sharedBy.isDefined) "" else "hide") & - ".article-sharedby *" #> article.sharedBy.map(_.name.get) & - ".article-sharedby [href]" #> article.sharedBy.map(Site.userProfileLoc.calcHref) & - ".article-sharedby-img [src]" #> BlogWriterUser(article.sharedBy).image & - ".article-key [class+]" #> bwu.id & + ".article-sharedby *" #> sharedBy.map { + bwu => + "a *" #> s"Shared by ${bwu.name}" & + "a [href]" #> bwu.user.map(Site.userProfileLoc.calcHref) & + "img [src]" #> bwu.image + } & + ".article-key [class+]" #> bwu.toString & ".article-user-link [href]" #> bwu.link & "#article-user-image [src]" #> bwu.image & "#article-user-name *" #> bwu.name & - ".timeago [datetime]" #> entry.df.format(entry.date) & + ".timeago [datetime]" #> entry.dateFormatted & ".title a [href]" #> entry.link & ".title a [rel]" #> "nofollow" & ".title a *" #> entry.title & @@ -120,53 +126,152 @@ trait ArticleSnip { f => val bwu = f._1 - "#writer [id]" #> bwu.id & + "#writer [id]" #> bwu.toString & "#writer-image [src]" #> bwu.image & "#writer-name *" #> bwu.name & "a [href]" #> bwu.link } } - -} - -trait ArticleTrait { - def separator = "~" - def createId(blog: Blog, entry: FeedEntry) = - blog.id.get + separator + entry.id } -object Article extends ArticleTrait - -object SharedArticle extends ArticleTrait { - def createId(blog: Blog, entry: FeedEntry, date: Date) = - super.createId(blog, entry) + separator + date.getTime -} - -class Article(_bwu: BlogWriterUser, _entry: FeedEntry) { - - def id: String = - bwu.blog.map(Article.createId(_, entry)). - getOrElse("") - - def date: Date = entry.date - def entry = _entry - def bwu = _bwu - - val sharedBy: Option[User] = None -} - -class SharedArticle( - bwu: BlogWriterUser, - entry: FeedEntry, - _sharedBy: User, - sharedDate: Date = new Date) extends Article(bwu, entry) { - - override def date = sharedDate - override def id = - bwu.blog.map(SharedArticle.createId(_, entry, date)). - getOrElse("") +/** + * This is where the magic happens. Based on page location, + * get the articles that you need from here. + */ +object Articles extends ArticleSnip with SnippetHelper with Logger { + + protected def serveArticles(snip: Loc[_] => List[Article]) = + (for { + loc <- S.location ?~ "Location not found" + } yield { + articles { + snip(loc) + } + }): Box[CssSel] + + def render: CssSel = serveArticles { + loc => + import Site._ + + def name(m: MenuLoc) = m.menu.loc.name + def testing = TestUser.isLoggedIn + + // reader.html + if (name(reader) == loc.name && testing) { + followingArticles ::: followingSharedArticles + } + + // index.html + else if (name(home) == loc.name) { + def blog = Blog.findByBlogName("readmeans") + blogArticles(blog).headOption.toList + } + + // saved.html + else if (name(savedArticles) == loc.name && testing) { + userSaved + } + + // /*[User].html + else if (userProfileLoc.name == loc.name) { + def user = userProfileLoc.currentValue + userArticles(user) ::: userShared(user) + } + + // /blog/*[Blog].html + else if (blogProfileLoc.name == loc.name) { + def blog = blogProfileLoc.currentValue + blogArticles(blog) + } + + // /blog/*[Blog]/*[BlogWriter].html + else if (blogWriterProfileLoc.name == loc.name) { + def blogWriter = blogWriterProfileLoc.currentValue + blogWriterArticles(blogWriter) + } + + // Nothing + else { + warn("Called Articles snippet from a page" + + " that cannot be handled") + Nil + } + } + + def blogWriterArticles(bwu: Box[BlogWriterUser]) = + (for { + bwu <- bwu + blog <- bwu.blog + blogWriter <- bwu.blogWriter + } yield { + entries(blog). + filter(_.author.name == blogWriter.name.get). + map(new Article(new BlogWriterUser(blog, blogWriter), _)) + }) openOr Nil + + /* Articles from a blog. */ + def blogArticles(blog: Box[Blog]) = + (for (blog <- blog) yield { + entries(blog). + filter(e => blog.writers.existsStr(e.author.name)). + map { + entry => + val blogWriter = blog.writers.findStr(entry.author.name) + val bwu = new BlogWriterUser( + blogWriter.flatMap(_.user.obj), Some(blog), blogWriter) + new Article(bwu, entry) + } + }) openOr Nil + + + + /* Articles written by user. */ + def userArticles(user: Box[User]) = + (for (user <- user) + yield (for { + blogs <- user.blogs.get + blog <- Blog.find(blogs) + blogWriter <- blog.writers.find(user) + } yield { + entries(blog). + filter(_.author.name == blogWriter.name.get). + map(new Article(new BlogWriterUser(user, blog, blogWriter), _)) + }) flatten + ) openOr Nil + + /* + * All articles by blog writers that the + * logged in user follows. + */ + def followingArticles = + (for (user <- User.currentUser) + yield (for (bwu <- user.following.allUsers) + yield (for { + blog <- bwu.blog + blogWriter <- bwu.blogWriter + } yield { + entries(blog). + filter(_.author.name == blogWriter.name.get). + map(new Article(bwu, _)) + }) getOrElse Nil + ) flatten + ) openOr Nil + + + /* + * All articles shared by blog writers that the + * logged in user follows. + */ + def followingSharedArticles = User.currentUser.map { + _.following.usersSharedArticles.flatMap(_.shared.get) + } openOr Nil + + /* The logged in user's saved articles. */ + def userSaved = User.currentUser.map(_.saved.get) openOr Nil + + /* Articles shared by user. */ + def userShared(u: Box[User]) = u.map(_.shared.get) openOr Nil - override val sharedBy = Some(_sharedBy) } diff --git a/src/main/scala/com/joereader/snippet/BackgroundSnip.scala b/src/main/scala/com/joereader/snippet/BackgroundSnip.scala @@ -77,7 +77,7 @@ trait BackgroundSnip { "#bg-fileupload-container *" #> insertFileUpload(bg, ImageUpload.userUrl(bg)) & "#backgrounds-list *" #> - listBackgrounds(user.bgImg.get, BlogWriterUser(Some(user))) & + listBackgrounds(user.bgImg.get, new BlogWriterUser(user)) & "#backgrounds-q-yes" #> radios(0) & "#backgrounds-q-no" #> radios(1) } @@ -89,7 +89,7 @@ trait BackgroundSnip { "#bg-fileupload-container *" #> insertFileUpload(bg, ImageUpload.blogUrl(blog, bg)) & "#backgrounds-list *" #> - listBackgrounds(blog.bgImg.get, BlogWriterUser(None, Some(blog))) & + listBackgrounds(blog.bgImg.get, new BlogWriterUser(None, Some(blog))) & "#backgrounds-q-yes" #> radios(0) & "#backgrounds-q-no" #> radios(1) } diff --git a/src/main/scala/com/joereader/snippet/BlogSnipEdit.scala b/src/main/scala/com/joereader/snippet/BlogSnipEdit.scala @@ -23,7 +23,7 @@ trait BlogSnipEdit extends BlogSnipView with BackgroundSnip { // only allow blog owner // must be kept private so it won't be overridden by sub or super classes - private def test = blog.exists(_ isOwner User.currentUser) + private def test = blog.exists(_.owner.is(User.currentUser)) def name(html: NodeSeq) = serve(html) { blog => diff --git a/src/main/scala/com/joereader/snippet/BlogSnipView.scala b/src/main/scala/com/joereader/snippet/BlogSnipView.scala @@ -16,7 +16,7 @@ import dispatch._, Defaults._ /** * Snippets for the public to view Blog model information. */ -trait BlogSnipView extends BlogSnip with ArticleSnip with BackgroundSnip { +trait BlogSnipView extends BlogSnip with BackgroundSnip { // true so anyone can view blog snippets // must be kept private so it won't be overridden by subclasses @@ -51,23 +51,4 @@ trait BlogSnipView extends BlogSnip with ArticleSnip with BackgroundSnip { "* [id]" #> imgBgId & "* [src]" #> imageBgUrl(blog) }(test, NodeSeq.Empty) - - def articles(html: NodeSeq): NodeSeq = serve(html) { - blog => - - def url = blog.urlRss.get.head - val entries = - for (entries <- url.entries.right) - yield entries.filter( - e => blog.writerExists(e.author.name)).map { - entry => - val blogWriter: Box[BlogWriter] = blog.writer(entry.author.name) - val bwu = BlogWriterUser( - blogWriter.flatMap(_.user.obj), Some(blog), blogWriter) - new Article(bwu, entry) - } - - parse(articles(entries)) - - }(test, NodeSeq.Empty) } diff --git a/src/main/scala/com/joereader/snippet/BlogWriterCategoriesSnip.scala b/src/main/scala/com/joereader/snippet/BlogWriterCategoriesSnip.scala @@ -27,7 +27,7 @@ object BlogWriterCategoriesSnip extends BlogWriterSnip { for { blog <- blog user <- User.currentUser - blogWriter <- blog.writer(user) + blogWriter <- blog.writers.find(user) } yield blogWriter private def test: Boolean = blogWriter.exists( @@ -52,11 +52,13 @@ object BlogWriterCategoriesSnip extends BlogWriterSnip { blogWriter <- blogWriter blog <- blog } { - blogWriter.addCategory(cat) + blogWriter.categories.add(cat) blog.save - val c = Category.find(cat).map(_.addWriter(blog, blogWriter).update) + val c = Category.find(cat).map(_.writers.add( + new BlogWriterUser(blog, blogWriter)).update) if (c.isEmpty) - Category.createRecord.id(cat).addWriter(blog, blogWriter).save + Category.createRecord.id(cat).writers.add( + new BlogWriterUser(blog, blogWriter)).save } Prepend("chosen-categories", categoryNode(cat)) & @@ -69,9 +71,10 @@ object BlogWriterCategoriesSnip extends BlogWriterSnip { blogWriter <- blogWriter blog <- blog } { - blogWriter.removeCategory(cat) + blogWriter.categories.remove(cat) blog.save - Category.find(cat).map(_.removeWriter(blog, blogWriter).update) + Category.find(cat).map(_.writers.remove( + new BlogWriterUser(blog, blogWriter)).update) } Remove("chosen-cat-" + cat.split(" ").mkString("-")) diff --git a/src/main/scala/com/joereader/snippet/BlogWriterColorSnip.scala b/src/main/scala/com/joereader/snippet/BlogWriterColorSnip.scala @@ -26,7 +26,7 @@ object BlogWriterColorSnip extends BlogWriterSnip { for { blog <- blog user <- User.currentUser - blogWriter <- blog.writer(user) + blogWriter <- blog.writers.find(user) } yield blogWriter private def test: Boolean = blogWriter.exists( diff --git a/src/main/scala/com/joereader/snippet/BlogWriterSnipView.scala b/src/main/scala/com/joereader/snippet/BlogWriterSnipView.scala @@ -8,6 +8,7 @@ import Defaults._ import com.joereader._ import config._ +import model._ import lib.rss._ import snippet.SnipHelpers._ import scala.xml._ @@ -16,18 +17,7 @@ import com.joereader.model.BlogWriterUser /** * Snippets to view Blog Writer information. */ -trait BlogWriterSnipView extends BlogWriterUserSnip with ArticleSnip with BackgroundSnip { - - def articles(html: NodeSeq): NodeSeq = serve(html) { - (blog, blogWriter) => - - val entries = blog.urlRss.get.head.entries.right.map(_. - filter(_.author.name == blogWriter.name.get). - map(new Article(BlogWriterUser(None, Some(blog), Some(blogWriter)), _))) - - parse(articles(entries)) - - }(test = true, NodeSeq.Empty) +trait BlogWriterSnipView extends BlogWriterUserSnip with BackgroundSnip { def name: NodeSeq = serve { (blog, blogWriter) => Text(blogWriter.name.get) diff --git a/src/main/scala/com/joereader/snippet/BlogWritersSnipView.scala b/src/main/scala/com/joereader/snippet/BlogWritersSnipView.scala @@ -20,81 +20,84 @@ import net.liftmodules.extras.Gravatar trait BlogWritersSnipView extends BlogSnip { def showIfOwner(html: NodeSeq): NodeSeq = - blog.map(b => if(b.isOwner(User.currentUser)) html else NodeSeq.Empty) + blog.map(b => if (b.owner.is(User.currentUser)) html else NodeSeq.Empty) def writers(html: NodeSeq) = serve(html) { blog => - val owner = blog.isOwner(User.currentUser) + val owner = blog.owner.is(User.currentUser) ".writer *" #> blog.writers.get.map { blogWriter => var msg = "" - val bwu = BlogWriterUser( + val bwu = new BlogWriterUser( blogWriter.user.obj, Some(blog), Some(blogWriter)) def inviteForm: NodeSeq = - if (!blogWriter.hasUser && owner) { + if (blogWriter.user.isEmpty && owner) { msg = "Who is " + bwu.name + "? Has he registered already? If" + " so, input the email that the person used to sign up. If" + " he's not registered, we'll send " + bwu.name + " an invite. " - <input type="text" id={"email-" + bwu.dashName} placeholder="email" value={blogWriter.email.get}/> - <button onclick={ajaxCall(JsArray(ValById("email-" + + <input type="text" id={ "email-" + bwu.dashName } placeholder="email" value={ blogWriter.email.get }/> + <button onclick={ + ajaxCall(JsArray(ValById("email-" + bwu.dashName), Str(bwu.name)), invite)._2.toJsCmd.toString + - "; return false;"} class="btn btn-primary">Invite</button> - } - else NodeSeq.Empty + "; return false;" + } class="btn btn-primary">Invite</button> + } else NodeSeq.Empty def removeButton(): NodeSeq = if (owner) { msg = msg + "If you would like to remove " + bwu.name + ", click remove button. " - <button onclick={ajaxCall(JsArray(Str(blogWriter.name.get), - Str(bwu.name)), removeAlert)._2.toJsCmd.toString + - "; return false;"} class="btn btn-primary">Remove</button> - } - else NodeSeq.Empty + <button onclick={ + ajaxCall(JsArray(Str(blogWriter.name.get), + Str(bwu.name)), removeAlert)._2.toJsCmd.toString + + "; return false;" + } class="btn btn-primary">Remove</button> + } else NodeSeq.Empty def detachButton(): NodeSeq = - if (owner && blogWriter.hasUser) { + if (owner && blogWriter.user.isDefined) { msg = msg + "To remove the user account connected to " + bwu.name + ", click Detach User button." val user = bwu.user.getOrElse(User.createRecord) - <button id={"detach-btn-" + bwu.dashName} onclick={ajaxCall( - JsArray(Str(user.username.get), Str(blogWriter.name.get), - Str(bwu.name)), detachUserAlert)._2.toJsCmd.toString + - "; return false;"} class="btn btn-primary">Detach User</button> - } - else NodeSeq.Empty + <button id={ "detach-btn-" + bwu.dashName } onclick={ + ajaxCall( + JsArray(Str(user.username.get), Str(blogWriter.name.get), + Str(bwu.name)), detachUserAlert)._2.toJsCmd.toString + + "; return false;" + } class="btn btn-primary">Detach User</button> + } else NodeSeq.Empty ".blog-writer [id]" #> ("writer-" + bwu.dashName) & - ".blog-writer" #> { - ".writer-link [href]" #> bwu.link & - "img [id]" #> ("img-" + bwu.dashName) & - "img [src]" #> bwu.image & - ".name .text *" #> bwu.name & - ".name .edit-link a [onclick]" #> - Show("writer-edit-" + bwu.dashName).toJsCmd.toString & - ".input-area *" #> (inviteForm ++ removeButton ++ detachButton) & - ".categories *" #> bwu.categories - } & - ".writer-edit-area [id]" #> ("writer-edit-" + bwu.dashName) & - ".writer-edit-area" #> { - ".name *" #> bwu.name & - ".msg-area small *" #> msg & - ".input-area *" #> (inviteForm ++ removeButton ++ detachButton) & - ".exit-link [onclick]" #> + ".blog-writer" #> { + ".writer-link [href]" #> bwu.link & + "img [id]" #> ("img-" + bwu.dashName) & + "img [src]" #> bwu.image & + ".name .text *" #> bwu.name & + ".name .edit-link a [onclick]" #> + Show("writer-edit-" + bwu.dashName).toJsCmd.toString & + ".input-area *" #> (inviteForm ++ removeButton ++ detachButton) & + ".categories *" #> bwu.categories + } & + ".writer-edit-area [id]" #> ("writer-edit-" + bwu.dashName) & + ".writer-edit-area" #> { + ".name *" #> bwu.name & + ".msg-area small *" #> msg & + ".input-area *" #> (inviteForm ++ removeButton ++ detachButton) & + ".exit-link [onclick]" #> Hide("writer-edit-" + bwu.dashName).toJsCmd.toString - } + } } & - "style *" #> ("#blog-writers .overview{width:" + - (blog.writers.get.size * 200) + "px}") + "style *" #> ("#blog-writers .overview{width:" + + (blog.writers.get.size * 200) + "px}") }(test = true, NodeSeq.Empty) @@ -104,7 +107,7 @@ trait BlogWritersSnipView extends BlogSnip { for { blog <- blog - blogWriter <- blog.writer(name) + blogWriter <- blog.writers.findStr(name) } { val followSize = blogWriter.followers.get.size @@ -115,14 +118,15 @@ trait BlogWritersSnipView extends BlogSnip { S.warning( <div> - {msg} + { msg } </div> - <div> - <button onclick={ajaxCall(JsArray(Str(name), Str(displayName)), remove). - _2.toJsCmd.toString + "; return false;"} class="btn btn-warning">Yes</button> - <button onclick="$(document).trigger('clear-alerts')" class="btn">No</button> - </div> - ) + <div> + <button onclick={ + ajaxCall(JsArray(Str(name), Str(displayName)), remove). + _2.toJsCmd.toString + "; return false;" + } class="btn btn-warning">Yes</button> + <button onclick="$(document).trigger('clear-alerts')" class="btn">No</button> + </div>) } } @@ -132,12 +136,15 @@ trait BlogWritersSnipView extends BlogSnip { for { blog <- blog - blogWriter <- blog.writer(name) + blogWriter <- blog.writers.findStr(name) } yield { - blogWriter.followers.objs.map(_.unFollow(blogWriter, blog).update) - blog.removeWriter(blogWriter).update + blogWriter.followers.objs.map(_.following.remove( + new BlogWriterUser(blog, blogWriter)).update) + blog.writers.remove(blogWriter).update val dashName = displayName.split(" ").mkString("-") - Remove("writer-" + dashName) & Remove("writer-edit-" + dashName) & Hide("writer-edit-" + dashName) + Remove("writer-" + dashName) & + Remove("writer-edit-" + dashName) & + Hide("writer-edit-" + dashName) } } @@ -148,15 +155,17 @@ trait BlogWritersSnipView extends BlogSnip { for (blog <- blog) { S.warning( - <div>Are you sure you want to detach from - {blog.name.get} + <div> + Are you sure you want to detach from + { blog.name.get } </div> - <div> - <button onclick={ajaxCall(JsArray(Str(username), Str(name), Str(displayName)), detachUser). - _2.toJsCmd.toString + "; return false;"} class="btn btn-warning">Yes</button> - <button onclick="$(document).trigger('clear-alerts')" class="btn">No</button> - </div> - ) + <div> + <button onclick={ + ajaxCall(JsArray(Str(username), Str(name), Str(displayName)), detachUser). + _2.toJsCmd.toString + "; return false;" + } class="btn btn-warning">Yes</button> + <button onclick="$(document).trigger('clear-alerts')" class="btn">No</button> + </div>) } } @@ -167,13 +176,13 @@ trait BlogWritersSnipView extends BlogSnip { for { blog <- blog - blogWriter <- blog.writer(name) + blogWriter <- blog.writers.findStr(name) user <- User.findByUsername(username) } yield { - user.removeBlog(blog).update - blogWriter.removeUser() + user.blogs.remove(blog).update + blogWriter.user.remove - if(blog.isOwner(user)) + if (blog.owner.is(user)) blog.owner(blog.owner.defaultValue) blog.save @@ -192,7 +201,7 @@ trait BlogWritersSnipView extends BlogSnip { blog <- blog user <- User.currentUser } { - blog.writer(name).map(_.email(email)) + blog.writers.findStr(name).map(_.email(email)) blog.save User.sendInviteToken(name, email, user, blog) } diff --git a/src/main/scala/com/joereader/snippet/FollowSnip.scala b/src/main/scala/com/joereader/snippet/FollowSnip.scala @@ -59,13 +59,15 @@ trait FollowSnip extends BlogWriterUserSnip { loggedInUser <- User.currentUser user <- User.find(user.id.get) } yield { - loggedInUser.follow(user).update - user.addFollower(loggedInUser).update + loggedInUser.following.add( + new BlogWriterUser(user)).update + user.followers.add(loggedInUser).update user.blogs.objs.map { blog => - blog.writer(user).map { writer => - writer.addFollower(loggedInUser) - loggedInUser.follow(writer, blog).update + blog.writers.find(user).map { writer => + writer.followers.add(loggedInUser) + loggedInUser.following.add( + new BlogWriterUser(blog, writer)).update } blog.save } @@ -78,13 +80,15 @@ trait FollowSnip extends BlogWriterUserSnip { loggedInUser <- User.currentUser user <- User.find(user.id.get) } yield { - loggedInUser.unFollow(user).update - user.removeFollower(loggedInUser).update + loggedInUser.following.remove( + new BlogWriterUser(user)).update + user.followers.remove(loggedInUser).update user.blogs.objs.map { blog => - blog.writer(user).map { writer => - writer.removeFollower(loggedInUser) - loggedInUser.unFollow(writer, blog).update + blog.writers.find(user).map { writer => + writer.followers.remove(loggedInUser) + loggedInUser.following.remove( + new BlogWriterUser(blog, writer)).update } blog.save } @@ -127,10 +131,11 @@ trait FollowSnip extends BlogWriterUserSnip { for { user <- User.currentUser blog <- Blog.find(blog.id.get) - blogWriter <- blog.writer(blogWriter.name.get) + blogWriter <- blog.writers.findStr(blogWriter.name.get) } yield { - user.follow(blogWriter, blog).update - blogWriter.addFollower(user) + user.following.add( + new BlogWriterUser(blog, blogWriter)).update + blogWriter.followers.add(user) blog.save } Replace(btnId, unFollowBtn) @@ -140,10 +145,11 @@ trait FollowSnip extends BlogWriterUserSnip { for { user <- User.currentUser blog <- Blog.find(blog.id.get) - blogWriter <- blog.writer(blogWriter.name.get) + blogWriter <- blog.writers.findStr(blogWriter.name.get) } yield { - user.unFollow(blogWriter, blog).update - blogWriter.removeFollower(user) + user.following.remove( + new BlogWriterUser(blog, blogWriter)).update + blogWriter.followers.remove(user) blog.save } Replace(btnId, followBtn) @@ -169,7 +175,7 @@ trait FollowSnip extends BlogWriterUserSnip { val blogFollowers = (for { blog <- user.blogs.objs - blogWriter <- blog.writer(user) + blogWriter <- blog.writers.find(user) } yield blogWriter.followers.get).flatten val followers = (blogFollowers ::: user.followers.get).distinct diff --git a/src/main/scala/com/joereader/snippet/LiftExtras.scala b/src/main/scala/com/joereader/snippet/LiftExtras.scala @@ -2,7 +2,7 @@ package com.joereader.snippet import net.liftweb._ import util._, Helpers._ -import http.js._ +import http._, js._ import com.joereader.model._ import com.joereader.config._, S3Config._ @@ -28,19 +28,28 @@ object ProductionOnly { else NodeSeq.Empty } - object SnipHelpers { def head = { val desc = "RSS reader that allows you to discover and follow blog writers." - val keywords = List("rss","reader","blogs","news") + val keywords = List("rss", "reader", "blogs", "news") val copyright = "Copyright Read Means 2013. All Rights Reserved." - + "@description [content]" #> desc & - "@keywords [content]" #> keywords.mkString(",") & - "@Copyright [content]" #> copyright + "@keywords [content]" #> keywords.mkString(",") & + "@Copyright [content]" #> copyright } - + + def comingSoon: NodeSeq = + if (TestUser.isLoggedIn) NodeSeq.Empty + else { + <div style="margin: 100px 0" class="text-center"> + <h3>Have another blog?</h3> + <a data-lift="UserBlogsSnip.addBlog" class="btn btn-primary" style="margin-bottom: 50px">Add Blog</a> + { Templates("templates-hidden" :: "parts" :: "social" :: Nil).openOr(NodeSeq.Empty) } + </div> + } + val imgProfileId = "img-profile" val imgBgId = "img-bg" @@ -48,20 +57,19 @@ object SnipHelpers { if (b.img.get.nonEmpty) s3.fileUrl(b.img.is) else s3.fileUrl("mr_noblog_color") - def insertFileUpload(id: ImageType, path: String): NodeSeq = { - <input id={id.toString + "-fileupload"} type="file" name="files2[]" data-url={ - path} accept={ImageUpload.acceptedImages.mkString(",")}/> - <div id={id.toString + "-progress"} style="width:20em; border: 1pt solid silver; display: none; margin: 10px 0"> - <div id={id.toString + "-progress-bar"} style="background: green; height: 1em; width:0%"></div> - </div> + <input id={ id.toString + "-fileupload" } type="file" name="files2[]" data-url={ + path + } accept={ ImageUpload.acceptedImages.mkString(",") }/> + <div id={ id.toString + "-progress" } style="width:20em; border: 1pt solid silver; display: none; margin: 10px 0"> + <div id={ id.toString + "-progress-bar" } style="background: green; height: 1em; width:0%"></div> + </div> } def setNoticeContainer() = "#notices-container [id]" #> (if (SignUp.hideSignup) "notices-container-top" else "notices-container") - class JQuery(id: String, func: String, params: String*) extends JsCmd { val script = "$('#%s').%s('%s')".format(id, func, params.mkString("','")) diff --git a/src/main/scala/com/joereader/snippet/MyBlog.scala b/src/main/scala/com/joereader/snippet/MyBlog.scala @@ -1,45 +0,0 @@ -package com.joereader.snippet - -import com.joereader._ -import model._ -import config._ -import lib.rss._ - -import dispatch._ -import Defaults._ - -import scala.xml._ - -import net.liftweb._ -import util.Helpers._ -import common._ - -/** - * Stuff for Read Means blog. - */ -class MyBlog extends BlogSnip with ArticleSnip { - - override def blog = Blog.findByBlogName("readmeans") - - def link = "* [href]" #> blog.map(Site.blogProfileLoc.calcHref) - - def blogExists(html: NodeSeq) = if(blog.isDefined) html else NodeSeq.Empty - - def entry(html: NodeSeq): NodeSeq = serve(html) { - blog => - - val entry = - blog.urlRss.is.head.entries.right.map(_. - find(e => blog.writerExists(e.author.name)).map { - entry => - - val blogWriter: Box[BlogWriter] = blog.writer(entry.author.name) - val bwu = BlogWriterUser( - blogWriter.flatMap(_.user.obj), Some(blog), blogWriter) - new Article(bwu, entry) - }.toList) - - parse(articles(entry)) - - }(test = true, NodeSeq.Empty) -} diff --git a/src/main/scala/com/joereader/snippet/ReaderSnip.scala b/src/main/scala/com/joereader/snippet/ReaderSnip.scala @@ -1,141 +0,0 @@ -package com.joereader.snippet - -import net.liftweb._ -import common._ -import http._ -import util.Helpers._ - -import com.joereader._ -import model._ -import lib.rss._ - -import dispatch._ -import Defaults._ - -import scala.xml._ -import java.util.Date - -/** - * View all of your articles. - */ -class ReaderSnip extends UserSnip with ArticleSnip { - protected def user: Box[User] = User.currentUser - - def articles(html: NodeSeq): NodeSeq = serve(html) { - user => - - val articlesFollowing: List[Future[Either[String, List[Article]]]] = - user.following.allUsers.flatMap(bwu => - for { - blog <- bwu.blog - blogWriter <- bwu.blogWriter - } yield { - val url = blog.urlRss.get.head - url.entries.right.map( entries => - entries.filter(_.author.name == blogWriter.name.get.toString). - map(new Article(bwu, _))) - } - ) - - - val articlesShared: List[Future[Either[String, List[Article]]]] = { - user.following.usersSharedArticles.flatMap { user => - - val blogIds: List[String] = - user.shared.get.flatMap(str => - str.split(Article.separator).headOption).distinct - - val blogs: List[Blog] = - blogIds.flatMap(Blog.findByStringId) - - val allEntries: List[(Blog, Future[Either[String, List[FeedEntry]]])] = - blogs.map(b => (b, b.urlRss.get.head.entries)) - - val sharedEntries: List[(Blog, Future[Either[String, List[FeedEntry]]])] = - allEntries.map(f => - (f._1, f._2.right.map(_.filter { e => - user.shared.get.exists(_.startsWith(Article.createId(f._1, e))) - }))).filterNot(_._2.right.map(_.isEmpty)() match { - case Left(_) => false - case Right(b) => b - }) - - sharedEntries.map(f => - f._2.right.map(_.map { entry => - val id = user.shared.get.find(_.startsWith(Article.createId(f._1, entry))) - val time = id.map(_.split(SharedArticle.separator).last) - val date: Option[Date] = tryo(time.map(t => new Date(t.toLong))).openOr(None) - - val blog = Some(f._1) - val blogWriter = f._1.writer(entry.author.name) - val u = blogWriter.flatMap(_.user.obj) - new SharedArticle(BlogWriterUser(u, blog, blogWriter), entry, user, date.getOrElse(entry.date)) - })) - } - } - - def mergeEntries(entries: List[Future[Either[String, List[Article]]]]): List[Article] = - entries.flatMap(_() match { - case Left(msg) => - warn(msg); Nil - case Right(entries) => entries - }) - - parse(articles(Future(Right(mergeEntries(articlesFollowing) ::: mergeEntries(articlesShared))))) - - }(TestUser.isLoggedIn, NodeSeq.Empty) - - def savedArticles(html: NodeSeq): NodeSeq = serve(html) { - user => - - def entries: List[Future[Either[String, List[Article]]]] = { - val blogIds: List[String] = - user.saved.get.flatMap(str => - str.split(Article.separator).headOption).distinct - - val blogs: List[Blog] = - blogIds.flatMap(Blog.findByStringId) - - val allEntries: List[(Blog, Future[Either[String, List[FeedEntry]]])] = - blogs.map(b => (b, b.urlRss.get.head.entries)) - - val savedEntries: List[(Blog, Future[Either[String, List[FeedEntry]]])] = - allEntries.map(f => - (f._1, f._2.right.map(_.filter(e => user.saved.get. - exists(_ == Article.createId(f._1, e)))))). - filterNot(_._2.right.map(_.isEmpty)() match { - case Left(_) => false - case Right(b) => b - }) - - savedEntries.map(f => - f._2.right.map(_.map { entry => - val blog = Some(f._1) - val blogWriter = f._1.writer(entry.author.name) - val user = blogWriter.flatMap(_.user.obj) - new Article(BlogWriterUser(user, blog, blogWriter), entry) - })) - } - - def mergeEntries(entries: List[Future[Either[String, List[Article]]]]): List[Article] = - entries.flatMap(_() match { - case Left(msg) => - warn(msg); Nil - case Right(entries) => entries - }) - - parse(articles(Future(Right(mergeEntries(entries))))) - - }(TestUser.isLoggedIn, NodeSeq.Empty) - - def comingSoon: NodeSeq = - if (TestUser.isLoggedIn) NodeSeq.Empty - else { - <div style="margin: 100px 0" class="text-center"> - <h3>Have another blog?</h3> - <a data-lift="UserBlogsSnip.addBlog" class="btn btn-primary" style="margin-bottom: 50px">Add Blog</a> - { Templates("templates-hidden" :: "parts" :: "social" :: Nil).openOr(NodeSeq.Empty) } - </div> - - } -} diff --git a/src/main/scala/com/joereader/snippet/SignUp.scala b/src/main/scala/com/joereader/snippet/SignUp.scala @@ -123,7 +123,7 @@ class SignUp { assert(!verified || user.isEmpty || blog.isEmpty) // skip to next page if this user is not owner - if (blog.exists {blog => User.currentUser.exists(blog.nonOwner)}) + if (blog.exists {blog => User.currentUser.exists(blog.owner.isNot)}) S.redirectTo(Site.signUp5.url, () => VerifiedVar(verified)) def continue(): JsCmd = diff --git a/src/main/scala/com/joereader/snippet/UserReaderSnipView.scala b/src/main/scala/com/joereader/snippet/UserReaderSnipView.scala @@ -18,7 +18,7 @@ trait UserReaderSnipView extends UserSnip { def img(html: NodeSeq) = serve(html) { user => - val bwu = BlogWriterUser(Some(user)) + val bwu = new BlogWriterUser(user) "* [id]" #> imgProfileId & "* [class+]" #> "profile-img-size" & "* [src]" #> bwu.image diff --git a/src/main/scala/com/joereader/snippet/UserSnips.scala b/src/main/scala/com/joereader/snippet/UserSnips.scala @@ -47,7 +47,7 @@ object CurrentWriter extends UserWriterSnipEdit { object ProfileLocReader extends UserReaderSnipEdit with FollowSnip { override protected def user = Site.userProfileLoc.currentValue - protected def bwu = user.map(u => BlogWriterUser(user)) + protected def bwu = user.map(u => new BlogWriterUser(u)) } object ProfileLocWriter extends UserWriterSnipEdit { @@ -56,7 +56,7 @@ object ProfileLocWriter extends UserWriterSnipEdit { object PreviewLocReader extends UserReaderSnipView with FollowSnip { override protected def user = Site.userPreviewLoc.currentValue - protected def bwu = user.map(u => BlogWriterUser(user)) + protected def bwu = user.map(u => new BlogWriterUser(u)) } object PreviewLocWriter extends UserWriterSnipView { diff --git a/src/main/scala/com/joereader/snippet/UserTopbar.scala b/src/main/scala/com/joereader/snippet/UserTopbar.scala @@ -20,7 +20,7 @@ object UserTopbar { <li class="dropdown" data-dropdown="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown"> <img src={ - val bwu = BlogWriterUser(Some(user)) + val bwu = new BlogWriterUser(user) bwu.image } width="20" height="20" style="margin-right:10px"></img> <span> diff --git a/src/main/scala/com/joereader/snippet/UserWriterSnipEdit.scala b/src/main/scala/com/joereader/snippet/UserWriterSnipEdit.scala @@ -104,7 +104,7 @@ trait UserWriterSnipEdit extends UserWriterSnipView with BackgroundSnip { newVids.map { id => if (validVideo(id)) { - user.addVideo(id).update + user.otherVid.add(id).update vids = id :: vids } else failed = id :: failed } diff --git a/src/main/scala/com/joereader/snippet/UserWriterSnipView.scala b/src/main/scala/com/joereader/snippet/UserWriterSnipView.scala @@ -10,12 +10,13 @@ import com.joereader._ import config._ import lib.rss._ import model._ +import Article._ import SnipHelpers._ /** * Snippets to view information about a writer only. */ -trait UserWriterSnipView extends UserSnip with ArticleSnip with BackgroundSnip { +trait UserWriterSnipView extends UserSnip with BackgroundSnip { private def test = user.exists(_ isWriter) @@ -34,7 +35,7 @@ trait UserWriterSnipView extends UserSnip with ArticleSnip with BackgroundSnip { def followingList(html: NodeSeq) = serve(html) { user => - val following = user.following.randomUsers(6) + val following = user.following.random(6) "*" #> following.map(bwu => <a href={ bwu.link }> @@ -48,73 +49,12 @@ trait UserWriterSnipView extends UserSnip with ArticleSnip with BackgroundSnip { if (following > 0) "*" #> <a href={ Site.userFollowingLoc.calcHref(user) }> - Following{ following } + Following { following } </a> else "*" #> NodeSeq.Empty }(test, NodeSeq.Empty) - def articles(html: NodeSeq): NodeSeq = serve(html) { - user => - - val entries: List[Future[Either[String, List[Article]]]] = - for { - blogs <- user.blogs.get - blog <- Blog.find(blogs) - blogWriter <- blog.writer(user) - } yield { - val url = blog.urlRss.get.head - url.entries.right.map(entries => - entries.filter(_.author.name == blogWriter.name.get). - map(new Article(BlogWriterUser( - Some(user), Some(blog), Some(blogWriter)), _))) - } - - val sharedEntries: List[Future[Either[String, List[SharedArticle]]]] = { - val blogIds: List[String] = - user.shared.get.flatMap(str => - str.split(Article.separator).headOption).distinct - - val blogs: List[Blog] = - blogIds.flatMap(Blog.findByStringId) - - val allEntries: List[(Blog, Future[Either[String, List[FeedEntry]]])] = - blogs.map(b => (b, b.urlRss.get.head.entries)) - - val sharedEntries: List[(Blog, Future[Either[String, List[FeedEntry]]])] = - allEntries.map(f => - (f._1, f._2.right.map { entries => - entries.filter(e => - user.shared.get.exists(_.startsWith(Article.createId(f._1, e)))) - })).filterNot(_._2.right.map(_.isEmpty)() match { - case Left(_) => false - case Right(b) => b - }) - - sharedEntries.map(f => - f._2.right.map(_.map { entry => - val id = user.shared.get.find(_.startsWith(Article.createId(f._1, entry))) - val time = id.map(_.split(SharedArticle.separator).last) - val date: Option[Date] = tryo(time.map(t => new Date(t.toLong))).openOr(None) - - val blog = Some(f._1) - val blogWriter = f._1.writer(entry.author.name) - val u = blogWriter.flatMap(_.user.obj) - new SharedArticle(BlogWriterUser(u, blog, blogWriter), entry, user, date.getOrElse(entry.date)) - })) - } - - def mergeEntries(entries: List[Future[Either[String, List[Article]]]]): List[Article] = - entries.flatMap(_() match { - case Left(msg) => - warn(msg); Nil - case Right(entries) => entries - }) - - parse(articles(Future(Right(mergeEntries(entries) ::: mergeEntries(sharedEntries))))) - - }(test, NodeSeq.Empty) - def showIfWriter(html: NodeSeq) = if (test) html else NodeSeq.Empty diff --git a/src/main/scala/com/joereader/snippet/Verification.scala b/src/main/scala/com/joereader/snippet/Verification.scala @@ -63,7 +63,7 @@ class Verification extends VerificationDesigns { site <- WordpressClient.site(me)().right } { if (URLFormatter.same(set.urlHtml, site.URL.toString) && - set.blog.writersNames.exists(_ == me.username)) { + set.blog.writers.names.exists(_ == me.username)) { writerName.set(me.username) set.verified = true } @@ -82,7 +82,7 @@ class Verification extends VerificationDesigns { val bw = BlogWriter.createRecord.name(writerName.is).user(set.user.id.is) if (owner) set.blog.owner(set.user.id.is) - set.blog.addWriterSafely(bw) + set.blog.writers.addSafely(bw) if (set.blog.blogname.is.isEmpty) set.blog.blogname(StringHelpers.randomString(15).toLowerCase) @@ -124,7 +124,7 @@ class Verification extends VerificationDesigns { } else { existing.map(set.blog = _) writers.right.map(_.map( - writer => set.blog.addWriterSafely(writer))) + writer => set.blog.writers.addSafely(writer))) } set.searched = true @@ -149,8 +149,8 @@ class Verification extends VerificationDesigns { } def writersRadios = ajaxRadio[String]( - set.blog.unregisteredWritersNames, - Full(set.blog.unregisteredWritersNames headOr ""), { + set.blog.writers.unregisteredNames, + Full(set.blog.writers.unregisteredNames headOr ""), { s => onWritersRadiosChange(s) & onWritersRadiosChangeSignUp(s) }).unregisteredWritersChoicesToPictureForm(set.blog) @@ -163,32 +163,32 @@ class Verification extends VerificationDesigns { }) def registeredUsers = - set.blog.registeredWriters. + set.blog.writers.registered. registeredWritersChoicesToPictureForm(set.blog) def completedMsg = - if (set.blog.unregisteredWriters.isEmpty && set.searched) + if (set.blog.writers.unregistered.isEmpty && set.searched) Text("Looks like all writers have joined") else NodeSeq.Empty def hideVerification: String = { - set.blog.unregisteredWritersNames.nonEmpty ? "" | "hide" + set.blog.writers.unregisteredNames.nonEmpty ? "" | "hide" } def hideHelp: String = hideVerification def hideOwnerQuestion: String = { - val size = set.blog.unregisteredWritersNames.size + val size = set.blog.writers.unregisteredNames.size owner.set((size == 1) ? true | false) (size < 2) ? "hide" | "" } // returns the radio group of writers def listWriters(): NodeSeq = - if (set.blog.writersNames.isEmpty && set.searched) + if (set.blog.writers.names.isEmpty && set.searched) Text("Writers not found. Double check your url.") else { - writerName.set(set.blog.unregisteredWritersNames headOr "") + writerName.set(set.blog.writers.unregisteredNames headOr "") if (!EmailVar.is.isEmpty) { set.user.name(writerName.is) } @@ -205,7 +205,7 @@ class Verification extends VerificationDesigns { BetaUser.find(EmailVar.is).map(_.delete_!) set.user - .addBlog(set.blog) + .blogs.add(set.blog) .email(EmailVar.is) .password(Helpers.randomString(20)) .username(StringHelpers.randomString(15).toLowerCase) @@ -228,7 +228,7 @@ class Verification extends VerificationDesigns { protected def onVerified(): JsCmd = if (EmailVar.is.isEmpty) { - set.user.addBlog(set.blog).update + set.user.blogs.add(set.blog).update if (owner) S.redirectTo(Site.editBlogs.url) else S.notice("Congratulations! You have verified your blog!") @@ -254,13 +254,13 @@ sealed trait VerificationDesigns { def unregisteredWritersChoicesToPictureForm(blog: Blog): NodeSeq = { - val blogWriters = blog.unregisteredWriters + val blogWriters = blog.writers.unregistered if (blogWriters.size != choices.items.size) NodeSeq.Empty else for (i <- 0 until choices.items.size) yield { val item = choices.items(i) - val bwu = BlogWriterUser(None, Some(blog), Some(blogWriters(i))) + val bwu = new BlogWriterUser(blog, blogWriters(i)) <label> { item.xhtml }<div> @@ -278,9 +278,9 @@ sealed trait VerificationDesigns { for (i <- 0 until writers.size) yield { val blogWriter = writers(i) - val user = User.findByStringId(blogWriter.user.is.toString) + val user = User.findByStringId(blogWriter.user.get.toString) - val bwu = BlogWriterUser(user, Some(blog), Some(blogWriter)) + val bwu = new BlogWriterUser(user, Some(blog), Some(blogWriter)) <label> <div> diff --git a/src/main/webapp/blogwriter.html b/src/main/webapp/blogwriter.html @@ -50,60 +50,7 @@ </div> <div id="profile-articles-area"> - <div class="container"> - <div data-lift="ProfileLocBlogWriter.articles" class="row-fluid"> - - <div id="reader-nav-wrapper" class="span2 offset0"> - <div - class="reader-nav-blog reader-nav reader-span-spacing nolink-decoration"> - <ul id="reader-writers" class="nav text-center"> - <li id="writer"><a href=""> <img id="writer-image" - src="" /> - </a> <a id="writer-name" href=""></a></li> - </ul> - </div> - </div> - - <div id="boxed-articles-wrapper" class="span8 offset0"> - <div class="boxed-articles boxed-articles-span"> - <div class="article keynav-article"> - <div class="article-inner"> - <div class="article-key"></div> - <div class="row-fluid header text-center"> - <div class="span12"> - <a class="article-user-link" href=""> <img - id="article-user-image" src="" /> - </a> - <div class="writer-name nolink-decoration"> - <a id="article-user-name" class="article-user-link" href=""></a> - </div> - </div> - </div> - <div class="title"> - <a href="" target="_blank"></a> - </div> - <div class="row-fluid sub-header"> - <div class="span4 left-sub-header"> - <div class="left-sub-header-inner"> - <img src="" class="article-sharedby-img" /> <span>Shared - by <a class="article-sharedby"></a> - </span> - </div> - </div> - <div class="span8 right-sub-header"> - <time class="timeago" datetime=""></time> - <span class="article-share"></span> <span - class="article-save"></span> - </div> - </div> - <span id="article-content"></span> - </div> - </div> - </div> - </div> - - </div> - </div> + <span data-lift="embed?what=/templates-hidden/parts/articles"></span> </div> </div> diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html @@ -112,56 +112,8 @@ </div> - <div data-lift="MyBlog.entry" class="row-fluid" - style="margin: 100px 0"> - <h1 style="margin: 50px 0">Latest Blog Post</h1> - <div id="reader-nav-wrapper" class="span2 offset0"> - <div - class="reader-nav-blog reader-nav reader-span-spacing nolink-decoration"> - <ul id="reader-writers" class="nav text-center"> - <li id="writer"><a href=""> <img id="writer-image" src="" /> - </a> <a id="writer-name" href=""></a></li> - </ul> - - </div> - </div> - - <div id="boxed-articles-wrapper" class="span8 offset0"> - <div class="boxed-articles boxed-articles-span"> - <div class="article keynav-article"> - <div class="article-inner"> - <div class="article-key"></div> - <div class="row-fluid header text-center"> - <div class="span12"> - <a class="article-user-link" href=""> <img - id="article-user-image" src="" /> - </a> - <div class="writer-name nolink-decoration"> - <a id="article-user-name" class="article-user-link" href=""></a> - </div> - </div> - </div> - <div class="title"> - <a href="" target="_blank"></a> - </div> - <div class="row-fluid sub-header"> - <div class="span4 left-sub-header"> - <div class="left-sub-header-inner"> - <img src="" class="article-sharedby-img" /> <span>Shared - by <a class="article-sharedby"></a> - </span> - </div> - </div> - <div class="span8 right-sub-header"> - <time class="timeago" datetime=""></time> - <span class="article-share"></span> <span class="article-save"></span> - </div> - </div> - <span id="article-content"></span> - </div> - </div> - </div> - </div> + <div style="margin: 100px 0"> + <span data-lift="embed?what=/templates-hidden/parts/articles"></span> </div> </div> diff --git a/src/main/webapp/reader.html b/src/main/webapp/reader.html @@ -1,106 +1,9 @@ <div data-lift="surround?with=base-wrap;at=content"> <div class="container"> - <div data-lift="ReaderSnip.comingSoon"></div> - <div data-lift="ReaderSnip.articles" class="row-fluid"> - - <div id="reader-nav-wrapper" class="span2 offset0"> - <div id="reader-nav" - class="reader-nav reader-span-spacing nolink-decoration"> - <ul id="reader-writers" class="nav text-center"> - <li id="writer"><a href=""> <img id="writer-image" src="" /> - </a> <a id="writer-name" href=""></a></li> - </ul> - - <div id="reader-categories"> - - <div class="scrollbar" style="float: left; display: none;"> - <div class="track"> - <div class="thumb"> - <div class="end"></div> - </div> - </div> - </div> - <div class="viewport" style="height: 200px"> - <div class="overview" style="width: 100%"> - <ul class="nav"> - - <li><a href="#">Sports</a></li> - <li><a href="#">Fashion</a></li> - <li><a href="#">Programming</a></li> - <li><a href="#">Technology</a></li> - <li><a href="#">Cooking</a></li> - <li><a href="#">Traveling</a></li> - <li><a href="#">Education</a></li> - - </ul> - </div> - </div> - - - </div> - - </div> - </div> - - <div id="boxed-articles-wrapper" class="span8 offset0"> - <div class="boxed-articles boxed-articles-span"> - <div class="article keynav-article"> - <div class="article-inner"> - <div class="article-key"></div> - <div class="row-fluid header text-center"> - <div class="span12"> - <a class="article-user-link" href=""> <img - id="article-user-image" src="" /> - </a> - <div class="writer-name nolink-decoration"> - <a id="article-user-name" class="article-user-link" href=""></a> - </div> - </div> - </div> - <div class="title"> - <a href="" target="_blank"></a> - </div> - <div class="row-fluid sub-header"> - <div class="span4 left-sub-header"> - <div class="left-sub-header-inner"> - <img src="" class="article-sharedby-img" /> <span>Shared - by <a class="article-sharedby"></a> - </span> - </div> - </div> - <div class="span8 right-sub-header"> - <time class="timeago" datetime=""></time> - <span class="article-share"></span> <span class="article-save"></span> - </div> - </div> - <span id="article-content"></span> - </div> - </div> - </div> - </div> - - <div data-lift="ignore" class="span2 offset0"> - <div id="reader-ad-nav" class="reader-span-spacing text-center"> - <h5>Functional Programming</h5> - <ul class="nav nolink-decoration"> - <li><img - src="https://secure.gravatar.com/avatar/7d6356b8b56a9a71583787904a970daa?s=80&d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-user-420.png" /> - <a href="/julio">Julio Enrique Cabrera</a></li> - <li><img - src="https://secure.gravatar.com/avatar/51e9266cad7460b23daac1179a72da2b?s=80&d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-user-420.png" /> - <a href="/">Nathan Hamblen</a></li> - <li><img - src="https://secure.gravatar.com/avatar/375ca615cb17ebcb05c417f3433fea4a?s=80&d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-user-420.png" /> - <a href="/">Tim Nelson</a></li> - </ul> - </div> - </div> - - - </div> + <div data-lift="SnipHelpers.comingSoon"></div> </div> - - + + <span data-lift="embed?what=/templates-hidden/parts/articles"></span> </div> diff --git a/src/main/webapp/saved.html b/src/main/webapp/saved.html @@ -1,78 +1,9 @@ <div data-lift="surround?with=base-wrap;at=content"> <div class="container"> - <div data-lift="ReaderSnip.comingSoon"></div> - <div data-lift="ReaderSnip.savedArticles" class="row-fluid"> - - <div id="reader-nav-wrapper" class="span2 offset0"> - <div id="reader-nav" - class="reader-nav reader-span-spacing nolink-decoration"> - <ul id="reader-writers" class="nav text-center"> - <li id="writer"><a href=""> <img id="writer-image" src="" /> - </a> <a id="writer-name" href=""></a></li> - </ul> - - </div> - </div> - - <div id="boxed-articles-wrapper" class="span8 offset0"> - <div class="boxed-articles boxed-articles-span"> - <div class="article keynav-article"> - <div class="article-inner"> - <div class="article-key"></div> - <div class="row-fluid header text-center"> - <div class="span12"> - <a class="article-user-link" href=""> <img - id="article-user-image" src="" /> - </a> - <div class="writer-name nolink-decoration"> - <a id="article-user-name" class="article-user-link" href=""></a> - </div> - </div> - </div> - <div class="title"> - <a href="" target="_blank"></a> - </div> - <div class="row-fluid sub-header"> - <div class="span4 left-sub-header"> - <div class="left-sub-header-inner"> - <img src="" class="article-sharedby-img" /> <span>Shared - by <a class="article-sharedby"></a> - </span> - </div> - </div> - <div class="span8 right-sub-header"> - <time class="timeago" datetime=""></time> - <span class="article-share"></span> <span class="article-save"></span> - </div> - </div> - <span id="article-content"></span> - </div> - </div> - </div> - </div> - - <div data-lift="ignore" class="span2 offset0"> - <div id="reader-ad-nav" class="reader-span-spacing text-center"> - <h5>Functional Programming</h5> - <ul class="nav nolink-decoration"> - <li><img - src="https://secure.gravatar.com/avatar/7d6356b8b56a9a71583787904a970daa?s=80&d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-user-420.png" /> - <a href="/julio">Julio Enrique Cabrera</a></li> - <li><img - src="https://secure.gravatar.com/avatar/51e9266cad7460b23daac1179a72da2b?s=80&d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-user-420.png" /> - <a href="/">Nathan Hamblen</a></li> - <li><img - src="https://secure.gravatar.com/avatar/375ca615cb17ebcb05c417f3433fea4a?s=80&d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-user-420.png" /> - <a href="/">Tim Nelson</a></li> - </ul> - </div> - </div> - - - </div> + <div data-lift="SnipHelpers.comingSoon"></div> </div> - + <span data-lift="embed?what=/templates-hidden/parts/articles"></span> </div> diff --git a/src/main/webapp/templates-hidden/parts/articles.html b/src/main/webapp/templates-hidden/parts/articles.html @@ -0,0 +1,98 @@ +<div class="container"> + <div data-lift="Articles" class="row-fluid"> + + <div id="reader-nav-wrapper" class="span2 offset0"> + <div + class="reader-nav-blog reader-nav reader-span-spacing nolink-decoration"> + + <ul id="reader-writers" class="nav text-center"> + <li id="writer"><a href=""> <img id="writer-image" src="" /> + </a> <a id="writer-name" href=""></a></li> + </ul> + + <div data-lift="ignore" id="reader-categories"> + <div class="scrollbar" style="float: left; display: none;"> + <div class="track"> + <div class="thumb"> + <div class="end"></div> + </div> + </div> + </div> + <div class="viewport" style="height: 200px"> + <div class="overview" style="width: 100%"> + <ul class="nav"> + <li><a href="#">Sports</a></li> + <li><a href="#">Fashion</a></li> + <li><a href="#">Programming</a></li> + <li><a href="#">Technology</a></li> + <li><a href="#">Cooking</a></li> + <li><a href="#">Traveling</a></li> + <li><a href="#">Education</a></li> + </ul> + </div> + </div> + </div> + + </div> + </div> + + + <div id="boxed-articles-wrapper" class="span8 offset0"> + <div class="boxed-articles boxed-articles-span"> + + <div class="article keynav-article"> + <div class="article-inner"> + <div class="article-key"></div> + <div class="row-fluid header text-center"> + <div class="span12"> + <a class="article-user-link" href=""> <img + id="article-user-image" src="" /> + </a> + <div class="writer-name nolink-decoration"> + <a id="article-user-name" class="article-user-link" href=""></a> + </div> + </div> + </div> + <div class="title"> + <a href="" target="_blank"></a> + </div> + <div class="row-fluid sub-header"> + <div class="span4 left-sub-header"> + <div class="left-sub-header-inner"> + <div class="article-sharedby"> + <img src="" /> + <a id="article-sharedby-name" href=""></a> + </div> + </div> + </div> + <div class="span8 right-sub-header"> + <time class="timeago" datetime=""></time> + <span class="article-share"></span> <span class="article-save"></span> + </div> + </div> + <span id="article-content"></span> + </div> + </div> + + </div> + </div> + + <div data-lift="ignore" class="span2 offset0"> + <div id="reader-ad-nav" class="reader-span-spacing text-center"> + <h5>Functional Programming</h5> + <ul class="nav nolink-decoration"> + <li><img + src="https://secure.gravatar.com/avatar/7d6356b8b56a9a71583787904a970daa?s=80&d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-user-420.png" /> + <a href="/julio">Julio Enrique Cabrera</a></li> + <li><img + src="https://secure.gravatar.com/avatar/51e9266cad7460b23daac1179a72da2b?s=80&d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-user-420.png" /> + <a href="/">Nathan Hamblen</a></li> + <li><img + src="https://secure.gravatar.com/avatar/375ca615cb17ebcb05c417f3433fea4a?s=80&d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-user-420.png" /> + <a href="/">Tim Nelson</a></li> + </ul> + </div> + </div> + + </div> +</div> +\ No newline at end of file diff --git a/src/main/webapp/templates-hidden/parts/blog-profile.html b/src/main/webapp/templates-hidden/parts/blog-profile.html @@ -75,61 +75,7 @@ </div> <div id="profile-articles-area"> - <div class="container"> - <div data-lift="ProfileLocBlog.articles" class="row-fluid"> - - <div id="reader-nav-wrapper" class="span2 offset0"> - <div class="reader-nav-blog reader-nav reader-span-spacing nolink-decoration"> - <ul id="reader-writers" class="nav text-center"> - <li id="writer"> - <a href=""> - <img id="writer-image" src="" /> - </a> - <a id="writer-name" href=""></a> - </li> - </ul> - </div> - </div> - - <div id="boxed-articles-wrapper" class="span8 offset0"> - <div class="boxed-articles boxed-articles-span"> - <div class="article keynav-article"> - <div class="article-inner"> - <div class="article-key"></div> - <div class="row-fluid header text-center"> - <div class="span12"> - <a class="article-user-link" href=""> - <img id="article-user-image" src=""/> - </a> - <div class="writer-name nolink-decoration"> - <a id="article-user-name" class="article-user-link" href=""></a> - </div> - </div> - </div> - <div class="title"> - <a href="" target="_blank"></a> - </div> - <div class="row-fluid sub-header"> - <div class="span4 left-sub-header"> - <div class="left-sub-header-inner"> - <img src="" class="article-sharedby-img" /> - <span>Shared by <a class="article-sharedby"></a></span> - </div> - </div> - <div class="span8 right-sub-header"> - <time class="timeago" datetime=""></time> - <span class="article-share"></span> - <span class="article-save"></span> - </div> - </div> - <span id="article-content"></span> - </div> - </div> - </div> - </div> - - </div> - </div> + <span data-lift="embed?what=/templates-hidden/parts/articles"></span> </div> </div> \ No newline at end of file diff --git a/src/main/webapp/templates-hidden/parts/user-profile.html b/src/main/webapp/templates-hidden/parts/user-profile.html @@ -60,62 +60,7 @@ </div> <div id="profile-articles-area"> - <div class="container"> - <div data-lift="ProfileLocWriter.articles" class="row-fluid"> - - <div id="reader-nav-wrapper" class="span2 offset0"> - <div class="reader-nav-blog reader-nav reader-span-spacing nolink-decoration"> - <ul id="reader-writers" class="nav text-center"> - <li id="writer"> - <a href=""> - <img id="writer-image" src="" /> - </a> - <a id="writer-name" href=""></a> - </li> - </ul> - </div> - </div> - - - <div id="boxed-articles-wrapper" class="span8 offset0"> - <div class="boxed-articles boxed-articles-span"> - <div class="article keynav-article"> - <div class="article-inner"> - <div class="article-key"></div> - <div class="row-fluid header text-center"> - <div class="span12"> - <a class="article-user-link" href=""> - <img id="article-user-image" src=""/> - </a> - <div class="writer-name nolink-decoration"> - <a id="article-user-name" class="article-user-link" href=""></a> - </div> - </div> - </div> - <div class="title"> - <a href="" target="_blank"></a> - </div> - <div class="row-fluid sub-header"> - <div class="span4 left-sub-header"> - <div class="left-sub-header-inner"> - <img src="" class="article-sharedby-img" /> - <span>Shared by <a class="article-sharedby"></a></span> - </div> - </div> - <div class="span8 right-sub-header"> - <time class="timeago" datetime=""></time> - <span class="article-share"></span> - <span class="article-save"></span> - </div> - </div> - <span id="article-content"></span> - </div> - </div> - </div> - </div> - - </div> - </div> + <span data-lift="embed?what=/templates-hidden/parts/articles"></span> </div>