diff --git a/collects/web-server/scribblings/tutorial/iteration-1.ss b/collects/web-server/scribblings/tutorial/iteration-1.ss new file mode 100644 index 0000000000..480574817d --- /dev/null +++ b/collects/web-server/scribblings/tutorial/iteration-1.ss @@ -0,0 +1,38 @@ +#lang plai/web + +;; A blog is a (listof post) +;; and a post is a (make-post title body) +;; +(define-struct post (title body)) + +;; BLOG: blog +;; The static blog. +(define BLOG (list (make-post "First Post" "This is my first post") + (make-post "Second Post" "This is another post"))) + +;; start: request -> html-response +;; Consumes a request, and produces a page that displays all of the +;; web content. +(define (start request) + (render-blog-page BLOG request)) + +;; render-blog-page: blog request -> html-response +;; Consumes a blog and a request, and produces an html-response page +;; of the content of the blog. +(define (render-blog-page a-blog request) + `(html (head (title "My Blog")) + (body (h1 "My Blog") + ,(render-posts a-blog)))) + +;; render-post: post -> html-response +;; Consumes a post, produces an html-response fragment of the post. +(define (render-post a-post) + `(div ((class "post")) + ,(post-title a-post) + (p ,(post-body a-post)))) + +;; render-posts: blog -> html-response +;; Consumes a blog, produces an html-response fragment of all its posts. +(define (render-posts a-blog) + `(div ((class "posts")) + ,@(map render-post a-blog))) diff --git a/collects/web-server/scribblings/tutorial/iteration-2.ss b/collects/web-server/scribblings/tutorial/iteration-2.ss new file mode 100644 index 0000000000..e7c1ff1416 --- /dev/null +++ b/collects/web-server/scribblings/tutorial/iteration-2.ss @@ -0,0 +1,66 @@ +#lang plai/web + +;; A blog is a (listof post) +;; and a post is a (make-post title body) +;; +(define-struct post (title body)) + +;; BLOG: blog +;; The static blog. +(define BLOG (list (make-post "First Post" "This is my first post") + (make-post "Second Post" "This is another post"))) + +;; start: request -> html-response +;; Consumes a request and produces a page that displays all of the web +;; content. +(define (start request) + (local [(define a-blog + (cond [(can-parse-post? (request-bindings request)) + (cons (parse-post (request-bindings request)) + BLOG)] + [else + BLOG]))] + (render-blog-page a-blog request))) + + +;; can-parse-post?: bindings -> boolean +;; Produces true if bindings contains values for 'title and 'body. +(define (can-parse-post? bindings) + (and (exists-binding? 'title bindings) + (exists-binding? 'body bindings))) + + +;; parse-post: bindings -> post +;; Consuems a bindings, and produces a post out of the bindings. +(define (parse-post bindings) + (make-post (extract-binding/single 'title bindings) + (extract-binding/single 'body bindings))) + +;; render-blog-page: blog request -> html-response +;; Consumes a blog and a request, and produces an html-response page +;; of the content of the blog. +(define (render-blog-page a-blog request) + `(html (head (title "My Blog")) + (body + (h1 "My Blog") + ,(render-posts a-blog) + (form + (input ((name "title"))) + (input ((name "body"))) + (input ((type "submit"))))))) + + + +;; render-post: post -> html-response +;; Consumes a post, produces an html-response fragment of the post. +(define (render-post a-post) + `(div ((class "post")) + ,(post-title a-post) + (p ,(post-body a-post)))) + + +;; render-posts: blog -> html-response +;; Consumes a blog, produces an html-response fragment of all its posts. +(define (render-posts a-blog) + `(div ((class "posts")) + ,@(map render-post a-blog))) diff --git a/collects/web-server/scribblings/tutorial/iteration-3.ss b/collects/web-server/scribblings/tutorial/iteration-3.ss new file mode 100644 index 0000000000..107e7acdf8 --- /dev/null +++ b/collects/web-server/scribblings/tutorial/iteration-3.ss @@ -0,0 +1,56 @@ +#lang plai/web + +;; A blog is a (listof post) +;; and a post is a (make-post title body) +;; +(define-struct post (title body)) + +;; BLOG: blog +;; The static blog. +(define BLOG (list (make-post "First Post" "This is my first post") + (make-post "Second Post" "This is another post"))) + +;; start: request -> html-response +;; Consumes a request and produces a page that displays all of the web content. +(define (start request) + (render-blog-page BLOG request)) + +;; parse-post: bindings -> post +;; Extracts a post out of the bindings. +(define (parse-post bindings) + (make-post (extract-binding/single 'title bindings) + (extract-binding/single 'body bindings))) + +;; render-blog-page: blog request -> html-response +;; Consumes a blog and a request, and produces an html-response page of the content of the +;; blog. +(define (render-blog-page a-blog request) + (local [(define (response-generator make-url) + `(html (head (title "My Blog")) + (body + (h1 "My Blog") + ,(render-posts a-blog) + (form ((action ,(make-url insert-post-handler))) + (input ((name "title"))) + (input ((name "body"))) + (input ((type "submit"))))))) + + (define (insert-post-handler request) + (render-blog-page (cons (parse-post (request-bindings request)) + a-blog) + request))] + + (send/suspend/dispatch response-generator))) + +;; render-post: post -> html-response +;; Consumes a post, produces an html-response fragment of the post. +(define (render-post a-post) + `(div ((class "post")) + ,(post-title a-post) + (p ,(post-body a-post)))) + +;; render-posts: blog -> html-response +;; Consumes a blog, produces an html-response fragment of all its posts. +(define (render-posts a-blog) + `(div ((class "posts")) + ,@(map render-post a-blog))) diff --git a/collects/web-server/scribblings/tutorial/iteration-4.ss b/collects/web-server/scribblings/tutorial/iteration-4.ss new file mode 100644 index 0000000000..cc0b9bde48 --- /dev/null +++ b/collects/web-server/scribblings/tutorial/iteration-4.ss @@ -0,0 +1,66 @@ +#lang plai/web + +;; A blog is a (make-blog posts) +;; where posts is a (listof post) +;; +;; and post is a (make-post title body) +;; where title is a string, and body is a string +;; +(define-struct blog (posts) #:mutable) +(define-struct post (title body)) + +;; BLOG: blog +;; The initial BLOG. +(define BLOG (make-blog + (list (make-post "First Post" "This is my first post") + (make-post "Second Post" "This is another post")))) + +;; blog-insert-post!: blog post -> void +;; Consumes a blog and a post, adds the post at the top of the blog. +(define (blog-insert-post! a-blog a-post) + (set-blog-posts! a-blog + (cons a-post (blog-posts a-blog)))) + +;; start: request -> html-response +;; Consumes a request and produces a page that displays +;; all of the web content. +(define (start request) + (render-blog-page request)) + +;; parse-post: bindings -> post +;; Extracts a post out of the bindings. +(define (parse-post bindings) + (make-post (extract-binding/single 'title bindings) + (extract-binding/single 'body bindings))) + +;; render-blog-page: request -> html-response +;; Produces an html-response page of the content of the BLOG. +(define (render-blog-page request) + (local [(define (response-generator make-url) + `(html (head (title "My Blog")) + (body + (h1 "My Blog") + ,(render-posts) + (form ((action ,(make-url insert-post-handler))) + (input ((name "title"))) + (input ((name "body"))) + (input ((type "submit"))))))) + + (define (insert-post-handler request) + (blog-insert-post! BLOG (parse-post (request-bindings request))) + (render-blog-page request))] + + (send/suspend/dispatch response-generator))) + +;; render-post: post -> html-response +;; Consumes a post, produces an html-response fragment of the post. +(define (render-post a-post) + `(div ((class "post")) + ,(post-title a-post) + (p ,(post-body a-post)))) + +;; render-posts: -> html-response +;; Consumes a blog, produces an html-response fragment of all its posts. +(define (render-posts) + `(div ((class "posts")) + ,@(map render-post (blog-posts BLOG)))) diff --git a/collects/web-server/scribblings/tutorial/iteration-5.ss b/collects/web-server/scribblings/tutorial/iteration-5.ss new file mode 100644 index 0000000000..53e5c59bd9 --- /dev/null +++ b/collects/web-server/scribblings/tutorial/iteration-5.ss @@ -0,0 +1,124 @@ +#lang plai/web + +;; A blog is a (make-blog posts) +;; where posts is a (listof post) +;; +;; and post is a (make-post title body comments) +;; where title is a string, body is a string, and comments is a (listof string) +;; +(define-struct blog (posts) #:mutable) +(define-struct post (title body comments) #:mutable) + +;; BLOG: blog +;; The initial BLOG. +(define BLOG (make-blog + (list (make-post "First Post" + "This is my first post" + (list "First comment!")) + (make-post "Second Post" + "This is another post" + (list))))) + +;; blog-insert-post!: blog post -> void +;; Consumes a blog and a post, adds the post at the top of the blog. +(define (blog-insert-post! a-blog a-post) + (set-blog-posts! a-blog + (cons a-post (blog-posts a-blog)))) + + +;; post-insert-comment!: post string -> void +;; Consumes a post and a comment string. As a side-efect, +;; adds the comment to the bottom of the post's list of comments. +(define (post-insert-comment! a-post a-comment) + (set-post-comments! a-post + (append (post-comments a-post) (list a-comment)))) + +;; start: request -> html-response +;; Consumes a request, and produces a page that displays +;; all of the web content. +(define (start request) + (render-blog-page request)) + +;; render-blog-page: request -> html-response +;; Produces an html-response page of the content of the +;; BLOG. +(define (render-blog-page request) + (local [(define (response-generator make-url) + `(html (head (title "My Blog")) + (body + (h1 "My Blog") + ,(render-posts make-url) + (form ((action ,(make-url insert-post-handler))) + (input ((name "title"))) + (input ((name "body"))) + (input ((type "submit"))))))) + + ;; parse-post: bindings -> post + ;; Extracts a post out of the bindings. + (define (parse-post bindings) + (make-post (extract-binding/single 'title bindings) + (extract-binding/single 'body bindings) + (list))) + + (define (insert-post-handler request) + (blog-insert-post! BLOG (parse-post (request-bindings request))) + (render-blog-page request))] + + (send/suspend/dispatch response-generator))) + +;; render-post-detail-page: post request -> html-response +;; Consumes a post and request, and produces a detail page of the post. +;; The user will be able to insert new comments. +(define (render-post-detail-page a-post request) + (local [(define (response-generator make-url) + `(html (head (title "Post Details")) + (body + (h1 "Post Details") + (h2 ,(post-title a-post)) + (p ,(post-body a-post)) + ,(render-as-itemized-list (post-comments a-post)) + (form ((action ,(make-url insert-comment-handler))) + (input ((name "comment"))) + (input ((type "submit"))))))) + + (define (parse-comment bindings) + (extract-binding/single 'comment bindings)) + + (define (insert-comment-handler a-request) + (post-insert-comment! a-post (parse-comment (request-bindings a-request))) + (render-post-detail-page a-post a-request))] + + + (send/suspend/dispatch response-generator))) + + +;; render-post: post (handler -> string) -> html-response +;; Consumes a post, produces an html-response fragment of the post. +;; The fragment contains a link to show a detailed view of the post. +(define (render-post a-post make-url) + (local [(define (view-post-handler request) + (render-post-detail-page a-post request))] + `(div ((class "post")) + (a ((href ,(make-url view-post-handler))) ,(post-title a-post)) + (p ,(post-body a-post)) + (div ,(number->string (length (post-comments a-post))) + " comment(s)")))) + +;; render-posts: (handler -> string) -> html-response +;; Consumes a make-url, and produces an html-response fragment of all its posts. +(define (render-posts make-url) + (local [(define (render-post/make-url a-post) + (render-post a-post make-url))] + `(div ((class "posts")) + ,@(map render-post/make-url (blog-posts BLOG))))) + +;; render-as-itemized-list: (listof html-response) -> html-response +;; Consumes a list of items, and produces a rendering as +;; an unorderered list. +(define (render-as-itemized-list fragments) + `(ul ,@(map render-as-item fragments))) + +;; render-as-item: html-response -> html-response +;; Consumes an html-response, and produces a rendering as a list item. +(define (render-as-item a-fragment) + `(li ,a-fragment)) \ No newline at end of file diff --git a/collects/web-server/scribblings/tutorial/iteration-6.ss b/collects/web-server/scribblings/tutorial/iteration-6.ss new file mode 100644 index 0000000000..5b7c9463d8 --- /dev/null +++ b/collects/web-server/scribblings/tutorial/iteration-6.ss @@ -0,0 +1,152 @@ +#lang plai/web + +;; A blog is a (make-blog posts) +;; where posts is a (listof post) +;; +;; and post is a (make-post title body comments) +;; where title is a string, body is a string, and comments is a (listof string) +;; +(define-struct blog (posts) #:mutable) +(define-struct post (title body comments) #:mutable) + +;; BLOG: blog +;; The initial BLOG. +(define BLOG (make-blog + (list (make-post "First Post" + "This is my first post" + (list "First comment!")) + (make-post "Second Post" + "This is another post" + (list))))) + +;; blog-insert-post!: blog post -> void +;; Consumes a blog and a post, adds the post at the top of the blog. +(define (blog-insert-post! a-blog a-post) + (set-blog-posts! a-blog + (cons a-post (blog-posts a-blog)))) + + +;; post-insert-comment!: post string -> void +;; Consumes a post and a comment string. As a side-efect, +;; adds the comment to the bottom of the post's list of comments. +(define (post-insert-comment! a-post a-comment) + (set-post-comments! a-post + (append (post-comments a-post) (list a-comment)))) + +;; start: request -> html-response +;; Consumes a request and produces a page that displays +;; all of the web content. +(define (start request) + (render-blog-page request)) + +;; render-blog-page: request -> html-response +;; Produces an html-response page of the content of the +;; BLOG. +(define (render-blog-page request) + (local [(define (response-generator make-url) + `(html (head (title "My Blog")) + (body + (h1 "My Blog") + ,(render-posts make-url) + (form ((action ,(make-url insert-post-handler))) + (input ((name "title"))) + (input ((name "body"))) + (input ((type "submit"))))))) + + ;; parse-post: bindings -> post + ;; Extracts a post out of the bindings. + (define (parse-post bindings) + (make-post (extract-binding/single 'title bindings) + (extract-binding/single 'body bindings) + (list))) + + (define (insert-post-handler request) + (blog-insert-post! BLOG (parse-post (request-bindings request))) + (render-blog-page request))] + + (send/suspend/dispatch response-generator))) + +;; render-post-detail-page: post request -> html-response +;; Consumes a post and produces a detail page of the post. +;; The user will be able to either insert new comments or go back to render-blog-page. +(define (render-post-detail-page a-post request) + (local [(define (response-generator make-url) + `(html (head (title "Post Details")) + (body + (h1 "Post Details") + (h2 ,(post-title a-post)) + (p ,(post-body a-post)) + ,(render-as-itemized-list (post-comments a-post)) + (form ((action ,(make-url insert-comment-handler))) + (input ((name "comment"))) + (input ((type "submit")))) + (a ((href ,(make-url back-handler))) "Back to the blog")))) + + (define (parse-comment bindings) + (extract-binding/single 'comment bindings)) + + (define (insert-comment-handler request) + (render-confirm-add-comment-page (parse-comment (request-bindings request)) + a-post + request)) + + (define (back-handler request) + (render-blog-page request))] + + (send/suspend/dispatch response-generator))) + +;; render-confirm-add-comment-page: comment post request -> html-response +;; Consumes a comment that we intend to add to a post, as well as the request. +;; If the user follows through, adds a comment and goes back to the display page. +;; Otherwise, goes back to the detail page of the post. +(define (render-confirm-add-comment-page a-comment a-post request) + (local [(define (response-generator make-url) + `(html (head (title "Add a Comment")) + (body + (h1 "Add a Comment") + "The comment: " (div (p ,a-comment)) + "will be added to " + (div ,(post-title a-post)) + + (p (a ((href ,(make-url yes-handler))) "Yes, add the comment.")) + (p (a ((href ,(make-url cancel-handler))) "No, I changed my mind!"))))) + + (define (yes-handler request) + (post-insert-comment! a-post a-comment) + (render-post-detail-page a-post request)) + + (define (cancel-handler request) + (render-post-detail-page a-post request))] + + (send/suspend/dispatch response-generator))) + +;; render-post: post (handler -> string) -> html-response +;; Consumes a post, produces an html-response fragment of the post. +;; The fragment contains a link to show a detailed view of the post. +(define (render-post a-post make-url) + (local [(define (view-post-handler request) + (render-post-detail-page a-post request))] + `(div ((class "post")) + (a ((href ,(make-url view-post-handler))) ,(post-title a-post)) + (p ,(post-body a-post)) + (div ,(number->string (length (post-comments a-post))) + " comment(s)")))) + +;; render-posts: (handler -> string) -> html-response +;; Consumes a make-url, produces an html-response fragment of all its posts. +(define (render-posts make-url) + (local [(define (render-post/make-url a-post) + (render-post a-post make-url))] + `(div ((class "posts")) + ,@(map render-post/make-url (blog-posts BLOG))))) + +;; render-as-itemized-list: (listof html-response) -> html-response +;; Consumes a list of items, and produces a rendering as +;; an unorderered list. +(define (render-as-itemized-list fragments) + `(ul ,@(map render-as-item fragments))) + +;; render-as-item: html-response -> html-response +;; Consumes an html-response, and produces a rendering as a list item. +(define (render-as-item a-fragment) + `(li ,a-fragment)) \ No newline at end of file diff --git a/collects/web-server/scribblings/tutorial/iteration-7.ss b/collects/web-server/scribblings/tutorial/iteration-7.ss new file mode 100644 index 0000000000..bfdbb22a05 --- /dev/null +++ b/collects/web-server/scribblings/tutorial/iteration-7.ss @@ -0,0 +1,152 @@ +#lang plai/web + +;; A blog is a (make-blog posts) +;; where posts is a (listof post) +;; +;; and post is a (make-post title body comments) +;; where title is a string, body is a string, and comments is a (listof string) +;; +(define-struct blog (posts) #:mutable) +(define-struct post (title body comments) #:mutable) + +;; BLOG: blog +;; The initial BLOG. +(define BLOG (make-blog + (list (make-post "First Post" + "This is my first post" + (list "First comment!")) + (make-post "Second Post" + "This is another post" + (list))))) + +;; blog-insert-post!: blog post -> void +;; Consumes a blog and a post, adds the post at the top of the blog. +(define (blog-insert-post! a-blog a-post) + (set-blog-posts! a-blog + (cons a-post (blog-posts a-blog)))) + + +;; post-insert-comment!: post string -> void +;; Consumes a post and a comment string. As a side-efect, +;; adds the comment to the bottom of the post's list of comments. +(define (post-insert-comment! a-post a-comment) + (set-post-comments! a-post + (append (post-comments a-post) (list a-comment)))) + +;; start: request -> html-response +;; Consumes a request and produces a page that displays +;; all of the web content. +(define (start request) + (render-blog-page request)) + +;; render-blog-page: request -> html-response +;; Produces an html-response page of the content of the +;; BLOG. +(define (render-blog-page request) + (local [(define (response-generator make-url) + `(html (head (title "My Blog")) + (body + (h1 "My Blog") + ,(render-posts make-url) + (form ((action ,(make-url insert-post-handler))) + (input ((name "title"))) + (input ((name "body"))) + (input ((type "submit"))))))) + + ;; parse-post: bindings -> post + ;; Extracts a post out of the bindings. + (define (parse-post bindings) + (make-post (extract-binding/single 'title bindings) + (extract-binding/single 'body bindings) + (list))) + + (define (insert-post-handler request) + (blog-insert-post! BLOG (parse-post (request-bindings request))) + (render-blog-page (redirect/get)))] + + (send/suspend/dispatch response-generator))) + +;; render-post-detail-page: post request -> html-response +;; Consumes a post and produces a detail page of the post. +;; The user will be able to either insert new comments or go back to render-blog-page. +(define (render-post-detail-page a-post request) + (local [(define (response-generator make-url) + `(html (head (title "Post Details")) + (body + (h1 "Post Details") + (h2 ,(post-title a-post)) + (p ,(post-body a-post)) + ,(render-as-itemized-list (post-comments a-post)) + (form ((action ,(make-url insert-comment-handler))) + (input ((name "comment"))) + (input ((type "submit")))) + (a ((href ,(make-url back-handler))) "Back to the blog")))) + + (define (parse-comment bindings) + (extract-binding/single 'comment bindings)) + + (define (insert-comment-handler request) + (render-confirm-add-comment-page (parse-comment (request-bindings request)) + a-post + request)) + + (define (back-handler request) + (render-blog-page request))] + + (send/suspend/dispatch response-generator))) + +;; render-confirm-add-comment-page: comment post request -> html-response +;; Consumes a comment that we intend to add to a post, as well as the request. +;; If the user follows through, adds a comment and goes back to the display page. +;; Otherwise, goes back to the detail page of the post. +(define (render-confirm-add-comment-page a-comment a-post request) + (local [(define (response-generator make-url) + `(html (head (title "Add a Comment")) + (body + (h1 "Add a Comment") + "The comment: " (div (p ,a-comment)) + "will be added to " + (div ,(post-title a-post)) + + (p (a ((href ,(make-url yes-handler))) "Yes, add the comment.")) + (p (a ((href ,(make-url cancel-handler))) "No, I changed my mind!"))))) + + (define (yes-handler request) + (post-insert-comment! a-post a-comment) + (render-post-detail-page a-post (redirect/get))) + + (define (cancel-handler request) + (render-post-detail-page a-post request))] + + (send/suspend/dispatch response-generator))) + +;; render-post: post (handler -> string) -> html-response +;; Consumes a post, produces an html-response fragment of the post. +;; The fragment contains a link to show a detailed view of the post. +(define (render-post a-post make-url) + (local [(define (view-post-handler request) + (render-post-detail-page a-post request))] + `(div ((class "post")) + (a ((href ,(make-url view-post-handler))) ,(post-title a-post)) + (p ,(post-body a-post)) + (div ,(number->string (length (post-comments a-post))) + " comment(s)")))) + +;; render-posts: (handler -> string) -> html-response +;; Consumes a make-url, produces an html-response fragment of all its posts. +(define (render-posts make-url) + (local [(define (render-post/make-url a-post) + (render-post a-post make-url))] + `(div ((class "posts")) + ,@(map render-post/make-url (blog-posts BLOG))))) + +;; render-as-itemized-list: (listof html-response) -> html-response +;; Consumes a list of items, and produces a rendering as +;; an unorderered list. +(define (render-as-itemized-list fragments) + `(ul ,@(map render-as-item fragments))) + +;; render-as-item: html-response -> html-response +;; Consumes an html-response, and produces a rendering as a list item. +(define (render-as-item a-fragment) + `(li ,a-fragment)) \ No newline at end of file diff --git a/collects/web-server/scribblings/tutorial/iteration-8.ss b/collects/web-server/scribblings/tutorial/iteration-8.ss new file mode 100644 index 0000000000..565f979344 --- /dev/null +++ b/collects/web-server/scribblings/tutorial/iteration-8.ss @@ -0,0 +1,122 @@ +#lang plai/web + +(require "model.ss") + + +;; start: request -> html-response +;; Consumes a request and produces a page that displays +;; all of the web content. +(define (start request) + (render-blog-page request)) + +;; render-blog-page: request -> html-response +;; Produces an html-response page of the content of the +;; BLOG. +(define (render-blog-page request) + (local [(define (response-generator make-url) + `(html (head (title "My Blog")) + (body + (h1 "My Blog") + ,(render-posts make-url) + (form ((action ,(make-url insert-post-handler))) + (input ((name "title"))) + (input ((name "body"))) + (input ((type "submit"))))))) + + ;; parse-post: bindings -> post + ;; Extracts a post out of the bindings. + (define (parse-post bindings) + (make-post (extract-binding/single 'title bindings) + (extract-binding/single 'body bindings) + (list))) + + (define (insert-post-handler request) + (blog-insert-post! BLOG (parse-post (request-bindings request))) + (render-blog-page (redirect/get)))] + + (send/suspend/dispatch response-generator))) + +;; render-post-detail-page: post request -> html-response +;; Consumes a post and produces a detail page of the post. +;; The user will be able to either insert new comments or go back to render-blog-page. +(define (render-post-detail-page a-post request) + (local [(define (response-generator make-url) + `(html (head (title "Post Details")) + (body + (h1 "Post Details") + (h2 ,(post-title a-post)) + (p ,(post-body a-post)) + ,(render-as-itemized-list (post-comments a-post)) + (form ((action ,(make-url insert-comment-handler))) + (input ((name "comment"))) + (input ((type "submit")))) + (a ((href ,(make-url back-handler))) "Back to the blog")))) + + (define (parse-comment bindings) + (extract-binding/single 'comment bindings)) + + (define (insert-comment-handler request) + (render-confirm-add-comment-page (parse-comment (request-bindings request)) + a-post + request)) + + (define (back-handler request) + (render-blog-page request))] + + (send/suspend/dispatch response-generator))) + +;; render-confirm-add-comment-page: comment post request -> html-response +;; Consumes a comment that we intend to add to a post, as well as the request. +;; If the user follows through, adds a comment and goes back to the display page. +;; Otherwise, goes back to the detail page of the post. +(define (render-confirm-add-comment-page a-comment a-post request) + (local [(define (response-generator make-url) + `(html (head (title "Add a Comment")) + (body + (h1 "Add a Comment") + "The comment: " (div (p ,a-comment)) + "will be added to " + (div ,(post-title a-post)) + + (p (a ((href ,(make-url yes-handler))) "Yes, add the comment.")) + (p (a ((href ,(make-url cancel-handler))) "No, I changed my mind!"))))) + + (define (yes-handler request) + (post-insert-comment! a-post a-comment) + (render-post-detail-page a-post (redirect/get))) + + (define (cancel-handler request) + (render-post-detail-page a-post request))] + + (send/suspend/dispatch response-generator))) + +;; render-post: post (handler -> string) -> html-response +;; Consumes a post, produces an html-response fragment of the post. +;; The fragment contains a link to show a detailed view of the post. +(define (render-post a-post make-url) + (local [(define (view-post-handler request) + (render-post-detail-page a-post request))] + `(div ((class "post")) + (a ((href ,(make-url view-post-handler))) ,(post-title a-post)) + (p ,(post-body a-post)) + (div ,(number->string (length (post-comments a-post))) + " comment(s)")))) + +;; render-posts: (handler -> string) -> html-response +;; Consumes a make-url, produces an html-response fragment of all its posts. +(define (render-posts make-url) + (local [(define (render-post/make-url a-post) + (render-post a-post make-url))] + `(div ((class "posts")) + ,@(map render-post/make-url (blog-posts BLOG))))) + +;; render-as-itemized-list: (listof html-response) -> html-response +;; Consumes a list of items, and produces a rendering as +;; an unorderered list. +(define (render-as-itemized-list fragments) + `(ul ,@(map render-as-item fragments))) + +;; render-as-item: html-response -> html-response +;; Consumes an html-response, and produces a rendering as a list item. +(define (render-as-item a-fragment) + `(li ,a-fragment)) \ No newline at end of file diff --git a/collects/web-server/scribblings/tutorial/model.ss b/collects/web-server/scribblings/tutorial/model.ss new file mode 100644 index 0000000000..4f79c18bc4 --- /dev/null +++ b/collects/web-server/scribblings/tutorial/model.ss @@ -0,0 +1,37 @@ +#lang scheme + +;; A blog is a (make-blog posts) +;; where posts is a (listof post) +;; +;; and post is a (make-post title body comments) +;; where title is a string, body is a string, and comments is a (listof string) +;; +(define-struct blog (posts) #:mutable) +(define-struct post (title body comments) #:mutable) + +;; BLOG: blog +;; The initial BLOG. +(define BLOG (make-blog + (list (make-post "First Post" + "This is my first post" + (list "First comment!")) + (make-post "Second Post" + "This is another post" + (list))))) + +;; blog-insert-post!: blog post -> void +;; Consumes a blog and a post, adds the post at the top of the blog. +(define (blog-insert-post! a-blog a-post) + (set-blog-posts! a-blog + (cons a-post (blog-posts a-blog)))) + + +;; post-insert-comment!: post string -> void +;; Consumes a post and a comment string. As a side-efect, +;; adds the comment to the bottom of the post's list of comments. +(define (post-insert-comment! a-post a-comment) + (set-post-comments! a-post + (append (post-comments a-post) (list a-comment)))) + + +(provide (all-defined-out)) diff --git a/collects/web-server/scribblings/tutorial/no-use-redirect.ss b/collects/web-server/scribblings/tutorial/no-use-redirect.ss new file mode 100644 index 0000000000..6694ffb560 --- /dev/null +++ b/collects/web-server/scribblings/tutorial/no-use-redirect.ss @@ -0,0 +1,43 @@ +#lang plai/web + +;; A roster is a (make-roster names) +;; where names is a list of string. +(define-struct roster (names) #:mutable) + +;; roster-add-name!: roster string -> void +;; Given a roster and a name, adds the name to the end of the roster. +(define (roster-add-name! a-roster a-name) + (set-roster-names! a-roster + (append (roster-names a-roster) + (list a-name)))) + +(define ROSTER (make-roster '("kathi" "shriram" "dan"))) + +;; start: request -> html-response +(define (start request) + (show-roster request)) + +;; show-roster: request -> html-response +(define (show-roster request) + (local [(define (response-generator make-url) + `(html (head (title "Roster")) + (body (h1 "Roster") + ,(render-as-itemized-list (roster-names ROSTER)) + (form ((action ,(make-url add-name-handler))) + (input ((name "a-name"))) + (input ((type "submit"))))))) + (define (parse-name bindings) + (extract-binding/single 'a-name bindings)) + + (define (add-name-handler request) + (roster-add-name! ROSTER (parse-name (request-bindings request))) + (show-roster request))] + (send/suspend/dispatch response-generator))) + +;; render-as-itemized-list: (listof html-response) -> html-response +(define (render-as-itemized-list fragments) + `(ul ,@(map render-as-item fragments))) + +;; render-as-item: html-response -> html-response +(define (render-as-item a-fragment) + `(li ,a-fragment)) \ No newline at end of file diff --git a/collects/web-server/scribblings/tutorial/send-suspend-1.ss b/collects/web-server/scribblings/tutorial/send-suspend-1.ss new file mode 100644 index 0000000000..6a3dd03068 --- /dev/null +++ b/collects/web-server/scribblings/tutorial/send-suspend-1.ss @@ -0,0 +1,20 @@ +#lang plai/web + +;; start: request -> html-response +(define (start request) + (send/suspend/dispatch + (lambda (make-url) + `(html + (body + (a ((href ,(make-url link-1))) "Link 1") + (a ((href ,(make-url link-2))) "Link 2")))))) + + +;; link-1: request -> html-response +(define (link-1 request) + "This is link-1") + + +;; link-2: request -> html-response +(define (link-2 request) + "This is link-2") diff --git a/collects/web-server/scribblings/tutorial/send-suspend-2.ss b/collects/web-server/scribblings/tutorial/send-suspend-2.ss new file mode 100644 index 0000000000..b679c30956 --- /dev/null +++ b/collects/web-server/scribblings/tutorial/send-suspend-2.ss @@ -0,0 +1,15 @@ +#lang plai/web + +(define (start request) + (show-counter 0)) + +;; show-counter: number -> html-response +(define (show-counter n) + (send/suspend/dispatch + (lambda (make-url) + `(html (head (title "Counting example")) + (body + (a ((href ,(make-url + (lambda (request) + (show-counter (+ n 1)))))) + ,(number->string n))))))) \ No newline at end of file diff --git a/collects/web-server/scribblings/tutorial/test-static.ss b/collects/web-server/scribblings/tutorial/test-static.ss new file mode 100644 index 0000000000..efc88f9beb --- /dev/null +++ b/collects/web-server/scribblings/tutorial/test-static.ss @@ -0,0 +1,9 @@ +#lang plai/web +(define (start request) + '(html (head (title "Testing")) + (link ((rel "stylesheet") + (href "/test-static.css") + (type "text/css"))) + (body (h1 "This is a header") + (p "This is " (span ((class "hot")) "hot") ".")))) +(static-files-path "htdocs") \ No newline at end of file diff --git a/collects/web-server/scribblings/tutorial/tutorial.txt b/collects/web-server/scribblings/tutorial/tutorial.txt new file mode 100644 index 0000000000..4579ce7740 --- /dev/null +++ b/collects/web-server/scribblings/tutorial/tutorial.txt @@ -0,0 +1,865 @@ +Web Application tutorial +======================== + +How do we make dynamic web applications? This guide will show how we +can build web applications using PLT Scheme. As our working example, +we'll build a simple web journal (a "blog"). We'll cover how to start +up a web server, how to generate dynamic web content, and how to +interact with the user. + +The target audience for this guide are students who've gone through +the design and use of structures in How to Design Programs, as well as +use of higher-order functions, local, and a minor bit of mutation. + + +Getting started +--------------- + +In addition to PLT Scheme (http://plt-scheme.org/), we also need to +grab the PLAI package. Open up DrScheme; in the File menu, select +Install .PLT File, and paste in the URL: + + http://www.cs.brown.edu/~sk/Publications/Books/ProgLangs/plai-4.plt + +Once the package is installed, make sure DrScheme is in the Module +language, and enter the following into the Definition window. + +#lang plai/web +(define (start request) + '(html + (head (title "My Blog")) + (body (h1 "Under construction")))) + + +Press the Run button. If a web browser comes up with an "Under +Construction" page, then clap your hands with delight: you've built +your first web application! It doesn't do much yet, but we will get +there. Press the Stop button to shut the server down for now. + + + +The application +--------------- + +We want to motivate this tutorial by showing how to develop a +web-application that allows users to create posts and add comments to +any posts. We'll take an iterative approach, with one or two pitfalls +along the way. The game plan, roughly, will be: + + * Show a static list of posts. + + * Allow a user to add new posts to the system. + + * Extend the model to let a user add comments to a post. + + * Allow all users to share the same set of posts. + + * Serialize our data structures to disk. + +By the end of this tutorial, we'll have a simple blogging application. + + +Iteration 1 +----------- + +We start by considering our data definitions. We want to represent a +list of posts. Let's say that a post is a. + + (make-post title body) + +where title and body are each a string. + +*** +Exercise: make a few examples of posts. +*** + + +A blog, then, will be a (listof post). + +As a very simple example of a blog: + +(define BLOG (list (make-post "First Post!" + "Hey, this is my first post!") + ;; add your examples here ... + )) + + +Now that we have a sample BLOG structure, let's get our web +application to show it. + + + +Rendering HTML with quasiquotation +---------------------------------- + +When a web browser visits our application's URL, the browser +constructs a request structure and sends it off to our web +application. Our start function will consume requests and produce +responses. One basic kind of response is to show an HTML page. + +An html-response is either a: + + string, + + (cons symbol (listof html-response)), or + + (cons symbol + (cons (listof (list symbol string)) + (listof html-response))) + +For example: + + HTML html-response + ------------------------- ------------------------- + hello "hello" + +
This is an example
'(p "This is an example") + + Past '(a ((href "link.html")) "Past") + +This is '(p "This is " +