User logon and list fixes
User logon now works; tweaked queries and display items on post, page, and category list pages
This commit is contained in:
parent
197a19d339
commit
7c99da8cb5
@ -32,18 +32,11 @@ let getAllCategories conn (webLogId : string) =
|
|||||||
r.Table(Table.Category)
|
r.Table(Table.Category)
|
||||||
.GetAll(webLogId).OptArg("index", "webLogId")
|
.GetAll(webLogId).OptArg("index", "webLogId")
|
||||||
.OrderBy("name")
|
.OrderBy("name")
|
||||||
.RunCursorAsync<Category>(conn)
|
.RunListAsync<Category>(conn)
|
||||||
|> await
|
|> await
|
||||||
|> Seq.toList
|
|> Seq.toList
|
||||||
|> sortCategories
|
|> sortCategories
|
||||||
|
|
||||||
/// Count categories for a web log
|
|
||||||
let countCategories conn (webLogId : string) =
|
|
||||||
r.Table(Table.Category)
|
|
||||||
.GetAll(webLogId).OptArg("index", "webLogId")
|
|
||||||
.Count()
|
|
||||||
.RunAtomAsync<int>(conn) |> await
|
|
||||||
|
|
||||||
/// Get a specific category by its Id
|
/// Get a specific category by its Id
|
||||||
let tryFindCategory conn webLogId catId : Category option =
|
let tryFindCategory conn webLogId catId : Category option =
|
||||||
match (category webLogId catId)
|
match (category webLogId catId)
|
||||||
|
@ -37,20 +37,13 @@ let tryFindPageByPermalink conn (webLogId : string) (permalink : string) =
|
|||||||
|> await
|
|> await
|
||||||
|> Seq.tryHead
|
|> Seq.tryHead
|
||||||
|
|
||||||
/// Count pages for a web log
|
|
||||||
let countPages conn (webLogId : string) =
|
|
||||||
r.Table(Table.Page)
|
|
||||||
.GetAll(webLogId).OptArg("index", "webLogId")
|
|
||||||
.Count()
|
|
||||||
.RunAtomAsync<int>(conn) |> await
|
|
||||||
|
|
||||||
/// Get a list of all pages (excludes page text and revisions)
|
/// Get a list of all pages (excludes page text and revisions)
|
||||||
let findAllPages conn (webLogId : string) =
|
let findAllPages conn (webLogId : string) =
|
||||||
r.Table(Table.Page)
|
r.Table(Table.Page)
|
||||||
.GetAll(webLogId)
|
.GetAll(webLogId).OptArg("index", "webLogId")
|
||||||
.OrderBy("title")
|
.OrderBy("title")
|
||||||
.Without("text", "revisions")
|
.Without("text", "revisions")
|
||||||
.RunCursorAsync<Page>(conn)
|
.RunListAsync<Page>(conn)
|
||||||
|> await
|
|> await
|
||||||
|> Seq.toList
|
|> Seq.toList
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ let private toPostList conn pageNbr nbrPerPage (filter : ReqlExpr) =
|
|||||||
/// Shorthand to get a newer or older post
|
/// Shorthand to get a newer or older post
|
||||||
// TODO: older posts need to sort by published on DESC
|
// TODO: older posts need to sort by published on DESC
|
||||||
//let private adjacentPost conn post (theFilter : ReqlExpr -> ReqlExpr) (sort :ReqlExpr) : Post option =
|
//let private adjacentPost conn post (theFilter : ReqlExpr -> ReqlExpr) (sort :ReqlExpr) : Post option =
|
||||||
let private adjacentPost conn post (theFilter : obj) (sort : obj) : Post option =
|
let private adjacentPost conn post (theFilter : ReqlExpr -> obj) (sort : obj) : Post option =
|
||||||
(publishedPosts post.webLogId)
|
(publishedPosts post.webLogId)
|
||||||
.Filter(theFilter)
|
.Filter(theFilter)
|
||||||
.OrderBy(sort)
|
.OrderBy(sort)
|
||||||
@ -58,36 +58,37 @@ let findPageOfTaggedPosts conn webLogId (tag : string) pageNbr nbrPerPage =
|
|||||||
|> toPostList conn pageNbr nbrPerPage
|
|> toPostList conn pageNbr nbrPerPage
|
||||||
|
|
||||||
/// Try to get the next newest post from the given post
|
/// Try to get the next newest post from the given post
|
||||||
let tryFindNewerPost conn post = newerPost conn post (fun p -> (p :> ReqlExpr).["publishedOn"].Gt(post.publishedOn))
|
let tryFindNewerPost conn post = newerPost conn post (fun p -> upcast p.["publishedOn"].Gt(post.publishedOn))
|
||||||
|
|
||||||
/// Try to get the next newest post assigned to the given category
|
/// Try to get the next newest post assigned to the given category
|
||||||
let tryFindNewerCategorizedPost conn (categoryId : string) post =
|
let tryFindNewerCategorizedPost conn (categoryId : string) post =
|
||||||
newerPost conn post (fun p -> (p :> ReqlExpr).["publishedOn"].Gt(post.publishedOn)
|
newerPost conn post (fun p -> upcast p.["publishedOn"].Gt(post.publishedOn)
|
||||||
.And(p.["categoryIds"].Contains(categoryId)))
|
.And(p.["categoryIds"].Contains(categoryId)))
|
||||||
|
|
||||||
/// Try to get the next newest tagged post from the given tagged post
|
/// Try to get the next newest tagged post from the given tagged post
|
||||||
let tryFindNewerTaggedPost conn (tag : string) post =
|
let tryFindNewerTaggedPost conn (tag : string) post =
|
||||||
newerPost conn post (fun p -> (p :> ReqlExpr).["publishedOn"].Gt(post.publishedOn).And(p.["tags"].Contains(tag)))
|
newerPost conn post (fun p -> upcast p.["publishedOn"].Gt(post.publishedOn).And(p.["tags"].Contains(tag)))
|
||||||
|
|
||||||
/// Try to get the next oldest post from the given post
|
/// Try to get the next oldest post from the given post
|
||||||
let tryFindOlderPost conn post = olderPost conn post (fun p -> (p :> ReqlExpr).["publishedOn"].Lt(post.publishedOn))
|
let tryFindOlderPost conn post = olderPost conn post (fun p -> upcast p.["publishedOn"].Lt(post.publishedOn))
|
||||||
|
|
||||||
/// Try to get the next oldest post assigned to the given category
|
/// Try to get the next oldest post assigned to the given category
|
||||||
let tryFindOlderCategorizedPost conn (categoryId : string) post =
|
let tryFindOlderCategorizedPost conn (categoryId : string) post =
|
||||||
olderPost conn post (fun p -> (p :> ReqlExpr).["publishedOn"].Lt(post.publishedOn)
|
olderPost conn post (fun p -> upcast p.["publishedOn"].Lt(post.publishedOn)
|
||||||
.And(p.["categoryIds"].Contains(categoryId)))
|
.And(p.["categoryIds"].Contains(categoryId)))
|
||||||
|
|
||||||
/// Try to get the next oldest tagged post from the given tagged post
|
/// Try to get the next oldest tagged post from the given tagged post
|
||||||
let tryFindOlderTaggedPost conn (tag : string) post =
|
let tryFindOlderTaggedPost conn (tag : string) post =
|
||||||
olderPost conn post (fun p -> (p :> ReqlExpr).["publishedOn"].Lt(post.publishedOn).And(p.["tags"].Contains(tag)))
|
olderPost conn post (fun p -> upcast p.["publishedOn"].Lt(post.publishedOn).And(p.["tags"].Contains(tag)))
|
||||||
|
|
||||||
/// Get a page of all posts in all statuses
|
/// Get a page of all posts in all statuses
|
||||||
let findPageOfAllPosts conn (webLogId : string) pageNbr nbrPerPage =
|
let findPageOfAllPosts conn (webLogId : string) pageNbr nbrPerPage =
|
||||||
|
// FIXME: sort unpublished posts by their last updated date
|
||||||
r.Table(Table.Post)
|
r.Table(Table.Post)
|
||||||
.GetAll(webLogId).OptArg("index", "webLogId")
|
.GetAll(webLogId).OptArg("index", "webLogId")
|
||||||
.OrderBy(fun p -> r.Desc(r.Branch(p.["publishedOn"].Eq(int64 0), p.["lastUpdatedOn"], p.["publishedOn"])))
|
.OrderBy(r.Desc("publishedOn"))
|
||||||
.Slice((pageNbr - 1) * nbrPerPage, pageNbr * nbrPerPage)
|
.Slice((pageNbr - 1) * nbrPerPage, pageNbr * nbrPerPage)
|
||||||
.RunCursorAsync<Post>(conn)
|
.RunListAsync<Post>(conn)
|
||||||
|> await
|
|> await
|
||||||
|> Seq.toList
|
|> Seq.toList
|
||||||
|
|
||||||
@ -107,15 +108,15 @@ let tryFindPostByPermalink conn webLogId permalink =
|
|||||||
.GetAll(r.Array(webLogId, permalink)).OptArg("index", "permalink")
|
.GetAll(r.Array(webLogId, permalink)).OptArg("index", "permalink")
|
||||||
.Filter(fun p -> p.["status"].Eq(PostStatus.Published))
|
.Filter(fun p -> p.["status"].Eq(PostStatus.Published))
|
||||||
.Without("revisions")
|
.Without("revisions")
|
||||||
.Merge(fun post -> ExpandoObject()?categories <-
|
.Merge(fun post -> r.HashMap("categories",
|
||||||
post.["categoryIds"]
|
post.["categoryIds"]
|
||||||
.Map(ReqlFunction1(fun cat -> upcast r.Table(Table.Category).Get(cat).Without("children")))
|
.Map(ReqlFunction1(fun cat -> upcast r.Table(Table.Category).Get(cat).Without("children")))
|
||||||
.CoerceTo("array"))
|
.CoerceTo("array")))
|
||||||
.Merge(fun post -> ExpandoObject()?comments <-
|
.Merge(fun post -> r.HashMap("comments",
|
||||||
r.Table(Table.Comment)
|
r.Table(Table.Comment)
|
||||||
.GetAll(post.["id"]).OptArg("index", "postId")
|
.GetAll(post.["id"]).OptArg("index", "postId")
|
||||||
.OrderBy("postedOn")
|
.OrderBy("postedOn")
|
||||||
.CoerceTo("array"))
|
.CoerceTo("array")))
|
||||||
.RunCursorAsync<Post>(conn)
|
.RunCursorAsync<Post>(conn)
|
||||||
|> await
|
|> await
|
||||||
|> Seq.tryHead
|
|> Seq.tryHead
|
||||||
@ -145,11 +146,3 @@ let savePost conn post =
|
|||||||
.RunResultAsync(conn)
|
.RunResultAsync(conn)
|
||||||
|> ignore
|
|> ignore
|
||||||
post.id
|
post.id
|
||||||
|
|
||||||
/// Count posts for a web log
|
|
||||||
let countPosts conn (webLogId : string) =
|
|
||||||
r.Table(Table.Post)
|
|
||||||
.GetAll(webLogId).OptArg("index", "webLogId")
|
|
||||||
.Count()
|
|
||||||
.RunAtomAsync<int>(conn)
|
|
||||||
|> await
|
|
||||||
|
@ -80,7 +80,7 @@ let checkIndexes cfg =
|
|||||||
"webLogAndStatus", webLogField "status"
|
"webLogAndStatus", webLogField "status"
|
||||||
"permalink", webLogField "permalink"
|
"permalink", webLogField "permalink"
|
||||||
]
|
]
|
||||||
Table.User, [ "logOn", Some <| fun row -> upcast r.Array(row.["userName"], row.["passwordHash"])
|
Table.User, [ "userName", None
|
||||||
]
|
]
|
||||||
Table.WebLog, [ "urlBase", None
|
Table.WebLog, [ "urlBase", None
|
||||||
]
|
]
|
||||||
|
@ -6,10 +6,13 @@ open Rethink
|
|||||||
let private r = RethinkDb.Driver.RethinkDB.R
|
let private r = RethinkDb.Driver.RethinkDB.R
|
||||||
|
|
||||||
/// Log on a user
|
/// Log on a user
|
||||||
// FIXME: the password hash may be longer than the significant size of a RethinkDB index
|
// NOTE: The significant length of a RethinkDB index is 238 - [PK size]; as we're storing 1,024 characters of password,
|
||||||
|
// including it in an index does not get any performance gain, and would unnecessarily bloat the index. See
|
||||||
|
// http://rethinkdb.com/docs/secondary-indexes/java/ for more information.
|
||||||
let tryUserLogOn conn (email : string) (passwordHash : string) =
|
let tryUserLogOn conn (email : string) (passwordHash : string) =
|
||||||
r.Table(Table.User)
|
r.Table(Table.User)
|
||||||
.GetAll(email, passwordHash).OptArg("index", "logOn")
|
.GetAll(email).OptArg("index", "userName")
|
||||||
|
.Filter(fun u -> u.["passwordHash"].Eq(passwordHash))
|
||||||
.RunCursorAsync<User>(conn)
|
.RunCursorAsync<User>(conn)
|
||||||
|> await
|
|> await
|
||||||
|> Seq.tryHead
|
|> Seq.tryHead
|
||||||
|
@ -1,12 +1,20 @@
|
|||||||
module myWebLog.Data.WebLog
|
module myWebLog.Data.WebLog
|
||||||
|
|
||||||
open FSharp.Interop.Dynamic
|
|
||||||
open myWebLog.Entities
|
open myWebLog.Entities
|
||||||
open Rethink
|
open Rethink
|
||||||
open System.Dynamic
|
|
||||||
|
|
||||||
let private r = RethinkDb.Driver.RethinkDB.R
|
let private r = RethinkDb.Driver.RethinkDB.R
|
||||||
|
|
||||||
|
/// Counts of items displayed on the admin dashboard
|
||||||
|
type DashboardCounts = {
|
||||||
|
/// The number of pages for the web log
|
||||||
|
pages : int
|
||||||
|
/// The number of pages for the web log
|
||||||
|
posts : int
|
||||||
|
/// The number of categories for the web log
|
||||||
|
categories : int
|
||||||
|
}
|
||||||
|
|
||||||
/// Detemine the web log by the URL base
|
/// Detemine the web log by the URL base
|
||||||
// TODO: see if we can make .Merge work for page list even though the attribute is ignored
|
// TODO: see if we can make .Merge work for page list even though the attribute is ignored
|
||||||
// (needs to be ignored for serialization, but included for deserialization)
|
// (needs to be ignored for serialization, but included for deserialization)
|
||||||
@ -24,3 +32,12 @@ let tryFindWebLogByUrlBase conn (urlBase : string) =
|
|||||||
.Pluck("title", "permalink")
|
.Pluck("title", "permalink")
|
||||||
.RunListAsync<PageListEntry>(conn) |> await |> Seq.toList }
|
.RunListAsync<PageListEntry>(conn) |> await |> Seq.toList }
|
||||||
| None -> None
|
| None -> None
|
||||||
|
|
||||||
|
/// Get counts for the admin dashboard
|
||||||
|
let findDashboardCounts conn (webLogId : string) =
|
||||||
|
r.Expr( r.HashMap("pages", r.Table(Table.Page ).GetAll(webLogId).OptArg("index", "webLogId").Count()))
|
||||||
|
.Merge(r.HashMap("posts", r.Table(Table.Post ).GetAll(webLogId).OptArg("index", "webLogId").Count()))
|
||||||
|
.Merge(r.HashMap("categories", r.Table(Table.Category).GetAll(webLogId).OptArg("index", "webLogId").Count()))
|
||||||
|
.RunAtomAsync<DashboardCounts>(conn)
|
||||||
|
|> await
|
||||||
|
|
36
src/myWebLog.Resources/Resources.Designer.cs
generated
36
src/myWebLog.Resources/Resources.Designer.cs
generated
@ -123,6 +123,15 @@ namespace myWebLog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to and {0} more....
|
||||||
|
/// </summary>
|
||||||
|
public static string andXMore {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("andXMore", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Categories.
|
/// Looks up a localized string similar to Categories.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -150,6 +159,15 @@ namespace myWebLog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Close.
|
||||||
|
/// </summary>
|
||||||
|
public static string Close {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Close", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Dashboard.
|
/// Looks up a localized string similar to Dashboard.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -249,6 +267,15 @@ namespace myWebLog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Error.
|
||||||
|
/// </summary>
|
||||||
|
public static string Error {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Error", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Last Updated.
|
/// Looks up a localized string similar to Last Updated.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -626,5 +653,14 @@ namespace myWebLog {
|
|||||||
return ResourceManager.GetString("View", resourceCulture);
|
return ResourceManager.GetString("View", resourceCulture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Warning.
|
||||||
|
/// </summary>
|
||||||
|
public static string Warning {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Warning", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -306,4 +306,16 @@
|
|||||||
<data name="ShowInPageList" xml:space="preserve">
|
<data name="ShowInPageList" xml:space="preserve">
|
||||||
<value>Show in Page List</value>
|
<value>Show in Page List</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="andXMore" xml:space="preserve">
|
||||||
|
<value>and {0} more...</value>
|
||||||
|
</data>
|
||||||
|
<data name="Close" xml:space="preserve">
|
||||||
|
<value>Close</value>
|
||||||
|
</data>
|
||||||
|
<data name="Error" xml:space="preserve">
|
||||||
|
<value>Error</value>
|
||||||
|
</data>
|
||||||
|
<data name="Warning" xml:space="preserve">
|
||||||
|
<value>Warning</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
@ -1,8 +1,6 @@
|
|||||||
namespace myWebLog
|
namespace myWebLog
|
||||||
|
|
||||||
open myWebLog.Data.Category
|
open myWebLog.Data.WebLog
|
||||||
open myWebLog.Data.Page
|
|
||||||
open myWebLog.Data.Post
|
|
||||||
open myWebLog.Entities
|
open myWebLog.Entities
|
||||||
open Nancy
|
open Nancy
|
||||||
open RethinkDb.Driver.Net
|
open RethinkDb.Driver.Net
|
||||||
@ -17,9 +15,6 @@ type AdminModule(conn : IConnection) as this =
|
|||||||
/// Admin dashboard
|
/// Admin dashboard
|
||||||
member this.Dashboard () =
|
member this.Dashboard () =
|
||||||
this.RequiresAccessLevel AuthorizationLevel.Administrator
|
this.RequiresAccessLevel AuthorizationLevel.Administrator
|
||||||
let model = DashboardModel(this.Context, this.WebLog)
|
let model = DashboardModel(this.Context, this.WebLog, findDashboardCounts conn this.WebLog.id)
|
||||||
model.posts <- countPosts conn this.WebLog.id
|
|
||||||
model.pages <- countPages conn this.WebLog.id
|
|
||||||
model.categories <- countCategories conn this.WebLog.id
|
|
||||||
model.pageTitle <- Resources.Dashboard
|
model.pageTitle <- Resources.Dashboard
|
||||||
this.View.["admin/dashboard", model]
|
this.View.["admin/dashboard", model]
|
||||||
|
@ -18,7 +18,10 @@ type PostModule(conn : IConnection, clock : IClock) as this =
|
|||||||
|
|
||||||
/// Get the page number from the dictionary
|
/// Get the page number from the dictionary
|
||||||
let getPage (parameters : DynamicDictionary) =
|
let getPage (parameters : DynamicDictionary) =
|
||||||
match parameters.ContainsKey "page" with | true -> downcast parameters.["page"] | _ -> 1
|
match parameters.ContainsKey "page" with | true -> System.Int32.Parse (parameters.["page"].ToString ()) | _ -> 1
|
||||||
|
|
||||||
|
/// Convert a list of posts to a list of posts for display
|
||||||
|
let forDisplay posts = posts |> List.map (fun post -> PostForDisplay(this.WebLog, post))
|
||||||
|
|
||||||
do
|
do
|
||||||
this.Get .["/" ] <- fun _ -> upcast this.HomePage ()
|
this.Get .["/" ] <- fun _ -> upcast this.HomePage ()
|
||||||
@ -39,13 +42,15 @@ type PostModule(conn : IConnection, clock : IClock) as this =
|
|||||||
member this.PublishedPostsPage pageNbr =
|
member this.PublishedPostsPage pageNbr =
|
||||||
let model = PostsModel(this.Context, this.WebLog)
|
let model = PostsModel(this.Context, this.WebLog)
|
||||||
model.pageNbr <- pageNbr
|
model.pageNbr <- pageNbr
|
||||||
model.posts <- findPageOfPublishedPosts conn this.WebLog.id pageNbr 10
|
model.posts <- findPageOfPublishedPosts conn this.WebLog.id pageNbr 10 |> forDisplay
|
||||||
model.hasNewer <- match List.isEmpty model.posts with
|
model.hasNewer <- match pageNbr with
|
||||||
| true -> false
|
| 1 -> false
|
||||||
| _ -> Option.isSome <| tryFindNewerPost conn (List.last model.posts)
|
| _ -> match List.isEmpty model.posts with
|
||||||
|
| true -> false
|
||||||
|
| _ -> Option.isSome <| tryFindNewerPost conn (List.last model.posts).post
|
||||||
model.hasOlder <- match List.isEmpty model.posts with
|
model.hasOlder <- match List.isEmpty model.posts with
|
||||||
| true -> false
|
| true -> false
|
||||||
| _ -> Option.isSome <| tryFindOlderPost conn (List.head model.posts)
|
| _ -> Option.isSome <| tryFindOlderPost conn (List.head model.posts).post
|
||||||
model.urlPrefix <- "/posts"
|
model.urlPrefix <- "/posts"
|
||||||
model.pageTitle <- match pageNbr with
|
model.pageTitle <- match pageNbr with
|
||||||
| 1 -> ""
|
| 1 -> ""
|
||||||
@ -93,15 +98,15 @@ type PostModule(conn : IConnection, clock : IClock) as this =
|
|||||||
| Some cat -> let pageNbr = getPage parameters
|
| Some cat -> let pageNbr = getPage parameters
|
||||||
let model = PostsModel(this.Context, this.WebLog)
|
let model = PostsModel(this.Context, this.WebLog)
|
||||||
model.pageNbr <- pageNbr
|
model.pageNbr <- pageNbr
|
||||||
model.posts <- findPageOfCategorizedPosts conn this.WebLog.id cat.id pageNbr 10
|
model.posts <- findPageOfCategorizedPosts conn this.WebLog.id cat.id pageNbr 10 |> forDisplay
|
||||||
model.hasNewer <- match List.isEmpty model.posts with
|
model.hasNewer <- match List.isEmpty model.posts with
|
||||||
| true -> false
|
| true -> false
|
||||||
| _ -> Option.isSome <| tryFindNewerCategorizedPost conn cat.id
|
| _ -> Option.isSome <| tryFindNewerCategorizedPost conn cat.id
|
||||||
(List.last model.posts)
|
(List.last model.posts).post
|
||||||
model.hasOlder <- match List.isEmpty model.posts with
|
model.hasOlder <- match List.isEmpty model.posts with
|
||||||
| true -> false
|
| true -> false
|
||||||
| _ -> Option.isSome <| tryFindOlderCategorizedPost conn cat.id
|
| _ -> Option.isSome <| tryFindOlderCategorizedPost conn cat.id
|
||||||
(List.last model.posts)
|
(List.last model.posts).post
|
||||||
model.urlPrefix <- sprintf "/category/%s" slug
|
model.urlPrefix <- sprintf "/category/%s" slug
|
||||||
model.pageTitle <- sprintf "\"%s\" Category%s" cat.name
|
model.pageTitle <- sprintf "\"%s\" Category%s" cat.name
|
||||||
(match pageNbr with | 1 -> "" | n -> sprintf " | Page %i" n)
|
(match pageNbr with | 1 -> "" | n -> sprintf " | Page %i" n)
|
||||||
@ -117,13 +122,13 @@ type PostModule(conn : IConnection, clock : IClock) as this =
|
|||||||
let pageNbr = getPage parameters
|
let pageNbr = getPage parameters
|
||||||
let model = PostsModel(this.Context, this.WebLog)
|
let model = PostsModel(this.Context, this.WebLog)
|
||||||
model.pageNbr <- pageNbr
|
model.pageNbr <- pageNbr
|
||||||
model.posts <- findPageOfTaggedPosts conn this.WebLog.id tag pageNbr 10
|
model.posts <- findPageOfTaggedPosts conn this.WebLog.id tag pageNbr 10 |> forDisplay
|
||||||
model.hasNewer <- match List.isEmpty model.posts with
|
model.hasNewer <- match List.isEmpty model.posts with
|
||||||
| true -> false
|
| true -> false
|
||||||
| _ -> Option.isSome <| tryFindNewerTaggedPost conn tag (List.last model.posts)
|
| _ -> Option.isSome <| tryFindNewerTaggedPost conn tag (List.last model.posts).post
|
||||||
model.hasOlder <- match List.isEmpty model.posts with
|
model.hasOlder <- match List.isEmpty model.posts with
|
||||||
| true -> false
|
| true -> false
|
||||||
| _ -> Option.isSome <| tryFindOlderTaggedPost conn tag (List.last model.posts)
|
| _ -> Option.isSome <| tryFindOlderTaggedPost conn tag (List.last model.posts).post
|
||||||
model.urlPrefix <- sprintf "/tag/%s" tag
|
model.urlPrefix <- sprintf "/tag/%s" tag
|
||||||
model.pageTitle <- sprintf "\"%s\" Tag%s" tag (match pageNbr with | 1 -> "" | n -> sprintf " | Page %i" n)
|
model.pageTitle <- sprintf "\"%s\" Tag%s" tag (match pageNbr with | 1 -> "" | n -> sprintf " | Page %i" n)
|
||||||
model.subtitle <- Some <| sprintf "Posts tagged \"%s\"" tag
|
model.subtitle <- Some <| sprintf "Posts tagged \"%s\"" tag
|
||||||
@ -137,10 +142,10 @@ type PostModule(conn : IConnection, clock : IClock) as this =
|
|||||||
this.RequiresAccessLevel AuthorizationLevel.Administrator
|
this.RequiresAccessLevel AuthorizationLevel.Administrator
|
||||||
let model = PostsModel(this.Context, this.WebLog)
|
let model = PostsModel(this.Context, this.WebLog)
|
||||||
model.pageNbr <- pageNbr
|
model.pageNbr <- pageNbr
|
||||||
model.posts <- findPageOfAllPosts conn this.WebLog.id pageNbr 25
|
model.posts <- findPageOfAllPosts conn this.WebLog.id pageNbr 25 |> forDisplay
|
||||||
model.hasNewer <- pageNbr > 1
|
model.hasNewer <- pageNbr > 1
|
||||||
model.hasOlder <- List.length model.posts < 25
|
model.hasOlder <- List.length model.posts > 24
|
||||||
model.urlPrefix <- "/post/list"
|
model.urlPrefix <- "/posts/list"
|
||||||
model.pageTitle <- Resources.Posts
|
model.pageTitle <- Resources.Posts
|
||||||
this.View.["admin/post/list", model]
|
this.View.["admin/post/list", model]
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ type UserModule(conn : IConnection) as this =
|
|||||||
/// Hash the user's password
|
/// Hash the user's password
|
||||||
let pbkdf2 (pw : string) =
|
let pbkdf2 (pw : string) =
|
||||||
PassphraseKeyGenerator(pw, UTF8Encoding().GetBytes("// TODO: make this salt part of the config"), 4096).GetBytes 512
|
PassphraseKeyGenerator(pw, UTF8Encoding().GetBytes("// TODO: make this salt part of the config"), 4096).GetBytes 512
|
||||||
|> Seq.fold (fun acc bit -> System.String.Format("{0}{1:x2}", acc, bit)) ""
|
|> Seq.fold (fun acc byt -> sprintf "%s%s" acc (byt.ToString "x2")) ""
|
||||||
|
|
||||||
do
|
do
|
||||||
this.Get .["/logon" ] <- fun parms -> upcast this.ShowLogOn (downcast parms)
|
this.Get .["/logon" ] <- fun parms -> upcast this.ShowLogOn (downcast parms)
|
||||||
@ -28,16 +28,17 @@ type UserModule(conn : IConnection) as this =
|
|||||||
/// Show the log on page
|
/// Show the log on page
|
||||||
member this.ShowLogOn (parameters : DynamicDictionary) =
|
member this.ShowLogOn (parameters : DynamicDictionary) =
|
||||||
let model = LogOnModel(this.Context, this.WebLog)
|
let model = LogOnModel(this.Context, this.WebLog)
|
||||||
model.returnUrl <- match parameters.ContainsKey "returnUrl" with
|
model.form.returnUrl <- match parameters.ContainsKey "returnUrl" with
|
||||||
| true -> parameters.["returnUrl"].ToString ()
|
| true -> parameters.["returnUrl"].ToString ()
|
||||||
| _ -> ""
|
| _ -> ""
|
||||||
this.View.["admin/user/logon", model]
|
this.View.["admin/user/logon", model]
|
||||||
|
|
||||||
/// Process a user log on
|
/// Process a user log on
|
||||||
member this.DoLogOn (parameters : DynamicDictionary) =
|
member this.DoLogOn (parameters : DynamicDictionary) =
|
||||||
this.ValidateCsrfToken ()
|
this.ValidateCsrfToken ()
|
||||||
let model = this.Bind<LogOnModel> ()
|
let form = this.Bind<LogOnForm> ()
|
||||||
match tryUserLogOn conn model.email (pbkdf2 model.password) with
|
let model = MyWebLogModel(this.Context, this.WebLog)
|
||||||
|
match tryUserLogOn conn form.email (pbkdf2 form.password) with
|
||||||
| Some user -> this.Session.[Keys.User] <- user
|
| Some user -> this.Session.[Keys.User] <- user
|
||||||
{ level = Level.Info
|
{ level = Level.Info
|
||||||
message = Resources.MsgLogOnSuccess
|
message = Resources.MsgLogOnSuccess
|
||||||
@ -46,14 +47,14 @@ type UserModule(conn : IConnection) as this =
|
|||||||
this.Redirect "" model |> ignore // Save the messages in the session before the Nancy redirect
|
this.Redirect "" model |> ignore // Save the messages in the session before the Nancy redirect
|
||||||
// TODO: investigate if addMessage should update the session when it's called
|
// TODO: investigate if addMessage should update the session when it's called
|
||||||
this.LoginAndRedirect
|
this.LoginAndRedirect
|
||||||
(System.Guid.Parse user.id, fallbackRedirectUrl = defaultArg (Option.ofObj(model.returnUrl)) "/")
|
(System.Guid.Parse user.id, fallbackRedirectUrl = defaultArg (Option.ofObj(form.returnUrl)) "/")
|
||||||
| None -> { level = Level.Error
|
| None -> { level = Level.Error
|
||||||
message = Resources.ErrBadLogOnAttempt
|
message = Resources.ErrBadLogOnAttempt
|
||||||
details = None }
|
details = None }
|
||||||
|> model.addMessage
|
|> model.addMessage
|
||||||
this.Redirect "" model |> ignore // Save the messages in the session before the Nancy redirect
|
this.Redirect "" model |> ignore // Save the messages in the session before the Nancy redirect
|
||||||
// Can't redirect with a negotiator when the other leg uses a straight response... :/
|
// Can't redirect with a negotiator when the other leg uses a straight response... :/
|
||||||
this.Response.AsRedirect((sprintf "/user/logon?returnUrl=%s" model.returnUrl),
|
this.Response.AsRedirect((sprintf "/user/logon?returnUrl=%s" form.returnUrl),
|
||||||
Responses.RedirectResponse.RedirectType.SeeOther)
|
Responses.RedirectResponse.RedirectType.SeeOther)
|
||||||
|
|
||||||
/// Log a user off
|
/// Log a user off
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
namespace myWebLog
|
namespace myWebLog
|
||||||
|
|
||||||
|
open myWebLog.Data.WebLog
|
||||||
open myWebLog.Entities
|
open myWebLog.Entities
|
||||||
open Nancy
|
open Nancy
|
||||||
open Nancy.Session.Persistable
|
open Nancy.Session.Persistable
|
||||||
|
open Newtonsoft.Json
|
||||||
open NodaTime
|
open NodaTime
|
||||||
open NodaTime.Text
|
open NodaTime.Text
|
||||||
|
|
||||||
@ -10,10 +12,13 @@ open NodaTime.Text
|
|||||||
/// Levels for a user message
|
/// Levels for a user message
|
||||||
module Level =
|
module Level =
|
||||||
/// An informational message
|
/// An informational message
|
||||||
|
[<Literal>]
|
||||||
let Info = "Info"
|
let Info = "Info"
|
||||||
/// A message regarding a non-fatal but non-optimal condition
|
/// A message regarding a non-fatal but non-optimal condition
|
||||||
|
[<Literal>]
|
||||||
let Warning = "WARNING"
|
let Warning = "WARNING"
|
||||||
/// A message regarding a failure of the expected result
|
/// A message regarding a failure of the expected result
|
||||||
|
[<Literal>]
|
||||||
let Error = "ERROR"
|
let Error = "ERROR"
|
||||||
|
|
||||||
|
|
||||||
@ -28,11 +33,63 @@ type UserMessage = {
|
|||||||
}
|
}
|
||||||
with
|
with
|
||||||
/// An empty message
|
/// An empty message
|
||||||
static member empty =
|
static member empty = {
|
||||||
{ level = Level.Info
|
level = Level.Info
|
||||||
message = ""
|
message = ""
|
||||||
details = None }
|
details = None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Display version
|
||||||
|
[<JsonIgnore>]
|
||||||
|
member this.toDisplay =
|
||||||
|
let classAndLabel =
|
||||||
|
dict [
|
||||||
|
Level.Error, ("danger", Resources.Error)
|
||||||
|
Level.Warning, ("warning", Resources.Warning)
|
||||||
|
Level.Info, ("info", "")
|
||||||
|
]
|
||||||
|
seq {
|
||||||
|
yield "<div class=\"alert alert-dismissable alert-"
|
||||||
|
yield fst classAndLabel.[this.level]
|
||||||
|
yield "\" role=\"alert\"><button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-label=\""
|
||||||
|
yield Resources.Close
|
||||||
|
yield "\">×</button><strong>"
|
||||||
|
match snd classAndLabel.[this.level] with
|
||||||
|
| "" -> ()
|
||||||
|
| lbl -> yield lbl.ToUpper ()
|
||||||
|
yield " » "
|
||||||
|
yield this.message
|
||||||
|
yield "</strong>"
|
||||||
|
match this.details with
|
||||||
|
| Some d -> yield "<br />"
|
||||||
|
yield d
|
||||||
|
| None -> ()
|
||||||
|
yield "</div>"
|
||||||
|
}
|
||||||
|
|> Seq.reduce (fun acc x -> acc + x)
|
||||||
|
|
||||||
|
|
||||||
|
/// Helpers to format local date/time using NodaTime
|
||||||
|
module FormatDateTime =
|
||||||
|
|
||||||
|
/// Convert ticks to a zoned date/time
|
||||||
|
let zonedTime timeZone ticks = Instant(ticks).InZone(DateTimeZoneProviders.Tzdb.[timeZone])
|
||||||
|
|
||||||
|
/// Display a long date
|
||||||
|
let longDate timeZone ticks =
|
||||||
|
zonedTime timeZone ticks
|
||||||
|
|> ZonedDateTimePattern.CreateWithCurrentCulture("MMMM d',' yyyy", DateTimeZoneProviders.Tzdb).Format
|
||||||
|
|
||||||
|
/// Display a short date
|
||||||
|
let shortDate timeZone ticks =
|
||||||
|
zonedTime timeZone ticks
|
||||||
|
|> ZonedDateTimePattern.CreateWithCurrentCulture("MMM d',' yyyy", DateTimeZoneProviders.Tzdb).Format
|
||||||
|
|
||||||
|
/// Display the time
|
||||||
|
let time timeZone ticks =
|
||||||
|
(zonedTime timeZone ticks
|
||||||
|
|> ZonedDateTimePattern.CreateWithCurrentCulture("h':'mmtt", DateTimeZoneProviders.Tzdb).Format).ToLower()
|
||||||
|
|
||||||
|
|
||||||
/// Parent view model for all myWebLog views
|
/// Parent view model for all myWebLog views
|
||||||
type MyWebLogModel(ctx : NancyContext, webLog : WebLog) =
|
type MyWebLogModel(ctx : NancyContext, webLog : WebLog) =
|
||||||
@ -64,36 +121,25 @@ type MyWebLogModel(ctx : NancyContext, webLog : WebLog) =
|
|||||||
/// Add a message to the output
|
/// Add a message to the output
|
||||||
member this.addMessage message = this.messages <- message :: this.messages
|
member this.addMessage message = this.messages <- message :: this.messages
|
||||||
|
|
||||||
/// Convert ticks to a zoned date/time for the current web log
|
|
||||||
member this.zonedTime ticks = Instant(ticks).InZone(DateTimeZoneProviders.Tzdb.[this.webLog.timeZone])
|
|
||||||
|
|
||||||
/// Display a long date
|
/// Display a long date
|
||||||
member this.displayLongDate ticks =
|
member this.displayLongDate ticks = FormatDateTime.longDate this.webLog.timeZone ticks
|
||||||
this.zonedTime ticks
|
|
||||||
|> ZonedDateTimePattern.CreateWithCurrentCulture("MMMM d',' yyyy", DateTimeZoneProviders.Tzdb).Format
|
|
||||||
|
|
||||||
/// Display a short date
|
/// Display a short date
|
||||||
member this.displayShortDate ticks =
|
member this.displayShortDate ticks = FormatDateTime.shortDate this.webLog.timeZone ticks
|
||||||
this.zonedTime ticks
|
|
||||||
|> ZonedDateTimePattern.CreateWithCurrentCulture("MMM d',' yyyy", DateTimeZoneProviders.Tzdb).Format
|
|
||||||
|
|
||||||
/// Display the time
|
/// Display the time
|
||||||
member this.displayTime ticks =
|
member this.displayTime ticks = FormatDateTime.time this.webLog.timeZone ticks
|
||||||
(this.zonedTime ticks
|
|
||||||
|> ZonedDateTimePattern.CreateWithCurrentCulture("h':'mmtt", DateTimeZoneProviders.Tzdb).Format).ToLower()
|
|
||||||
|
|
||||||
|
|
||||||
// ---- Admin models ----
|
// ---- Admin models ----
|
||||||
|
|
||||||
/// Admin Dashboard view model
|
/// Admin Dashboard view model
|
||||||
type DashboardModel(ctx, webLog) =
|
type DashboardModel(ctx, webLog, counts : DashboardCounts) =
|
||||||
inherit MyWebLogModel(ctx, webLog)
|
inherit MyWebLogModel(ctx, webLog)
|
||||||
/// The number of posts for the current web log
|
/// The number of posts for the current web log
|
||||||
member val posts = 0 with get, set
|
member val posts = counts.posts with get, set
|
||||||
/// The number of pages for the current web log
|
/// The number of pages for the current web log
|
||||||
member val pages = 0 with get, set
|
member val pages = counts.pages with get, set
|
||||||
/// The number of categories for the current web log
|
/// The number of categories for the current web log
|
||||||
member val categories = 0 with get, set
|
member val categories = counts.categories with get, set
|
||||||
|
|
||||||
|
|
||||||
// ---- Category models ----
|
// ---- Category models ----
|
||||||
@ -110,7 +156,7 @@ with
|
|||||||
indent = snd cat
|
indent = snd cat
|
||||||
selected = isSelected (fst cat).id }
|
selected = isSelected (fst cat).id }
|
||||||
/// Display name for a category on the list page, complete with indents
|
/// Display name for a category on the list page, complete with indents
|
||||||
member this.listName = sprintf "%s%s" (String.replicate this.indent " ઻ ") this.category.name
|
member this.listName = sprintf "%s%s" (String.replicate this.indent " » ") this.category.name
|
||||||
/// Display for this category as an option within a select box
|
/// Display for this category as an option within a select box
|
||||||
member this.option =
|
member this.option =
|
||||||
seq {
|
seq {
|
||||||
@ -121,6 +167,9 @@ with
|
|||||||
yield "</option>"
|
yield "</option>"
|
||||||
}
|
}
|
||||||
|> String.concat ""
|
|> String.concat ""
|
||||||
|
/// Does the category have a description?
|
||||||
|
member this.hasDescription = this.category.description.IsSome
|
||||||
|
|
||||||
|
|
||||||
/// Model for the list of categories
|
/// Model for the list of categories
|
||||||
type CategoryListModel(ctx, webLog, categories) =
|
type CategoryListModel(ctx, webLog, categories) =
|
||||||
@ -237,6 +286,28 @@ type PostModel(ctx, webLog, post) =
|
|||||||
|> List.sort
|
|> List.sort
|
||||||
|> List.map (fun tag -> tag, tag.Replace(' ', '+'))
|
|> List.map (fun tag -> tag, tag.Replace(' ', '+'))
|
||||||
|
|
||||||
|
|
||||||
|
/// Wrapper for a post with additional properties
|
||||||
|
type PostForDisplay(webLog : WebLog, post : Post) =
|
||||||
|
/// Turn tags into a pipe-delimited string of tags
|
||||||
|
let pipedTags tags = tags |> List.reduce (fun acc x -> sprintf "%s | %s" acc x)
|
||||||
|
/// The actual post
|
||||||
|
member this.post = post
|
||||||
|
/// The time zone for the web log to which this post belongs
|
||||||
|
member this.timeZone = webLog.timeZone
|
||||||
|
/// The date the post was published
|
||||||
|
member this.publishedDate = FormatDateTime.longDate this.timeZone this.post.publishedOn
|
||||||
|
/// The time the post was published
|
||||||
|
member this.publishedTime = FormatDateTime.time this.timeZone this.post.publishedOn
|
||||||
|
/// Tags
|
||||||
|
member this.tags =
|
||||||
|
match List.length this.post.tags with
|
||||||
|
| 0 -> ""
|
||||||
|
| 1 | 2 | 3 | 4 | 5 -> this.post.tags |> pipedTags
|
||||||
|
| count -> sprintf "%s %s" (this.post.tags |> List.take 3 |> pipedTags)
|
||||||
|
(System.String.Format(Resources.andXMore, count - 3))
|
||||||
|
|
||||||
|
|
||||||
/// Model for all page-of-posts pages
|
/// Model for all page-of-posts pages
|
||||||
type PostsModel(ctx, webLog) =
|
type PostsModel(ctx, webLog) =
|
||||||
inherit MyWebLogModel(ctx, webLog)
|
inherit MyWebLogModel(ctx, webLog)
|
||||||
@ -245,7 +316,7 @@ type PostsModel(ctx, webLog) =
|
|||||||
member val subtitle = Option<string>.None with get, set
|
member val subtitle = Option<string>.None with get, set
|
||||||
|
|
||||||
/// The posts to display
|
/// The posts to display
|
||||||
member val posts = List.empty<Post> with get, set
|
member val posts = List.empty<PostForDisplay> with get, set
|
||||||
|
|
||||||
/// The page number of the post list
|
/// The page number of the post list
|
||||||
member val pageNbr = 0 with get, set
|
member val pageNbr = 0 with get, set
|
||||||
@ -320,12 +391,18 @@ type EditPostModel(ctx, webLog, post, revision) =
|
|||||||
|
|
||||||
// ---- User models ----
|
// ---- User models ----
|
||||||
|
|
||||||
/// Model to support the user log on page
|
/// Form for the log on page
|
||||||
type LogOnModel(ctx, webLog) =
|
type LogOnForm() =
|
||||||
inherit MyWebLogModel(ctx, webLog)
|
|
||||||
/// The URL to which the user will be directed upon successful log on
|
/// The URL to which the user will be directed upon successful log on
|
||||||
member val returnUrl = "" with get, set
|
member val returnUrl = "" with get, set
|
||||||
/// The e-mail address
|
/// The e-mail address
|
||||||
member val email = "" with get, set
|
member val email = "" with get, set
|
||||||
/// The user's passwor
|
/// The user's passwor
|
||||||
member val password = "" with get, set
|
member val password = "" with get, set
|
||||||
|
|
||||||
|
|
||||||
|
/// Model to support the user log on page
|
||||||
|
type LogOnModel(ctx, webLog) =
|
||||||
|
inherit MyWebLogModel(ctx, webLog)
|
||||||
|
/// The log on form
|
||||||
|
member val form = LogOnForm() with get, set
|
||||||
|
@ -51,6 +51,7 @@
|
|||||||
<Content Include="data-config.json">
|
<Content Include="data-config.json">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
|
<Content Include="views\admin\message.html" />
|
||||||
<Content Include="views\themes\default\content\bootstrap-theme.css.map">
|
<Content Include="views\themes\default\content\bootstrap-theme.css.map">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
|
@ -32,7 +32,9 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<!-- Partial['admin/messages', Model] // TODO -->
|
@Each.messages
|
||||||
|
@Current.toDisplay
|
||||||
|
@EndEach
|
||||||
@Section['Content'];
|
@Section['Content'];
|
||||||
</div>
|
</div>
|
||||||
<footer>
|
<footer>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
@Section['Content']
|
@Section['Content']
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p><a class="btn btn-primary" href="/category/new/edit"><i class="fa fa-plus"></i> @Translate.AddNew"</a></p>
|
<p><a class="btn btn-primary" href="/category/new/edit"><i class="fa fa-plus"></i> @Translate.AddNew</a></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
@ -20,7 +20,14 @@
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>@Current.listName</td>
|
<td>@Current.listName</td>
|
||||||
<td>@Current.category.description</td>
|
<td>
|
||||||
|
@If.hasDescription
|
||||||
|
@Current.category.description.Value
|
||||||
|
@EndIf
|
||||||
|
@IfNot.hasDescription
|
||||||
|
|
||||||
|
@EndIf
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@EndEach
|
@EndEach
|
||||||
</table>
|
</table>
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
<div class="col-xs-6">
|
<div class="col-xs-6">
|
||||||
<h3>@Translate.Categories <span class="badge">@Model.categories</span></h3>
|
<h3>@Translate.Categories <span class="badge">@Model.categories</span></h3>
|
||||||
<p>
|
<p>
|
||||||
<a href="/categories"><i class="fa fa-list.ul"></i> @Translate.ListAll</a>
|
<a href="/categories"><i class="fa fa-list-ul"></i> @Translate.ListAll</a>
|
||||||
|
|
||||||
<a href="/category/new/edit"><i class="fa fa-plus"></i> @Translate.AddNew</a>
|
<a href="/category/new/edit"><i class="fa fa-plus"></i> @Translate.AddNew</a>
|
||||||
</p>
|
</p>
|
||||||
|
18
src/myWebLog/views/admin/message.html
Normal file
18
src/myWebLog/views/admin/message.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
if session && 0 < (session.messages || []).length
|
||||||
|
while 0 < session.messages.length
|
||||||
|
- var message = session.messages.shift()
|
||||||
|
<div class="alert alert-dismissable alert-@Model.level" role="alert">
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="@Translate.Close">×</button>
|
||||||
|
<strong>
|
||||||
|
if 'danger' == message.type
|
||||||
|
=__("Error").toUpperCase()
|
||||||
|
| »
|
||||||
|
else if 'warning' == message.type
|
||||||
|
=__("Warning").toUpperCase()
|
||||||
|
| »
|
||||||
|
!= message.text
|
||||||
|
</strong>
|
||||||
|
if message.detail
|
||||||
|
br
|
||||||
|
!= message.detail
|
||||||
|
</div>
|
@ -15,7 +15,7 @@
|
|||||||
<td>
|
<td>
|
||||||
@Current.title<br />
|
@Current.title<br />
|
||||||
<a href="/@Current.permalink">@Translate.View</a>
|
<a href="/@Current.permalink">@Translate.View</a>
|
||||||
<a href="/page/@Current.id}/edit">@Translate.Edit</a>
|
<a href="/page/@Current.id/edit">@Translate.Edit</a>
|
||||||
<a href="javascript:void(0)" onclick="deletePage('@Current.id', '@Current.title')">@Translate.Delete</a>
|
<a href="javascript:void(0)" onclick="deletePage('@Current.id', '@Current.title')">@Translate.Delete</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<a class="btn btn-primary" href="/post/new/edit">
|
<a class="btn btn-primary" href="/post/new/edit">
|
||||||
<i class="fa fa-plus"></i> | @Translate.AddNew
|
<i class="fa fa-plus"></i> @Translate.AddNew
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -17,30 +17,19 @@
|
|||||||
<th>@Translate.Tags</th>
|
<th>@Translate.Tags</th>
|
||||||
</tr>
|
</tr>
|
||||||
@Each.posts
|
@Each.posts
|
||||||
<!-- - var theDate = moment(post.publishedDate ? post.publishedDate : post.lastUpdatedDate)
|
|
||||||
- var tags = (post.tag || []).sort() -->
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td style="white-space:nowrap;">
|
||||||
<!-- =theDate.format('MMM D, YYYY')
|
@Current.publishedDate<br />
|
||||||
br
|
@Translate.at @Current.publishedTime
|
||||||
#{__("at")} #{theDate.format('h:mma')} -->
|
|
||||||
// TODO format date
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@Current.title<br />
|
@Current.post.title<br />
|
||||||
<a href="/@Current.permalink">@Translate.View</a> |
|
<a href="/@Current.post.permalink">@Translate.View</a> |
|
||||||
<a href="/post/@Current.id/edit">@Translate.Edit</a> |
|
<a href="/post/@Current.post.id/edit">@Translate.Edit</a> |
|
||||||
<a href="/post/@Current.id/delete">@Translate.Delete</a>
|
<a href="/post/@Current.post.id/delete">@Translate.Delete</a>
|
||||||
</td>
|
|
||||||
<td>@Current.status</td>
|
|
||||||
<td>
|
|
||||||
<!-- if 5 > tags.length
|
|
||||||
=tags.join(' | ')
|
|
||||||
else
|
|
||||||
=tags.slice(0, 3).join(' | ') + ' | '
|
|
||||||
span(title=tags.slice(3).join(' | '))= __("and %d more...", tags.slice(3).length) -->
|
|
||||||
// TODO fix tags
|
|
||||||
</td>
|
</td>
|
||||||
|
<td>@Current.post.status</td>
|
||||||
|
<td>@Current.tags</td>
|
||||||
</tr>
|
</tr>
|
||||||
@EndEach
|
@EndEach
|
||||||
</table>
|
</table>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
@Section['Content']
|
@Section['Content']
|
||||||
<form action="/user/logon" method="post">
|
<form action="/user/logon" method="post">
|
||||||
@AntiForgeryToken
|
@AntiForgeryToken
|
||||||
<input type="hidden" name="returnUrl" value="@Model.returnUrl" />
|
<input type="hidden" name="returnUrl" value="@Model.form.returnUrl" />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-offset-1 col-sm-8 col-md-offset-3 col-md-6">
|
<div class="col-sm-offset-1 col-sm-8 col-md-offset-3 col-md-6">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
@If.subTitle.IsSome
|
@Each.messages
|
||||||
|
@Current.toDisplay
|
||||||
|
@EndEach
|
||||||
|
@If.subTitle.IsSome
|
||||||
<h2>
|
<h2>
|
||||||
<span class="label label-info">@Model.subTitle</span>
|
<span class="label label-info">@Model.subTitle</span>
|
||||||
</h2>
|
</h2>
|
||||||
@ -8,14 +11,14 @@
|
|||||||
<div class="col-xs-12">
|
<div class="col-xs-12">
|
||||||
<article>
|
<article>
|
||||||
<h1>
|
<h1>
|
||||||
<a href="/@Current.permalink" title="@Translate.PermanentLinkTo "@Current.title@quot;">@Current.title</a>
|
<a href="/@Current.post.permalink"
|
||||||
|
title="@Translate.PermanentLinkTo "@Current.post.title@quot;">@Current.post.title</a>
|
||||||
</h1>
|
</h1>
|
||||||
<!-- var pubDate = moment(post.publishedDate) -->
|
|
||||||
<p>
|
<p>
|
||||||
<i class="fa fa-calendar" title="@Translate.Date"></i> @Current.publishedDate <!-- #{pubDate.format('MMMM Do, YYYY')} -->
|
<i class="fa fa-calendar" title="@Translate.Date"></i> @Current.publishedDate
|
||||||
<i class="fa fa-clock-o" title="@Translate.Time"></i> @Current.publishedTime <!-- #{pubDate.format('h:mma')} -->
|
<i class="fa fa-clock-o" title="@Translate.Time"></i> @Current.publishedTime
|
||||||
</p>
|
</p>
|
||||||
@Current.text
|
@Current.post.text
|
||||||
</article>
|
</article>
|
||||||
<hr />
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,8 +5,8 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12">
|
<div class="col-xs-12">
|
||||||
<h4>
|
<h4>
|
||||||
<i class="fa fa-calendar" title="@Translate.Date"></i> @Model.publishedDate
|
<i class="fa fa-calendar" title="@Translate.Date"></i> @Model.publishedDate
|
||||||
<i class="fa fa-clock-o" title="@Translate.Time"></i> @Model.publishedTime
|
<i class="fa fa-clock-o" title="@Translate.Time"></i> @Model.publishedTime
|
||||||
@Each.post.categories
|
@Each.post.categories
|
||||||
<i class="fa fa-folder-open-o" title="@Translate.Category"></i>
|
<i class="fa fa-folder-open-o" title="@Translate.Category"></i>
|
||||||
<!-- <a href="/category/@Current.slug" title=__("Categorized under %s", category.name)) -->
|
<!-- <a href="/category/@Current.slug" title=__("Categorized under %s", category.name)) -->
|
||||||
|
Loading…
Reference in New Issue
Block a user