Another interim commit

Home page and some post stuff written
This commit is contained in:
Daniel J. Summers 2016-07-08 23:18:44 -05:00
parent b86ba7b6f6
commit 3656bb384c
21 changed files with 481 additions and 43 deletions

View File

@ -1,9 +1,47 @@
namespace myWebLog.Data namespace myWebLog.Data
open RethinkDb.Driver
open RethinkDb.Driver.Net open RethinkDb.Driver.Net
open Newtonsoft.Json
/// Data configuration
type DataConfig = { type DataConfig = {
/// The hostname for the RethinkDB server
hostname : string
/// The port for the RethinkDB server
port : int
/// The authorization key to use when connecting to the server
authKey : string
/// How long an attempt to connect to the server should wait before giving up
timeout : int
/// The name of the default database to use on the connection
database : string database : string
/// A connection to the RethinkDB server using the configuration in this object
conn : IConnection conn : IConnection
} }
with
/// Create a data configuration from JSON
static member fromJson json =
let mutable cfg = JsonConvert.DeserializeObject<DataConfig> json
cfg <- match cfg.hostname with
| null -> { cfg with hostname = RethinkDBConstants.DefaultHostname }
| _ -> cfg
cfg <- match cfg.port with
| 0 -> { cfg with port = RethinkDBConstants.DefaultPort }
| _ -> cfg
cfg <- match cfg.authKey with
| null -> { cfg with authKey = RethinkDBConstants.DefaultAuthkey }
| _ -> cfg
cfg <- match cfg.timeout with
| 0 -> { cfg with timeout = RethinkDBConstants.DefaultTimeout }
| _ -> cfg
cfg <- match cfg.database with
| null -> { cfg with database = RethinkDBConstants.DefaultDbName }
| _ -> cfg
{ cfg with conn = RethinkDB.R.Connection()
.Hostname(cfg.hostname)
.Port(cfg.port)
.AuthKey(cfg.authKey)
.Db(cfg.database)
.Timeout(cfg.timeout)
.Connect() }

14
src/myWebLog.Data/Page.fs Normal file
View File

@ -0,0 +1,14 @@
module myWebLog.Data.Page
open myWebLog.Entities
open Rethink
/// Get a page by its Id
let tryFindPage conn webLogId pageId : Page option =
match table Table.Page
|> get pageId
|> filter (fun p -> upcast p.["webLogId"].Eq(webLogId))
|> runAtomAsync<Page> conn
|> box with
| null -> None
| page -> Some <| unbox page

49
src/myWebLog.Data/Post.fs Normal file
View File

@ -0,0 +1,49 @@
module myWebLog.Data.Post
open myWebLog.Entities
open Rethink
open RethinkDb.Driver
let private r = RethinkDB.R
/// Get a page of published posts
let findPageOfPublishedPosts conn webLogId pageNbr nbrPerPage =
table Table.Post
|> getAll [| webLogId, PostStatus.Published |]
|> optArg "index" "webLogAndStatus"
|> orderBy (fun p -> upcast r.Desc(p.["publishedOn"]))
|> slice ((pageNbr - 1) * nbrPerPage) (pageNbr * nbrPerPage)
|> runCursorAsync<Post> conn
|> Seq.toList
/// Try to get the next newest post from the given post
let tryFindNewerPost conn post =
table Table.Post
|> getAll [| post.webLogId, PostStatus.Published |]
|> optArg "index" "webLogAndStatus"
|> filter (fun p -> upcast p.["publishedOn"].Gt(post.publishedOn))
|> orderBy (fun p -> upcast p.["publishedOn"])
|> limit 1
|> runCursorAsync<Post> conn
|> Seq.tryHead
/// Try to get the next oldest post from the given post
let tryFindOlderPost conn post =
table Table.Post
|> getAll [| post.webLogId, PostStatus.Published |]
|> optArg "index" "webLogAndStatus"
|> filter (fun p -> upcast p.["publishedOn"].Lt(post.publishedOn))
|> orderBy (fun p -> upcast r.Desc(p.["publishedOn"]))
|> limit 1
|> runCursorAsync<Post> conn
|> Seq.tryHead
/// Get a page of all posts in all statuses
let findPageOfAllPosts conn webLogId pageNbr nbrPerPage =
table Table.Post
|> getAll [| webLogId |]
|> optArg "index" "webLogId"
|> orderBy (fun p -> upcast r.Desc(r.Branch(p.["publishedOn"].Eq(int64 0), p.["lastUpdatedOn"], p.["publishedOn"])))
|> slice ((pageNbr - 1) * nbrPerPage) (pageNbr * nbrPerPage)
|> runCursorAsync<Post> conn
|> Seq.toList

View File

@ -7,17 +7,20 @@ let private r = RethinkDb.Driver.RethinkDB.R
let private await task = task |> Async.AwaitTask |> Async.RunSynchronously let private await task = task |> Async.AwaitTask |> Async.RunSynchronously
let delete (expr : ReqlExpr) = expr.Delete () let delete (expr : ReqlExpr) = expr.Delete ()
let filter (expr : ReqlExpr -> ReqlExpr) (table : ReqlExpr) = table.Filter expr
let get (expr : obj) (table : Table) = table.Get expr let get (expr : obj) (table : Table) = table.Get expr
let getAll (exprs : obj[]) (table : Table) = table.GetAll exprs let getAll (exprs : obj[]) (table : Table) = table.GetAll exprs
let insert (expr : obj) (table : Table) = table.Insert expr let insert (expr : obj) (table : Table) = table.Insert expr
let limit (expr : obj) (table : ReqlExpr) = table.Limit expr
let optArg key (value : obj) (expr : GetAll) = expr.OptArg (key, value) let optArg key (value : obj) (expr : GetAll) = expr.OptArg (key, value)
let orderBy (exprA : obj) (expr : ReqlExpr) = expr.OrderBy exprA let orderBy (exprA : ReqlExpr -> ReqlExpr) (expr : ReqlExpr) = expr.OrderBy exprA
let replace (exprA : obj) (expr : ReqlExpr) = expr.Replace exprA let replace (exprA : obj) (expr : ReqlExpr) = expr.Replace exprA
let runAtomAsync<'T> (conn : IConnection) (ast : ReqlAst) = ast.RunAtomAsync<'T> conn |> await let runAtomAsync<'T> (conn : IConnection) (ast : ReqlAst) = ast.RunAtomAsync<'T> conn |> await
let runCursorAsync<'T> (conn : IConnection) (ast : ReqlAst) = ast.RunCursorAsync<'T> conn |> await let runCursorAsync<'T> (conn : IConnection) (ast : ReqlAst) = ast.RunCursorAsync<'T> conn |> await
let runListAsync<'T> (conn : IConnection) (ast : ReqlAst) = ast.RunAtomAsync<System.Collections.Generic.List<'T>> conn let runListAsync<'T> (conn : IConnection) (ast : ReqlAst) = ast.RunAtomAsync<System.Collections.Generic.List<'T>> conn
|> await |> await
let runResultAsync (conn : IConnection) (ast : ReqlAst) = ast.RunResultAsync conn |> await let runResultAsync (conn : IConnection) (ast : ReqlAst) = ast.RunResultAsync conn |> await
let slice (exprA : obj) (exprB : obj) (ast : ReqlExpr) = ast.Slice (exprA, exprB)
let table (expr : obj) = r.Table expr let table (expr : obj) = r.Table expr
let update (exprA : obj) (expr : ReqlExpr) = expr.Update exprA let update (exprA : obj) (expr : ReqlExpr) = expr.Update exprA
let without (exprs : obj[]) (expr : ReqlExpr) = expr.Without exprs let without (exprs : obj[]) (expr : ReqlExpr) = expr.Without exprs

View File

@ -3,18 +3,19 @@
open myWebLog.Entities open myWebLog.Entities
open Rethink open Rethink
open RethinkDb.Driver open RethinkDb.Driver
open RethinkDb.Driver.Net
let private r = RethinkDB.R let private r = RethinkDB.R
type PageList = { pageList : Ast.CoerceTo } type PageList = { pageList : Ast.CoerceTo }
/// Detemine the web log by the URL base /// Detemine the web log by the URL base
let tryFindWebLogByUrlBase (cfg : DataConfig) (urlBase : string) = let tryFindWebLogByUrlBase (conn : IConnection) (urlBase : string) =
r.Table(Table.WebLog).GetAll([| urlBase |]).OptArg("index", "urlBase") r.Table(Table.WebLog).GetAll([| urlBase |]).OptArg("index", "urlBase")
.Merge(fun webLog -> { pageList = r.Table(Table.Page) .Merge(fun webLog -> { pageList = r.Table(Table.Page)
.GetAll([| webLog.["id"], true |]).OptArg("index", "pageList") .GetAll([| webLog.["id"], true |]).OptArg("index", "pageList")
.OrderBy("title") .OrderBy("title")
.Pluck([| "title", "permalink" |]) .Pluck([| "title", "permalink" |])
.CoerceTo("array") }) .CoerceTo("array") })
|> runCursorAsync<WebLog> cfg.conn |> runCursorAsync<WebLog> conn
|> Seq.tryHead |> Seq.tryHead

View File

@ -56,6 +56,8 @@
<Compile Include="DataConfig.fs" /> <Compile Include="DataConfig.fs" />
<Compile Include="Rethink.fs" /> <Compile Include="Rethink.fs" />
<Compile Include="SetUp.fs" /> <Compile Include="SetUp.fs" />
<Compile Include="Page.fs" />
<Compile Include="Post.fs" />
<Compile Include="WebLog.fs" /> <Compile Include="WebLog.fs" />
<Content Include="packages.config" /> <Content Include="packages.config" />
</ItemGroup> </ItemGroup>

View File

@ -11,37 +11,32 @@ open Nancy.Bootstrapper
open Nancy.Cryptography open Nancy.Cryptography
open Nancy.Owin open Nancy.Owin
open Nancy.Security open Nancy.Security
open Nancy.Session
open Nancy.Session.Persistable open Nancy.Session.Persistable
open Nancy.Session.RethinkDb open Nancy.Session.RethinkDb
open Nancy.TinyIoc open Nancy.TinyIoc
open Nancy.ViewEngines.SuperSimpleViewEngine open Nancy.ViewEngines.SuperSimpleViewEngine
open RethinkDb.Driver
open RethinkDb.Driver.Net open RethinkDb.Driver.Net
open Suave open Suave
open Suave.Owin open Suave.Owin
open System
open System.Text.RegularExpressions open System.Text.RegularExpressions
/// Set up a database connection /// Set up a database connection
let cfg = let cfg = try DataConfig.fromJson (System.IO.File.ReadAllText "data-config.json")
{ database = "myWebLog" with ex -> ApplicationException("Could not convert data-config.json to RethinkDB connection", ex)
conn = RethinkDB.R.Connection() |> raise
.Hostname(RethinkDBConstants.DefaultHostname)
.Port(RethinkDBConstants.DefaultPort)
.AuthKey(RethinkDBConstants.DefaultAuthkey)
.Db("myWebLog")
.Timeout(RethinkDBConstants.DefaultTimeout)
.Connect() }
do do
startUpCheck cfg startUpCheck cfg
/// Support RESX lookup via the @Translate SSVE alias
type TranslateTokenViewEngineMatcher() = type TranslateTokenViewEngineMatcher() =
static let regex = Regex("@Translate\.(?<TranslationKey>[a-zA-Z0-9-_]+);?", RegexOptions.Compiled) static let regex = Regex("@Translate\.(?<TranslationKey>[a-zA-Z0-9-_]+);?", RegexOptions.Compiled)
interface ISuperSimpleViewEngineMatcher with interface ISuperSimpleViewEngineMatcher with
member this.Invoke (content, model, host) = member this.Invoke (content, model, host) =
regex.Replace(content, fun m -> let key = m.Groups.["TranslationKey"].Value regex.Replace(content, fun m -> let key = m.Groups.["TranslationKey"].Value
match Resources.ResourceManager.GetString key with match myWebLog.Resources.ResourceManager.GetString key with
| null -> key | null -> key
| xlat -> xlat) | xlat -> xlat)
@ -51,8 +46,8 @@ type MyWebLogUser(name, claims) =
interface IUserIdentity with interface IUserIdentity with
member this.UserName with get() = name member this.UserName with get() = name
member this.Claims with get() = claims member this.Claims with get() = claims
member this.UserName with get() = (this :> IUserIdentity).UserName (*member this.UserName with get() = (this :> IUserIdentity).UserName
member this.Claims with get() = (this :> IUserIdentity).Claims member this.Claims with get() = (this :> IUserIdentity).Claims -- do we need these? *)
type MyWebLogUserMapper(container : TinyIoCContainer) = type MyWebLogUserMapper(container : TinyIoCContainer) =
@ -63,25 +58,30 @@ type MyWebLogUserMapper(container : TinyIoCContainer) =
| _ -> null | _ -> null
/// Set up the RethinkDB connection instance to be used by the IoC container /// Set up the application environment
type ApplicationBootstrapper() = type MyWebLogBootstrapper() =
inherit DefaultNancyBootstrapper() inherit DefaultNancyBootstrapper()
override this.ConfigureRequestContainer (container, context) = override this.ConfigureRequestContainer (container, context) =
base.ConfigureRequestContainer (container, context) base.ConfigureRequestContainer (container, context)
/// User mapper for forms authentication
container.Register<IUserMapper, MyWebLogUserMapper>() container.Register<IUserMapper, MyWebLogUserMapper>()
|> ignore |> ignore
override this.ApplicationStartup (container, pipelines) = override this.ApplicationStartup (container, pipelines) =
base.ApplicationStartup (container, pipelines) base.ApplicationStartup (container, pipelines)
// Data configuration // Data configuration (both config and the connection; Nancy modules just need the connection)
container.Register<DataConfig>(cfg) container.Register<DataConfig>(cfg)
|> ignore |> ignore
container.Register<IConnection>(cfg.conn)
|> ignore
// I18N in SSVE // I18N in SSVE
container.Register<seq<ISuperSimpleViewEngineMatcher>>(fun _ _ -> container.Register<seq<ISuperSimpleViewEngineMatcher>>(fun _ _ ->
Seq.singleton (TranslateTokenViewEngineMatcher() :> ISuperSimpleViewEngineMatcher)) Seq.singleton (TranslateTokenViewEngineMatcher() :> ISuperSimpleViewEngineMatcher))
|> ignore |> ignore
// Forms authentication configuration // Forms authentication configuration
let salt = (System.Text.ASCIIEncoding()).GetBytes "NoneOfYourBeesWax" let salt = (System.Text.ASCIIEncoding()).GetBytes "NoneOfYourBeesWax"
let auth = let auth =
FormsAuthenticationConfiguration( FormsAuthenticationConfiguration(
CryptographyConfiguration = CryptographyConfiguration CryptographyConfiguration = CryptographyConfiguration
(RijndaelEncryptionProvider(PassphraseKeyGenerator("Secrets", salt)), (RijndaelEncryptionProvider(PassphraseKeyGenerator("Secrets", salt)),
@ -99,7 +99,7 @@ type ApplicationBootstrapper() =
let version = let version =
let v = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version let v = Reflection.Assembly.GetExecutingAssembly().GetName().Version
match v.Build with match v.Build with
| 0 -> match v.Minor with | 0 -> match v.Minor with
| 0 -> string v.Major | 0 -> string v.Major
@ -112,13 +112,13 @@ type RequestEnvironment() =
interface IRequestStartup with interface IRequestStartup with
member this.Initialize (pipelines, context) = member this.Initialize (pipelines, context) =
pipelines.BeforeRequest.AddItemToStartOfPipeline pipelines.BeforeRequest.AddItemToStartOfPipeline
(fun ctx -> ctx.Items.["requestStart"] <- System.DateTime.Now.Ticks (fun ctx -> ctx.Items.[Keys.RequestStart] <- DateTime.Now.Ticks
match tryFindWebLogByUrlBase cfg ctx.Request.Url.HostName with match tryFindWebLogByUrlBase cfg.conn ctx.Request.Url.HostName with
| Some webLog -> ctx.Items.["webLog"] <- webLog | Some webLog -> ctx.Items.[Keys.WebLog] <- webLog
| None -> System.ApplicationException | None -> ApplicationException
(sprintf "%s is not properly configured for myWebLog" ctx.Request.Url.HostName) (sprintf "%s is not properly configured for myWebLog" ctx.Request.Url.HostName)
|> raise |> raise
ctx.Items.["version"] <- version ctx.Items.[Keys.Version] <- version
null) null)

View File

@ -1,3 +1,11 @@
module myWebLog.Keys module myWebLog.Keys
let User = "user" let Messages = "messages"
let RequestStart = "request-start"
let User = "user"
let Version = "version"
let WebLog = "web-log"

View File

@ -0,0 +1,24 @@
namespace myWebLog
open myWebLog.Entities
open Nancy
open Nancy.Security
/// Parent class for all myWebLog Nancy modules
[<AbstractClass>]
type MyWebLogModule() =
inherit NancyModule()
/// Strongly-typed access to the web log for the current request
member this.WebLog = this.Context.Items.[Keys.WebLog] :?> WebLog
/// Display a view using the theme specified for the web log
member this.ThemedRender view model = this.View.[(sprintf "%s/%s" this.WebLog.themePath view), model]
/// Return a 404
member this.NotFound () = this.Negotiate.WithStatusCode 404
/// Require a specific level of access for the current web log
member this.RequiresAccessLevel level =
this.RequiresAuthentication()
this.RequiresClaims [| sprintf "%s|%s" this.WebLog.id level |]

View File

@ -0,0 +1,67 @@
namespace myWebLog
open myWebLog.Data.Page
open myWebLog.Data.Post
open Nancy
open Nancy.Authentication.Forms
open Nancy.Security
open RethinkDb.Driver.Net
open myWebLog.Entities
type PostModule(conn : IConnection) as this =
inherit MyWebLogModule()
do
this.Get.["/"] <- fun _ -> upcast this.HomePage ()
this.Get.["/posts/page/{page:int}"] <- fun parms -> upcast this.GetPageOfPublishedPosts (downcast parms)
this.Get.["/posts/list"] <- fun _ -> upcast this.PostList 1
this.Get.["/posts/list/page/{page:int"] <- fun parms -> upcast this.PostList
((parms :?> DynamicDictionary).["page"] :?> int)
// ---- Display posts to users ----
/// Display a page of published posts
member private this.DisplayPageOfPublishedPosts pageNbr =
let model = PostsModel(this.Context, this.WebLog)
model.pageNbr <- pageNbr
model.posts <- findPageOfPublishedPosts conn this.WebLog.id pageNbr 10
model.hasNewer <- match List.isEmpty model.posts with
| true -> false
| _ -> Option.isSome <| tryFindNewerPost conn (List.last model.posts)
model.hasOlder <- match List.isEmpty model.posts with
| true -> false
| _ -> Option.isSome <| tryFindOlderPost conn (List.head model.posts)
model.urlPrefix <- "/posts"
model.pageTitle <- match pageNbr with
| 1 -> ""
| _ -> sprintf "Page #%i" pageNbr
this.ThemedRender "posts" model
/// Display either the newest posts or the configured home page
member this.HomePage () =
match this.WebLog.defaultPage with
| "posts" -> this.DisplayPageOfPublishedPosts 1
| page -> match tryFindPage conn this.WebLog.id page with
| Some page -> let model = PageModel(this.Context, this.WebLog, page)
model.pageTitle <- page.title
this.ThemedRender "page" model
| None -> this.Negotiate.WithStatusCode 404
/// Get a page of public posts (other than the first one if the home page is a page of posts)
member this.GetPageOfPublishedPosts (parameters : DynamicDictionary) =
this.DisplayPageOfPublishedPosts (parameters.["page"] :?> int)
// ---- Administer posts ----
/// Display a page of posts in the admin area
member this.PostList pageNbr =
this.RequiresAccessLevel AuthorizationLevel.Administrator
let model = PostsModel(this.Context, this.WebLog)
model.pageNbr <- pageNbr
model.posts <- findPageOfAllPosts conn this.WebLog.id pageNbr 25
model.hasNewer <- pageNbr > 1
model.hasOlder <- 25 > List.length model.posts
model.urlPrefix <- "/post/list"
model.pageTitle <- "Posts"
this.View.["admin/post/list", model]

View File

@ -0,0 +1,95 @@
namespace myWebLog
open myWebLog.Entities
open Nancy
open Nancy.Session.Persistable
/// Levels for a user message
module Level =
/// An informational message
let Info = "Info"
/// A message regarding a non-fatal but non-optimal condition
let Warning = "WARNING"
/// A message regarding a failure of the expected result
let Error = "ERROR"
/// A message for the user
type UserMessage = {
/// The level of the message (use Level module constants)
level : string
/// The text of the message
message : string
/// Further details regarding the message
details : string option
}
with
/// An empty message
static member empty =
{ level = Level.Info
message = ""
details = None }
/// Parent view model for all myWebLog views
type MyWebLogModel(ctx : NancyContext, webLog : WebLog) =
/// Get the messages from the session
let getMessages () =
let msg = ctx.Request.PersistableSession.GetOrDefault<UserMessage list>(Keys.Messages, List.empty)
match List.length msg with
| 0 -> ()
| _ -> ctx.Request.Session.Delete Keys.Messages
msg
/// The web log for this request
member this.webLog = webLog
/// User messages
member val messages = getMessages () with get, set
/// The currently logged in user
member this.user = ctx.Request.PersistableSession.GetOrDefault<User>(Keys.User, User.empty)
/// The title of the page
member val pageTitle = "" with get, set
/// The request start time
member this.requestStart = ctx.Items.[Keys.RequestStart] :?> int64
/// Is a user authenticated for this request?
member this.isAuthenticated = "" <> this.user.id
/// Add a message to the output
member this.addMessage message = this.messages <- message :: this.messages
/// Model for all page-of-posts pages
type PostsModel(ctx, webLog) =
inherit MyWebLogModel(ctx, webLog)
/// The posts to display
member val posts = List.empty<Post> with get, set
/// The page number of the post list
member val pageNbr = 0 with get, set
/// Whether there is a newer page of posts for the list
member val hasNewer = false with get, set
/// Whether there is an older page of posts for the list
member val hasOlder = true with get, set
/// The prefix for the next/prior links
member val urlPrefix = "" with get, set
/// The link for the next newer page of posts
member this.newerLink =
match this.urlPrefix = "/posts" && this.pageNbr = 2 && this.webLog.defaultPage = "posts" with
| true -> "/"
| _ -> sprintf "%s/page/%i" this.urlPrefix (this.pageNbr - 1)
/// The link for the prior (older) page of posts
member this.olderLink = sprintf "%s/page/%i" this.urlPrefix (this.pageNbr + 1)
/// Model for page display
type PageModel(ctx, webLog, page) =
inherit MyWebLogModel(ctx, webLog)
/// The page to be displayed
member this.page : Page = page

View File

@ -52,6 +52,9 @@
<ItemGroup> <ItemGroup>
<Compile Include="AssemblyInfo.fs" /> <Compile Include="AssemblyInfo.fs" />
<Compile Include="Keys.fs" /> <Compile Include="Keys.fs" />
<Compile Include="ViewModels.fs" />
<Compile Include="MyWebLogModule.fs" />
<Compile Include="PostModule.fs" />
<Compile Include="App.fs" /> <Compile Include="App.fs" />
<Content Include="packages.config" /> <Content Include="packages.config" />
</ItemGroup> </ItemGroup>

View File

@ -2,7 +2,7 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
[assembly: AssemblyTitle("myWebLog")] [assembly: AssemblyTitle("myWebLog")]
[assembly: AssemblyDescription("A lightweight blogging platform built on Nancy and RethinkDB")] [assembly: AssemblyDescription("A lightweight blogging platform built on Suave, Nancy, and RethinkDB")]
[assembly: AssemblyConfiguration("")] [assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")] [assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("myWebLog")] [assembly: AssemblyProduct("myWebLog")]

View File

@ -0,0 +1,5 @@
footer {
background-color: #808080;
border-top: solid 1px black;
color: white;
}

View File

@ -0,0 +1,3 @@
{
"database": "myWebLog"
}

View File

@ -48,6 +48,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="App.config" /> <None Include="App.config" />
<None Include="data-config.json" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\myWebLog.Data\myWebLog.Data.fsproj"> <ProjectReference Include="..\myWebLog.Data\myWebLog.Data.fsproj">
@ -65,9 +66,14 @@
</ItemGroup> </ItemGroup>
<ItemGroup /> <ItemGroup />
<ItemGroup> <ItemGroup>
<Content Include="content\styles\admin.css" />
<Content Include="views\admin\admin-layout.html" />
<Content Include="views\admin\post\list.html" />
<Content Include="views\default\index-content.html" /> <Content Include="views\default\index-content.html" />
<Content Include="views\default\index.html" /> <Content Include="views\default\index.html" />
<Content Include="views\default\layout.html" /> <Content Include="views\default\layout.html" />
<Content Include="views\default\page-content.html" />
<Content Include="views\default\page.html" />
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.

View File

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>@Model.pageTitle | @Translate.Admin | @Model.webLog.name</title>
<link rel="stylesheet" type="text/css" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.css" />
<link rel="stylesheet" type="text/css" href="//maxcdn.bootstrapcdn.com/bootswatch/3.3.4/cosmo/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" />
<link rel="stylesheet" type="text/css" href="/content/styles/admin.css" />
</head>
<body>
<header>
<nav class="navbar navbar-inverse">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="/">@Model.webLog.name</a>
</div>
<div class="navbar-left">
<p class="navbar-text">@Model.pageTitle</p>
</div>
<ul class="nav navbar-nav navbar-right">
@If.isAuthenticated
<li><a href="/admin">@Translate.Dashboard</a></li>
<li><a href="/user/logoff">@Translate.LogOff</a></li>
@EndIf
@IfNot.isAuthenticated
<li><a href="/user/logon">@Translate.LogOn</a></li>
@EndIf
</ul>
</div>
</nav>
</header>
<div class="container">
<!-- Partial['admin/messages', Model] // TODO -->
@Section['Content'];
</div>
<footer>
<div class="container-fluid">
<div class="row">
<div class="col-xs-12 text-right">@Model.generator</div>
</div>
</div>
</footer>
<script type="text/javascript" src="//ajax.aspnetcdn.com/ajax/jQuery/jquery-2.1.3.min.js"></script>
<script type="text/javascript" src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>
<script type="text/javascript" src="//tinymce.cachefly.net/4.1/tinymce.min.js"></script>
@Section['Scripts'];
</body>
</html>

View File

@ -0,0 +1,60 @@
@Master['admin/admin-layout']
@Section['Content']
<div class="row">
<p>
<a class="btn btn-primary" href="/post/new/edit">
<i class="fa fa-plus"></i> | &nbsp; @Translate.AddNew
</a>
</p>
</div>
<div class="row">
<table class="table table-hover">
<tr>
<th>@Translate.Date</th>
<th>@Translate.Title</th>
<th>@Translate.Status</th>
<th>@Translate.Tags</th>
</tr>
@Each.posts
<!-- - var theDate = moment(post.publishedDate ? post.publishedDate : post.lastUpdatedDate)
- var tags = (post.tag || []).sort() -->
<tr>
<td>
<!-- =theDate.format('MMM D, YYYY')
br
#{__("at")} #{theDate.format('h:mma')} -->
// TODO format date
</td>
<td>
@Current.title<br />
<a href="/@Current.permalink">@Translate.View</a> | &nbsp;
<a href="/post/@Current.id/edit">@Translate.Edit</a> | &nbsp;
<a href="/post/@Current.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>
</tr>
@EndEach
</table>
</div>
<div class="row">
<div class="col-xs-3 col-xs-offset-2">
@If.hasNewer
<p><a class="btn btn-default" href="@Model.newerLink">&#xab; &nbsp;@Translate.NewerPosts</a></p>
@EndIf
</div>
<div class="col-xs-3 col-xs-offset-1 text-right">
@If.hasOlder
<p><a class="btn btn-default" href="@Model.olderLink">@Translate.OlderPosts&nbsp; &#xbb;</a></p>
@EndIf
</div>
</div>
@EndSection

View File

@ -23,16 +23,17 @@
@EndEach @EndEach
<div class="row"> <div class="row">
<div class="col-xs-3 col-xs-offset-3"> <div class="col-xs-3 col-xs-offset-3">
// TODO: stopped here @If.hasNewer
if hasNext <p>
- var nextLink = (2 < page) ? '/page/' + (page - 1) : '/' <a class="btn btn-primary" href="@Model.newerLink">@Translate.NewerPosts</a>
if '/posts' == extraUrl && 2 == page </p>
//- Leave the "next" link at '/' @EndIf
else </div>
- nextLink = extraUrl + nextLink <div class="col-xs-3 text-right">
p: a.btn.btn-primary(href="#{nextLink}")= __("Newer Posts") @If.hasOlder
</div> <p>
.col-xs-3.text-right <a class="btn btn-primary" href="@Model.olderLink">@Translate.OlderPosts</a>
if hasPrior </p>
p: a.btn.btn-primary(href=extraUrl + '/page/' + (page + 1))= __("Older Posts") @EndIf
</div> </div>
</div>

View File

@ -0,0 +1,4 @@
<article>
<h1>@Model.page.title</h1>
@Model.page.text
</article>

View File

@ -0,0 +1,5 @@
@Master['default/layout']
@Section['Content']
@Partial['default/page-content', Model]
@EndSection