scala-news-reader
rss/atom news reader in scala
git clone https://9o.is/git/scala-news-reader.git
commit 0519dd8b705d77ddfe98a534544b7dab65c49738 parent 4cdc146388e82f7173701194c834c18e6c574f54 Author: Jul <jul@9o.is> Date: Fri, 28 Jun 2013 19:18:41 -0400 update. but some parts may be broken. Diffstat:
49 files changed, 1285 insertions(+), 1119 deletions(-)
diff --git a/project/Build.scala b/project/Build.scala @@ -5,6 +5,8 @@ object LiftProjectBuild extends Build { import BuildSettings._ + + lazy val root = Project("joe-reader", file(".")) .settings(liftAppSettings: _*) .settings(libraryDependencies ++= diff --git a/project/Build.scala.save b/project/Build.scala.save @@ -1,26 +0,0 @@ -import sbt._ -import sbt.Keys._ - -object LiftProjectBuild extends Build { - - import BuildSettings._ - - lazy val root = Project("joe-reader", file(".")) - .settings(liftAppSettings: _*) - .settings(libraryDependencies ++= - Seq( - "net.liftweb" %% "lift-webkit" % Ver.lift % "compile", - "net.liftweb" %% "lift-mongodb-record" % Ver.lift % "compile", - "net.liftmodules" %% "mongoauth" % (Ver.lift+"-0.4") % "compile", - "net.liftmodules" %% ("extras_"+Ver.lift_edition) % "0.1" % "compile", - "org.eclipse.jetty" % "jetty-webapp" % Ver.jetty % "container", - "ch.qos.logback" % "logback-classic" % "1.0.6" % "compile", - "org.scalatest" %% "scalatest" % "1.9.1" % "test", - "net.databinder.dispatch" %% "dispatch-core" % "0.10.0", - "org.jsoup" % "jsoup" % "1.7.2", - "rome" % "rome" % "1.0", - - ) - ) -} -"com.typesafe.akka" %% "akka-actor" % "2.2-SNAPSHOT" diff --git a/project/BuildSettings.scala b/project/BuildSettings.scala @@ -8,6 +8,7 @@ import less.Plugin._ import sbtbuildinfo.Plugin._ import sbtclosure.SbtClosurePlugin._ + object BuildSettings { object Ver { val lift = "2.5-RC4" @@ -21,6 +22,7 @@ object BuildSettings { name := "joe-reader", version := "0.1-SNAPSHOT", organization := "Joe Reader", + scanDirectories := Nil, scalaVersion := "2.10.0", resolvers += "Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/", scalacOptions <<= scalaVersion map { sv: String => diff --git a/src/main/less/styles.less b/src/main/less/styles.less @@ -236,4 +236,9 @@ div.gravatar { input[type="radio"] { display: none; } } .selected { background-color: #ccc; } +} + +.alert ul { + list-style: none; + margin-bottom: 0; } \ No newline at end of file diff --git a/src/main/scala/bootstrap/liftweb/Boot.scala b/src/main/scala/bootstrap/liftweb/Boot.scala @@ -16,6 +16,7 @@ import net.liftmodules.extras.{Gravatar, LiftExtras} import net.liftmodules.mongoauth.MongoAuth import net.liftweb.http.SHtml.ChoiceHolder import net.liftweb.sitemap.SiteMap +import com.joereader.lib.ImageUpload /** * A class that's instantiated early and run. It allows the application @@ -87,6 +88,6 @@ class Boot extends Loggable { <label for={id}>{e} {ci.key.toString}</label>)(ci.xhtml) // API - LiftRules.dispatch.append(com.joereader.api.ImageUpload) + LiftRules.dispatch.append(com.joereader.lib.ImageUpload) } } diff --git a/src/main/scala/com/joereader/api/ImageUpload.scala b/src/main/scala/com/joereader/api/ImageUpload.scala @@ -1,110 +0,0 @@ -package com.joereader.api - -import net.liftweb.http.rest._ -import net.liftweb.common._ -import net.liftweb.http._ -import net.liftweb.util._ -import com.joereader.lib.aws._ -import com.joereader.config.S3Config._ -import com.joereader.model._ -import concurrent.ExecutionContext.Implicits.global - -object ImageUpload extends RestHelper with Logger { - - val acceptedImages = "image/jpeg" :: "image/png" :: "image/svg+xml" :: Nil - - def userUrl(imgType: String = "") = - "/api/user/upload/image"+includeImgType(imgType) - - def blogUrl(blog: Blog, imgType: String = "") = - "/api/blog/"+blog.id.is+"/upload/image"+includeImgType(imgType) - - def includeImgType(imgType: String) = - if(imgType.nonEmpty) "?type="+imgType else "" - - serve { - - case "api" :: "user" :: "upload" :: "image" :: Nil Post req => - - val imgType = S.param("type") getOrElse "" - - if(User.currentUser.isEmpty) - ResponseWithReason(BadResponse(), "Must be logged in.") - - else if(req.uploadedFiles.exists(file => acceptedImages.filter(file.mimeType == _).isEmpty)) - ResponseWithReason(BadResponse(), "Only JPEG, PNG and SVG are allowed.") - - else if(imgType == "bg" && req.uploadedFiles.exists(f => f.length > 5243000)) // 5 MB - bg img - ResponseWithReason(BadResponse(), "Image is too big. Must be 1MB or smaller.") - - else if(imgType != "bg" && req.uploadedFiles.exists(f => f.length > 2097000)) // 2 MB - logo - ResponseWithReason(BadResponse(), "Image is too big. Must be 2MB or smaller.") - - else { - for(user <- User.currentUser; file <- req.uploadedFiles) { - - val fn = StringHelpers.randomString(32) - val s3 = new S3(s3_access_key.get, s3_secret_key.get, s3_bucket.get) - val s3f = s3.createFile(fn, file.file, file.mimeType) - - if(imgType == "bg") { - if(user.bgImg.is != "") {val s3f = s3.deleteFile(user.bgImg.is)} - user.bgImg(fn).update - } - else { - if(user.img.is != "") {val s3f = s3.deleteFile(user.img.is)} - user.img(fn).update - } - - info("Successfully saved: "+fn+" for "+user.name.is) - } - OkResponse() - } - - - case "api" :: "blog" :: id :: "upload" :: "image" :: Nil Post req => - - val blog = Blog.findByStringId(id) - val user = User.currentUser - val imgType = S.param("type") getOrElse "" - - if(blog.isEmpty) - ResponseWithReason(BadResponse(), "Blog id does not exist.") - - else if(user.isEmpty) - ResponseWithReason(BadResponse(), "Must be logged in.") - - else if(user.exists(u => blog.exists(_.isOwner(u)))) - ResponseWithReason(BadResponse(), "You do not have permission to control the requested blog.") - - else if (req.uploadedFiles.exists(file => acceptedImages.filter(file.mimeType == _).isEmpty)) - ResponseWithReason(BadResponse(), "Only JPEG, PNG and SVG are allowed.") - - else if(imgType == "bg" && req.uploadedFiles.exists(f => f.length > 5243000)) // 5 MB - bg img - ResponseWithReason(BadResponse(), "Image is too big. Must be 1MB or smaller.") - - else if(imgType != "bg" && req.uploadedFiles.exists(f => f.length > 2097000)) // 2 MB - logo - ResponseWithReason(BadResponse(), "Image is too big. Must be 2MB or smaller.") - - else { - for(blog <- blog; file <- req.uploadedFiles) { - - val fn = StringHelpers.randomString(32) - val s3 = new S3(s3_access_key.get, s3_secret_key.get, s3_bucket.get) - val s3f = s3.createFile(fn, file.file, file.mimeType) - - if(imgType == "bg") { - if(blog.bgImg.is != "") {val s3f = s3.deleteFile(blog.bgImg.is)} - blog.bgImg(fn).update - } - else { - if(blog.img.is != "") {val s3f = s3.deleteFile(blog.img.is)} - blog.img(fn).update - } - - info("Successfully saved: "+fn+" for "+blog.name.is) - } - OkResponse() - } - } -} diff --git a/src/main/scala/com/joereader/comet/UserSignUp.scala b/src/main/scala/com/joereader/comet/UserSignUp.scala @@ -1,124 +0,0 @@ -package com.joereader.comet - -import net.liftweb.http._ -import net.liftweb.common._ -import net.liftweb.http.js.{JsCmd, JE, JsCmds} -import net.liftweb.actor.LiftActor -import net.liftweb.http.js.JsCmds._ -import net.liftweb.http.SHtml._ -import com.joereader.model._ -import net.liftweb.json.ext.JodaTimeSerializers -import net.liftweb.json._ -import scala.xml.{Text, NodeSeq} -import net.liftweb.util.CssSel -import com.joereader.config.Site -import net.liftmodules.extras.{Gravatar, SnippetHelper} -import net.liftweb.util.Helpers._ -import java.text.SimpleDateFormat - - -sealed trait UserSnippet extends SnippetHelper with Loggable { - - protected def user: Box[User] - - def serve(snip: User => NodeSeq): NodeSeq = - (for { - u <- user ?~ "User not found" - } yield { - snip(u) - }): NodeSeq - - def serve(html: NodeSeq)(snip: User => CssSel): NodeSeq = - (for { - u <- user ?~ "User not found" - } yield { - snip(u)(html) - }): NodeSeq - - def header(xhtml: NodeSeq): NodeSeq = serve { user => - <div id="user-header"> - {gravatar(xhtml)} - <h3>{name(xhtml)}</h3> - </div> - } - - def gravatar(xhtml: NodeSeq): NodeSeq = { - val size = S.attr("size").map(toInt) openOr Gravatar.defaultSize.vend - - serve { user => - Gravatar.imgTag(user.email.is, size) - } - } - - def username(xhtml: NodeSeq): NodeSeq = serve { user => - Text(user.username.is) - } - - def name(xhtml: NodeSeq): NodeSeq = serve { user => - if (user.name.is.length > 0) - Text("%s (%s)".format(user.name.is, user.username.is)) - else - Text(user.username.is) - } - - def title(xhtml: NodeSeq): NodeSeq = serve { user => - <lift:head> - <title lift="Menu.title">{"Joe Reader: %*% - "+user.username.is}</title> - </lift:head> - } -} - -object CurrentUser extends UserSnippet { - def user = User.currentUser -} - - - -abstract class UserProfileComet extends CometActor with CometListener with UserSnippet { - - protected def user: Box[User] = Full(User.createRecord) - - val df = new SimpleDateFormat("MMM d, yyyy") - - protected def registerWith = UserActor - - override def lowPriority = { - case cmd: JsCmd => - partialUpdate(cmd) - } - /* - def render = - "#id_avatar *" #> Gravatar.imgTag(user.email.is) & - "#id_name *" #> <h3>{user.name.is}</h3> & - "#id_location *" #> user.location.is & - "#id_whencreated" #> df.format(user.whenCreated.toDate).toString & - "#id_bio *" #> user.bio.is & - "#id_editlink *" #> editLink - */ -} - -object UserActor extends LiftActor with ListenerManager { - private var msgs: Vector[String] = Vector("") // private state - - /** - * When we update the listeners, what message do we send? - * We send the msgs, which is an immutable data structure, - * so it can be shared with lots of threads without any - * danger or locking. - */ - def createUpdate = msgs - - /** - * process messages that are sent to the Actor. In - * this case, we're looking for Strings that are sent - * to the ChatServer. We append them to our Vector of - * messages, and then update all the listeners. - */ - override def lowPriority = { - case m: String => - msgs :+= m - updateListeners() - - } -} - diff --git a/src/main/scala/com/joereader/config/Site.scala b/src/main/scala/com/joereader/config/Site.scala @@ -8,7 +8,7 @@ import net.liftweb.http.{Templates, S} import sitemap._ import sitemap.Loc._ -import net.liftmodules.mongoauth.Locs +import net.liftmodules.mongoauth.{MongoAuth, Locs} object MenuGroups { val SettingsGroup = LocGroup("settings") @@ -35,6 +35,7 @@ object Site extends Locs { val about = MenuLoc(Menu.i("About") / "about" >> TopBarGroup) val login = MenuLoc(Menu.i("Login") / "login" >> RequireNotLoggedIn) val loginToken = MenuLoc(buildLoginTokenMenu) + val inviteToken = MenuLoc(buildInviteTokenMenu) val logout = MenuLoc(buildLogoutMenu) private val userProfileParamMenu = Menu.param[User]("User", "Profile", @@ -45,18 +46,28 @@ object Site extends Locs { Blog.findByBlogName _, _.blogname.is) / "blog" lazy val blogProfileLoc = blogProfileParamMenu.toLoc - val signUp1 = MenuLoc(Menu.i("Sign Up ⋅ Writer") / "signup" / "writer" >> RequireNotLoggedIn) - val signUp2 = MenuLoc(Menu.i("Sign Up ⋅ Verify") / "signup" / "verify" >> RequireNotLoggedIn) - val signUp3 = MenuLoc(Menu.i("Sign Up ⋅ Categories") / "signup" / "categories" >> RequireLoggedIn) - val signUp4 = MenuLoc(Menu.i("Sign Up ⋅ Blog") / "signup" / "blog" >> RequireLoggedIn) - val signUp5 = MenuLoc(Menu.i("Sign Up ⋅ User") / "signup" / "user" >> RequireLoggedIn) - val signUp6 = MenuLoc(Menu.i("Sign Up ⋅ Password") / "signup" / "password" >> RequireLoggedIn) + val signUp1 = MenuLoc(Menu.i("Sign Up ⋅ Writer") / "signup" / "writer" >> RequireNotLoggedIn) + val signUp2 = MenuLoc(Menu.i("Sign Up ⋅ Verify") / "signup" / "verify" >> RequireNotLoggedIn) + val signUp3 = MenuLoc(Menu.i("Sign Up ⋅ Categories") / "signup" / "categories" >> RequireLoggedIn) + val signUp4 = MenuLoc(Menu.i("Sign Up ⋅ Blog") / "signup" / "blog" >> RequireLoggedIn) + val signUp5 = MenuLoc(Menu.i("Sign Up ⋅ User") / "signup" / "user" >> RequireLoggedIn) + val signUp6 = MenuLoc(Menu.i("Sign Up ⋅ Password") / "signup" / "password" >> RequireLoggedIn) val reader = MenuLoc(Menu.i("Reader") / "reader" >> RequireLoggedIn) val password = MenuLoc(Menu.i("Password Reset") / "settings" / "password" >> RequireLoggedIn >> Hidden) - val account = MenuLoc(Menu.i("Account") / "settings" / "account" >> SettingsGroup >> RequireLoggedIn) - val editProfile = MenuLoc(Menu("EditProfile", "Profile") / "settings" / "profile" >> SettingsGroup >> RequireLoggedIn) + val editAccount = MenuLoc(Menu.i("Account") / "settings" / "account" >> SettingsGroup >> RequireLoggedIn) + val editBlogs = MenuLoc(Menu.i("Blogs") / "settings" / "blogs" >> SettingsGroup >> RequireLoggedIn) + val blogVerify = MenuLoc(Menu.i("Blog Verification") / "settings" / "verify" >> RequireLoggedIn) + + private val editBlogParamMenu = Menu.param[Blog]("Edit Blog", "Settings", + Blog.findByBlogName _, _.blogname.is) / "settings" / "blog" / * >> TemplateBox(() => Templates("settings" :: "blog" :: Nil)) >> RequireLoggedIn + lazy val editBlogLoc = editBlogParamMenu.toLoc + + private val categoriesParamMenu = Menu.param[Blog]("Categories", "Categories", + Blog.findByBlogName _, _.blogname.is) / "settings" / "blog" / * / "categorize" >> TemplateBox(() => Templates("settings" :: "categories" :: Nil)) >> RequireLoggedIn + lazy val categoriesLoc = categoriesParamMenu.toLoc + val register = MenuLoc(Menu.i("Register") / "register" >> RequireNotLoggedIn) val passwordRecovery = MenuLoc(Menu.i("Password Recovery") / "help" / "recovery" >> RequireNotLoggedIn) @@ -64,11 +75,14 @@ object Site extends Locs { private def menus = List( userProfileParamMenu, blogProfileParamMenu, + editBlogParamMenu, + categoriesParamMenu, home.menu, about.menu, login.menu, register.menu, loginToken.menu, + inviteToken.menu, logout.menu, signUp1.menu, signUp2.menu, @@ -77,9 +91,10 @@ object Site extends Locs { signUp5.menu, signUp6.menu, reader.menu, - account.menu, + editAccount.menu, password.menu, - editProfile.menu, + editBlogs.menu, + blogVerify.menu, passwordRecovery.menu, error.menu, notFound.menu, @@ -98,4 +113,11 @@ object Site extends Locs { } def isAvailableMenu(n: String) = invalidUsernames.forall(_ != n) + def buildInviteTokenMenu = Menu(Loc( + "InviteToken", InviteToken.inviteTokenUrl.split("/").filter(_.length > 0).toList, + S ? "liftmodule-monogoauth.locs.inviteToken", inviteTokenLocParams + )) + + protected def inviteTokenLocParams = + EarlyResponse(() => User.meta.handleInviteToken) :: Nil } diff --git a/src/main/scala/com/joereader/lib/DependencyFactory.scala b/src/main/scala/com/joereader/lib/DependencyFactory.scala @@ -1,56 +0,0 @@ -package com.joereader -package lib - -import java.util.Date - -import net.liftweb._ -import http._ -import util._ -import common._ - -/** - * A factory for generating new instances of Date. You can create - * factories for each kind of thing you want to vend in your application. - * An example is a payment gateway. You can change the default implementation, - * or override the default implementation on a session, request or current call - * stack basis. - */ -object DependencyFactory extends Factory { - implicit object time extends FactoryMaker(Helpers.now _) - - /** - * objects in Scala are lazily created. The init() - * method creates a List of all the objects. This - * results in all the objects getting initialized and - * registering their types with the dependency injector - */ - private def init() { - List(time) - } - init() -} - -/* -/** - * Examples of changing the implementation - */ -sealed abstract class Changer { - def changeDefaultImplementation() { - DependencyFactory.time.default.set(() => new Date()) - } - - def changeSessionImplementation() { - DependencyFactory.time.session.set(() => new Date()) - } - - def changeRequestImplementation() { - DependencyFactory.time.request.set(() => new Date()) - } - - def changeJustForCall(d: Date) { - DependencyFactory.time.doWith(d) { - // perform some calculations here - } - } -} -*/ diff --git a/src/main/scala/com/joereader/lib/EmailMsgs.scala b/src/main/scala/com/joereader/lib/EmailMsgs.scala @@ -0,0 +1,9 @@ +package com.joereader.lib + + + +object EmailMsgs { + + + +} diff --git a/src/main/scala/com/joereader/lib/Helper.scala b/src/main/scala/com/joereader/lib/Helper.scala @@ -50,7 +50,7 @@ object Helper { def pattern: Pattern = { val s = removeWWW(url.getHost) + removeFileExtension(getPath) val escS = escape(s) - Pattern.compile("""(https?://)?(www\.)?"""+escS+"""(\.("""+ext.mkString("|")+"""))?""", Pattern.CASE_INSENSITIVE) + Pattern.compile("""(https?://)?(www\.)?(^"""+escS+"""$)(\.("""+ext.mkString("|")+"""))?""", Pattern.CASE_INSENSITIVE) } def getPath = if(url.getPath=="/") "" else url.getPath diff --git a/src/main/scala/com/joereader/lib/ImageUpload.scala b/src/main/scala/com/joereader/lib/ImageUpload.scala @@ -0,0 +1,134 @@ +package com.joereader.lib + +import net.liftweb.http.rest._ +import net.liftweb.common._ +import net.liftweb.http._ +import net.liftweb.util._ +import com.joereader.lib.aws._ +import com.joereader.config.S3Config._ +import com.joereader.model._ +import concurrent.ExecutionContext.Implicits.global +import com.joereader.snippet.SnipHelpers.UpdateImg +import com.joereader.snippet.SnipHelpers + +object ImageUpload extends RestHelper with Logger { + + val acceptedImages = "image/jpeg" :: "image/png" :: "image/svg+xml" :: Nil + + def userUrl(imgType: String = "") = + "/api/user/upload/image"+includeImgType(imgType) + + def blogUrl(blog: Blog, imgType: String = "") = + "/api/blog/"+blog.id.is+"/upload/image"+includeImgType(imgType) + + def includeImgType(imgType: String) = + if(imgType.nonEmpty) "?type="+imgType else "" + + // 2 MB max + def imgTooLarge(req: Req) = req.uploadedFiles.exists(f => f.length > 2097000) + + // 5 MB max + def bgTooLarge(req: Req) = req.uploadedFiles.exists(f => f.length > 5243000) + + def isValidImg(req: Req) = req.uploadedFiles.exists(file => acceptedImages.filter(file.mimeType == _).isEmpty) + + serve { + + case "api" :: "user" :: "upload" :: "image" :: Nil Post req => + + val imgType = S.param("type") getOrElse "" + + if(User.currentUser.isEmpty) + ResponseWithReason(BadResponse(), "Must be logged in.") + + else if(isValidImg(req)) + ResponseWithReason(BadResponse(), "Only JPEG, PNG and SVG are allowed.") + + else if(imgType == "bg" && bgTooLarge(req)) + ResponseWithReason(BadResponse(), "Image is too big. Must be 1MB or smaller.") + + else if(imgType != "bg" && imgTooLarge(req)) + ResponseWithReason(BadResponse(), "Image is too big. Must be 2MB or smaller.") + + else { + var id, img = "" + for(user <- User.currentUser; file <- req.uploadedFiles) { + + val fn = StringHelpers.randomString(32) + val s3 = new S3(s3_access_key.get, s3_secret_key.get, s3_bucket.get) + val s3f = s3.createFile(fn, file.file, file.mimeType) + + s3f onSuccess { + case _ => + if(imgType == "bg") { + if(user.bgImg.is != "") {val s3f = s3.deleteFile(user.bgImg.is)} + user.bgImg(fn).update + id = SnipHelpers.imgBgId + img = s3.fileUrl(fn) + } + else { + if(user.img.is != "") {val s3f = s3.deleteFile(user.img.is)} + user.img(fn).update + id = SnipHelpers.imgProfileId + img = s3.fileUrl(fn) + } + info("Successfully saved: "+file.fileName+" for "+user.name.is) + } + } + JavaScriptResponse(UpdateImg(id,img)) + } + + + case "api" :: "blog" :: id :: "upload" :: "image" :: Nil Post req => + + val blog = Blog.findByStringId(id) + val user = User.currentUser + val imgType = S.param("type") getOrElse "" + + if(blog.isEmpty) + ResponseWithReason(BadResponse(), "Blog id does not exist.") + + else if(user.isEmpty) + ResponseWithReason(BadResponse(), "Must be logged in.") + + else if(user.exists(u => blog.exists(_.nonOwner(u)))) + ResponseWithReason(BadResponse(), "You do not have permission to control the requested blog.") + + else if (req.uploadedFiles.exists(file => acceptedImages.filter(file.mimeType == _).isEmpty)) + ResponseWithReason(BadResponse(), "Only JPEG, PNG and SVG are allowed.") + + else if(imgType == "bg" && req.uploadedFiles.exists(f => f.length > 5243000)) // 5 MB - bg img + ResponseWithReason(BadResponse(), "Image is too big. Must be 1MB or smaller.") + + else if(imgType != "bg" && req.uploadedFiles.exists(f => f.length > 2097000)) // 2 MB - logo + ResponseWithReason(BadResponse(), "Image is too big. Must be 2MB or smaller.") + + else { + var id, img = "" + for(blog <- blog; file <- req.uploadedFiles) { + + val fn = StringHelpers.randomString(32) + val s3 = new S3(s3_access_key.get, s3_secret_key.get, s3_bucket.get) + val s3f = s3.createFile(fn, file.file, file.mimeType) + + s3f onSuccess { + case _ => + if(imgType == "bg") { + if(blog.bgImg.is != "") {val s3f = s3.deleteFile(blog.bgImg.is)} + blog.bgImg(fn).update + id = SnipHelpers.imgBgId + img = s3.fileUrl(fn) + } + else { + if(blog.img.is != "") {val s3f = s3.deleteFile(blog.img.is)} + blog.img(fn).update + id = SnipHelpers.imgProfileId + img = s3.fileUrl(fn) + } + info("Successfully saved: "+file.fileName+" for "+blog.name.is) + } + } + JavaScriptResponse(UpdateImg(id,img)) + } + } +} diff --git a/src/main/scala/com/joereader/lib/VideoInfo.scala b/src/main/scala/com/joereader/lib/VideoInfo.scala @@ -4,7 +4,6 @@ import dispatch._ import xml._ import concurrent.Future import concurrent.ExecutionContext.Implicits.global -import scala.util.parsing.json._ /** * Video information of certain video service providers. diff --git a/src/main/scala/com/joereader/lib/aws/S3.scala b/src/main/scala/com/joereader/lib/aws/S3.scala @@ -16,12 +16,12 @@ class S3(access_key: String, secret_key: String, bucket: String) { } def createFile(fn: String, file: java.io.File, contentType: String) = - Http(Bucket(bucket) / fn <<< file <:< + Http.configure(_ setCompressionEnabled true)(Bucket(bucket) / fn <<< file <:< Map("content-type" -> contentType, "x-amz-acl" -> "public-read") <@(access_key, secret_key)) def deleteFile(fn: String) = - Http(Bucket(bucket).DELETE / fn <@(access_key, secret_key)) + Http.configure(_ setCompressionEnabled true)(Bucket(bucket).DELETE / fn <@(access_key, secret_key)) def fileUrl(fn: String) = "http://"+Root+"/"+bucket+"/"+fn diff --git a/src/main/scala/com/joereader/lib/rss/Rss.scala b/src/main/scala/com/joereader/lib/rss/Rss.scala @@ -20,11 +20,18 @@ import net.liftweb.util.PCDataXmlParser */ object Rss { - implicit class RssImplicitString(link: String) { + case class RSSHtmlResponse(loc: String, content: String, redirectTo: String) + + implicit class RssImplicitString(str: String) { def entries: List[FeedEntry] = { - val response = requestLink(link)() - Feed.build(extractFeed(response), List(link)).entries + val response = requestLink(str)() + Feed.build(extractFeed(response), List(str)).entries + } + + def response: RSSHtmlResponse = { + val req = RSSHtmlResponse(str, "", str) + requestLink(req) } /** @@ -32,18 +39,17 @@ object Rss { * @return list of rss links and the html content. */ def rssLinks: List[String] = { - val response = requestLink(link)() - findRssLinks(response) + findRssLinks(str) } } - implicit class RssImplicitListOfString(links: List[String]) { + implicit class RssImplicitListOfString(strs: List[String]) { /** * Given a url link to rss feed, it returns a syndicated feed. * @return a syndicated feed */ def feed: Feed = { - val feeds = links.map { link => + val feeds = strs.map { link => val response = requestLink(link)() Feed.build(extractFeed(response), List(link)) }.toList @@ -53,13 +59,30 @@ object Rss { /** * Returns a result of the page content (as a string) requested by a link. - * @param link the web link to the page + * @param link the web link to the page. * @return either a failure with a message or successful content as a string */ private def requestLink(link: String) : Future[Option[String]] = { val request = url(URLFormatter(link).toString) - Http.configure(_.setFollowRedirects(true).setCompressionEnabled(true)) - Http(request OK as.String).option + Http.configure(_.setFollowRedirects(true).setCompressionEnabled(true))(request OK as.String).option + } + + /** + * Returns a result of the page content (as a string) requested by a link. + * Note: Redirects can be handled by dispatch with setFollowRedirects as true, + * but we want to retrieve the url of the content. Limit is 5 redirects. + */ + private def requestLink(req: RSSHtmlResponse, redirectCount: Int = 0) : RSSHtmlResponse = { + + if(req.redirectTo == null || redirectCount > 4) return req + + val request = url(URLFormatter(req.redirectTo).toString) + val response = + Http.configure(_.setCompressionEnabled(true))(request > { r=> + RSSHtmlResponse(req.redirectTo, r.getResponseBody, r.getHeader("Location")) + }) + + requestLink(response(), redirectCount+1) } /** diff --git a/src/main/scala/com/joereader/model/Blog.scala b/src/main/scala/com/joereader/model/Blog.scala @@ -28,13 +28,16 @@ class Blog private () extends MongoRecord[Blog] with ObjectIdPk[Blog] { object bgImg extends StringField(this, 50) // blogname is the blog's site url - object blogname extends StringField(this,64) + object blogname extends StringField(this,64) { + override def setFilter = toLower _ :: trim _ :: super.setFilter + } // 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(u: User) = owner.is == u.id.is + def nonOwner(u: User) = !isOwner(u) def hasOwner: Boolean = owner.is != owner.defaultValue // other writers @@ -55,6 +58,7 @@ class Blog private () extends MongoRecord[Blog] with ObjectIdPk[Blog] { def addWriterSafely(bw: BlogWriter) = { val existing = writer(bw.name.is).map(_.user(bw.user.is)) if(!existing.isDefined) addWriter(bw) + else this } def removeWriter(bw: BlogWriter) = writers(writers.get.filter(_.user.is != bw.user.is)) @@ -107,6 +111,11 @@ class BlogWriter private () extends BsonRecord[BlogWriter] { override def defaultValue = new ObjectId("0"*24) } + def isUser(u: User) = user.is == u.id.is + def nonUser(u: User) = !isUser(u) + def hasUser: Boolean = user.is != user.defaultValue + def removeUser() {user(user.defaultValue)} + // todo does this go here? object followers extends ObjectIdRefListField(this, User) } diff --git a/src/main/scala/com/joereader/model/InviteToken.scala b/src/main/scala/com/joereader/model/InviteToken.scala @@ -0,0 +1,52 @@ +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 + +/** + * This is a token to automatically invite a user if a blog owner claims him as writer. + */ +class InviteToken private () extends MongoRecord[InviteToken] with ObjectIdPk[InviteToken] { + def meta = InviteToken + + object name extends StringField(this, 254) + object email extends EmailField(this, 254) + object blogId extends ObjectIdField(this) + object expires extends ExpiresField(this, meta.inviteTokenExpires) + + def url: String = meta.url(this) +} + +object InviteToken extends InviteToken with MongoMetaRecord[InviteToken] { + + override def collectionName = "user.invitetokens" + + lazy val inviteTokenUrl = "/invite-token" + lazy val inviteTokenExpires: ReadablePeriod = Days.days(30) + + def url(inst: InviteToken): String = "%s%s?token=%s".format(S.hostAndPath, inviteTokenUrl, inst.id.toString()) + + def create(email: String, name: String, blogId: ObjectId): InviteToken = { + createRecord.email(email).name(name).blogId(blogId).save + } + + def deleteAllByEmail(e: String) { + delete(email.name, e) + } + + def findByStringId(in: String): Box[InviteToken] = + if (ObjectId.isValid(in)) find(new ObjectId(in)) + else Failure("Invalid ObjectId: "+in) +} + diff --git a/src/main/scala/com/joereader/model/User.scala b/src/main/scala/com/joereader/model/User.scala @@ -12,6 +12,9 @@ import record.field._ import net.liftmodules.mongoauth._, model._ import net.liftweb.mongodb.record.{BsonMetaRecord, BsonRecord} import net.liftweb.common.Full +import com.joereader.config.Site +import net.liftweb.util.{StringHelpers, Helpers} +import com.joereader.snippet.{VerifiedVar, BlogIdVar} class User private () extends ProtoAuthUser[User] with ObjectIdPk[User] { def meta = User @@ -26,6 +29,13 @@ class User private () extends ProtoAuthUser[User] with ObjectIdPk[User] { object bgImg extends StringField(this, 50) 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 blogs extends BsonRecordListField(this, UserBlog) def modifyBlog(ub: UserBlog) = { @@ -47,26 +57,22 @@ class User private () extends ProtoAuthUser[User] with ObjectIdPk[User] { case None => Empty } - // todo figure out if this goes here - object following extends MongoMapField[User,Blog](this) { - import com.mongodb.{BasicDBObject, DBObject} - // todo test - override def asDBObject: DBObject = { - val dbo = new BasicDBObject - for((k,v) <- value) dbo.put(k, v.id.is) - dbo - } + /* Contains a list of blog id to writer name separated by separator. */ + object following extends MongoListField[User,String](this) { + lazy val separator: String = "~" - override def setFromDBObject(dbo: DBObject): Box[Map[String, Blog]] = { - import scala.collection.JavaConversions._ + def create(writer: BlogWriter, blog: Blog): String = + blog.id.is + following.separator + writer.name.is + } + // todo test follow and unfollow + def follow(writer: BlogWriter, blog: Blog) = { + val addMe = following.create(writer,blog) + if(!blog.writerExists(writer.name.is) || following.get.exists(_ == addMe)) this + else following(addMe :: following.get) + } - setBox(Full( - Map() ++ dbo.keySet.map { - k => (k, Blog.findByStringId(dbo.get(k).toString). - getOrElse(Blog.createRecord)) - } - )) - } + def unFollow(writer: BlogWriter, blog: Blog) = { + following(following.get.filter(_ != following.create(writer,blog))) } } @@ -80,7 +86,7 @@ object User extends User with ProtoAuthUserMeta[User] with Loggable { def findByEmail(in: String): Box[User] = find(email.name, in) def findByName(in: String): Box[User] = find(name.name, in) - def findByUsername(in: String): Box[User] = find(username.name, in) + def findByUsername(in: String): Box[User] = find(username.name, in.toLowerCase) def findByStringId(id: String): Box[User] = if (ObjectId.isValid(id) && id != "0"*24) find(new ObjectId(id)) else Empty @@ -113,10 +119,9 @@ object User extends User with ProtoAuthUserMeta[User] with Loggable { } case Full(at) => find(at.userId.is).map(user => { if (user.validate.length == 0) { - user.verified(true) - user.save + user.verified(true).save logUserIn(user) - //at.delete_! + //at.delete_! (token is deleted at Password Reset) RedirectResponse(loginTokenAfterUrl) } else { @@ -159,6 +164,85 @@ object User extends User with ProtoAuthUserMeta[User] with Loggable { ) } + def handleInviteToken(): Box[LiftResponse] = { + var resp: LiftResponse = DoRedirectResponse("/") + S.param("token").flatMap(InviteToken.findByStringId) match { + case Full(at) if at.expires.isExpired => + at.delete_! + resp = RedirectWithState(indexUrl, RedirectState(() => { S.error("Invite token has expired") })) + + case Full(at) => + val userBoxed = findByEmail(at.email.is) + val user: User = userBoxed openOr { + createRecord. + email(at.email.is). + verified(true). + name(at.name.is). + addBlog(UserBlog.createRecord.blog(at.blogId.is)). + password(StringHelpers.randomString(20), true). + username(StringHelpers.randomString(15)). + save + } + val blog = Blog.findByStringId(at.blogId.is.toString) + blog.map(_.addWriterSafely(BlogWriter.createRecord.user(user.id.is).name(at.name.is)).save) + + if(isLoggedIn) logUserOut() + logUserIn(user, isAuthed = true) + + blog.map { blog => + if(userBoxed.isDefined) + resp = RedirectWithState(Site.categoriesLoc.calcHref(blog), RedirectState(() => { + S.notice("Congratulations! You have verified "+blog.name.is+"!") + })) + else + resp = RedirectWithState(Site.signUp3.url, RedirectState(() => { + BlogIdVar(at.blogId.is.toString) + VerifiedVar(true) + S.notice("Congratulations! You have verified "+blog.name.is+"!") + })) + } + at.delete_! + + case _ => + resp = RedirectWithState(indexUrl, RedirectState(() => { S.warning("Invite token not provided") })) + } + + Full(resp) + } + + def sendInvite(name: String, email: String, user: User, blog: Blog) { + sendInvite(name, email, Site.home.fullUrl, user, blog) + } + def sendInviteToken(name: String, email: String, user: User, blog: Blog) { + val token = InviteToken.create(email, name, blog.id.is) + sendInvite(name, email, token.url, user, blog) + } + + private def sendInvite(name: String, email: String, url: String, user: User, blog: Blog) { + import net.liftweb.util.Mailer._ + + val msgTxt = + """ + |Hi %s, + | + |%s has invited you to Joe Reader to become a member of your + |current blog, %s. + | + |Click below to join: + |%s + | + |Yours truly, + |%s + """.format(name, user.name.is, blog.name.is, url, MongoAuth.systemUsername.vend).stripMargin + + sendMail( + From(MongoAuth.systemFancyEmail), + Subject("%s: Invite to %s".format(user.name.is, MongoAuth.siteName.vend)), + To(email), + PlainMailBodyType(msgTxt) + ) + } + // used during login process (holds email address) object loginCredentials extends SessionVar[String]("") object regUser extends SessionVar[User](createRecord.email(loginCredentials.is)) @@ -186,15 +270,14 @@ class UserBlog private () extends BsonRecord[UserBlog] { override def defaultValue = new ObjectId("0"*24) } - object categories extends MongoListField[UserBlog, String](this) { + object categories extends MongoListField[UserBlog, String](this) - def remove(category: String) = super.set(get.filter(_ != category)) - } def addCategory(category: String) = if(categories.get.exists(_ == category)) this else categories(category :: categories.get) def removeCategory(category: String) = categories(categories.get.filter(_ != category)) + } object UserBlog extends UserBlog with BsonMetaRecord[UserBlog] diff --git a/src/main/scala/com/joereader/snippet/BlogSnips.scala b/src/main/scala/com/joereader/snippet/BlogSnips.scala @@ -16,8 +16,7 @@ import net.liftmodules.extras.SnippetHelper import com.joereader._ import com.joereader.model._ import snippet.SnipHelpers._ -import lib.rss.Rss._ -import api._ +import lib._, Helper._, rss.Rss._ import config._ @@ -42,6 +41,8 @@ sealed trait BlogSnippet extends SnippetHelper with Loggable { def name = serve(blog => Text(blog.name.is))(test, NodeSeq.Empty) + def blogname = serve(user => Text(user.blogname.is))(test, NodeSeq.Empty) + def url = serve(blog => Text(blog.urlHtml.is))(test, NodeSeq.Empty) def description = serve(blog => Text(blog.description.is))(test, NodeSeq.Empty) @@ -55,76 +56,77 @@ sealed trait BlogSnippet extends SnippetHelper with Loggable { def writers = serve(blog => writersList(blog.writers.is, owner = false))(test, NodeSeq.Empty) def writersList(writers: List[BlogWriter], owner: Boolean): NodeSeq = - for(i <- 0 until writers.size; writer <- writers) yield { + for(writer <- writers) yield { val user = User.findByStringId(writer.user.is.toString).openOr(User.createRecord) val name = writer.name.is val dashName = name.split(" ").mkString("-") <div id={"writer-"+dashName} class="blog-writer"> <img id={"img-"+dashName} src={imageUrl(user)} /> - <span class="name">{name}</span>{ - - if(owner && writer.user.is.toString.isEmpty) - <label class="inline"> - <input id={"email-"+dashName} placeholder="email" value={writer.email.is} /> - <button onclick={ajaxCall(JsArray(ValById("email-"+dashName), Str(name)), invite _)._2.toJsCmd. - toString+"; return false;"} class="btn btn-primary">Invite</button> - <button onclick={ajaxCall(Str(name), remove _)._2.toJsCmd. - toString+"; return false;"} class="close category">×</button> - </label> - - }</div> + <span class="name">{name}</span> + <label class="inline">{ + if(!writer.hasUser && User.isLoggedIn) { + <input id={"email-"+dashName} placeholder="email" value={if(owner) writer.email.is else ""} /> + <button onclick={ajaxCall(JsArray(ValById("email-"+dashName), Str(name)), + {if(owner) inviteAsOwner _ else invite _})._2.toJsCmd.toString+"; return false;"} class="btn btn-primary">Invite</button> + } + if(owner) { + <button onclick={ajaxCall(Str(name), remove _)._2.toJsCmd.toString+"; return false;"} class="close category">×</button> + } + }</label> + </div> } - def remove(name: String): JsCmd = { - blog.map(_.removeWriter(name).update) - Remove("writer-"+name.split(" ").mkString("-")) - } + def remove(name: String): JsCmd = + for(blog <- blog; blogWriter <- blog.writer(name)) { + val msg = s"Are you sure you want to remove $name? ${ + if(blogWriter.hasUser) blogWriter.followers.is.size+"followers will be affected."}" + + S.warning( + <div>{msg}</div> + <div> + <button onclick={ajaxCall(Str(name), removeForSure _)._2.toJsCmd.toString+"; return false;"} class="btn btn-warning">Yes</button> + <button onclick="$(document).trigger('clear-alerts')" class="btn">No</button> + </div> + ) + } - def invite(nameAndEmail: String): JsCmd = { - import net.liftweb.util.Mailer._ - import net.liftmodules.mongoauth.MongoAuth + def removeForSure(name: String): JsCmd = + for(blog <- blog; blogWriter <- blog.writer(name)) { + if(blogWriter.hasUser) { + blogWriter.removeUser() + blog.save + Noop // todo update writers image with mr_noman (need to create js function) + } else { + blogWriter.followers.objs.par.map(_.unFollow(blogWriter, blog).update) + blog.removeWriter(blogWriter).update + Remove("writer-"+name.split(" ").mkString("-")) + } + } + def inviteAsOwner(nameAndEmail: String): JsCmd = { val name = nameAndEmail.split(",").headOption.getOrElse("") val email = nameAndEmail.split(",").tail.headOption.getOrElse("") - blog.map { blog => + for(blog <- blog; user <- User.currentUser) + User.sendInviteToken(name, email, user, blog) + Noop + } + def invite(nameAndEmail: String): JsCmd = { + val name = nameAndEmail.split(",").headOption.getOrElse("") + val email = nameAndEmail.split(",").tail.headOption.getOrElse("") + + for(blog <- blog; user <- User.currentUser) { blog.writer(name).map(_.email(email)) - blog.update - - User.currentUser.map { user => - // todo create a token to sign him up as blog writer - val token = "" - val msgTxt = - """ - |Hi %s, - | - |%s has invited you to Joe Reader to become a member of your - |current blog, %s. - | - |Click below to sign up: - |%s - | - |Yours truly, - |%s - """.format(name, user.name.is, blog.name.is, token, MongoAuth.systemUsername.vend).stripMargin - - sendMail( - From(MongoAuth.systemFancyEmail), - Subject("%s Invite from %s".format(MongoAuth.siteName.vend, user.name.is)), - To(email), - PlainMailBodyType(msgTxt) - ) - } + blog.save + User.sendInvite(name, email, user, blog) } - S.notice(name+" has been invited") } } -object ProfileLocBlog extends BlogSnippet { +trait OwnerBlogSnippet extends BlogSnippet { - protected def blog = Site.blogProfileLoc.currentValue protected def test = blog.exists(b => User.currentUser.exists(_.id.is == b.owner.is)) override def writers = serve { blog => @@ -139,12 +141,139 @@ object ProfileLocBlog extends BlogSnippet { "*" #> ajaxTextarea(blog.description.is, {s => blog.description(s).update; Noop}) } (test, super.description) - def uploadImg(html: NodeSeq) = serve(html) { user => - "*" #> insertFileUpload("pic", ImageUpload.userUrl()) + def uploadImg(html: NodeSeq) = serve(html) { blog => + "*" #> insertFileUpload("pic", ImageUpload.blogUrl(blog)) }(test, NodeSeq.Empty) - def uploadBgImg(html: NodeSeq) = serve(html) { user => - "*" #> insertFileUpload("bg", ImageUpload.userUrl("bg")) + def uploadBgImg(html: NodeSeq) = serve(html) { blog => + "*" #> insertFileUpload("bg", ImageUpload.blogUrl(blog, "bg")) }(test, NodeSeq.Empty) + + def blogname(html: NodeSeq) = serve(html) { blog => + + def check(s: String): JsCmd = { + val b = Blog.findByBlogName(s) + def isYou = b.exists(_.id.is == blog.id.is) + + if(!s.matches("^[a-z0-9-]{2,}$")) + S.error("Blog name can only contain alphanumeric or dash characters.") + else if(isYou) + Noop + else if(b.isEmpty) { + blog.blogname(s).update + S.notice("New blog name is saved") + } + else S.error("Blog name is taken") + } + + "*" #> ajaxText(blog.blogname.is, { s: String => check(s.toLowerCase)}) + } (test, super.blogname) +} + +object ProfileLocBlog extends OwnerBlogSnippet { + protected def blog = Site.blogProfileLoc.currentValue +} + +class ProfileLocBlog(b: Box[Blog]) extends OwnerBlogSnippet { + protected def blog = b + override def test = super.test +} + +object ProfileLocBlogReq extends OwnerBlogSnippet { + protected def blog = Blog.findByStringId(BlogIdVar.is) +} + +object ProfileLocBlogEdit extends OwnerBlogSnippet { + protected def blog = Site.editBlogLoc.currentValue +} + +case class CategoriesSnippet(b: Box[Blog]) extends BlogSnippet { + override protected def blog = b + + val user: Box[User] = User.currentUser + + val userblog: Box[UserBlog] = + for { + user <- user + blog <- blog + } yield user.blog(blog) openOr UserBlog.createRecord.blog(blog.id.is) + + var category = "" + + def toHtml = serve { blog => + userblog.map { userblog => + <div class="row"> + <div class="span12"> + <a href={Site.editBlogLoc.calcHref(blog)}>{name ++ img(<img/>)}</a><br/>{url} + </div> + <div class="span12 well"> + <form data-lift="form.ajax"> + <label class="inline"> + {text(category, category = _, "placeholder" -> "Submit New Category", "id" -> "category-input")} + {ajaxSubmit("Add", ()=> addCategory(category), "class" -> "btn btn-primary")} + </label> + </form> + <br/> + <label id="chosen-categories" class="inline">{ + userblog.categories.is.flatMap(categoryNode(_)).toSeq + }</label> + </div> + </div> + } + } (test = true, NodeSeq.Empty) + + def addCategory(cat: String): JsCmd = { + + for(ub <- userblog; u <- user) { + ub.addCategory(cat) + u.modifyBlog(ub).save // change to update (but wont work with embed docs) + val c = Category.find(cat).map(_.addUser(u).update) + if(!c.isDefined) Category.createRecord.id(cat).addUser(u).save + } + + category = "" + Prepend("chosen-categories", categoryNode(cat)) & + SetValById("category-input","") + } + + def removeCategory(cat: String): JsCmd = { + super.remove("") + for(ub <- userblog; u <- user) { + ub.removeCategory(cat) + u.modifyBlog(ub).save + Category.find(cat).map(_.removeUser(u).update) + } + + Remove("chosen-cat-"+cat.split(" ").mkString("-")) + } + + def categoryNode(cat: String): Elem = + <span id={"chosen-cat-"+cat.split(" ").mkString("-")} class="uneditable-input">{cat}<button onclick={ + ajaxCall(Str(cat), removeCategory _)._2.toJsCmd.toString.singleQuoteToDouble +"; return false;" + } class="close category">×</button></span> +} + +class CategoriesSnip { + + protected def blog = Site.categoriesLoc.currentValue + val snip = CategoriesSnippet(blog) + + def render = { + "#chosen-categories *" #> snip.userblog.map(_.categories.is.map(snip.categoryNode(_))) & + "#category-input" #> text(snip.category, snip.category = _) & + "#category-add" #> ajaxSubmit("Add", ()=> snip.addCategory(snip.category)) + } +} + +class BlogsSnip extends UserSnippet { + + def user = User.currentUser + + def blogs: NodeSeq = serve { user => + val blogs: List[CategoriesSnippet] = user.blogs.is.map(ub => CategoriesSnippet(ub.blog.find)) + blogs.flatMap(_.toHtml).toSeq + } (test = true, NodeSeq.Empty) + + def addBlog = "* [href]" #> Site.blogVerify.url } diff --git a/src/main/scala/com/joereader/snippet/LiftExtras.scala b/src/main/scala/com/joereader/snippet/LiftExtras.scala @@ -2,7 +2,11 @@ package com.joereader.snippet import net.liftweb.util.Props import net.liftmodules.extras._, snippet._ -import scala.xml.NodeSeq +import scala.xml.{Elem, NodeSeq} +import com.joereader.model.{Blog, User} +import com.joereader.config.S3Config._ +import net.liftweb.http.js.{JE, JsCmd} +import com.joereader.lib.ImageUpload object Menus extends BsMenu object Notices extends BsAlerts @@ -18,4 +22,37 @@ object ProductionOnly { def render(in: NodeSeq): NodeSeq = if (Props.productionMode) in else NodeSeq.Empty +} + +object SnipHelpers { + + val imgProfileId = "img-profile" + val imgBgId = "img-bg" + def imageUrl(u: User) = if(u.img.is.nonEmpty) s3.fileUrl(u.img.is) else Gravatar.imageUrl(u.email.is) + def imageUrl(b: Blog) = if(b.img.is.nonEmpty) s3.fileUrl(b.img.is) else Gravatar.imageUrl("blah", 200, "G", "wavatar") + def imageBgUrl(u: User) = if(u.bgImg.is.nonEmpty) s3.fileUrl(u.bgImg.is) else "" + def imageBgUrl(b: Blog) = if(b.bgImg.is.nonEmpty) s3.fileUrl(b.bgImg.is) else "" + + def insertFileUpload(id: String, path: String): NodeSeq = { + <input id={id+"-fileupload"} type="file" name="files2[]" data-url={path} accept={ImageUpload.acceptedImages.mkString(",")} /> + <div id={id+"-progress"} style="width:20em; border: 1pt solid silver; display: none"><div id={id+"-progress-bar"} style="background: green; height: 1em; width:0%"></div></div> + } + + + class JQuery(id: String, func: String, params: String*) extends JsCmd { + val script = """$("#%s").%s('%s')""".format(id, func, params.mkString("','")) + override def toJsCmd = JE.JsRaw(script).cmd.toJsCmd + } + + case class DisableInput(id: String) extends JQuery(id,"attr", "disabled", "") + case class AddClass(id: String, className: String) extends JQuery(id,"addClass", className) + case class RemoveClass(id: String, className: String) extends JQuery(id,"removeClass", className) + case class ToggleClass(id: String, className: String) extends JQuery(id,"toggleClass", className) + case class ToggleHide(id: String) extends JQuery(id,"toggleClass", "hide") + case class Hide(id: String) extends JQuery(id,"addClass", "hide") + case class Show(id: String) extends JQuery(id,"removeClass", "hide") + case class Prepend(id: String, node: Elem) extends JQuery(id,"prepend", node.toString()) + case class LoadButton(id: String) extends JQuery(id, "button", "loading") + case class Remove(id: String) extends JQuery(id, "remove", "") + case class UpdateImg(id: String, img: String) extends JQuery(id, "attr", "src", img) } \ No newline at end of file diff --git a/src/main/scala/com/joereader/snippet/SignUp.scala b/src/main/scala/com/joereader/snippet/SignUp.scala @@ -0,0 +1,149 @@ +package com.joereader.snippet + +import com.joereader._ +import model._ +import lib.Helper._ +import config._ + +import net.liftweb._ +import util.Helpers._ +import http._ +import SHtml._ +import js._ +import JsCmds._ +import scala.xml.NodeSeq + +// Requests variables sent from page to page during sign up +object EmailVar extends RequestVar("") +object BlogIdVar extends RequestVar("") +object VerifiedVar extends RequestVar(false) + +class SignUp { + + var email = EmailVar.is + val blogId = BlogIdVar.is + + /** + * First Step: Collect email from user to start wizard. + */ + def setupEmail = { + + def invitedMsg: JsCmd = S.notice("You've been invited already silly") + + def joinedMsg: JsCmd = S.notice("You joined Joe Reader a while back. " + + "We'll invite you as soon as we're ready!") + + + "#email-join" #> ajaxText(email, {s=> email = s; Noop}) & + "#join-btn" #> ajaxSubmit("Join", () => { + if(email.isEmail) { + if(BetaUser.find(email).isDefined) + joinedMsg & SetValById("email-join", "") + else if(User.findByEmail(email).isDefined) + invitedMsg & SetValById("email-join", "") + else { + BetaUser.createRecord.id(email).save + S.redirectTo(Site.signUp1.url,() => EmailVar(email)) + } + } + else S.error("Please enter a valid email.") + }) + } + + /** + * Second Step: Check if he's a blog writer. If so, continue to next steps + * and allow him to setup and customize his blog. + */ + def setupWriter = { + assert(email.isEmpty) + + "#yes-button" #> ajaxSubmit("Yes", () => S.redirectTo(Site.signUp2.url, () => EmailVar(email))) & + "#no-button" #> ajaxSubmit("No", () => S.redirectTo("/")) + } + + // Third step is blog verification which is handled by + // Verification.render. + def setupVerification = { + assert(email.isEmpty) + "*" #> NodeSeq.Empty + } + + /** + * Fourth Step: Set the categories that the user writes for his blog. + */ + def setupCategories = { + val user = User.currentUser + val blog = Blog.findByStringId(blogId) + val verified = VerifiedVar.is + + assert(!verified || user.isEmpty || blog.isEmpty) + + def continue(): JsCmd = S.redirectTo(Site.signUp4.url, () => { + BlogIdVar(blogId) + VerifiedVar(verified) + }) + + "#categories-area" #> Templates("templates-hidden" :: "parts" :: "categories" :: Nil).map{ ns => + Site.categoriesLoc.requestValue(blog) + ns + } & + "#categories-continue" #> ajaxSubmit("Continue", continue) + } + + /** + * Fifth Step: if the user is the owner of the blog, allow him to customize + * the blog page, else continue to step six. + */ + def setupBlog = { + val user = User.currentUser + val blog = Blog.findByStringId(blogId) + val verified = VerifiedVar.is + + assert(!verified || user.isEmpty || blog.isEmpty) + + if(!new ProfileLocBlog(blog).test) + S.redirectTo(Site.signUp5.url, () => VerifiedVar(verified)) + + def continue(): JsCmd = S.redirectTo(Site.signUp5.url, () => VerifiedVar(verified)) + + "#blog-area" #> Templates("templates-hidden" :: "parts" :: "blog-profile" :: Nil).map{ ns => + Site.blogProfileLoc.requestValue(blog) + ns + } & + "#blog-continue" #> ajaxSubmit("Continue", continue) + } + + /** + * Sixth Step: Customize the user's page. + */ + def setupUser = { + val user = User.currentUser + val verified = VerifiedVar.is + + assert(!verified || user.isEmpty) + + def continue(): JsCmd = S.redirectTo(Site.signUp6.url, () => VerifiedVar(verified)) + + "#user-area" #> Templates("templates-hidden" :: "parts" :: "user-profile" :: Nil).map{ ns => + Site.userProfileLoc.requestValue(user) + ns + } & + "#user-continue" #> ajaxSubmit("Continue", continue) + } + + /** + * Seventh Step: Set the user's password. + */ + def setupPassword = { + val user = User.currentUser + val verified = VerifiedVar.is + + assert(!verified || user.isEmpty) + def continue(): JsCmd = S.redirectTo("/") + + "#user-continue" #> ajaxSubmit("Continue", continue) + } + + def assert(bool: Boolean) { if(bool) S.redirectTo(Site.notFound.url) } + +} diff --git a/src/main/scala/com/joereader/snippet/SnipHelpers.scala b/src/main/scala/com/joereader/snippet/SnipHelpers.scala @@ -1,71 +0,0 @@ -package com.joereader.snippet - -import com.joereader._ -import model._ -import config.S3Config._ - -import net.liftmodules.extras._ -import net.liftweb.http.SHtml._ -import scala.xml._ -import net.liftweb.http.js._ -import com.joereader.api.ImageUpload - - -object SnipHelpers { - - - def imageUrl(u: User) = if(u.img.is.nonEmpty) s3.fileUrl(u.img.is) else Gravatar.imageUrl(u.email.is) - def imageUrl(b: Blog) = if(b.img.is.nonEmpty) s3.fileUrl(b.img.is) else Gravatar.imageUrl("blah", 200, "G", "wavatar") - def imageBgUrl(u: User) = if(u.bgImg.is.nonEmpty) s3.fileUrl(u.bgImg.is) else "" - def imageBgUrl(b: Blog) = if(b.bgImg.is.nonEmpty) s3.fileUrl(b.bgImg.is) else "" - - - implicit class InputRadioDesignImplicit(choices: ChoiceHolder[String]) { - - def unregisteredWritersChoicesToPictureForm : NodeSeq = { - - for(i <- 0 until choices.items.size) yield { - val choice = choices.items(i) - val user = User.createRecord - - <label onClick="$(this).addClass('selected').siblings().removeClass('selected')"> - {choice.xhtml}<img src={imageUrl(user)} />{choice.key.toString}</label> - } - } - } - - implicit class InputRadioDesign2Implicit(writers: List[BlogWriter]) { - - def registeredWritersChoicesToPictureForm : NodeSeq = { - - for(i <- 0 until writers.size) yield { - val writer = writers(i) - val user = User.findByStringId(writer.user.is.toString).openOr(User.createRecord) - - <label><img src={imageUrl(user)} />{writer.name.is}</label> - } - } - } - - def insertFileUpload(id: String, path: String): NodeSeq = { - <input id={id+"-fileupload"} type="file" name="files2[]" data-url={path} accept={ImageUpload.acceptedImages.mkString(",")} /> - <div id={id+"-progress"} style="width:20em; border: 1pt solid silver; display: none"><div id={id+"-progress-bar"} style="background: green; height: 1em; width:0%"></div></div> - } - - - class JQuery(id: String, func: String, params: String*) extends JsCmd { - val script = """$("#%s").%s('%s')""".format(id, func, params.mkString("','")) - override def toJsCmd = JE.JsRaw(script).cmd.toJsCmd - } - - case class DisableInput(id: String) extends JQuery(id,"attr", "disabled", "") - case class AddClass(id: String, className: String) extends JQuery(id,"addClass", className) - case class RemoveClass(id: String, className: String) extends JQuery(id,"removeClass", className) - case class ToggleClass(id: String, className: String) extends JQuery(id,"toggleClass", className) - case class ToggleHide(id: String) extends JQuery(id,"toggleClass", "hide") - case class Hide(id: String) extends JQuery(id,"addClass", "hide") - case class Show(id: String) extends JQuery(id,"removeClass", "hide") - case class Prepend(id: String, node: Elem) extends JQuery(id,"prepend", node.toString()) - case class LoadButton(id: String) extends JQuery(id, "button", "loading") - case class Remove(id: String) extends JQuery(id, "remove", "") -} diff --git a/src/main/scala/com/joereader/snippet/UserSnips.scala b/src/main/scala/com/joereader/snippet/UserSnips.scala @@ -20,15 +20,13 @@ import net.liftmodules.mongoauth.model.{LoginToken, ExtSession} import com.joereader._ import com.joereader.model._ import com.joereader.snippet.SnipHelpers._ -import api._ import config._ -import lib._, rss.Rss._ +import lib._, Helper._, rss.Rss._ import net.liftweb.common.Full -sealed trait UserSnippet extends SnippetHelper with Loggable { +trait UserSnippet extends SnippetHelper with Loggable { protected def user: Box[User] - private def test = true protected def serve(snip: User => NodeSeq)(test: Boolean, fallback: NodeSeq): NodeSeq = (for { @@ -43,14 +41,35 @@ sealed trait UserSnippet extends SnippetHelper with Loggable { } yield { if(test) snip(u)(html) else fallback }): NodeSeq +} + +trait ReaderUserSnippet extends UserSnippet { + private def testReaderUserSnippet = true + + def email(html: NodeSeq) = serve(html)(user => "*" #> user.email.is)(testReaderUserSnippet, NodeSeq.Empty) + def name = serve(user => Text(user.name.is))(testReaderUserSnippet, NodeSeq.Empty) + + def img(html: NodeSeq) = serve(html){user => + "* [id]" #> imgProfileId & + "* [src]" #> imageUrl(user) + }(testReaderUserSnippet, NodeSeq.Empty) +} - def name = serve(user => Text(user.name.is))(test, NodeSeq.Empty) +trait WriterUserSnippet extends UserSnippet { - def about = serve(user => Text(user.about.is))(test, NodeSeq.Empty) + private def testWriterUserSnippet = user.exists(!_.blogs.is.isEmpty) - def img(html: NodeSeq) = serve(html)(user => "* [src]" #> imageUrl(user))(test, NodeSeq.Empty) + def username = serve(user => Text(user.username.is))(testWriterUserSnippet, NodeSeq.Empty) + def about = serve(user => Text(user.about.is))(testWriterUserSnippet, NodeSeq.Empty) - def bgImg(html: NodeSeq) = serve(html)(user => "* [src]" #> imageBgUrl(user))(test, NodeSeq.Empty) + def introVid(html: NodeSeq) = serve(html)(user => + "*" #> ("http://www.youtube.com/watch?v="+user.introVid.is) + )(testWriterUserSnippet, NodeSeq.Empty) + + def bgImg(html: NodeSeq) = serve(html){ user => + "* [id]" #> imgBgId & + "* [src]" #> imageBgUrl(user) + }(testWriterUserSnippet, NodeSeq.Empty) def articles = serve { user => user.blogs.is.map { @@ -61,50 +80,158 @@ sealed trait UserSnippet extends SnippetHelper with Loggable { blog.urlRss.is.head.entries.filter(_.author == bw.name.is).flatMap(_.toForm) } }.flatten - }(test, NodeSeq.Empty) + }(testWriterUserSnippet, NodeSeq.Empty) } -trait CurrentUser extends UserSnippet { +trait CurrentReaderUser extends ReaderUserSnippet with UserPassword { + + protected def test = user.exists(u => true) + + override def email(html: NodeSeq) = serve(html) { user => + def checkEmail(email: String): JsCmd = { + val u = User.findByEmail(email) + def isYou = u.exists(_.id.is == user.id.is) + + if(!email.isEmail) + S.error("Not a valid email") + else if(isYou) + Noop + else if(u.isDefined) + S.error("That email is registered already") + else { + user.email(email).verified(false).update + S.notice("New email is saved") + } + } + "*" #> ajaxText(user.email.is, checkEmail(_)) + } (test, super.email(html)) + + def password(html: NodeSeq) = serve(html) { user => + "*" #> ajaxText(user.name.is, {s => + val (pass,msg) = savePassword(s) + if(pass) Noop else S.error(msg) + }, "type" -> "password", "placeholder" -> "password") + } (test, super.name) + + def setPassword(html: NodeSeq) = serve(html) { user => + var oldpwd, pwd, pwd2 = "" + + def process(): JsCmd = { + val (pass, msg) = savePassword(oldpwd, pwd, pwd2) + if(pass) SetValById("old-pwd","") & SetValById("new-pwd","") & SetValById("new-pwd2","") + else S.error(msg) + } - protected def test = true + "#old-pwd" #> SHtml.password(oldpwd, oldpwd = _) & + "#new-pwd" #> SHtml.password(pwd, pwd = _) & + "#new-pwd2" #> SHtml.password(pwd2, pwd2 = _) & + "#submit-pwd" #> ajaxSubmit("Set Password", process) + } (test, NodeSeq.Empty) def name(html: NodeSeq) = serve(html) { user => "*" #> ajaxText(user.name.is, {s => user.name(s).update; Noop}) } (test, super.name) + def uploadImg(html: NodeSeq) = serve(html) { user => + "*" #> insertFileUpload("pic", ImageUpload.userUrl()) + }(test, NodeSeq.Empty) +} + +trait CurrentWriterUser extends WriterUserSnippet { + + protected def test = user.exists(!_.blogs.is.isEmpty) + def about(html: NodeSeq) = serve(html) { user => "*" #> ajaxTextarea(user.about.is, {s => user.about(s).update; Noop}) } (test, super.about) - def introVid(html: NodeSeq) = serve(html) { user => + def uploadBgImg(html: NodeSeq) = serve(html) { user => + "*" #> insertFileUpload("bg", ImageUpload.userUrl("bg")) + }(test, NodeSeq.Empty) + + def username(html: NodeSeq) = serve(html) { user => + + def check(s: String): JsCmd = { + val u = User.findByUsername(s) + def isYou = u.exists(_.id.is == user.id.is) + + if(!s.matches("^[a-z0-9-]{2,}$")) + S.error("User name can only contain alphanumeric or dash characters") + else if(isYou) + Noop + else if(u.isEmpty && Site.isAvailableMenu(s)) { + user.username(s).update + S.notice("New username is saved") + } + else S.error("Username is taken") + } - def checkVideo(id: String): JsCmd = { + "*" #> ajaxText(user.username.is, { s: String => check(s.toLowerCase)}) + } (test, super.username) + + override def introVid(html: NodeSeq) = serve(html) { user => + + def check(id: String): JsCmd = { import VideoService._, dispatch._ val time: Int = Youtube.info.videoDuration(id)().getOrElse(-1) if(time < 0) S.error("Video could not be found") else if(time <= 30) { user.introVid(id).update; Noop } else S.error("Video duration must be 30 seconds or less") } + "*" #> ajaxText(user.introVid.is, {s => check(s); Noop}) + }(test, super.introVid(html)) - "*" #> ajaxText(user.introVid.is, {s => checkVideo(s); Noop}) - }(test, NodeSeq.Empty) + def otherVid(html: NodeSeq) = serve(html) { user => - def uploadImg(html: NodeSeq) = serve(html) { user => - "*" #> insertFileUpload("pic", ImageUpload.userUrl()) - }(test, NodeSeq.Empty) + var vids: List[String] = user.otherVid.is - def uploadBgImg(html: NodeSeq) = serve(html) { user => - "*" #> insertFileUpload("bg", ImageUpload.userUrl("bg")) + def addVideo(in: String): JsCmd = { + val res = if(in.isEmpty) Nil else in.split(",").map(_.trim).toList + val newVids: List[String] = res diff vids + val sameVids: List[String] = res diff newVids + + user.otherVid(sameVids).update + vids = sameVids + + var failed: List[String] = Nil + newVids.par.map{ id => + if(validVideo(id)) { + user.addVideo(id).update + vids = id :: vids + } + else failed = id :: failed + } + + if(!failed.isEmpty) + S.error("Video ids ~ "+failed.mkString(", ")+" ~ could not be found") + else Noop + } + + def validVideo(id: String): Boolean = { + import VideoService._, dispatch._ + val time = Youtube.info.videoDuration(id)() + if(time.isDefined) true else false + } + + "*" #> ajaxText(vids.mkString(", "), addVideo(_)) }(test, NodeSeq.Empty) } -object CurrentUser extends CurrentUser { +object CurrentReader extends CurrentReaderUser { + protected def user = User.currentUser +} +object CurrentWriter extends CurrentWriterUser { protected def user = User.currentUser } -object ProfileLocUser extends CurrentUser { +object ProfileLocReader extends CurrentReaderUser { protected def user = Site.userProfileLoc.currentValue - override protected def test = user.exists(u => User.currentUser.exists(_.id.is == u.id.is)) + override def test = user.exists(u => User.currentUser.exists(_.id.is == u.id.is)) && super.test +} + +object ProfileLocWriter extends CurrentWriterUser { + protected def user = Site.userProfileLoc.currentValue + override def test = user.exists(u => User.currentUser.exists(_.id.is == u.id.is)) && super.test } object UserLogin extends Loggable { @@ -122,7 +249,6 @@ object UserLogin extends Loggable { if (email.length > 0 && password.length > 0) { User.findByEmail(email) match { case Full(user) if user.password.isMatch(password) => - logger.debug("pwd matched") User.logUserIn(user, isAuthed = true) ExtSession.deleteExtCookie() RedirectTo(LoginRedirect.openOr(Site.home.url)) @@ -170,7 +296,7 @@ object UserRecovery extends Loggable { case Full(user) => User.sendLoginToken(user) User.loginCredentials.remove() - S.notice("An email has been sent to you with instructions for accessing your account") + S.notice("An email has been sent to you with instructions to access your account") Noop case _ => S.error("id_email_err", "The email you entered cannot be found") @@ -191,14 +317,13 @@ object PasswordReset extends Loggable { def render = { - User.currentUserId.map { id => - val token = LoginToken.find(LoginToken.userId.name, id) + User.currentUser.map { user => + val token = LoginToken.find(LoginToken.userId.name, user.id.is) if(token.isEmpty) S.redirectTo(Site.notFound.url) else token.map(_.delete_!) } - var pwd = "" - var pwd2 = "" + var pwd, pwd2 = "" def doSubmit(): JsCmd = if(pwd == pwd2) { @@ -210,18 +335,48 @@ object PasswordReset extends Loggable { S.redirectTo(Site.home.url) } else - S.error("Passwords do not match.") - - def passwordsMatch: JsCmd = - if(pwd == pwd2) Noop - else S.error("id_pwd2_err", "Password does not match") + S.error("id_pwd2_err", "Password does not match") - "#id_pwd" #> ajaxText(pwd, {s=> pwd = s; Noop}) & - "#id_pwd2" #> ajaxText(pwd2, {s=> pwd2 = s; passwordsMatch}) & + "#id_pwd" #> password(pwd, pwd = _) & + "#id_pwd2" #> password(pwd2, pwd2 = _) & "#id_submit" #> ajaxSubmit("Save", doSubmit) } } +trait UserPassword { + // Checks if password is stealthy enough + def validPassword(pw: String): (Boolean, String) = { + if(pw.length < 1) (false, "You forgot to enter a password") + if(pw.length < 8) (false, "Password must be at least 8 figures long") + (true,"") + } + + def resettablePassword(oldPw: String, newPw: String, confirmPw: String): (Boolean, String) = { + if(!correctPassword(oldPw)) + (false, "Your old password is incorrect") + if(newPw != confirmPw) + (false, "Your passwords do not match") + (true,"") + } + + def savePassword(oldPw: String, newPw: String, confirmPw: String): (Boolean, String) = { + val reset = resettablePassword(oldPw, newPw, confirmPw) + if(reset._1) savePassword(newPw) else (false, reset._2) + } + + def savePassword(pw: String): (Boolean, String) = User.currentUser.map { user => + val valid = validPassword(pw) + if(valid._1) { + user.password(pw) + user.password.hashIt + user.update + (true,"") + } else (false, valid._2) + } openOr(false, "???") + + def correctPassword(pw: String) = User.currentUser.exists(_.password.isMatch(pw)) +} + object UserTopbar { def render = { User.currentUser match { diff --git a/src/main/scala/com/joereader/snippet/Verification.scala b/src/main/scala/com/joereader/snippet/Verification.scala @@ -0,0 +1,217 @@ +package com.joereader.snippet + +import net.liftweb._ +import util._ +import Helpers._ +import http._ +import SHtml._ +import js._, JsCmds._ +import common._ + +import com.joereader._ +import lib.rss.Rss._ +import snippet.SnipHelpers._ +import config._ +import model._ +import scala.xml._ +import net.liftmodules.mongoauth.LoginRedirect + +/** + * Verifies a blog. + */ +class Verification extends VerificationDesigns { + + val user: User = setUser() + var blog = Blog.createRecord + + var searched, verified = false + var urlHtml = "" + + val writerName = ValueCell("") + val owner = ValueCell(false) + + val metaName = "joe_reader" + val metaContent = (writerName lift owner)( + (w,o) => user.id.is+":"+w+{if(o)":owner" else ""}) + + val verification = metaContent.lift(metaContent => + s"""<meta name="$metaName" content="$metaContent"/>""") + + def render = "#verify-state" #> idMemoize(verifyBlog _) + + def process(): JsCmd = { + verified = + MetaVerification(metaName, metaContent.get, urlHtml).verified + + if(verified) { + + // save blog stuff + val bw = BlogWriter.createRecord.name(writerName.is).user(user.id.is) + + if(owner) blog.owner(user.id.is) + blog.addWriterSafely(bw) + + if(blog.blogname.is.isEmpty) + blog.blogname(StringHelpers.randomString(15)) + + blog.save + + onVerified() & onVerifiedSignUp() // support signup too + } + else + S.error("Verification is unsuccessful. " + + "Double check if your <head> contains the correct meta content.") + } + + def verifyBlog(outer: IdMemoizeTransform) = { + + // if idmemoize updated, get urlhtml and find feed + if(urlHtml.nonEmpty) { + val existing = Blog.findByUrl(urlHtml) + if(existing.isEmpty) { + val res = urlHtml.response + val links = res.content.rssLinks + val feed: Feed = links.feed + val writers = feed.writers.map(BlogWriter.createRecord.name(_)) + + blog = Blog.createRecord + .urlHtml(res.loc) + .name(feed.name) + .description(feed.description) + .writers(writers) + .urlRss(feed.links) + } else + existing.map(blog = _) + + searched = true + urlHtml = blog.urlHtml.is // nicely formatted + } + + "#blog-url" #> text(urlHtml, urlHtml = _) & + "#search-blog" #> ajaxSubmit("Search Blog", () => ajaxInvoke(outer.setHtml _)) & + "#writer-list *" #> listWriters & + "#blog-owner-q-yes" #> radios(0) & + "#blog-owner-q-no" #> radios(1) & + "#verification-info [class]" #> hideVerification & + "#blog-owner-q [class]" #> hideOwnerQuestion & + "#verification-info-input" #> WiringUI.asText(verification) & + "#verify-blog" #> ajaxSubmit("Verify", process) + } + + def writersRadios = ajaxRadio[String]( + blog.unregisteredWritersNames, + Full(blog.unregisteredWritersNames headOr ""), + { s => onWritersRadiosChange(s) & onWritersRadiosChangeSignUp(s) } + ).unregisteredWritersChoicesToPictureForm + + val radios = ajaxRadio[Boolean]( + Seq(true, false), + Full(false), + { bool => + owner.set(bool) + Noop + } + ) + + def registeredUsers = + blog.registeredWriters.registeredWritersChoicesToPictureForm + + def completedMsg = + if(blog.unregisteredWriters.isEmpty && searched) + Text("Looks like all writers have joined") + else NodeSeq.Empty + + def hideVerification: String = { + blog.unregisteredWritersNames.nonEmpty ? "" | "hide" + } + + def hideOwnerQuestion: String = { + val size = blog.unregisteredWritersNames.size + owner.set((size == 1) ? true | false) + (size < 2) ? "hide" | "" + } + + // returns the radio group of writers + def listWriters(): NodeSeq = + if(blog.writersNames.isEmpty && searched) + Text("Writers not found. Double check your url.") + else { + writerName.set(blog.unregisteredWritersNames headOr "") + user.name(writerName.is) + writersRadios ++ registeredUsers ++ completedMsg + } + + // Below is a hack to separate verification between signup and a logged in user + // who has multiple blogs. I chose to do it this way to avoid multiple templates. + // We differentiate by checking if EmailVar is set (which only happens during signup.) + + def setUser(): User = { + if(EmailVar.is.isEmpty) User.currentUser.openOr(User.createRecord) + else User.createRecord + } + + def onVerified(): JsCmd = if(!EmailVar.is.isEmpty) { + BetaUser.find(EmailVar.is).map(_.delete_!) + + user + .addBlog(UserBlog.createRecord.blog(blog.id.is)) + .email(EmailVar.is) + .password(Helpers.randomString(20)) + .username(StringHelpers.randomString(15)) + user.password.hashIt + user.save + User.logUserIn(user, isAuthed = true) + + RedirectTo(Site.signUp3.url,() => { + BlogIdVar(blog.id.is.toString) + VerifiedVar(verified) + }) + } else Noop + + def onWritersRadiosChange(s: String): JsCmd = if(!EmailVar.is.isEmpty) { + writerName.set(s) + user.name(s) + Noop + } else Noop + + protected def onVerifiedSignUp(): JsCmd = if(EmailVar.is.isEmpty) { + user.addBlog(UserBlog.createRecord.blog(blog.id.is)).update + + if(owner) S.redirectTo(Site.editBlogLoc.calcHref(blog)) + else S.notice("Congratulations! You have verified your blog!") + } else Noop + + protected def onWritersRadiosChangeSignUp(s: String): JsCmd = if(EmailVar.is.isEmpty) { + writerName.set(s) + Noop + } else Noop +} + +sealed trait VerificationDesigns { + implicit class InputRadioDesignImplicit(choices: ChoiceHolder[String]) { + + def unregisteredWritersChoicesToPictureForm : NodeSeq = { + + for(i <- 0 until choices.items.size) yield { + val choice = choices.items(i) + val user = User.createRecord + + <label onClick="$(this).addClass('selected').siblings().removeClass('selected')"> + {choice.xhtml}<img src={imageUrl(user)} />{choice.key.toString}</label> + } + } + } + + implicit class InputRadioDesign2Implicit(writers: List[BlogWriter]) { + + def registeredWritersChoicesToPictureForm : NodeSeq = { + + for(i <- 0 until writers.size) yield { + val writer = writers(i) + val user = User.findByStringId(writer.user.is.toString).openOr(User.createRecord) + + <label><img src={imageUrl(user)} />{writer.name.is}</label> + } + } + } +} diff --git a/src/main/scala/com/joereader/snippet/signup/SignUp.scala b/src/main/scala/com/joereader/snippet/signup/SignUp.scala @@ -1,28 +0,0 @@ -package com.joereader.snippet - -import net.liftweb.http.RequestVar -import net.liftweb.http.js.JsCmd - - -abstract class SignUp { - - var email = "" - object EmailVar extends RequestVar("") - - var blogId = "" - object BlogIdVar extends RequestVar("") - - var verified = false - object VerifiedVar extends RequestVar(false) - - - /** - * Database storing happens here. - */ - def process(): JsCmd - - /** - * What happens when the wizard page is done. Usually store vars in RequestVars - */ - def continue: JsCmd -} diff --git a/src/main/scala/com/joereader/snippet/signup/SignUpBlog.scala b/src/main/scala/com/joereader/snippet/signup/SignUpBlog.scala @@ -1,58 +0,0 @@ -package com.joereader.snippet - -import scala.xml._ - -import net.liftweb._ -import util._ -import http._ -import SHtml._ -import js._, JsCmds._ -import common._ -import Helpers._ - -import com.joereader._ -import model._ -import config._ - -/** - * Fifth Step: if the user is the owner of the blog, allow him to customize - * the blog page, else continue to step six. - */ -class SignUpBlog extends SignUp { - - blogId = BlogIdVar.is - verified = VerifiedVar.is - - val user: Box[User] = User.currentUser - val blog: Box[Blog] = Blog.findByStringId(blogId) - - var siteUrl = "" - - def render = { - if(!verified || user.isEmpty || blog.isEmpty) - S.redirectTo(Site.notFound.url) - - "#blog-area" #> Templates("templates-hidden" :: "parts" :: "blog-profile" :: Nil).map{ ns => - Site.blogProfileLoc.requestValue(blog) - ns - } & - "#blog-site-url" #> ajaxText(siteUrl, checkSiteUrl(_)) & - "#blog-continue" #> ajaxSubmit("Continue", process) - } - - def process(): JsCmd = continue - - def continue: JsCmd = S.redirectTo(Site.signUp5.url, () => { - VerifiedVar(verified) - }) - - def checkSiteUrl(s: String): JsCmd = - if(!s.matches("^[a-z0-9-]{2,}$")) - S.error("Blog name can only contain alphanumeric or dash characters.") - else if(Blog.findByBlogName(s).isEmpty) { - siteUrl = s - blog.map(_.blogname(s).update) - Noop - } - else S.error("Blog name is taken") -} diff --git a/src/main/scala/com/joereader/snippet/signup/SignUpCategories.scala b/src/main/scala/com/joereader/snippet/signup/SignUpCategories.scala @@ -1,85 +0,0 @@ -package com.joereader.snippet - -import net.liftweb._ -import util._ -import Helpers._ -import http._ -import SHtml._ -import js._, JsCmds._, JE._ -import common._ - -import com.joereader._ -import model._ -import config._ -import snippet.SnipHelpers._ -import lib.Helper._ - -import scala.xml._ - - -/** - * Fourth Step: Set the categories that the user writes for his blog. - */ -class SignUpCategories extends SignUp { - - blogId = BlogIdVar.is - verified = VerifiedVar.is - - val user: Box[User] = User.currentUser - val blog: Box[Blog] = Blog.findByStringId(blogId) - - val userblog: Box[UserBlog] = - for { - user <- user - blog <- blog - } yield user.blog(blog) openOr UserBlog.createRecord.blog(blog.id.is) - - var category = "" - - def render = { - if(!verified || user.isEmpty || blog.isEmpty) - S.redirectTo(Site.notFound.url) - - "#chosen-categories *" #> userblog.map(_.categories.is.map(categoryNode(_))) & - "#category-input" #> text(category, category = _) & - "#category-add" #> ajaxSubmit("Add", ()=> add(category)) & - "#categories-continue" #> ajaxSubmit("Continue", process) - } - - def process(): JsCmd = continue - - def continue: JsCmd = S.redirectTo(Site.signUp4.url, () => { - BlogIdVar(blogId) - VerifiedVar(verified) - }) - - def add(cat: String): JsCmd = { - - for(ub <- userblog; u <- user) { - ub.addCategory(cat) - u.modifyBlog(ub).save // change to update (but wont work with embed docs) - val c = Category.find(cat).map(_.addUser(u).update) - if(!c.isDefined) Category.createRecord.id(cat).addUser(u).save - } - - category = "" - Prepend("chosen-categories", categoryNode(cat)) & - SetValById("category-input","") - } - - def remove(cat: String): JsCmd = { - - for(ub <- userblog; u <- user) { - ub.removeCategory(cat) - u.modifyBlog(ub).save - Category.find(cat).map(_.removeUser(u).update) - } - - Remove("chosen-cat-"+cat) - } - - def categoryNode(cat: String): Elem = - <span id={"chosen-cat-"+cat} class="uneditable-input">{cat}<button onclick={ - ajaxCall(Str(cat), remove _)._2.toJsCmd.toString.singleQuoteToDouble + - "; return false;"} class="close category">×</button></span> -} diff --git a/src/main/scala/com/joereader/snippet/signup/SignUpEmail.scala b/src/main/scala/com/joereader/snippet/signup/SignUpEmail.scala @@ -1,51 +0,0 @@ -package com.joereader.snippet - -import com.joereader._ -import model._ -import lib.Helper._ -import config._ - -import net.liftweb._ -import util.Helpers._ -import http._ -import SHtml._ -import js._ -import JsCmds._ -import common._ - - -/** - * First Step: get user's email. - */ -class SignUpEmail extends SignUp with Logger { - - def render = { - "#email-join" #> ajaxText(email, {s=> email = s; Noop}) & - "#join-btn" #> ajaxSubmit("Join", process) - } - - def process(): JsCmd = { - if(email.isEmail) { - if(BetaUser.find(email).isDefined) - joinedMsg & SetValById("email-join", "") - else if(User.findByEmail(email).isDefined) - invitedMsg & SetValById("email-join", "") - else { - BetaUser.createRecord.id(email).save - continue - } - } - else failureMsg - } - - def continue: JsCmd = S.redirectTo(Site.signUp1.url,() => { - EmailVar(email) - }) - - def invitedMsg: JsCmd = S.notice("You've been invited already silly") - - def joinedMsg: JsCmd = S.notice("You joined Joe Reader a while back. " + - "We'll invite you as soon as we're ready!") - - def failureMsg: JsCmd = S.error("Please enter a valid email.") -} diff --git a/src/main/scala/com/joereader/snippet/signup/SignUpPassword.scala b/src/main/scala/com/joereader/snippet/signup/SignUpPassword.scala @@ -1,43 +0,0 @@ -package com.joereader.snippet - -import net.liftweb._ -import util.Helpers._ -import http._ -import SHtml._ -import js._ - -import com.joereader._ -import config._ -import model._ - -/** - * Seventh Step: Set the user's password. - */ -class SignUpPassword extends SignUp { - - verified = VerifiedVar.is - - val user = User.currentUser - - var pwd = "" - - def render = { - if(!verified || user.isEmpty) S.redirectTo(Site.notFound.url) - - "#user-password" #> password(pwd, pwd = _) & - "#user-continue" #> ajaxSubmit("Continue", process) - } - - def process(): JsCmd = { - user.map{ u => - u.password(pwd) - u.password.hashIt - u.update - } - continue - } - - def continue: JsCmd = S.redirectTo("/", () => { - verified = false - }) -} diff --git a/src/main/scala/com/joereader/snippet/signup/SignUpUser.scala b/src/main/scala/com/joereader/snippet/signup/SignUpUser.scala @@ -1,57 +0,0 @@ -package com.joereader.snippet - -import scala.xml._ - -import net.liftweb._ -import util._ -import http._ -import SHtml._ -import js._, JsCmds._ -import common._ -import Helpers._ - -import com.joereader._ -import model._ -import config._ - -/** - * Sixth Step: Customize the user's page. - */ -class SignUpUser extends SignUp { - - verified = VerifiedVar.is - - val user: Box[User] = User.currentUser - - var siteUrl = "" - - def render = { - if(!verified || user.isEmpty) - S.redirectTo(Site.notFound.url) - - "#user-area" #> Templates("templates-hidden" :: "parts" :: "user-profile" :: Nil).map{ ns => - Site.userProfileLoc.requestValue(user) - ns - } & - "#user-site-url" #> ajaxText(siteUrl, checkSiteUrl(_)) & - "#user-continue" #> ajaxSubmit("Continue", process) - } - - def process(): JsCmd = continue - - def continue: JsCmd = S.redirectTo(Site.signUp6.url, () => { - VerifiedVar(verified) - }) - - def checkSiteUrl(s: String): JsCmd = - if(!s.matches("^[a-z0-9-]{2,}$")) - S.error("User name can only contain alphanumeric or dash characters.") - else if(User.findByUsername(s).isEmpty && - Site.isAvailableMenu(s)) - { - siteUrl = s - user.map(_.username(s).update) - Noop - } - else S.error("Username is taken") -} -\ No newline at end of file diff --git a/src/main/scala/com/joereader/snippet/signup/SignUpVerification.scala b/src/main/scala/com/joereader/snippet/signup/SignUpVerification.scala @@ -1,162 +0,0 @@ -package com.joereader.snippet - -import net.liftweb._ -import util._ -import Helpers._ -import http._ -import SHtml._ -import js._, JsCmds._ -import common._ - -import com.joereader._ -import lib.rss.Rss._ -import snippet.SnipHelpers._ -import config._ -import model._ -import scala.xml._ - -/** - * Third Step: Verify the user's blog. If verified, continue to next step. - */ -class SignUpVerification extends SignUp { - - email = EmailVar.is - - var searched = false - var urlHtml = "" - - val user = User.createRecord - var blog = Blog.createRecord - - val writerName = ValueCell("") - val owner = ValueCell(false) - - val metaName = "joe_reader" - val metaContent = (writerName lift owner)( - (w,o) => user.id.is+":"+w+{if(o)":owner" else ""}) - - val verification = metaContent.lift(metaContent => - s"""<meta name="$metaName" content="$metaContent"/>""") - - def render = { - if(email.isEmpty) S.redirectTo(Site.notFound.url) - - "#verify-state" #> idMemoize(verifyBlog _) - } - - def process(): JsCmd = { - verified = - MetaVerification(metaName, metaContent.get, urlHtml).verified - - if(verified) { - BetaUser.find(email).map(_.delete_!) - - user - .addBlog(UserBlog.createRecord.blog(blog.id.is)) - .email(email) - .password(Helpers.randomString(20)) - .username(StringHelpers.randomString(15)) - user.password.hashIt - user.save - User.logUserIn(user, isAuthed = true, isRemember = true) - - val bw = BlogWriter.createRecord.name(writerName.is).user(user.id.is) - - if(owner) blog.owner(user.id.is) - blog.addWriterSafely(bw) - - if(blog.blogname.is.isEmpty) - blog.blogname(StringHelpers.randomString(15)) - - blog.save - continue - } - else - Show("verification-unsuccessful") - } - - def continue: JsCmd = S.redirectTo(Site.signUp3.url,() => { - BlogIdVar(blog.id.is.toString) - VerifiedVar(verified) - }) - - def verifyBlog(outer: IdMemoizeTransform) = { - - // if idmemoize updated, get urlhtml and find feed - if(urlHtml.nonEmpty) { - val existing = Blog.findByUrl(blog.urlHtml.is) - if(existing.isEmpty) { - val links = urlHtml.rssLinks - val feed: Feed = links.feed - val writers = feed.writers.map(BlogWriter.createRecord.name(_)) - - blog = Blog.createRecord - .urlHtml(urlHtml) - .name(feed.name) - .description(feed.description) - .writers(writers) - .urlRss(feed.links) - } else - existing.map(blog = _) - - searched = true - } - - "#blog-url" #> text(urlHtml, urlHtml = _) & - "#search-blog" #> ajaxSubmit("Search Blog", () => ajaxInvoke(outer.setHtml _)) & - "#writer-list *" #> listWriters & - "#blog-owner-q-yes" #> radios(0) & - "#blog-owner-q-no" #> radios(1) & - "#verification-info [class]" #> hideVerification & - "#blog-owner-q [class]" #> hideOwnerQuestion & - "#verification-info-input" #> WiringUI.asText(verification) & - "#verify-blog" #> ajaxSubmit("Verify", process) - } - - def writersRadios = ajaxRadio[String]( - blog.unregisteredWritersNames, - Full(blog.unregisteredWritersNames headOr ""), - { n => - writerName.set(n) - user.name(n) - Noop - } - ).unregisteredWritersChoicesToPictureForm - - val radios = ajaxRadio[Boolean]( - Seq(true, false), - Full(false), - { bool => - owner.set(bool) - Noop - } - ) - - def registeredUsers = - blog.registeredWriters.registeredWritersChoicesToPictureForm - - def completedMsg = - if(blog.unregisteredWriters.isEmpty && searched) - Text("Looks like all writers have joined") - else NodeSeq.Empty - - def hideVerification: String = { - blog.unregisteredWritersNames.nonEmpty ? "" | "hide" - } - - def hideOwnerQuestion: String = { - val size = blog.unregisteredWritersNames.size - owner.set((size == 1) ? true | false) - (size < 2) ? "hide" | "" - } - - // returns the radio group of writers - def listWriters(): NodeSeq = - if(blog.writersNames.isEmpty && searched) - Text("Writers not found. Double check your url.") - else { - writerName.set(blog.unregisteredWritersNames headOr "") - writersRadios ++ registeredUsers ++ completedMsg - } - -} diff --git a/src/main/scala/com/joereader/snippet/signup/SignUpWriter.scala b/src/main/scala/com/joereader/snippet/signup/SignUpWriter.scala @@ -1,34 +0,0 @@ -package com.joereader.snippet - -import net.liftweb._ -import util.Helpers._ -import http._ -import SHtml._ -import js._ -import JsCmds._ - -import com.joereader.config.Site - - - -/** - * Second Step: Check if he's a blog writer. If so, continue to next steps - * and allow him to setup and customize his blog. - */ -class SignUpWriter extends SignUp { - - email = EmailVar.is - - def render = { - if(email.isEmpty) S.redirectTo(Site.notFound.url) - - "#yes-button" #> ajaxSubmit("Yes", process) & - "#no-button" #> ajaxSubmit("No", () => S.redirectTo("/")) - } - - def process(): JsCmd = continue - - def continue: JsCmd = S.redirectTo(Site.signUp2.url, () => { - EmailVar(email) - }) -} diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html @@ -11,7 +11,7 @@ <span lift="Menu.item?name=About;donthide=true;linktoself=true;a:class=btn primary large">About Joe Reader »</span> </p> - <div data-lift="SignUpEmail"> + <div data-lift="SignUp.setupEmail"> <form data-lift="form.ajax"> Email: <input id="email-join" placeholder="email"> <input id="join-btn" class="btn btn-primary" data-loading-text="Loading..." /> diff --git a/src/main/webapp/settings/account.html b/src/main/webapp/settings/account.html @@ -1,5 +1,25 @@ -<div data-lift="layout?with=settings-wrap;at=content"> - <div class="row"><div class="span12">Name: <span id="user-name"></span></div></div> - <div class="row"><div class="span12">Name: <span id="user-about"></span></div></div> +<div data-lift="surround?with=settings-wrap;at=content"> + <div class="row"><div class="span12">Youtube Vid: <span data-lift="CurrentWriter.introVid" id="intro-vid"></span></div></div> + + <div class="row"><div class="span12">Email: <span data-lift="CurrentReader.email" id="user-email"></span></div></div> + + <div class="row"><div class="span12">Name: <span data-lift="CurrentReader.name" id="user-name"></span></div></div> + <div class="row"><div class="span12">Img Upload: <span data-lift="CurrentReader.uploadImg" id="pic-fileupload-outer"></span></div></div> + <div class="row"><div class="span12">Bg Img Upload: <span data-lift="CurrentWriter.uploadBgImg" id="bg-fileupload-outer"></span></div></div> + <div class="row"><div class="span12">About: <span data-lift="CurrentWriter.about" id="user-about"></span></div></div> + + <div class="row"><div class="span12">Username: <span data-lift="CurrentWriter.username" id="user-username"></span></div></div> + <div class="row"><div class="span12">Other Videos: <span data-lift="CurrentWriter.otherVid" id="user-other-vid"></span></div></div> + + <div class="row"> + <div data-lift="CurrentReader.setPassword" class="span12"> + <form data-lift="form.ajax"> + Old Password: <span id="old-pwd"></span><br> + New Password: <span id="new-pwd"></span><br> + Confirm Password: <span id="new-pwd2"></span><br> + <span id="submit-pwd"></span> + </form> + </div> + </div> </div> diff --git a/src/main/webapp/settings/blog.html b/src/main/webapp/settings/blog.html @@ -0,0 +1,8 @@ +<div lift="surround?with=settings-wrap;at=content"> + + <div class="row"><div class="span12">Name: <span data-lift="ProfileLocBlogEdit.name" id="blog-name" ></span></div></div> + <div class="row"><div class="span12">Blog Name: <span data-lift="ProfileLocBlogEdit.blogname" id="blog-blogname" ></span></div></div> + <div class="row"><div class="span12">Description: <span data-lift="ProfileLocBlogEdit.description" id="blog-about" ></span></div></div> + + <div class="row"><div class="span12">Writers: <span data-lift="ProfileLocBlogEdit.writers" id="blog-writers" ></span></div></div> +</div> +\ No newline at end of file diff --git a/src/main/webapp/settings/blogs.html b/src/main/webapp/settings/blogs.html @@ -0,0 +1,4 @@ +<div lift="surround?with=settings-wrap;at=content"> + <div data-lift="BlogsSnip.blogs"></div> + <a data-lift="BlogsSnip.addBlog" class="btn btn-primary">Add Blog</a> +</div> diff --git a/src/main/webapp/settings/categories.html b/src/main/webapp/settings/categories.html @@ -0,0 +1,3 @@ +<div lift="surround?with=settings-wrap;at=content"> + <span data-lift="embed?what=/templates-hidden/parts/categories"></span> +</div> +\ No newline at end of file diff --git a/src/main/webapp/settings/profile.html b/src/main/webapp/settings/profile.html @@ -1,3 +0,0 @@ -<div lift="layout?with=settings-wrap;at=content"> - <div lift="ProfileScreen?ajax=true"></div> -</div> diff --git a/src/main/webapp/settings/verify.html b/src/main/webapp/settings/verify.html @@ -0,0 +1,3 @@ +<div lift="surround?with=settings-wrap;at=content"> + <span data-lift="embed?what=/templates-hidden/parts/verification"></span> +</div> diff --git a/src/main/webapp/signup/blog.html b/src/main/webapp/signup/blog.html @@ -1,11 +1,11 @@ <div data-lift="surround?with=default;at=content"> - <div data-lift="SignUpBlog"> + <div data-lift="SignUp.setupBlog"> <div lift="Notices"></div> <div class="hero-unit"> <div class="inner"> <form data-lift="form.ajax"> - www.joereader.com/blog/ <input id="blog-site-url" placeholder="Blog Url"><br> + www.joereader.com/blog/ <input data-lift="ProfileLocBlogReq.blogname" id="blog-site-url" placeholder="Blog Url"><br> <input id="blog-continue" class="btn btn-success"> </form> diff --git a/src/main/webapp/signup/categories.html b/src/main/webapp/signup/categories.html @@ -5,23 +5,7 @@ <div class="main-content"> <div lift="Notices"></div> <div class="hero-unit"> - <div data-lift="SignUpCategories" class="inner"> - - <form data-lift="form.ajax"> - <label class="inline"> - <input id="category-input" placeholder="Submit New Category"> - <input id="category-add" class="btn btn-primary"> - </label> - </form> - - <br> - - <label id="chosen-categories" class="inline"></label> - - <form data-lift="form.ajax"> - <input id="categories-continue" class="btn btn-success"> - </form> - </div> + <div id="categories-area"></div> </div> </div> <span data-lift="embed?what=/templates-hidden/parts/footer"></span> diff --git a/src/main/webapp/signup/password.html b/src/main/webapp/signup/password.html @@ -5,10 +5,10 @@ <div class="main-content"> <div lift="Notices"></div> <div class="hero-unit"> - <div data-lift="SignUpPassword" class="inner"> + <div data-lift="SignUp.setupPassword" class="inner"> <form data-lift="form.ajax"> - the password <input id="user-password" placeholder="password"><br> + the password <input data-lift="CurrentReader.password" id="user-password" placeholder="password"><br> <input id="user-continue" class="btn btn-success"> </form> diff --git a/src/main/webapp/signup/user.html b/src/main/webapp/signup/user.html @@ -1,11 +1,11 @@ <div data-lift="surround?with=default;at=content"> - <div data-lift="SignUpUser"> + <div data-lift="SignUp.setupUser"> <div lift="Notices"></div> <div class="hero-unit"> <div class="inner"> <form data-lift="form.ajax"> - www.joereader.com/ <input id="user-site-url" placeholder="Your Url"><br> + www.joereader.com/ <input data-lift="CurrentWriter.username" id="user-site-url" placeholder="Your Url"><br> <input id="user-continue" class="btn btn-success"> </form> diff --git a/src/main/webapp/signup/verify.html b/src/main/webapp/signup/verify.html @@ -4,37 +4,10 @@ </lift:head> <div class="main-content"> <div lift="Notices"></div> - <div data-lift="SignUpVerification" class="hero-unit"> - <div id="verify-state" class="inner"> - <h2>Verify your Blog</h2> - <form data-lift="form.ajax"> - Blog Url: <input id="blog-url" placeholder="ie. www.example.com/blog"> - <span data-alertid="blog-url_err" class="notice-block"></span> - <br> - <input id="search-blog" data-loading-text="Loading..."> - </form> + <div data-lift="SignUp.setupVerification"></div> + <span data-lift="embed?what=/templates-hidden/parts/verification"></span> - Select yourself as a writer for your blog. If there's more than one writer, answer if you're an owner. - <div id="writer-list"></div><br> - - <div id="blog-owner-q" class="hide"> - Are you the owner of the blog? - <input id="blog-owner-q-yes" class="radio" /> <span>Yes</span> - <input id="blog-owner-q-no" class="radio" /> <span>No</span> - </div> - - <div id="verification-info" class="hide"> - <form data-lift="form.ajax"> - <div id="verification-info-input"></div> - <p>Copy and paste this meta tag to the <head> of your index.html file or equivalent.</p><br> - <input id="verify-blog"> - </form> - <div id="verification-unsuccessful" class="hide">Verification was unsuccessful. Double check if your <head> contains the meta content above.</div> - </div> - - </div> - </div> </div> <span data-lift="embed?what=/templates-hidden/parts/footer"></span> </div> \ No newline at end of file diff --git a/src/main/webapp/signup/writer.html b/src/main/webapp/signup/writer.html @@ -5,7 +5,7 @@ <div class="main-content"> <div lift="Notices"></div> <div class="hero-unit"> - <div data-lift="SignUpWriter" class="inner"> + <div data-lift="SignUp.setupWriter" class="inner"> <h2>Thank you for joining!</h2> <p>Are you a blog writer?</p> <form data-lift="form.ajax"> diff --git a/src/main/webapp/templates-hidden/parts/categories.html b/src/main/webapp/templates-hidden/parts/categories.html @@ -0,0 +1,17 @@ +<div data-lift="CategoriesSnip" class="inner"> + + <form data-lift="form.ajax"> + <label class="inline"> + <input id="category-input" placeholder="Submit New Category"> + <input id="category-add" class="btn btn-primary"> + </label> + </form> + + <br> + + <label id="chosen-categories" class="inline"></label> + + <form data-lift="form.ajax"> + <input id="categories-continue" class="btn btn-success"> + </form> +</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 @@ -1,15 +1,15 @@ <div> - <div class="row"><div class="span12">Name: <span data-lift="ProfileLocUser.name" id="user-name" ></span></div></div> - <div class="row"><div class="span12">About: <span data-lift="ProfileLocUser.about" id="user-about" ></span></div></div> - <div class="row"><div class="span12">Youtube Vid: <span data-lift="ProfileLocUser.introVid" id="intro-vid" ></span></div></div> + <div class="row"><div class="span12">Name: <span data-lift="ProfileLocReader.name" id="user-name" ></span></div></div> + <div class="row"><div class="span12">About: <span data-lift="ProfileLocWriter.about" id="user-about" ></span></div></div> + <div class="row"><div class="span12">Youtube Vid: <span data-lift="ProfileLocWriter.introVid" id="intro-vid" ></span></div></div> - <div class="row"><div class="span12">Img: <img data-lift="ProfileLocUser.img" id="user-img"></div></div> - <div class="row"><div class="span12">Bg Img: <img data-lift="ProfileLocUser.bgImg" id="user-bgimg"></div></div> + <div class="row"><div class="span12">Img: <img data-lift="ProfileLocReader.img" id="user-img"></div></div> + <div class="row"><div class="span12">Bg Img: <img data-lift="ProfileLocWriter.bgImg" id="user-bgimg"></div></div> - <div class="row"><div class="span12"><span data-lift="ProfileLocUser.uploadBgImg" id="bg-fileupload-outer" ></span></div></div> - <div class="row"><div class="span12"><span data-lift="ProfileLocUser.uploadBgImg" id="pic-fileupload-outer" ></span></div></div> + <div class="row"><div class="span12"><span data-lift="ProfileLocWriter.uploadBgImg" id="bg-fileupload-outer" ></span></div></div> + <div class="row"><div class="span12"><span data-lift="ProfileLocReader.uploadImg" id="pic-fileupload-outer" ></span></div></div> - <div class="row"><div class="span12">Articles: <br><span data-lift="ProfileLocUser.articles" id="user-articles" ></span></div></div> + <div class="row"><div class="span12">Articles: <br><span data-lift="ProfileLocWriter.articles" id="user-articles" ></span></div></div> </div> \ No newline at end of file diff --git a/src/main/webapp/templates-hidden/parts/verification.html b/src/main/webapp/templates-hidden/parts/verification.html @@ -0,0 +1,30 @@ +<div data-lift="Verification"> + <div id="verify-state" class="inner"> + + <h2>Verify your Blog</h2> + <form data-lift="form.ajax"> + Blog Url: <input id="blog-url" placeholder="ie. www.example.com/blog"> + <span data-alertid="blog-url_err" class="notice-block"></span> + <br> + <input id="search-blog" data-loading-text="Loading..."> + </form> + + Select yourself as a writer for your blog. If there's more than one writer, answer if you're an owner. + <div id="writer-list"></div><br> + + <div id="blog-owner-q" class="hide"> + Are you the owner of the blog? + <input id="blog-owner-q-yes" class="radio" /> <span>Yes</span> + <input id="blog-owner-q-no" class="radio" /> <span>No</span> + </div> + + <div id="verification-info" class="hide"> + <form data-lift="form.ajax"> + <div id="verification-info-input"></div> + <p>Copy and paste this meta tag to the <head> of your index.html file or equivalent.</p><br> + <input id="verify-blog"> + </form> + </div> + + </div> +</div> +\ No newline at end of file diff --git a/src/main/webapp/templates-hidden/settings-wrap.html b/src/main/webapp/templates-hidden/settings-wrap.html @@ -1,6 +1,7 @@ <div data-lift="surround?with=default;at=content"> <div class="row"> <div class="span12"> + <div data-lift="Notices"></div> <ul data-lift="Menus.group?group=settings" class="nav nav-tabs"></ul> </div> <div class="span10">