| @@ -38,66 +38,173 @@ Class PostsController_Class | |||||
| ' Action: Index | ' Action: Index | ||||
| '--------------------------------------------------------------- | '--------------------------------------------------------------- | ||||
| Public Sub Index() | Public Sub Index() | ||||
| ' TODO: Implement Index action | |||||
| Response.Write "Index action called" | |||||
| Dim posts | |||||
| Set posts = PostsRepository().FindAllWhere("IsPublished = 1", "PublishedDate DESC", 0, 20) | |||||
| %> | |||||
| <!--#include file="../views/Posts/index.asp" --> | |||||
| <% | |||||
| End Sub | End Sub | ||||
| '--------------------------------------------------------------- | '--------------------------------------------------------------- | ||||
| ' Action: Show | ' Action: Show | ||||
| '--------------------------------------------------------------- | '--------------------------------------------------------------- | ||||
| Public Sub Show(slug) | |||||
| ' TODO: Implement Show action | |||||
| Response.Write "<strong>Show called</strong><br>" | |||||
| Response.Write "Parameters:<br>" | |||||
| Response.Write "slug = " & Server.HTMLEncode(CStr(slug)) & "<br>" | |||||
| Public Sub Show(ByVal slug) | |||||
| Dim matches | |||||
| Set matches = PostsRepository().Find(Array("Slug", slug, "IsPublished", 1), Empty) | |||||
| If matches.Count = 0 Then | |||||
| Response.Status = "404 Not Found" | |||||
| %> | |||||
| <!--#include file="../views/Error/NotFound.asp" --> | |||||
| <% | |||||
| Exit Sub | |||||
| End If | |||||
| Dim post | |||||
| Set post = matches.Front() | |||||
| %> | |||||
| <!--#include file="../views/Posts/show.asp" --> | |||||
| <% | |||||
| End Sub | End Sub | ||||
| '--------------------------------------------------------------- | '--------------------------------------------------------------- | ||||
| ' Action: New | ' Action: New | ||||
| '--------------------------------------------------------------- | '--------------------------------------------------------------- | ||||
| Public Sub NewForm() | Public Sub NewForm() | ||||
| ' TODO: Implement NewForm action | |||||
| Response.Write "NewForm action called" | |||||
| %> | |||||
| <!--#include file="../views/Posts/new.asp" --> | |||||
| <% | |||||
| End Sub | End Sub | ||||
| '--------------------------------------------------------------- | '--------------------------------------------------------------- | ||||
| ' Action: Create | ' Action: Create | ||||
| '--------------------------------------------------------------- | '--------------------------------------------------------------- | ||||
| Public Sub Create() | Public Sub Create() | ||||
| ' TODO: Implement Create action | |||||
| Response.Write "Create action called" | |||||
| Dim title : title = Trim(Request.Form("Title")) | |||||
| If Len(title) = 0 Then | |||||
| Flash().AddError "Title is required." | |||||
| Response.Redirect "/posts/new" | |||||
| Exit Sub | |||||
| End If | |||||
| Dim post | |||||
| Set post = New POBO_Posts | |||||
| post.Title = title | |||||
| post.Summary = Request.Form("Summary") | |||||
| post.Body = Request.Form("Body") | |||||
| post.CategoryID = FormNumberOrZero(Request.Form("CategoryID")) | |||||
| post.Slug = BuildSlug(title) | |||||
| post.CreatedDate = Now() | |||||
| post.UpdatedDate = Now() | |||||
| post.IsPublished = 0 | |||||
| PostsRepository().AddNew post | |||||
| Flash().Success = "Post created." | |||||
| Response.Redirect "/posts" | |||||
| End Sub | End Sub | ||||
| '--------------------------------------------------------------- | '--------------------------------------------------------------- | ||||
| ' Action: Edit | ' Action: Edit | ||||
| '--------------------------------------------------------------- | '--------------------------------------------------------------- | ||||
| Public Sub Edit(id) | |||||
| ' TODO: Implement Edit action | |||||
| Response.Write "<strong>Edit called</strong><br>" | |||||
| Response.Write "Parameters:<br>" | |||||
| Response.Write "id = " & Server.HTMLEncode(CStr(id)) & "<br>" | |||||
| Public Sub Edit(ByVal id) | |||||
| Dim post | |||||
| On Error Resume Next | |||||
| Set post = PostsRepository().FindByID(id) | |||||
| If Err.Number <> 0 Then | |||||
| Err.Clear | |||||
| On Error GoTo 0 | |||||
| Response.Status = "404 Not Found" | |||||
| %> | |||||
| <!--#include file="../views/Error/NotFound.asp" --> | |||||
| <% | |||||
| Exit Sub | |||||
| End If | |||||
| On Error GoTo 0 | |||||
| %> | |||||
| <!--#include file="../views/Posts/edit.asp" --> | |||||
| <% | |||||
| End Sub | End Sub | ||||
| '--------------------------------------------------------------- | '--------------------------------------------------------------- | ||||
| ' Action: Update | ' Action: Update | ||||
| '--------------------------------------------------------------- | '--------------------------------------------------------------- | ||||
| Public Sub Update(id) | |||||
| ' TODO: Implement Update action | |||||
| Response.Write "<strong>Update called</strong><br>" | |||||
| Response.Write "Parameters:<br>" | |||||
| Response.Write "id = " & Server.HTMLEncode(CStr(id)) & "<br>" | |||||
| Public Sub Update(ByVal id) | |||||
| Dim post | |||||
| On Error Resume Next | |||||
| Set post = PostsRepository().FindByID(id) | |||||
| If Err.Number <> 0 Then | |||||
| Err.Clear | |||||
| On Error GoTo 0 | |||||
| Response.Status = "404 Not Found" | |||||
| %> | |||||
| <!--#include file="../views/Error/NotFound.asp" --> | |||||
| <% | |||||
| Exit Sub | |||||
| End If | |||||
| On Error GoTo 0 | |||||
| Dim title : title = Trim(Request.Form("Title")) | |||||
| If Len(title) = 0 Then | |||||
| Flash().AddError "Title is required." | |||||
| Response.Redirect "/posts/" & Server.URLEncode(CStr(id)) & "/edit" | |||||
| Exit Sub | |||||
| End If | |||||
| post.Title = title | |||||
| post.Summary = Request.Form("Summary") | |||||
| post.Body = Request.Form("Body") | |||||
| post.CategoryID = FormNumberOrZero(Request.Form("CategoryID")) | |||||
| post.Slug = BuildSlug(title) | |||||
| post.UpdatedDate = Now() | |||||
| PostsRepository().Update post | |||||
| Flash().Success = "Post updated." | |||||
| Response.Redirect "/posts" | |||||
| End Sub | End Sub | ||||
| '--------------------------------------------------------------- | '--------------------------------------------------------------- | ||||
| ' Action: Delete | ' Action: Delete | ||||
| '--------------------------------------------------------------- | '--------------------------------------------------------------- | ||||
| Public Sub Delete(id) | |||||
| ' TODO: Implement Delete action | |||||
| Response.Write "<strong>Delete called</strong><br>" | |||||
| Response.Write "Parameters:<br>" | |||||
| Response.Write "id = " & Server.HTMLEncode(CStr(id)) & "<br>" | |||||
| Public Sub Delete(ByVal id) | |||||
| PostsRepository().Delete id | |||||
| Flash().Success = "Post deleted." | |||||
| Response.Redirect "/posts" | |||||
| End Sub | End Sub | ||||
| Private Function FormNumberOrZero(ByVal value) | |||||
| If IsNumeric(value) Then | |||||
| FormNumberOrZero = CLng(value) | |||||
| Else | |||||
| FormNumberOrZero = 0 | |||||
| End If | |||||
| End Function | |||||
| Private Function BuildSlug(ByVal value) | |||||
| Dim raw : raw = LCase(Trim(CStr(value))) | |||||
| Dim i, ch, slug, previousDash | |||||
| slug = "" | |||||
| previousDash = False | |||||
| For i = 1 To Len(raw) | |||||
| ch = Mid(raw, i, 1) | |||||
| If (ch >= "a" And ch <= "z") Or (ch >= "0" And ch <= "9") Then | |||||
| slug = slug & ch | |||||
| previousDash = False | |||||
| ElseIf Not previousDash And Len(slug) > 0 Then | |||||
| slug = slug & "-" | |||||
| previousDash = True | |||||
| End If | |||||
| Next | |||||
| Do While Right(slug, 1) = "-" | |||||
| slug = Left(slug, Len(slug) - 1) | |||||
| Loop | |||||
| If Len(slug) = 0 Then slug = "post" | |||||
| BuildSlug = slug | |||||
| End Function | |||||
| End Class | End Class | ||||
| ' Singleton instance | ' Singleton instance | ||||
| @@ -27,6 +27,54 @@ Class PostsRepository_Class | |||||
| Set GetAll = Find(Empty, orderBy) | Set GetAll = Find(Empty, orderBy) | ||||
| End Function | End Function | ||||
| Public Function FindAllWhere(ByVal where_clause, ByVal order_by, ByVal offset, ByVal limit) | |||||
| Dim sql : sql = "Select [Body], [CategoryID], [CreatedDate], [IsPublished], [PostID], [PublishedDate], [Slug], [Summary], [Title], [UpdatedDate] FROM [Posts]" | |||||
| Dim whereText : whereText = "" | |||||
| If Not IsEmpty(where_clause) Then | |||||
| If Not IsNull(where_clause) Then whereText = Trim(CStr(where_clause)) | |||||
| End If | |||||
| If Len(whereText) > 0 Then | |||||
| sql = sql & " WHERE " & whereText | |||||
| End If | |||||
| sql = sql & BuildOrderBy(order_by, "[PostID]") | |||||
| Dim offsetCount, limitCount | |||||
| If IsNumeric(offset) Then | |||||
| offsetCount = CLng(offset) | |||||
| Else | |||||
| offsetCount = 0 | |||||
| End If | |||||
| If offsetCount < 0 Then offsetCount = 0 | |||||
| If IsNumeric(limit) Then | |||||
| limitCount = CLng(limit) | |||||
| Else | |||||
| limitCount = 0 | |||||
| End If | |||||
| Dim rs : Set rs = DAL.Query(sql, Empty) | |||||
| Dim list : Set list = new LinkedList_Class | |||||
| Dim skipped, added | |||||
| skipped = 0 | |||||
| added = 0 | |||||
| Do Until rs.EOF | |||||
| If skipped < offsetCount Then | |||||
| skipped = skipped + 1 | |||||
| ElseIf limitCount <= 0 Or added < limitCount Then | |||||
| list.Push Automapper.AutoMap(rs, "POBO_Posts") | |||||
| added = added + 1 | |||||
| End If | |||||
| If limitCount > 0 And added >= limitCount Then Exit Do | |||||
| rs.MoveNext | |||||
| Loop | |||||
| Set FindAllWhere = list | |||||
| Destroy rs | |||||
| End Function | |||||
| Public Function Find(where_kvarray, order_string_or_array) | Public Function Find(where_kvarray, order_string_or_array) | ||||
| Dim sql : sql = "Select [Body], [CategoryID], [CreatedDate], [IsPublished], [PostID], [PublishedDate], [Slug], [Summary], [Title], [UpdatedDate] FROM [Posts]" | Dim sql : sql = "Select [Body], [CategoryID], [CreatedDate], [IsPublished], [PostID], [PublishedDate], [Slug], [Summary], [Title], [UpdatedDate] FROM [Posts]" | ||||
| Dim where_keys, where_values, i | Dim where_keys, where_values, i | ||||
| @@ -149,21 +197,65 @@ Class PostsRepository_Class | |||||
| QI = "[" & Replace(CStr(name), "]", "]]") & "]" | QI = "[" & Replace(CStr(name), "]", "]]") & "]" | ||||
| End Function | End Function | ||||
| Private Function BuildOrderBy(orderArg, defaultCol) | |||||
| Private Function BuildOrderBy(ByVal orderArg, ByVal defaultCol) | |||||
| Dim s : s = "" | Dim s : s = "" | ||||
| If IsEmpty(orderArg) Or IsNull(orderArg) Or orderArg = "" Then | |||||
| s = " ORDER BY " & defaultCol & " ASC" | |||||
| ElseIf IsArray(orderArg) Then | |||||
| If IsArray(orderArg) Then | |||||
| Dim i : s = " ORDER BY " | Dim i : s = " ORDER BY " | ||||
| For i = 0 To UBound(orderArg) | For i = 0 To UBound(orderArg) | ||||
| If i > 0 Then s = s & ", " | If i > 0 Then s = s & ", " | ||||
| s = s & QI(orderArg(i)) | s = s & QI(orderArg(i)) | ||||
| Next | Next | ||||
| ElseIf IsEmpty(orderArg) Then | |||||
| s = " ORDER BY " & defaultCol & " ASC" | |||||
| ElseIf IsNull(orderArg) Then | |||||
| s = " ORDER BY " & defaultCol & " ASC" | |||||
| ElseIf CStr(orderArg) = "" Then | |||||
| s = " ORDER BY " & defaultCol & " ASC" | |||||
| ElseIf IsSafeOrderExpression(orderArg) Then | |||||
| Dim parts : parts = Split(Trim(CStr(orderArg)), " ") | |||||
| s = " ORDER BY " & QI(parts(0)) | |||||
| If UBound(parts) = 1 Then s = s & " " & UCase(parts(1)) | |||||
| Else | Else | ||||
| s = " ORDER BY " & QI(orderArg) | s = " ORDER BY " & QI(orderArg) | ||||
| End If | End If | ||||
| BuildOrderBy = s | BuildOrderBy = s | ||||
| End Function | End Function | ||||
| Private Function IsSafeOrderExpression(ByVal orderArg) | |||||
| Dim raw : raw = Trim(CStr(orderArg)) | |||||
| Dim parts : parts = Split(raw, " ") | |||||
| IsSafeOrderExpression = False | |||||
| If UBound(parts) < 0 Or UBound(parts) > 1 Then Exit Function | |||||
| If Not IsSafeIdentifier(parts(0)) Then Exit Function | |||||
| If UBound(parts) = 1 Then | |||||
| If UCase(parts(1)) <> "ASC" And UCase(parts(1)) <> "DESC" Then Exit Function | |||||
| End If | |||||
| IsSafeOrderExpression = True | |||||
| End Function | |||||
| Private Function IsSafeIdentifier(ByVal value) | |||||
| Dim i, ch | |||||
| value = CStr(value) | |||||
| If Len(value) = 0 Then | |||||
| IsSafeIdentifier = False | |||||
| Exit Function | |||||
| End If | |||||
| For i = 1 To Len(value) | |||||
| ch = Mid(value, i, 1) | |||||
| If Not ((ch >= "a" And ch <= "z") Or _ | |||||
| (ch >= "A" And ch <= "Z") Or _ | |||||
| (ch >= "0" And ch <= "9") Or _ | |||||
| ch = "_") Then | |||||
| IsSafeIdentifier = False | |||||
| Exit Function | |||||
| End If | |||||
| Next | |||||
| IsSafeIdentifier = True | |||||
| End Function | |||||
| End Class | End Class | ||||
| Dim PostsRepository__Singleton | Dim PostsRepository__Singleton | ||||
| @@ -0,0 +1,40 @@ | |||||
| <div class="row"> | |||||
| <div class="col-lg-8"> | |||||
| <div class="card shadow-sm"> | |||||
| <div class="card-body"> | |||||
| <h1 class="h3 mb-4">Edit Post</h1> | |||||
| <form method="post" action="/posts/<%= H(post.PostID) %>"> | |||||
| <div class="mb-3"> | |||||
| <label class="form-label" for="Title">Title</label> | |||||
| <input class="form-control" type="text" id="Title" name="Title" value="<%= H(post.Title) %>" required> | |||||
| </div> | |||||
| <div class="mb-3"> | |||||
| <label class="form-label" for="Summary">Summary</label> | |||||
| <textarea class="form-control" id="Summary" name="Summary" rows="3"><%= H(post.Summary) %></textarea> | |||||
| </div> | |||||
| <div class="mb-3"> | |||||
| <label class="form-label" for="Body">Body</label> | |||||
| <textarea class="form-control" id="Body" name="Body" rows="10" required><%= H(post.Body) %></textarea> | |||||
| </div> | |||||
| <div class="mb-4"> | |||||
| <label class="form-label" for="CategoryID">Category ID</label> | |||||
| <input class="form-control" type="number" id="CategoryID" name="CategoryID" min="0" value="<%= H(post.CategoryID) %>"> | |||||
| </div> | |||||
| <div class="d-flex flex-wrap gap-2"> | |||||
| <button class="btn btn-primary" type="submit">Update Post</button> | |||||
| <a class="btn btn-outline-secondary" href="/posts">Cancel</a> | |||||
| </div> | |||||
| </form> | |||||
| <form method="post" action="/posts/<%= H(post.PostID) %>/delete" class="mt-3"> | |||||
| <button class="btn btn-outline-danger" type="submit">Delete Post</button> | |||||
| </form> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| @@ -0,0 +1,62 @@ | |||||
| <div class="d-flex align-items-center justify-content-between mb-4"> | |||||
| <div> | |||||
| <h1 class="h3 mb-1">Posts</h1> | |||||
| <p class="text-muted mb-0">Published articles from ASPBlogBrainOrdure.</p> | |||||
| </div> | |||||
| <a class="btn btn-primary" href="/posts/new">New Post</a> | |||||
| </div> | |||||
| <% | |||||
| Dim postIter, postItem, shownCount | |||||
| Set postIter = posts.Iterator() | |||||
| shownCount = 0 | |||||
| If posts.Count = 0 Then | |||||
| %> | |||||
| <div class="alert alert-secondary">No published posts are available yet.</div> | |||||
| <% | |||||
| Else | |||||
| %> | |||||
| <div class="row gy-3"> | |||||
| <% | |||||
| Do While postIter.HasNext And shownCount < 20 | |||||
| Set postItem = postIter.GetNext() | |||||
| shownCount = shownCount + 1 | |||||
| %> | |||||
| <div class="col-12"> | |||||
| <article class="card shadow-sm"> | |||||
| <div class="card-body"> | |||||
| <div class="d-flex flex-column flex-md-row justify-content-between gap-2"> | |||||
| <h2 class="h5 mb-1"> | |||||
| <a href="/posts/<%= Server.URLEncode(CStr(postItem.Slug)) %>" class="text-decoration-none"> | |||||
| <%= H(postItem.Title) %> | |||||
| </a> | |||||
| </h2> | |||||
| <% | |||||
| Dim publishedText | |||||
| publishedText = "" | |||||
| If IsDate(postItem.PublishedDate) Then | |||||
| If CDate(postItem.PublishedDate) > #1/1/1970# Then | |||||
| publishedText = FormatDateTime(postItem.PublishedDate, vbLongDate) | |||||
| End If | |||||
| End If | |||||
| If Len(publishedText) > 0 Then | |||||
| %> | |||||
| <span class="small text-muted"><%= H(publishedText) %></span> | |||||
| <% | |||||
| End If | |||||
| %> | |||||
| </div> | |||||
| <p class="text-muted mb-3"><%= H(postItem.Summary) %></p> | |||||
| <a class="btn btn-sm btn-outline-primary" href="/posts/<%= Server.URLEncode(CStr(postItem.Slug)) %>">Read</a> | |||||
| </div> | |||||
| </article> | |||||
| </div> | |||||
| <% | |||||
| Loop | |||||
| %> | |||||
| </div> | |||||
| <% | |||||
| End If | |||||
| %> | |||||
| @@ -0,0 +1,36 @@ | |||||
| <div class="row"> | |||||
| <div class="col-lg-8"> | |||||
| <div class="card shadow-sm"> | |||||
| <div class="card-body"> | |||||
| <h1 class="h3 mb-4">New Post</h1> | |||||
| <form method="post" action="/posts"> | |||||
| <div class="mb-3"> | |||||
| <label class="form-label" for="Title">Title</label> | |||||
| <input class="form-control" type="text" id="Title" name="Title" required> | |||||
| </div> | |||||
| <div class="mb-3"> | |||||
| <label class="form-label" for="Summary">Summary</label> | |||||
| <textarea class="form-control" id="Summary" name="Summary" rows="3"></textarea> | |||||
| </div> | |||||
| <div class="mb-3"> | |||||
| <label class="form-label" for="Body">Body</label> | |||||
| <textarea class="form-control" id="Body" name="Body" rows="10" required></textarea> | |||||
| </div> | |||||
| <div class="mb-4"> | |||||
| <label class="form-label" for="CategoryID">Category ID</label> | |||||
| <input class="form-control" type="number" id="CategoryID" name="CategoryID" min="0" value="0"> | |||||
| </div> | |||||
| <div class="d-flex gap-2"> | |||||
| <button class="btn btn-primary" type="submit">Create Post</button> | |||||
| <a class="btn btn-outline-secondary" href="/posts">Cancel</a> | |||||
| </div> | |||||
| </form> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| @@ -0,0 +1,33 @@ | |||||
| <article class="card shadow-sm"> | |||||
| <div class="card-body"> | |||||
| <div class="mb-3"> | |||||
| <a href="/posts" class="small text-decoration-none">← Back to posts</a> | |||||
| </div> | |||||
| <h1 class="h2 mb-2"><%= H(post.Title) %></h1> | |||||
| <% | |||||
| Dim publishedText | |||||
| publishedText = "" | |||||
| If IsDate(post.PublishedDate) Then | |||||
| If CDate(post.PublishedDate) > #1/1/1970# Then | |||||
| publishedText = FormatDateTime(post.PublishedDate, vbLongDate) | |||||
| End If | |||||
| End If | |||||
| If Len(publishedText) > 0 Then | |||||
| %> | |||||
| <p class="text-muted"><%= H(publishedText) %></p> | |||||
| <% | |||||
| End If | |||||
| Dim postBody | |||||
| postBody = H(post.Body) | |||||
| postBody = Replace(postBody, vbCrLf, "<br>") | |||||
| postBody = Replace(postBody, vbCr, "<br>") | |||||
| postBody = Replace(postBody, vbLf, "<br>") | |||||
| %> | |||||
| <div class="fs-5 lh-lg"><%= postBody %></div> | |||||
| </div> | |||||
| </article> | |||||
Powered by TurnKey Linux.