From 1b561ac7bee6b97975ee73418ac88ae54975e2ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phaneDucasse?= Date: Sun, 25 Aug 2024 21:18:08 +0200 Subject: [PATCH] grammarly full pass --- .../Chap06-TinyBlog-SeasideCategories-EN.md | 325 ++++++- ...hap06-TinyBlog-SeasideUserComponents-EN.md | 480 ++++++++++- .../Chap07-TinyBlog-Authentification-EN.md | 805 ++++++++++++++++-- Chapters/Chap08-TinyBlog-Admin-EN.md | 550 +++++++++++- Chapters/Chap13-TinyBlog-Deployment-EN.md | 133 ++- Chapters/Chap14-TinyBlog-Loading-EN.md | 160 +++- Chapters/Chap15-TinyBlog-SavingCode-EN.md | 20 +- 7 files changed, 2285 insertions(+), 188 deletions(-) diff --git a/Chapters/Chap06-TinyBlog-SeasideCategories-EN.md b/Chapters/Chap06-TinyBlog-SeasideCategories-EN.md index 6cd14d9..d4e3e0d 100644 --- a/Chapters/Chap06-TinyBlog-SeasideCategories-EN.md +++ b/Chapters/Chap06-TinyBlog-SeasideCategories-EN.md @@ -1,37 +1,214 @@ -## Managing Categories In this chapter, we add the possibility to sort posts in a category. Figure *@AssociationArchitectureUser@* shows you on which components we will work in this chapter. ![L'architecture des composants de la partie publique with categories.](figures/ApplicationArchitectureUser.pdf width=75&label=AssociationArchitectureUser) You can find instructions to load the code of previous chapter in Chapter *@cha:loading@*. ### Displaying Posts by Category Posts are sorted by a category. If no category is specified, posts are sorted in a special category called "Unclassified". To manage a list of categories, we will define a component named `TBCategoriesComponent`. #### Displaying Categories We need a component to display a list of categories defined in the blog. This component should support the selection of one category. This component should be able to communicate with the component `TBPostsListComponent` to give it the currently selected category. Figure *@AssociationArchitectureUser@* described the situation. Remember that a category is simply expressed as a string in the model we defined in Chapter *@cha:model@* and how the following test illustrates it: ``` testAllBlogPostsFromCategory - self assert: (blog allBlogPostsFromCategory: 'First Category') size equals: 1 ``` #### Component Definition Let us define a new component named `TBCategoriesComponent`. It keeps a sorted collection of string representing each category as well as a reference to the component managing the post list. ``` WAComponent subclass: #TBCategoriesComponent +## Managing Categories + + +In this chapter, we add the possibility of sorting posts in a category. Figure *@AssociationArchitectureUser@* shows you which components we will work on in this chapter. + +![Component architecture of the Public category. %width=75&label=AssociationArchitectureUser](figures/ApplicationArchitectureUser.pdf ) + +You can find instructions to load the code of the previous chapter in Chapter *@cha:loading@*. + + +### Displaying Posts by Category + + +Posts are sorted by a category. If no category is specified, posts are sorted into a special category called "Unclassified". +To manage a list of categories, we will define a component named `TBCategoriesComponent`. + + +#### Displaying Categories + + +We need a component to display a list of categories defined in the blog. This component should support the +selection of one category. +This component should be able to communicate with the component `TBPostsListComponent` to give it the currently selected category. Figure *@AssociationArchitectureUser@* described the situation. + + +Remember that a category is simply expressed as a string in the model we defined in Chapter *@cha:model@* and how the following test illustrates it: + +``` +testAllBlogPostsFromCategory + self assert: (blog allBlogPostsFromCategory: 'First Category') size equals: 1 +``` + + + +#### Component Definition + + +Let us define a new component named `TBCategoriesComponent`. It keeps a sorted collection of strings representing each category as well as a reference to the component managing the post list. + +``` +WAComponent subclass: #TBCategoriesComponent instanceVariableNames: 'categories postsList' classVariableNames: '' - package: 'TinyBlog-Components' ``` We define the associated accessors. ``` TBCategoriesComponent >> categories - ^ categories ``` ``` TBCategoriesComponent >> categories: aCollection - categories := aCollection asSortedCollection ``` ``` TBCategoriesComponent >> postsList: aComponent - postsList := aComponent ``` ``` TBCategoriesComponent >> postsList - ^ postsList ``` We define a creation method as a class method. ``` TBCategoriesComponent class >> categories: categories postsList: aTBScreen - ^ self new categories: categories; postsList: aTBScreen ``` #### From the Post List In the class `TBPostsListComponent`, we need to add an instance variable to store the current category. ``` TBScreenComponent subclass: #TBPostsListComponent + package: 'TinyBlog-Components' +``` + + +We define the associated accessors. + +``` +TBCategoriesComponent >> categories + ^ categories +``` + + +``` +TBCategoriesComponent >> categories: aCollection + categories := aCollection asSortedCollection +``` + + +``` +TBCategoriesComponent >> postsList: aComponent + postsList := aComponent +``` + + +``` +TBCategoriesComponent >> postsList + ^ postsList +``` + + +We define a creation method as a class method. + +``` +TBCategoriesComponent class >> categories: categories postsList: aTBScreen + ^ self new categories: categories; postsList: aTBScreen +``` + + + +#### From the Post List + + +In the class `TBPostsListComponent`, we need to add an instance variable to store the current category. + +``` +TBScreenComponent subclass: #TBPostsListComponent instanceVariableNames: 'currentCategory' classVariableNames: '' - package: 'TinyBlog-Components' ``` We define its associated accessors. ``` TBPostsListComponent >> currentCategory - ^ currentCategory ``` ``` TBPostsListComponent >> currentCategory: anObject - currentCategory := anObject ``` #### The method selectCategory: We define the method `selectCategory:` \(protocol 'actions'\) to communicate the current category to the `TBPostsListComponent` component. ``` TBCategoriesComponent >> selectCategory: aCategory - postsList currentCategory: aCategory ``` % Notez que si nous voulions avoir un effet visuel plus avancé le composant catégories devrait peut être lui aussi garder trace de la catégorie couramment sélectionnée. ### Category Rendering We can now define method for the rendering of the category component on the page. Let us call it `renderCategoryLinkOn:with:`, we define in particular that clicking on a category will select it as the current one. We use a callback \(message `callback:`\). The argument of this message is a block that can contains any Pharo expression. This illustrates how simple is to call function to react to event. ``` TBCategoriesComponent >> renderCategoryLinkOn: html with: aCategory + package: 'TinyBlog-Components' +``` + + +We define its associated accessors. + +``` +TBPostsListComponent >> currentCategory + ^ currentCategory +``` + + +``` +TBPostsListComponent >> currentCategory: anObject + currentCategory := anObject +``` + + + +#### The method selectCategory: + + +We define the method `selectCategory:` (protocol 'actions') to communicate the current category to the `TBPostsListComponent` component. + +``` +TBCategoriesComponent >> selectCategory: aCategory + postsList currentCategory: aCategory +``` + + +% Notez que si nous voulions avoir un effet visuel plus avancé le composant catégories devrait peut être lui aussi garder trace de la catégorie couramment sélectionnée. + + +### Category Rendering + + +We can now define method for the rendering of the category component on the page. +Let us call it `renderCategoryLinkOn:with:`, we define in particular that clicking on a category will select it as the current one. We use a callback \(message `callback:`\). The argument of this message is a block that can contain any Pharo expression. This illustrates how simple is to call a function to react to an event. + +``` +TBCategoriesComponent >> renderCategoryLinkOn: html with: aCategory html tbsLinkifyListGroupItem callback: [ self selectCategory: aCategory ]; - with: aCategory ``` The method `renderContentOn:` of `TBCategoriesComponent` is simple: we iterate on all categories and we display them using Bootstrap. ``` TBCategoriesComponent >> renderContentOn: html + with: aCategory +``` + + +The method `renderContentOn:` of `TBCategoriesComponent` is simple: we iterate on all categories and we display them using Bootstrap. + +``` +TBCategoriesComponent >> renderContentOn: html html tbsListGroup: [ html tbsListGroupItem with: [ html strong: 'Categories' ]. categories do: [ :cat | - self renderCategoryLinkOn: html with: cat ] ] ``` We are nearly there. We need to display the list of categories and update the posts based on the current category. ### Updating Post List Now we should update the list of posts. We modify the rendering method of the component `TBPostsListComponent`. The method `readSelectedPosts` collects the posts to be displayed. It filters them based on the current category. When the current category is nil, it means that the user did not select yet a category. Therefore we display all the posts. When the current category is something else than nil, the user selected a category and the application display the corresponding posts. ``` TBPostsListComponent >> readSelectedPosts + self renderCategoryLinkOn: html with: cat ] ] +``` + + +We are nearly there. We need to display the list of categories and update the posts based on the current category. + +### Updating Post List + + +Now we should update the list of posts. +We modify the rendering method of the component `TBPostsListComponent`. + +The method `readSelectedPosts` collects the posts to be displayed. It filters them based on the current category. +When the current category is nil, it means that the user did not select yet a category. Therefore we display all the posts. When the current category is something else than nil, +the user selected a category and the application displays the corresponding posts. + +``` +TBPostsListComponent >> readSelectedPosts ^ self currentCategory ifNil: [ self blog allVisibleBlogPosts ] - ifNotNil: [ self blog allVisibleBlogPostsFromCategory: self currentCategory ] ``` We modify now the method responsible of the post list rendering: ``` TBPostsListComponent >> renderContentOn: html + ifNotNil: [ self blog allVisibleBlogPostsFromCategory: self currentCategory ] +``` + + +We modify now the method responsible of the post list rendering: + +``` +TBPostsListComponent >> renderContentOn: html super renderContentOn: html. html render: (TBCategoriesComponent categories: (self blog allCategories) postsList: self). html tbsContainer: [ self readSelectedPosts do: [ :p | - html render: (TBPostComponent new post: p) ] ] ``` An instance of the component `TBCategoriesComponent` is added to the page and allows one to select the current category \(see Figure *@ugly@*\). As previously explained, a new instance of `TBCategoriesComponent` is created each time the component `TBPostsListComponent` is rendered, therefore it is not mandatory to add it to the `children` sublist of the component. ![Categories and Posts.](figures/categoriesUgly.png width=75&label=ugly) #### Possible Enhancements Hardcodeing class name and the creation logic of categories and posts is not really optimal. Propose some solution. ### Look and Layout We will not place better the component `TBPostsListComponent` using a more 'responsive' design \(as shown in Figure *@nicer5@*\). It means that the CSS style should adapt the component to the available space. Components are placed in a Bootstrap container then positioned on a line with two columns. Column dimension is determined based on the viewport and resolution of the device used. The 12 columsn of Bootstrap are distributed over the category and post lists. In the case of a low resolution, the list of categories is placed above the post list \(each element using 100% of the container width\). ``` TBPostsListComponent >> renderContentOn: html + html render: (TBPostComponent new post: p) ] ] +``` + + +An instance of the component `TBCategoriesComponent` is added to the page and allows one to select the current category (see Figure *@ugly@*). + +As previously explained, a new instance of `TBCategoriesComponent` is created each time the component + `TBPostsListComponent` is rendered, therefore it is not mandatory to add it to the `children` sublist of the component. + +![Categories and Posts. % width=75&label=ugly](figures/categoriesUgly.png) + +#### Possible Enhancements + + +Hardcoding class name and the creation logic of categories and posts is not really optimal. +Propose some solution. + +### Look and Layout + + +We will not place better the component `TBPostsListComponent` using a more 'responsive' design \(as shown in Figure *@nicer5@*\). It means that the CSS style should adapt the component to the available space. + +Components are placed in a Bootstrap container then positioned on a line with two columns. +Column dimension is determined based on the viewport and resolution of the device used. +The 12 columns of Bootstrap are distributed over the category and post lists. + +In the case of a low resolution, the list of categories is placed above the post list (each element using the one hundred percent of the container width). + + +``` +TBPostsListComponent >> renderContentOn: html super renderContentOn: html. html tbsContainer: [ html tbsRow showGrid; @@ -50,15 +227,50 @@ mediumSize: 8; with: [ self readSelectedPosts do: [ :p | - html render: (TBPostComponent new post: p) ] ] ] ] ``` You should obtain a situation close to the one presented in Figure *@nicer5@*. ![Post list with a better layout.](figures/NicerCategories.png width=75&label=nicer5) When one selects a category, the post list is updated. However, the selected category is not selected. We modify the following method to address this point. ``` TBCategoriesComponent >> renderCategoryLinkOn: html with: aCategory + html render: (TBPostComponent new post: p) ] ] ] ] +``` + + +You should obtain a situation close to the one presented in Figure *@nicer5@*. + +![Post list with a better layout. %width=75&label=nicer5](figures/NicerCategories.png ) + +When one selects a category, the post list is updated. However, the selected category is not selected. We modify the +following method to address this point. + +``` +TBCategoriesComponent >> renderCategoryLinkOn: html with: aCategory html tbsLinkifyListGroupItem class: 'active' if: aCategory = self postsList currentCategory; callback: [ self selectCategory: aCategory ]; - with: aCategory ``` Even if the code works, we cannot keep the method ` renderContentOn:` in such state. It is far too long and not reusable. Propose a solution. ### Modular Code with Small Methods Here is our solution to the previous problem. To ease reading and future reuse, we start to define component creation methods. ``` TBPostsListComponent >> categoriesComponent + with: aCategory +``` + + +Even if the code works, we cannot keep the method `renderContentOn:` in such a state. It is far too long and not reusable. Propose a solution. + + +### Modular Code with Small Methods + + +Here is our solution to the previous problem. To ease reading and future reuse, we start to define component creation methods. + +``` +TBPostsListComponent >> categoriesComponent ^ TBCategoriesComponent categories: self blog allCategories - postsList: self ``` ``` TBPostsListComponent >> postComponentFor: aPost - ^ TBPostComponent new post: aPost ``` ``` TBPostsListComponent >> renderContentOn: html + postsList: self +``` + + +``` +TBPostsListComponent >> postComponentFor: aPost + ^ TBPostComponent new post: aPost +``` + + +``` +TBPostsListComponent >> renderContentOn: html super renderContentOn: html. html tbsContainer: [ html tbsRow @@ -74,24 +286,85 @@ smallSize: 10; mediumSize: 8; with: [ self readSelectedPosts - do: [ :p | html render: (self postComponentFor: p) ] ] ] ] ``` #### Another Pass We continue to cut this method in other smaller methods. We create one method for each of the elementary tasks. ``` TBPostsListComponent >> basicRenderCategoriesOn: html - html render: self categoriesComponent ``` ``` TBPostsListComponent >> basicRenderPostsOn: html + do: [ :p | html render: (self postComponentFor: p) ] ] ] ] +``` + + +#### Another Pass + +We continue to cut this method into other smaller methods. We create one method for each of the elementary tasks. + +``` +TBPostsListComponent >> basicRenderCategoriesOn: html + html render: self categoriesComponent +``` + + +``` +TBPostsListComponent >> basicRenderPostsOn: html self readSelectedPosts do: [ :p | - html render: (self postComponentFor: p) ] ``` Then we use such tasks to simplify the method `renderContentOn:`. ``` TBPostsListComponent >> renderContentOn: html + html render: (self postComponentFor: p) ] +``` + + +Then we use such tasks to simplify the method `renderContentOn:`. + +``` +TBPostsListComponent >> renderContentOn: html super renderContentOn: html. html tbsContainer: [ html tbsRow showGrid; with: [ self renderCategoryColumnOn: html. - self renderPostColumnOn: html ] ] ``` ``` TBPostsListComponent >> renderCategoryColumnOn: html + self renderPostColumnOn: html ] ] +``` + + + +``` +TBPostsListComponent >> renderCategoryColumnOn: html html tbsColumn extraSmallSize: 12; smallSize: 2; mediumSize: 4; - with: [ self basicRenderCategoriesOn: html ] ``` ``` TBPostsListComponent >> renderPostColumnOn: html + with: [ self basicRenderCategoriesOn: html ] +``` + + + +``` +TBPostsListComponent >> renderPostColumnOn: html html tbsColumn extraSmallSize: 12; smallSize: 10; mediumSize: 8; - with: [ self basicRenderPostsOn: html ] ``` The final application is showing in Figure *@final@*. ![Final TinyBlog Public UI.](figures/finalPublicWebPage.png width=85&label=final) ### Conclusion We defined an interface for our blog using a set of components each specifying its own state and responsibility. Many web applications are built the same way reusing components. You have the foundation to build more advanced web application. In the next chapter, we show how to manage authentication to access the post admin part of our application. #### Possible Enhancements As exercise you can: - sort category alphabetically, or - add a link named 'All' in the category list to display all the posts. \ No newline at end of file + with: [ self basicRenderPostsOn: html ] +``` + + + +The final application is showing in Figure *@final@*. + +![Final TinyBlog Public UI. % width=85&label=final](figures/finalPublicWebPage.png) + +### Conclusion + +We defined an interface for our blog using a set of components each specifying its own state and responsibility. +Many web applications are built the same way reusing components. You have the foundation to build more advanced +web applications. + +In the next chapter, we show how to manage authentication to access the post-admin part of our application. + +#### Possible Enhancements + + +As exercise, you can: +- sort categories alphabetically, or +- add a link named 'All' in the category list to display all the posts. + + + + + + diff --git a/Chapters/Chap06-TinyBlog-SeasideUserComponents-EN.md b/Chapters/Chap06-TinyBlog-SeasideUserComponents-EN.md index be5b780..88a97ee 100644 --- a/Chapters/Chap06-TinyBlog-SeasideUserComponents-EN.md +++ b/Chapters/Chap06-TinyBlog-SeasideUserComponents-EN.md @@ -1,74 +1,482 @@ -## Web Components for TinyBlog In this chapter, we build the public view of TinyBlog that displays the posts of the blog. Figure *@ApplicationArchitectureUserWithoutCategory@* shows the components we will work on during this chapter. If you feel lost at any moment, please refer to it. ![Component Architecture of the Public View \(opposed to the Administration View\).](figures/ApplicationArchitectureUserWithoutCategory.pdf width=60&label=ApplicationArchitectureUserWithoutCategory) Before starting, you can load the code of previous chapters as described in the last chapter of this book. ### Visual Components Figure *@ComponentOverview@* shows the visual components we will define in this chapter and where they will be displayed. ![Visual Components of TinyBlog.](figures/ComponentOverview-ListPosts.pdf width=75&label=ComponentOverview) #### The TBScreenComponent component All components contained in `TBApplicationRootComponent` will be subclasses of the abstract class `TBScreenComponent`. This class allows us to factorize shared behavior between all our components. ``` WAComponent subclass: #TBScreenComponent +## Web Components for TinyBlog + + +In this chapter, we build the public view of TinyBlog that displays the posts of the blog. +Figure *@ApplicationArchitectureUserWithoutCategory@* shows the components we will work on during this chapter. +If you feel lost at any moment, please refer to it. + +![Component Architecture of the Public View \(opposed to the Administration View\).](figures/ApplicationArchitectureUserWithoutCategory.pdf width=60&label=ApplicationArchitectureUserWithoutCategory) + +Before starting, you can load the code of previous chapters as described in the last chapter of this book. + +### Visual Components + + +Figure *@ComponentOverview@* shows the visual components we will define in this chapter and where they will be displayed. + +![Visual Components of TinyBlog. % width=75&label=ComponentOverview](figures/ComponentOverview-ListPosts.pdf) + +#### The TBScreenComponent component + + + +All components contained in `TBApplicationRootComponent` will be subclasses of the abstract class `TBScreenComponent`. This class allows us to factorize shared behavior between all our components. + +``` +WAComponent subclass: #TBScreenComponent instanceVariableNames: '' classVariableNames: '' - package: 'TinyBlog-Components' ``` All components need to access the model of our application. Therefore, in the 'accessing' protocol, we add a `blog` method that returns the current instance of `TBBlog` \(the singleton\). In the future, if you want to manage multiple blogs, you will modify this method and return the blog object it has been configured with. ``` TBScreenComponent >> blog - "Return the current blog. In the future we will ask the + package: 'TinyBlog-Components' +``` + + +All components need to access the model of our application. Therefore, in the 'accessing' protocol, we add a `blog` method that returns the current instance of `TBBlog` (the singleton). +In the future, if you want to manage multiple blogs, you will modify this method and return the blog object it has been configured with. + +``` +TBScreenComponent >> blog + "Return the current blog. In the future, we will ask the session to return the blog of the currently logged in user." - ^ TBBlog current ``` Let's define a method `renderContentOn:` on this new component that temporarily displays a message. If you refresh your browser, nothing appears because this new component is not displayed at all yet. ``` TBScreenComponent >> renderContentOn: html - html text: 'Hello from TBScreenComponent' ``` ### Using the TBScreenComponent component In the final architecture, `TBScreenComponent` is an abstract component and should not be used directly. Nevertheless, we will use it temporarily while developing other components. ![`ApplicationRootComponent` temporarily uses a `ScreenComponent` that contains a `HeaderComponent`.](figures/ComponentRelationship1.pdf width=75&label=compt1) Let's add an instance variable `main` in `TBApplicationRootComponent` class. We obtain the situation described in Figure *@compt1@*. ``` WAComponent subclass: #TBApplicationRootComponent + ^ TBBlog current +``` + + +Let's define a method `renderContentOn:` on this new component that temporarily displays a message. +If you refresh your browser, nothing appears because this new component is not displayed at all yet. + +``` +TBScreenComponent >> renderContentOn: html + html text: 'Hello from TBScreenComponent' +``` + + + +### Using the TBScreenComponent component + + +In the final architecture, `TBScreenComponent` is an abstract component and should not be used directly. +Nevertheless, we will use it temporarily while developing other components. + +![`ApplicationRootComponent` temporarily uses a `ScreenComponent` that contains a `HeaderComponent`. %width=75&label=compt1](figures/ComponentRelationship1.pdf) + + +Let's add an instance variable `main` in `TBApplicationRootComponent` class. We obtain the situation described in Figure *@compt1@*. + +``` +WAComponent subclass: #TBApplicationRootComponent instanceVariableNames: 'main' classVariableNames: '' - package: 'TinyBlog-Components' ``` We initialize this instance variable in the `initialize` method with a new instance of `TBScreenComponent`. ``` TBApplicationRootComponent >> initialize + package: 'TinyBlog-Components' +``` + + + +We initialize this instance variable in the `initialize` method with a new instance of `TBScreenComponent`. + +``` +TBApplicationRootComponent >> initialize super initialize. - main := TBScreenComponent new ``` We make the `TBApplicationRootComponent` to render this sub-component. ``` TBApplicationRootComponent >> renderContentOn: html - html render: main ``` We do not forget to declare that the object contained in `main` instance variable is a sub-component of `TBApplicationRootComponent` by redefining the `children` method. ``` TBApplicationRootComponent >> children - ^ { main } ``` Figure *@fig:Hello@* shows the result that you should obtain in your browser. Currently, there is only the text: `Hello from TBScreenComponent` displayed by the `TBScreenComponent` sub-component. \(voir figure *@fig:Hello@*\). ![First visual rendering of `TBScreenComponent`.](figures/HelloFromScreenComponent.png width=75&label=fig:Hello) ### Pattern of Component Definition We will often use the same following steps: - first, we define a class and the behavior of a new component; - then, we reference it from an existing component that uses it; - and we express the composite/sub-component relationship by redefining the `children` method. ### Populating the Blog You can inspect the blog object returned by `TBBlog current` and verify that it contains some posts. You can also do it simply as: ``` TBBlog current allBlogPosts size ``` If it does not, execute: ``` TBBlog createDemoPosts ``` ### Definition of TBHeaderComponent Let's define a component named `TBHeaderComponent` that renders the common header of all pages of TinyBlog. This component will be inserted on the top of all components such as `TBPostsListComponent`. We use the pattern described above: define the class of the component, reference it from its enclosing component and redefine the `children` method. Here the class definition: ``` WAComponent subclass: #TBHeaderComponent + main := TBScreenComponent new +``` + + +We make the `TBApplicationRootComponent` to render this sub-component. + +``` +TBApplicationRootComponent >> renderContentOn: html + html render: main +``` + + +We do not forget to declare that the object contained in `main` instance variable is a sub-component of `TBApplicationRootComponent` by redefining the `children` method. + +``` +TBApplicationRootComponent >> children + ^ { main } +``` + + +Figure *@fig:Hello@* shows the result that you should obtain in your browser. +Currently, there is only the text: `Hello from TBScreenComponent` displayed by the `TBScreenComponent` sub-component. +\(voir figure *@fig:Hello@*\). + +![First visual rendering of `TBScreenComponent`. % width=75&label=fig:Hello](figures/HelloFromScreenComponent.png) + + +### Pattern of Component Definition + +We will often use the same following steps: +- first, we define a class and the behavior of a new component; +- then, we reference it from an existing component that uses it; +- and we express the composite/sub-component relationship by redefining the `children` method. + + +### Populating the Blog + + +You can inspect the blog object returned by `TBBlog current` and verify that it contains some posts. +You can also do it simply as: +``` +TBBlog current allBlogPosts size +``` + + + +If it does not, execute: + +``` +TBBlog createDemoPosts +``` + + + +### Definition of TBHeaderComponent + + +Let's define a component named `TBHeaderComponent` that renders the common header of all pages of TinyBlog. +This component will be inserted on the top of all components such as `TBPostsListComponent`. +We use the pattern described above: define the class of the component, reference it from its enclosing component, and redefine the `children` method. + +Here the class definition: + +``` +WAComponent subclass: #TBHeaderComponent instanceVariableNames: '' classVariableNames: '' - package: 'TinyBlog-Components' ``` ### Usage of TBHeaderComponent Remember that `TBScreenComponent` is the \(abstract\) root of all components in our final architecture. Therefore, we will introduce our header into `TBScreenComponent` so that all its subclasses will inherit it. Since, it is not desirable to instantiate the `TBHeaderComponent` each time a component is called, we store the header in an instance variable named `header`. ``` WAComponent subclass: #TBScreenComponent + package: 'TinyBlog-Components' +``` + + + +### Usage of TBHeaderComponent + + +Remember that `TBScreenComponent` is the (abstract) root of all components in our final architecture. +Therefore, we will introduce our header into `TBScreenComponent` so that all its subclasses will inherit it. +Since, it is not desirable to instantiate the `TBHeaderComponent` each time a component is called, we store the header in an instance variable named `header`. + +``` +WAComponent subclass: #TBScreenComponent instanceVariableNames: 'header' classVariableNames: '' - package: 'TinyBlog-Components' ``` We initialize it in the `initialize` method categorized in the 'initialization' protocol: ``` TBScreenComponent >> initialize + package: 'TinyBlog-Components' +``` + + +We initialize it in the `initialize` method categorized in the 'initialization' protocol: + +``` +TBScreenComponent >> initialize super initialize. - header := self createHeaderComponent ``` ``` TBScreenComponent >> createHeaderComponent - ^ TBHeaderComponent new ``` Note that we use a specific method named `createHeaderComponent` to create the instantiate the header component. Redefining this method makes it possible to completely change the header component that is used. We will use that to display a different header component for the administration view. ### Composite-Component Relationship In Seaside, sub-components of a component must be returned by the composite when sending it the `children` message. So, we must define that the `TBHeaderComponent` instance is a children of the `TBScreenComponent` component in the Seaside component hierarchy \(and not in the Pharo classes hierarchy\). We do so by specializing the method `children`. In this example, it returns a collection of one element which is the instance of `TBHeaderComponent` referenced by the `header` instance variable. ``` TBScreenComponent >> children - ^ { header } ``` ### Render an header In the `renderContentOn:` method \('rendering' protocol\), we can now display the sub-component \(the header\): ``` TBScreenComponent >> renderContentOn: html - html render: header ``` If you refresh your browser, nothing appears because the `TBHeaderComponent` has no rendering. Let's add a `renderContentOn:` method on it that displays a Bootstrap navigation header: ``` TBHeaderComponent >> renderContentOn: html + header := self createHeaderComponent +``` + + +``` +TBScreenComponent >> createHeaderComponent + ^ TBHeaderComponent new +``` + + +Note that we use a specific method named `createHeaderComponent` to create the instantiate the header component. +Redefining this method makes it possible to completely change the header component that is used. +We will use that to display a different header component for the administration view. + +### Composite-Component Relationship + + +In Seaside, sub-components of a component must be returned by the composite when sending it the `children` message. +So, we must define that the `TBHeaderComponent` instance is a children of the `TBScreenComponent` component in the Seaside component hierarchy \(and not in the Pharo classes hierarchy\). +We do so by specializing the method `children`. +In this example, it returns a collection of one element which is the instance of `TBHeaderComponent` referenced by the `header` instance variable. + +``` +TBScreenComponent >> children + ^ { header } +``` + + +### Render an header + + +In the `renderContentOn:` method ('rendering' protocol), we can now display the sub-component (the header): + +``` +TBScreenComponent >> renderContentOn: html + html render: header +``` + + +If you refresh your browser, nothing appears because the `TBHeaderComponent` has no rendering. +Let's add a `renderContentOn:` method on it that displays a Bootstrap navigation header: + +``` +TBHeaderComponent >> renderContentOn: html html tbsNavbar beDefault; with: [ html tbsContainer: [ self renderBrandOn: html - ]] ``` ``` TBHeaderComponent >> renderBrandOn: html + ]] +``` + + +``` +TBHeaderComponent >> renderBrandOn: html html tbsNavbarHeader: [ html tbsNavbarBrand url: self application url; - with: 'TinyBlog' ] ``` Your browser should now display what is shown on Figure *@navBlog@*. As usual in Bootstrap navigation bar, the link on the title of the application \(`tbsNavbarBrand`\) enable users to go back to home page of the application. ![TinyBlog with a Bootstrap header.](figures/navBlog.png width=75&label=navBlog) #### Possible Enhancements The blog name should be customizable using an instance variable in the `TBBlog` class and the application header component should display this title. ### List of Posts Let's create a `TBPostsListComponent` inheriting from `TBScreenComponent` to display the list of all posts. Remember that we speak about the public access to the blog here and not the administration interface that will be developed later. ``` TBScreenComponent subclass: #TBPostsListComponent + with: 'TinyBlog' ] +``` + + +Your browser should now display what is shown in Figure *@navBlog@*. +As usual in Bootstrap navigation bar, the link on the title of the application (`tbsNavbarBrand`) enable users to go back to home page of the application. + +![TinyBlog with a Bootstrap header.](figures/navBlog.png width=75&label=navBlog) + + +#### Possible Enhancements + + +The blog name should be customizable using an instance variable in the `TBBlog` class and the application header component should display this title. + +### List of Posts + + +Let's create a `TBPostsListComponent` inheriting from `TBScreenComponent` to display the list of all posts. +Remember that we speak about the public access to the blog here and not the administration interface that will be developed later. + +``` +TBScreenComponent subclass: #TBPostsListComponent instanceVariableNames: '' classVariableNames: '' - package: 'TinyBlog-Components' ``` ![The `ApplicationRootComponent` uses `PostsListComponent`.](figures/ComponentRelationship2.pdf width=75&label=compt2) We can now modify `TBApplicationRootComponent`, the main component of the application, so that it displays this new component as shown in figure *@compt2@*. To achieve this, we modify its `initialize` method: ``` TBApplicationRootComponent >> initialize + package: 'TinyBlog-Components' +``` + + + +![The `ApplicationRootComponent` uses `PostsListComponent`. % width=75&label=compt2](figures/ComponentRelationship2.pdf) + +We can now modify `TBApplicationRootComponent`, the main component of the application, so that it displays this new component as shown in figure *@compt2@*. +To achieve this, we modify its `initialize` method: + +``` +TBApplicationRootComponent >> initialize super initialize. - main := TBPostsListComponent new ``` We add a setter method named `main:` to dynamically change the sub-component to display but by default it is an instance of `TBPostsListComponent`. ``` TBApplicationRootComponent >> main: aComponent - main := aComponent ``` We now add a temporary `renderContentOn:` method \(in the 'rendering' protocol\) on `TBPostsListComponent` to test during development \(cf. Figure *@elementary@*\). In this method, we call the `renderContentOn:` of the super-class which renders the header component. ``` TBPostsListComponent >> renderContentOn: html + main := TBPostsListComponent new +``` + + +We add a setter method named `main:` to dynamically change the sub-component to display but by default it is an instance of `TBPostsListComponent`. + +``` +TBApplicationRootComponent >> main: aComponent + main := aComponent +``` + + + +We now add a temporary `renderContentOn:` method \(in the 'rendering' protocol\) on `TBPostsListComponent` to test during development \(cf. Figure *@elementary@*\). +In this method, we call the `renderContentOn:` of the super-class which renders the header component. + +``` +TBPostsListComponent >> renderContentOn: html super renderContentOn: html. - html text: 'Blog Posts here !!!' ``` ![TinyBlog displaying a basic posts list.](figures/ElementaryListPost.png width=65&label=elementary) If you refresh TinyBlog in your browser, you should now see what is shown in figure *@elementary@*. ### The PostComponent Now we will define `TBPostComponent` to display the details of a post. Each post will be graphically displayed by an instance of `TBPostComponent` which will show the post title, its date and its content as shown in figure *@compt3@*. ![Using PostComponents to diplays each Posts.](figures/ComponentRelationship3.pdf width=75&label=compt3) ``` WAComponent subclass: #TBPostComponent + html text: 'Blog Posts here !!!' +``` + + +![TinyBlog displaying a basic posts list. % width=65&label=elementary](figures/ElementaryListPost.png) + +If you refresh TinyBlog in your browser, you should now see what is shown in Figure *@elementary@*. + + +### The PostComponent + + +Now we will define `TBPostComponent` to display the details of a post. +Each post will be graphically displayed by an instance of `TBPostComponent` which will show the post title, its date, and its content as shown in figure *@compt3@*. + +![Using PostComponents to diplays each Posts. % width=75&label=compt3](figures/ComponentRelationship3.pdf) + +``` +WAComponent subclass: #TBPostComponent instanceVariableNames: 'post' classVariableNames: '' - package: 'TinyBlog-Components' ``` ``` TBPostComponent >> initialize + package: 'TinyBlog-Components' +``` + + +``` +TBPostComponent >> initialize super initialize. - post := TBPost new ``` ``` TBPostComponent >> title - ^ post title ``` ``` TBPostComponent >> text - ^ post text ``` ``` TBPostComponent >> date - ^ post date ``` The `renderContentOn:` method defines the HTML rendering of a post. ``` TBPostComponent >> renderContentOn: html + post := TBPost new +``` + + +``` +TBPostComponent >> title + ^ post title +``` + + +``` +TBPostComponent >> text + ^ post text +``` + + +``` +TBPostComponent >> date + ^ post date +``` + + +The `renderContentOn:` method defines the HTML rendering of a post. + +``` +TBPostComponent >> renderContentOn: html html heading level: 2; with: self title. html heading level: 6; with: self date. - html text: self text ``` #### About HTML Forms In a future chapter on the administration view, we will show how to use Magritte to add descriptions to model objects and then use them to automatically generate Seaside components. This is powerful and free developers to manually describe forms in Seaside. To give you a taste of that, here the equivalent code as above using Magritte: ``` TBPostComponent >> renderContentOn: html + html text: self text +``` + + + +#### About HTML Forms + + +In a future chapter on the administration view, we will show how to use Magritte to add descriptions to model objects and then use them to automatically generate Seaside components. +This is powerful and frees developers to manually describe forms in Seaside. + +To give you a taste of that, here the equivalent code as above using Magritte: + +``` +TBPostComponent >> renderContentOn: html "DON'T WRITE THIS YET" - html render: post asComponent ``` ### Display Posts Before displaying available posts in the database, you should check that your blog contains some posts: ``` TBBlog current allBlogPosts size ``` If it contains no posts, you can recreate some: ``` TBBlog createDemoPosts ``` Now, we just need to modify the `TBPostsListComponent >> renderContentOn:` method to display all visible posts in the database: ``` TBPostsListComponent >> renderContentOn: html + html render: post asComponent +``` + +### Display Posts + +Before displaying available posts in the database, you should check that your blog contains some posts: + +``` +TBBlog current allBlogPosts size +``` + + +If it contains no posts, you can recreate some: +``` +TBBlog createDemoPosts +``` + + +Now, we just need to modify the `TBPostsListComponent >> renderContentOn:` method to display all visible posts in the database: + +``` +TBPostsListComponent >> renderContentOn: html super renderContentOn: html. self blog allVisibleBlogPosts do: [ :p | - html render: (TBPostComponent new post: p) ] ``` Refresh you web browser and you should get an error. ### Debugging Errors By default, when an error occurs in a web application, Seaside returns an HTML page with the error message. You can change this message or during development, you can configure Seaside to open a debugger directly in Pharo IDE. To configure Seaside, just execute the following snippet: ``` (WAAdmin defaultDispatcher handlerAt: 'TinyBlog') - exceptionHandler: WADebugErrorHandler ``` Now, if you refresh the web page in your browser, a debugger should open on Pharo side. If you analyze the stack, you should see that we forgot to define the following method: ``` TBPostComponent >> post: aPost - post := aPost ``` You can define this method in the debugger using the `Create` button. After that, press the `Proceed` button. The web application should now correctly renders what is shown in Figure *@better@*. ![TinyBlog with a List of Posts.](figures/betterListPosts.png width=65&label=better) ### Displaying the List of Posts with Bootstrap Let's use Bootstrap to make the list of posts more beautiful using a Bootstrap container thanks to the message `tbsContainer:`: ``` TBPostsListComponent >> renderContentOn: html + html render: (TBPostComponent new post: p) ] +``` + + +Refresh your web browser and you should get an error. + +### Debugging Errors + + +By default, when an error occurs in a web application, Seaside returns an HTML page with the error message. You can change this message or during development, you can configure Seaside to open a debugger directly in Pharo IDE. To configure Seaside, just execute the following snippet: + +``` +(WAAdmin defaultDispatcher handlerAt: 'TinyBlog') + exceptionHandler: WADebugErrorHandler +``` + + +Now, if you refresh the web page in your browser, a debugger should open on the Pharo side. If you analyze the stack, you should see that we forgot to define the following method: + +``` +TBPostComponent >> post: aPost + post := aPost +``` + + +You can define this method in the debugger using the `Create` button. After that, press the `Proceed` button. The web application should now correctly render what is shown in Figure *@better@*. + +![TinyBlog with a List of Posts. % width=65&label=better](figures/betterListPosts.png) + + +### Displaying the List of Posts with Bootstrap + + +Let's use Bootstrap to make the list of posts more beautiful using a Bootstrap container thanks to the message `tbsContainer:`: + +``` +TBPostsListComponent >> renderContentOn: html super renderContentOn: html. html tbsContainer: [ self blog allVisibleBlogPosts do: [ :p | - html render: (TBPostComponent new post: p) ] ] ``` Your web application should look like Figure *@ComponentOverview@*. ### Instantiating Components in `renderContentOn:` We explained that the `children` method of a component should return its sub-components. Indeed, before executing the `renderContentOn:` method of a composite, Seaside needs to retrieve all its sub-components and their state. However, if sub-components are instantiated in the `renderContentOn:` method of the composite \(such as in `TBPostsListComponent>>renderContentOn:`\), it is not needed that `children` returns those sub-components. Note that, instantiating sub-components in the rendering method is not a good practice since it increases the loading time of the web page. If we would store all sub-components that display posts, we should add an instance variable `postComponents`. ``` TBPostsListComponent >> initialize + html render: (TBPostComponent new post: p) ] ] +``` + + +Your web application should look like Figure *@ComponentOverview@*. + +### Instantiating Components in `renderContentOn:` + + +We explained that the `children` method of a component should return its sub-components. +Indeed, before executing the `renderContentOn:` method of a composite, Seaside needs to retrieve all its sub-components and their state. +However, if sub-components are instantiated in the `renderContentOn:` method of the composite (such as in `TBPostsListComponent>>renderContentOn:`), it is not needed that `children` returns those sub-components. + +Note that, instantiating sub-components in the rendering method is not a good practice since it increases the loading time of the web page. + +If we would store all sub-components that display posts, we should add an instance variable `postComponents`. + +``` +TBPostsListComponent >> initialize super initialize. - postComponents := OrderedCollection new ``` Initialize it with posts. ``` TBPostsListComponent >> postComponents + postComponents := OrderedCollection new +``` + + +Initialize it with posts. + +``` +TBPostsListComponent >> postComponents postComponents := self readSelectedPosts collect: [ :each | TBPostComponent new post: each ]. - ^ postComponents ``` Redefine the `children` method and of course render these sub-components in `renderContentOn:`: ``` TBPostsListComponent >> children - ^ self postComponents, super children ``` ``` TBPostsListComponent >> renderContentOn: html + ^ postComponents +``` + + +Redefine the `children` method and of course render these sub-components in `renderContentOn:`: + +``` +TBPostsListComponent >> children + ^ self postComponents, super children +``` + + +``` +TBPostsListComponent >> renderContentOn: html super renderContentOn: html. html tbsContainer: [ self postComponents do: [ :p | - html render: p ] ] ``` We do not do this in TinyBlog because it makes the code more complex. ### Conclusion In this chapter, we developed a Seaside component that renders a list of posts. In the next chapter, we will improve this by displaying posts' categories. Notice that we did not care about web requests or the application state. A Seaside programmer only define components and compose them as we would do in desktop applications. A Seaside component is responsible of rendering itself by redefining its `renderContentOn:` method. It should also returns its sub-components \(if no instantiated during each rendering\) by redefining the `children` method. \ No newline at end of file + html render: p ] ] +``` + + +We do not do this in TinyBlog because it makes the code more complex. + +### Conclusion + + +In this chapter, we developed a Seaside component that renders a list of posts. +In the next chapter, we will improve this by displaying posts' categories. + +Notice that we did not care about web requests or the application state. +A Seaside programmer only define components and compose them as we would do in desktop applications. + +A Seaside component is responsible for rendering itself by redefining its `renderContentOn:` method. It should also return its sub-components (if not instantiated during each rendering) by redefining the `children` method. diff --git a/Chapters/Chap07-TinyBlog-Authentification-EN.md b/Chapters/Chap07-TinyBlog-Authentification-EN.md index 4779734..2094866 100644 --- a/Chapters/Chap07-TinyBlog-Authentification-EN.md +++ b/Chapters/Chap07-TinyBlog-Authentification-EN.md @@ -1,98 +1,393 @@ -## Authentication and Session In this chapter we will develop a traditional scenario: the user should login to access to the administration part of the application. He does it using a login and password. Figure *@ApplicationArchitectureAdminHeader@* shows the architecture that we will reach in this chapter. ![Authentication flow.](figures/ApplicationArchitectureAdminHeader.pdf width=75&label=ApplicationArchitectureAdminHeader) Let us start to put in place a first version that allows one to navigate between the part of TinyBlog rendered by the component `TBPostsListComponent` and a first draft of the administration component as shown in Figure *@SimpleAdminLink@*. This illustrates how to invoke a component. In the following we will build and integrate a component managing the login based on modal interaction. This will illustrate how we can elegantly map filed inputs to instance variables of a component. Finally we will show how the user information is stored into the current session. ### A Simple Admin Component \(v1\) Let us define a really super simple administration component. This component inherits from the class `TBScreenComponent` as mentioned in previous chapters and illustrated in Figure *@ApplicationArchitectureAdminHeader@*. ``` TBScreenComponent subclass: #TBAdminComponent +## Authentication and Session + + +In this chapter we will develop a traditional scenario: the user should log in to access the administration part of the application. He does it using a login and password. + +Figure *@ApplicationArchitectureAdminHeader@* shows the architecture that we will reach in this chapter. + +![Authentication flow. % width=75&label=ApplicationArchitectureAdminHeader](figures/ApplicationArchitectureAdminHeader.pdf) + +Let us start to put in place a first version that allows one to navigate between the part of TinyBlog rendered +by the component `TBPostsListComponent` and a first draft of the administration component as shown in Figure + *@SimpleAdminLink@*. This illustrates how to invoke a component. + +In the following, we will build and integrate a component managing the login based on modal interaction. +This will illustrate how we can elegantly map filed inputs to instance variables of a component. + +Finally, we will show how the user information is stored into the current session. + + +### A Simple Admin Component (v1) + + +Let us define a really super simple administration component. This component inherits from the class `TBScreenComponent` as mentioned in previous chapters and illustrated in Figure *@ApplicationArchitectureAdminHeader@*. + +``` +TBScreenComponent subclass: #TBAdminComponent instanceVariableNames: '' classVariableNames: '' - package: 'TinyBlog-Components' ``` We define a first version of the rendering method to be able to test our approach. ``` TBAdminComponent >> renderContentOn: html + package: 'TinyBlog-Components' +``` + + +We define a first version of the rendering method to be able to test our approach. + +``` +TBAdminComponent >> renderContentOn: html super renderContentOn: html. html tbsContainer: [ html heading: 'Blog Admin'. - html horizontalRule ] ``` ### Adding 'admin' Button We add now a button in the header of the site \(component `TBHeaderComponent`\) so that the user can access to the admin as shown in Figure *@SimpleAdminLink@*. To do so, we modify the existing components: `TBHeaderComponent` \(header\) et `TBPostsListComponent` \(public part\). ![Simple link to the admin part.](figures/SimpleAdminLink.png width=100&label=SimpleAdminLink) Let us add a button in the header: ``` TBHeaderComponent >> renderContentOn: html + html horizontalRule ] +``` + + +### Adding 'admin' Button + + +We add now a button in the header of the site (component `TBHeaderComponent`) so that the user can access to the admin as shown in Figure *@SimpleAdminLink@*. +To do so, we modify the existing components: `TBHeaderComponent` (header) et `TBPostsListComponent` (public part). + +![Simple link to the admin part. % width=100&label=SimpleAdminLink](figures/SimpleAdminLink.png) + +Let us add a button in the header: +``` +TBHeaderComponent >> renderContentOn: html html tbsNavbar beDefault; with: [ html tbsContainer: [ self renderBrandOn: html. self renderButtonsOn: html - ]] ``` ``` TBHeaderComponent >> renderButtonsOn: html - self renderSimpleAdminButtonOn: html ``` ``` TBHeaderComponent >> renderSimpleAdminButtonOn: html + ]] +``` + +``` +TBHeaderComponent >> renderButtonsOn: html + self renderSimpleAdminButtonOn: html +``` + + +``` +TBHeaderComponent >> renderSimpleAdminButtonOn: html html form: [ html tbsNavbarButton tbsPullRight; with: [ html tbsGlyphIcon iconListAlt. - html text: ' Admin View' ]] ``` When you refresh the web browser, the admin buttin is present but it does not have any effect \(See Figure *@withAdminView1@*\). We should define a callback on this button \(message `callback:`\) to replace the current component \(`TBPostsListComponent`\) by the administration component \(`TBAdminComponent`\). ![Header with an admin button.](figures/withAdminView1.png width=80&label=withAdminView1) ### Header Revision Let us revise the definition of `TBHeaderComponent` by adding a new instance variable named `component` to store and access to the current component \(either post list or admin component\). This will allow us to access to the component from the header. ``` WAComponent subclass: #TBHeaderComponent + html text: ' Admin View' ]] +``` + + +When you refresh the web browser, the admin buttin is present but it does not have any effect (See Figure *@withAdminView1@*). + +We should define a callback on this button (message `callback:`) to replace the current component + (`TBPostsListComponent`) by the administration component (`TBAdminComponent`). + +![Header with an admin button. % width=80&label=withAdminView1](figures/withAdminView1.png) + +### Header Revision + + +Let us revise the definition of + `TBHeaderComponent` by adding a new instance variable named `component` to store and access the current component (either post list or admin component). This will allow us to access the component from the header. + +``` +WAComponent subclass: #TBHeaderComponent instanceVariableNames: 'component' classVariableNames: '' - package: 'TinyBlog-Components' ``` ``` TBHeaderComponent >> component: anObject + package: 'TinyBlog-Components' +``` + + +``` +TBHeaderComponent >> component: anObject component := anObject TBHeaderComponent >> component - ^ component ``` We add a new class method. ``` TBHeaderComponent class >> from: aComponent + ^ component +``` + + +We add a new class method. + +``` +TBHeaderComponent class >> from: aComponent ^ self new component: aComponent; - yourself ``` ### Admin Button Activation We modify the component instantiation in `TBScreenComponent` method to pass the component which will be under the header. ``` TBScreenComponent >> createHeaderComponent - ^ TBHeaderComponent from: self ``` Note that the method `createHeaderComponent` is defined in the superclass `TBScreenComponent` and it is applicable to all the subclasses. We can add now the callback on the button: ``` TBHeaderComponent >> renderSimpleAdminButtonOn: html + yourself +``` + + + +### Admin Button Activation + +We modify the component instantiation in `TBScreenComponent` method to pass the component which will be under the header. + +``` +TBScreenComponent >> createHeaderComponent + ^ TBHeaderComponent from: self +``` + + +Note that the method `createHeaderComponent` is defined in the superclass +`TBScreenComponent` and it is applicable to all the subclasses. + +We can add now the callback on the button: + +``` +TBHeaderComponent >> renderSimpleAdminButtonOn: html html form: [ html tbsNavbarButton tbsPullRight; callback: [ component goToAdministrationView ]; with: [ html tbsGlyphIcon iconListAlt. - html text: ' Admin View' ]] ``` We just need to define the method `goToAdministrationView` on the component `TBPostsListComponent`: ``` TBPostsListComponent >> goToAdministrationView - self call: TBAdminComponent new ``` Before clicking on the admin button, you should renew the current session by clicking on 'New Session': it will recreate the component `TBHeaderComponent`. You should get the situation presented in Figure *@withAdminCom@*. The 'Admin' button allows one to access the admin part v1. Pay attention not to click twice on the admin button because we do not manage it yet for the admin part. We will replace it by a Disconnect button. ![Admin component under definition.](figures/WithAdminComp.png width=80&label=withAdminCom) ### 'disconnect' Button Addition When we display the admin part, we will replace the header component by a new one. This new header will display a disconnect button. Let us define this new header component: ``` TBHeaderComponent subclass: #TBAdminHeaderComponent + html text: ' Admin View' ]] +``` + + +We just need to define the method `goToAdministrationView` on the component `TBPostsListComponent`: + +``` +TBPostsListComponent >> goToAdministrationView + self call: TBAdminComponent new +``` + + +Before clicking on the admin button, you should renew the current session by clicking on + 'New Session': it will recreate the component `TBHeaderComponent`. + + You should get the situation presented in Figure *@withAdminCom@*. + The 'Admin' button allows one to access the admin part v1. + + Pay attention not to click twice on the admin button because we do not manage it yet for the admin part. We will +replace it by a Disconnect button. + +![Admin component under definition. % width=80&label=withAdminCom](figures/WithAdminComp.png) + + +### 'disconnect' Button Addition + + +When we display the admin part, we will replace the header component with a new one. +This new header will display a disconnect button. + +Let us define this new header component: + +``` +TBHeaderComponent subclass: #TBAdminHeaderComponent instanceVariableNames: '' classVariableNames: '' - package: 'TinyBlog-Components' ``` ``` TBAdminHeaderComponent >> renderButtonsOn: html - html form: [ self renderDisconnectButtonOn: html ] ``` The `TBAdminComponent` component must use this header: ``` TBAdminComponent >> createHeaderComponent - ^ TBAdminHeaderComponent from: self ``` Now we can specialize our new admin header to display a disconnect button. ``` TBAdminHeaderComponent >> renderDisconnectButtonOn: html + package: 'TinyBlog-Components' +``` + + +``` +TBAdminHeaderComponent >> renderButtonsOn: html + html form: [ self renderDisconnectButtonOn: html ] +``` + + +The `TBAdminComponent` component must use this header: + +``` +TBAdminComponent >> createHeaderComponent + ^ TBAdminHeaderComponent from: self +``` + + +Now we can specialize our new admin header to display a disconnect button. + +``` +TBAdminHeaderComponent >> renderDisconnectButtonOn: html html tbsNavbarButton tbsPullRight; callback: [ component goToPostListView ]; with: [ html text: 'Disconnect '. - html tbsGlyphIcon iconLogout ] ``` ``` TBAdminComponent >> goToPostListView - self answer ``` What is see is that the message `answer` gives back the control to the component that calls it. So we go back the post list. Reset the current session by clicking on 'New Session'. Then you can click on the 'Admin' button, you should see now the admin v1 display itself with a 'Disconnect' button. This button allows on to go back the public part as shown in Figure *@SimpleAdminLink@*. #### call:/answer: Notion When you study the previous code, you see that we use the `call:`/`answer:` mechanism of Seaside to navigate between the components `TBPostsListComponent` and `TBAdminComponent`. The message `call:` replaces the current component with the one passed in argument and gives it the flow of control. The message `answer:` returns a value to this call and gives back the flow of control to the calling argument. This mechanism is really poweful and elegant. It is explained in the vidéo 1 of week 5 of the Pharo Mooc \([http://rmod-pharo-mooc.lille.inria.fr/MOOC/WebPortal/co/content\_5.html](http://rmod-pharo-mooc.lille.inria.fr/MOOC/WebPortal/co/content_5.html)\). ### Modal Window for Authentication Let us develop now a authentication component that when invoked will open a dialog box to request the login and password. The result we want to obtain is shown in Figure *@authentification@*. There are are some libraries of components ready to be used. For example, the Heimdal project available at [http://www.github.com/DuneSt/](http://www.github.com/DuneSt/) offers an authentication component or the Steam project [https://github.com/guillep/steam](https://github.com/guillep/steam) offers ways to interrogate google ou twitter accounts. ![Authentication component.](figures/Authentification.png width=75&label=authentification) #### Authentication Component Definition We define a new subclass of `WAComponent` and its accessors. This component contains a login, a password and a component which invoked it to access to the admin part. ``` WAComponent subclass: #TBAuthenticationComponent + html tbsGlyphIcon iconLogout ] +``` + + +``` +TBAdminComponent >> goToPostListView + self answer +``` + + +What is see is that the message `answer` gives back the control to the component that calls it. So we go back to the post list. + +Reset the current session by clicking on 'New Session'. Then you can click on the 'Admin' button, you should see now the admin v1 display itself with a 'Disconnect' button. This button allows on to go back to the public part as shown in Figure *@SimpleAdminLink@*. + + +#### call:/answer: Notion + + +When you study the previous code, you see that we use the +`call:`/`answer:` mechanism of Seaside to navigate between the components `TBPostsListComponent` and `TBAdminComponent`. + +The message +`call:` replaces the current component with the one passed in the argument and gives it the flow of control. +The message `answer:` returns a value to this call and gives back the flow of control to the calling argument. +This mechanism is really powerful and elegant. It is explained in the vidéo 1 of week 5 of the Pharo Mooc ([http://rmod-pharo-mooc.lille.inria.fr/MOOC/WebPortal/co/content\_5.html](http://rmod-pharo-mooc.lille.inria.fr/MOOC/WebPortal/co/content_5.html)). + +### Modal Window for Authentication + + +Let us develop now a authentication component that when invoked will open a dialog box to request +the login and password. The result we want to obtain is shown in Figure *@authentification@*. + +There are some libraries of components ready to be used. +For example, the Heimdal project available at [http://www.github.com/DuneSt/](http://www.github.com/DuneSt/) offers an authentication component or the Steam project [https://github.com/guillep/steam](https://github.com/guillep/steam) offers ways to interrogate google ou twitter accounts. + +![Authentication component.](figures/Authentification.png width=75&label=authentification) + +#### Authentication Component Definition + + +We define a new subclass of `WAComponent` and its accessors. +This component contains a login, a password, and a component that invoked it to access the admin part. + +``` +WAComponent subclass: #TBAuthenticationComponent instanceVariableNames: 'password account component' classVariableNames: '' - package: 'TinyBlog-Components' ``` ``` TBAuthenticationComponent >> account - ^ account ``` ``` TBAuthenticationComponent >> account: anObject - account := anObject ``` ``` TBAuthenticationComponent >> password - ^ password ``` ``` TBAuthenticationComponent >> password: anObject - password := anObject ``` ``` TBAuthenticationComponent >> component - ^ component ``` ``` TBAuthenticationComponent >> component: anObject - component := anObject ``` The instance variable `component` will be initialized by the following class method: classe suivante : ``` TBAuthenticationComponent class >> from: aComponent + package: 'TinyBlog-Components' +``` + + +``` +TBAuthenticationComponent >> account + ^ account +``` + + +``` +TBAuthenticationComponent >> account: anObject + account := anObject +``` + + +``` +TBAuthenticationComponent >> password + ^ password +``` + + +``` +TBAuthenticationComponent >> password: anObject + password := anObject +``` + + +``` +TBAuthenticationComponent >> component + ^ component +``` + + +``` +TBAuthenticationComponent >> component: anObject + component := anObject +``` + + +The instance variable `component` will be initialized by the following class method: +classe suivante : + +``` +TBAuthenticationComponent class >> from: aComponent ^ self new component: aComponent; - yourself ``` ### Authentication Component Rendering The following method `renderContentOn:` defines the contents of a dialog box with the ID `myAuthDialog`. This ID will be used to select the component that should be made visible when in modal mode. This dialog box has a header and a body. Note the use of the messages `tbsModal`, `tbsModalBody:`, and `tbsModalContent:` which supports a modal interaction with the component. ``` TBAuthenticationComponent >> renderContentOn: html + yourself +``` + + + +### Authentication Component Rendering + + +The following method `renderContentOn:` defines the contents of a dialog box with the ID `myAuthDialog`. +This ID will be used to select the component that should be made visible when in modal mode. + +This dialog box has a header and a body. Note the use of the messages `tbsModal`, `tbsModalBody:`, and `tbsModalContent:` which supports a modal interaction with the component. + +``` +TBAuthenticationComponent >> renderContentOn: html html tbsModal id: 'myAuthDialog'; with: [ html tbsModalDialog: [ html tbsModalContent: [ self renderHeaderOn: html. - self renderBodyOn: html ] ] ] ``` The header displays a button to close the dialog box and a title with large fonts. Note that you can also use the ESC key to close the modal window box. ``` TBAuthenticationComponent >> renderHeaderOn: html + self renderBodyOn: html ] ] ] +``` + + +The header displays a button to close the dialog box and a title with large fonts. +Note that you can also use the ESC key to close the modal window box. + +``` +TBAuthenticationComponent >> renderHeaderOn: html html tbsModalHeader: [ html tbsModalCloseIcon. html tbsModalTitle level: 4; - with: 'Authentication' ] ``` The body of the component displays the input field for the login identifier, password and some buttons. ``` TBAuthenticationComponent >> renderBodyOn: html + with: 'Authentication' ] +``` + + +The body of the component displays the input field for the login identifier, password, and some buttons. + +``` +TBAuthenticationComponent >> renderBodyOn: html html tbsModalBody: [ html tbsForm: [ self renderAccountFieldOn: html. self renderPasswordFieldOn: html. - html tbsModalFooter: [ self renderButtonsOn: html ] ] ] ``` The method `renderAccountFieldOn:` shows how the value of an input field is passed and stored in an instance variable of a component when the user finishes its input. The parameter of the `callback:` message is a bloc which takes as argument the value of the input field. ``` TBAuthenticationComponent >> renderAccountFieldOn: html + html tbsModalFooter: [ self renderButtonsOn: html ] ] ] +``` + + +The method `renderAccountFieldOn:` shows how the value of an input field is passed and stored in an instance variable of a component when the user finishes its input. + +The parameter of the `callback:` message is a bloc which takes as argument the value of the input field. + +``` +TBAuthenticationComponent >> renderAccountFieldOn: html html tbsFormGroup: [ html label with: 'Account'. html textInput tbsFormControl; attributeAt: 'autofocus' put: 'true'; callback: [ :value | account := value ]; - value: account ] ``` The same process is used for the password. ``` TBAuthenticationComponent >> renderPasswordFieldOn: html + value: account ] +``` + + +The same process is used for the password. + +``` +TBAuthenticationComponent >> renderPasswordFieldOn: html html tbsFormGroup: [ html label with: 'Password'. html passwordInput tbsFormControl; callback: [ :value | password := value ]; - value: password ] ``` Finally in the following `renderContentOn:` method, two buttons are added at the bottom of the modal window. The `'Cancel'` button which allows one to close the window using the attribute 'data-dismiss' and the `'SignIn'` button which sends the `validate` using a callback. The enter key is bound to the `'SignIn'` button activation when using the method `tbsSubmitButton`. This method sets the 'type' attribute to 'submit'. ``` TBAuthenticationComponent >> renderButtonsOn: html + value: password ] +``` + + + +Finally in the following `renderContentOn:` method, two buttons are added at the bottom of the modal window. +The `'Cancel'` button allows one to close the window using the attribute 'data-dismiss' and the `'SignIn'` +button which sends the `validate` using a callback. + +The enter key is bound to the `'SignIn'` button activation when using the method `tbsSubmitButton`. This method sets the 'type' attribute to 'submit'. + + +``` +TBAuthenticationComponent >> renderButtonsOn: html html tbsButton attributeAt: 'type' put: 'button'; attributeAt: 'data-dismiss' put: 'modal'; @@ -101,9 +396,40 @@ TBHeaderComponent >> component html tbsSubmitButton bePrimary; callback: [ self validate ]; - value: 'SignIn' ``` In the `validate` method, we simply send a message to the main component giving it the information entered by the user. ``` TBAuthenticationComponent >> validate - ^ component tryConnectionWithLogin: self account andPassword: self password ``` % !!!!! Améliorations % Rechercher une autre méthode pour réaliser l'authentification de l'utilisateur (utilisation d'un backend de type base de données, LDAP ou fichier texte). En tout cas, ce n'est pas à la boite de login de faire ce travail, il faut le déléguer à un objet métier qui saura consulter le backend et authentifier l'utilisateur. % De plus le composant ==TBAuthenticationComponent== pourrait afficher l'utilisateur lorsque celui-ci est logué. ### Authentication Component Integration To integrate our authentication component, we modify the Admin button of the header component \(`TBHeaderComponent`\) as follows: ``` TBHeaderComponent >> renderButtonsOn: html - self renderModalLoginButtonOn: html ``` ``` TBHeaderComponent >> renderModalLoginButtonOn: html + value: 'SignIn' +``` + + +In the `validate` method, we simply send a message to the main component giving it the information entered by the user. + +``` +TBAuthenticationComponent >> validate + ^ component tryConnectionWithLogin: self account andPassword: self password +``` + + + + + +% !!!!! Améliorations +% Rechercher une autre méthode pour réaliser l'authentification de l'utilisateur (utilisation d'un backend de type base de données, LDAP ou fichier texte). En tout cas, ce n'est pas à la boite de login de faire ce travail, il faut le déléguer à un objet métier qui saura consulter le backend et authentifier l'utilisateur. + +% De plus le composant ==TBAuthenticationComponent== pourrait afficher l'utilisateur lorsque celui-ci est logué. + +### Authentication Component Integration + + +To integrate our authentication component, we modify the Admin button of the header +component (`TBHeaderComponent`) as follows: + +``` +TBHeaderComponent >> renderButtonsOn: html + self renderModalLoginButtonOn: html +``` + + +``` +TBHeaderComponent >> renderModalLoginButtonOn: html html render: (TBAuthenticationComponent from: component). html tbsNavbarButton tbsPullRight; @@ -111,66 +437,330 @@ TBHeaderComponent >> component attributeAt: 'data-toggle' put: 'modal'; with: [ html tbsGlyphIcon iconLock. - html text: ' Login' ] ``` The method `renderModalLoginButtonOn:` starts by rendering the component `TBAuthenticationComponent` within this web page. This component is created during each display and it does not have to be returned by the `children` method. In addition, we add 'Login' button with a icon lock. When the user clicks on this button, the modal dialog identified with the ID `myAuthDialog` will be displayed. Reloading the TinyBlog page, you should see now a 'Login' button in the header \(button that will pop up the authentication we just developed\) as illustrated by Figure *@authentification@*. ### Naively Managing Logins When you click on the 'SignIn' button you get an error. Using the Pharo debugger, you can see that we should define the method `tryConnectionWithLogin:andPassword:` on the component `TBPostsListComponent` since it is the one sent by the callback of the button. ``` TBPostsListComponent >> tryConnectionWithLogin: login andPassword: password + html text: ' Login' ] +``` + + +The method `renderModalLoginButtonOn:` starts by rendering the component `TBAuthenticationComponent` within this web page. +This component is created during each display and it does not have to be returned by the +`children` method. +In addition, we add 'Login' button with a icon lock. +When the user clicks on this button, the modal dialog identified with the ID `myAuthDialog` will be displayed. + +Reloading the TinyBlog page, you should see now a 'Login' button in the header (button that will pop up the authentication we just developed) as illustrated by Figure *@authentification@*. + +### Naively Managing Logins + + +When you click on the 'SignIn' button you get an error. +Using the Pharo debugger, you can see that we should define the method + `tryConnectionWithLogin:andPassword:` on the component `TBPostsListComponent` +since it is the one sent by the callback of the button. + +``` +TBPostsListComponent >> tryConnectionWithLogin: login andPassword: password (login = 'admin' and: [ password = 'topsecret' ]) ifTrue: [ self goToAdministrationView ] - ifFalse: [ self loginErrorOccurred ] ``` For the moment we store directly the login and password in the method and this is not really a good practice. ### Managing Errors We defined the method `goToAdministrationView`. Let us add the method `loginErrorOccured` and a mechanism to display an error message when the user does not use the correct identifiers as shown in Figure *@loginErrorMessage@*. For this we will add a new instance variable `showLoginError` that represents the fact that we should display an error. ``` TBScreenComponent subclass: #TBPostsListComponent + ifFalse: [ self loginErrorOccurred ] +``` + + +For the moment we store directly the login and password in the method and this is not really a good practice. + +### Managing Errors + + +We defined the method `goToAdministrationView`. Let us add the method `loginErrorOccured` and a mechanism +to display an error message when the user does not use the correct identifiers as shown in Figure *@loginErrorMessage@*. + +For this we will add a new instance variable `showLoginError` that represents the fact that we should display an error. + +``` +TBScreenComponent subclass: #TBPostsListComponent instanceVariableNames: 'currentCategory showLoginError' classVariableNames: '' - package: 'TinyBlog-Components' ``` The method `loginErrorOccurred` specifies that an error should be displayed. ``` TBPostsListComponent >> loginErrorOccurred - showLoginError := true ``` We add a method to test this state. ``` TBPostsListComponent >> hasLoginError - ^ showLoginError ifNil: [ false ] ``` We define also an error message. ``` TBPostsListComponent >> loginErrorMessage - ^ 'Incorrect login and/or password' ``` We modify the method `renderPostColumnOn:` to perform a specific task to handle the errors. ``` TBPostsListComponent >> renderPostColumnOn: html + package: 'TinyBlog-Components' +``` + + +The method `loginErrorOccurred` specifies that an error should be displayed. + +``` +TBPostsListComponent >> loginErrorOccurred + showLoginError := true +``` + + +We add a method to test this state. + +``` +TBPostsListComponent >> hasLoginError + ^ showLoginError ifNil: [ false ] +``` + + +We define also an error message. + +``` +TBPostsListComponent >> loginErrorMessage + ^ 'Incorrect login and/or password' +``` + + +We modify the method `renderPostColumnOn:` to perform a specific task to handle the errors. + +``` +TBPostsListComponent >> renderPostColumnOn: html html tbsColumn extraSmallSize: 12; smallSize: 10; mediumSize: 8; with: [ self renderLoginErrorMessageIfAnyOn: html. - self basicRenderPostsOn: html ] ``` The method `renderLoginErrorMessageIfAnyOn:` displays if necessary an error message. It sets the instance variable `showLoginError` so that we do not display the error undefinitely. ``` TBPostsListComponent >> renderLoginErrorMessageIfAnyOn: html + self basicRenderPostsOn: html ] +``` + + +The method `renderLoginErrorMessageIfAnyOn:` displays if necessary an error message. +It sets the instance variable `showLoginError` so that we do not display the error +undefinitely. + +``` +TBPostsListComponent >> renderLoginErrorMessageIfAnyOn: html self hasLoginError ifTrue: [ showLoginError := false. html tbsAlert beDanger ; with: self loginErrorMessage - ] ``` ![Error message in case wrong identifiers.](figures/LoginErrorMessage.png width=75&label=loginErrorMessage) ### Modeling the Admin We do not want to store the administrator identifiers in the code as we did previously. We revise this now and will store the identifiers in a model: a class `Admin`. Let us start to enrich our TinyBlog model with the notion of administrator. We define a class named `TBAdministrator` characterized by it pseudo, login and password. ``` Object subclass: #TBAdministrator + ] +``` + + +![Error message in case wrong identifiers. % width=75&label=loginErrorMessage](figures/LoginErrorMessage.png) + + +### Modeling the Admin + + +We do not want to store the administrator identifiers in the code as we did previously. We revise this now and will store the identifiers in a model: a class `Admin`. + +Let us start to enrich our TinyBlog model with the notion of administrator. We define a class +named `TBAdministrator` characterized by its pseudo, login, and password. + +``` +Object subclass: #TBAdministrator instanceVariableNames: 'login password' classVariableNames: '' - package: 'TinyBlog' ``` ``` TBAdministrator >> login - ^ login ``` ``` TBAdministrator >> login: anObject - login := anObject ``` ``` TBAdministrator >> password - ^ password ``` Note that we do not store the admin password in the instance variable `password` but its hash encoded in SHA256. ``` TBAdministrator >> password: anObject - password := SHA256 hashMessage: anObject ``` We define also a new instance creation method. ``` TBAdministrator class >> login: login password: password + package: 'TinyBlog' +``` + + +``` +TBAdministrator >> login + ^ login +``` + + +``` +TBAdministrator >> login: anObject + login := anObject +``` + + +``` +TBAdministrator >> password + ^ password +``` + + +Note that we do not store the admin password in the instance variable `password` but its hash is encoded in SHA256. + +``` +TBAdministrator >> password: anObject + password := SHA256 hashMessage: anObject +``` + + +We define also a new instance creation method. + +``` +TBAdministrator class >> login: login password: password ^ self new login: login; password: password; - yourself ``` You can verify that the model works by executing the following expression: ``` luc := TBAdministrator login: 'luc' password: 'topsecret'. ``` ### Blog admin We decide for simplicity that a blog has one admin. We add the instance variable `adminUser` and an accessor in the classe `TBBlog` to store the blog admin. ``` Object subclass: #TBBlog + yourself +``` + + +You can verify that the model works by executing the following expression: + +``` +luc := TBAdministrator login: 'luc' password: 'topsecret'. +``` + + +### Blog admin + + +We decided for simplicity that a blog has one admin. +We add the instance variable `adminUser` and an accessor in the classe `TBBlog` to store +the blog admin. + +``` +Object subclass: #TBBlog instanceVariableNames: 'adminUser posts' classVariableNames: '' - package: 'TinyBlog' ``` ``` TBBlog >> administrator - ^ adminUser ``` We define a default login and password that we use as default. As we will see later, we will modify such attributes and these modified attributes will be saved at the same time that the blog in a database. ``` TBBlog class >> defaultAdminPassword - ^ 'topsecret' ``` ``` TBBlog class >> defaultAdminLogin - ^ 'admin' ``` Now we create a default admin. ``` TBBlog >> createAdministrator + package: 'TinyBlog' +``` + + +``` +TBBlog >> administrator + ^ adminUser +``` + + +We define a default login and password that we use as default. +As we will see later, we will modify such attributes and these modified attributes will be +saved at the same time that the blog in a database. + +``` +TBBlog class >> defaultAdminPassword + ^ 'topsecret' +``` + + +``` +TBBlog class >> defaultAdminLogin + ^ 'admin' +``` + + +Now we create a default admin. + +``` +TBBlog >> createAdministrator ^ TBAdministrator login: self class defaultAdminLogin - password: self class defaultAdminPassword ``` And we initialize the blog to set a default administrateur. ``` TBBlog >> initialize + password: self class defaultAdminPassword +``` + + +And we initialize the blog to set a default administrator. + +``` +TBBlog >> initialize super initialize. posts := OrderedCollection new. - adminUser := self createAdministrator ``` ### Setting a New Admin We should not recreate the blog: ``` TBBlog reset; createDemoPosts ``` We can now modify the admin information as follows: ``` |admin| + adminUser := self createAdministrator +``` + + +### Setting a New Admin + + +We should not recreate the blog: + +``` + TBBlog reset; createDemoPosts +``` + + +We can now modify the admin information as follows: + +``` +| admin | admin := TBBlog current administrator. admin login: 'luke'. admin password: 'thebrightside'. -TBBlog current save ``` Note that without doing anything, the blog admin information has been saved by Voyage in the database. Indeed the class `TBBlog` is a Voyage root, all its atttributes are automatically stored in the database when it received the message `save`. #### Possible Enhancements Define some tests for the extensions by writing new unit tests. ### Integrating the Admin Information Let us modify the method `tryConnectionWithLogin:andPassword:` so that it uses the current blog admin identifiers. Note that we are comparing the hash SHA256 of the password since we do not store the password. ``` TBPostsListComponent >> tryConnectionWithLogin: login andPassword: password +TBBlog current save +``` + + +Note that without doing anything, the blog admin information has been saved by Voyage in the database. +Indeed the class `TBBlog` is a Voyage root, all its attributes are automatically stored in the database when it receives +the message `save`. + +#### Possible Enhancements + + +Define some tests for the extensions by writing new unit tests. + + +### Integrating the Admin Information + + +Let us modify the method `tryConnectionWithLogin:andPassword:` so that it uses the current blog admin identifiers. +Note that we are comparing the hash SHA256 of the password since we do not store the password. + +``` +TBPostsListComponent >> tryConnectionWithLogin: login andPassword: password (login = self blog administrator login and: [ (SHA256 hashMessage: password) = self blog administrator password ]) ifTrue: [ self goToAdministrationView ] - ifFalse: [ self loginErrorOccurred ] ``` ### Storing the Admin in the Current Session With the current setup, when the blog admin wants to navigate between the private and public part, he must reconnects each time. We will simplify this situation but storing the current admin information in the session when the connection is succesful. A session object is given to the each instance of the application. Such session allows on to keep information which are shared and accessible between components. We will then store the current admin in a session and modify the components to display buttons that support a simplified navigation when the admin is logged. When he explicitely disconnect or when the session expires, we delete the current session. Figure *@SessionNavigation@* shows the navigation between the pages of TinyBlog. ![Navigation and identification in TinyBlog.](figures/sessionAuthSimplifiedNavigation.pdf width=100&label=SessionNavigation) ### Definition and use of specific session Let us start to define a subclass of `WASession` and name it `TBSession`. We add in this new class an instance variable that stores the current admin. ``` WASession subclass: #TBSession + ifFalse: [ self loginErrorOccurred ] +``` + + +### Storing the Admin in the Current Session + + +With the current setup, when the blog admin wants to navigate between the private and public part, he must reconnect +each time. +We will simplify this situation but storing the current admin information in the session when the connection is +succesful. + +A session object is given to each instance of the application. +Such a session allows one to keep information which is shared and accessible between components. + +We will then store the current admin in a session and modify the components to display buttons that support +simplified navigation when the admin is logged. + +When he explicitly disconnect or when the session expires, we delete the current session. + +Figure *@SessionNavigation@* shows the navigation between the pages of TinyBlog. + +![Navigation and identification in TinyBlog. %width=100&label=SessionNavigation](figures/sessionAuthSimplifiedNavigation.pdf) + + +### Definition and use of specific session + + +Let us start to define a subclass of `WASession` and name it `TBSession`. We add in this new class an instance variable that stores the current admin. + +``` +WASession subclass: #TBSession instanceVariableNames: 'currentAdmin' classVariableNames: '' - package: 'TinyBlog-Components' ``` ``` TBSession >> currentAdmin - ^ currentAdmin ``` ``` TBSession >> currentAdmin: anObject - currentAdmin := anObject ``` We define a method `isLogged` allows one to know if the administration is logged. ``` TBSession >> isLogged - ^ self currentAdmin notNil ``` Now we should indicate to Seaside to use `TBSession` as the class of the current session for our application. This initialization is done in the class method `initialize` in the class `TBApplicationRootComponent` as follows: ``` TBApplicationRootComponent class >> initialize + package: 'TinyBlog-Components' +``` + + +``` +TBSession >> currentAdmin + ^ currentAdmin +``` + + +``` +TBSession >> currentAdmin: anObject + currentAdmin := anObject +``` + + +We define a method `isLogged` allows one to know if the administration is logged. + +``` +TBSession >> isLogged + ^ self currentAdmin notNil +``` + + +Now we should indicate to Seaside to use `TBSession` as the class of the current session for our application. +This initialization is done in the class method `initialize` in the class `TBApplicationRootComponent` as follows: + +``` +TBApplicationRootComponent class >> initialize "self initialize" | app | app := WAAdmin register: self asApplicationAt: 'TinyBlog'. @@ -179,32 +769,115 @@ TBBlog current save ``` Note that without doing anything, the blog admin infor app addLibrary: JQDeploymentLibrary; addLibrary: JQUiDeploymentLibrary; - addLibrary: TBSDeploymentLibrary ``` Do not forget to exectute this expression `TBApplicationRootComponent initialize` before testing the application. ### Storing the Current Admin When a connection is successful, we add the admin object to the current session using the message `currentAdmin:`. Note that the current session is available to every Seaside component via `self session`. ``` TBPostsListComponent >> tryConnectionWithLogin: login andPassword: password + addLibrary: TBSDeploymentLibrary +``` + + +Do not forget to exectute this expression `TBApplicationRootComponent initialize` before testing the application. + +### Storing the Current Admin + + +When a connection is successful, we add the admin object to the current session using the message `currentAdmin:`. +Note that the current session is available to every Seaside component via `self session`. + +``` +TBPostsListComponent >> tryConnectionWithLogin: login andPassword: password (login = self blog administrator login and: [ (SHA256 hashMessage: password) = self blog administrator password ]) ifTrue: [ self session currentAdmin: self blog administrator. self goToAdministrationView ] - ifFalse: [ self loginErrorOccurred ] ``` ### Simplified navigation To put in place the simplified navigation we discussed above, we modify the header to display either a login button or a a simple navigation button to the admin part without forcing any reconnection. For this we use the session and the fact that we can know if a user is logged. ``` TBHeaderComponent >> renderButtonsOn: html + ifFalse: [ self loginErrorOccurred ] +``` + + + +### Simplified navigation + +To put in place the simplified navigation we discussed above, we modify the header to display either a login button or a +a simple navigation button to the admin part without forcing any reconnection. For this we use the session and the fact that we can know if a user is logged. + +``` +TBHeaderComponent >> renderButtonsOn: html self session isLogged ifTrue: [ self renderSimpleAdminButtonOn: html ] - ifFalse: [ self renderModalLoginButtonOn: html ] ``` You can test this new navigation but first create a new session \('New Session' button\). One reconnected the admin is added in session. Note that the deconnection button does not work correctly since it does invalidate the session. ### Managing Deconnection We add a method `reset` on our session object to delete the current admin, invalidate the current session and redirect to the application entry point. ``` TBSession >> reset + ifFalse: [ self renderModalLoginButtonOn: html ] +``` + + +You can test this new navigation but first create a new session ('New Session' button). +Once reconnected the admin is added in session. +Note that the disconnection button does not work correctly since it does invalidate the session. + +### Managing Disconnection + + +We add a method `reset` on our session object to delete the current admin, invalidate the current session and redirect to the application entry point. + +``` +TBSession >> reset currentAdmin := nil. self requestContext redirectTo: self application url. - self unregister. ``` Now we modify the header deconnection button to send the message `reset` to the correct session. ``` TBAdminHeaderComponent >> renderDisconnectButtonOn: html + self unregister. +``` + + +Now we modify the header disconnection button to send the message `reset` to the correct session. + +``` +TBAdminHeaderComponent >> renderDisconnectButtonOn: html html tbsNavbarButton tbsPullRight; callback: [ self session reset ]; with: [ html text: 'Disconnect '. - html tbsGlyphIcon iconLogout ] ``` Now we 'Disconnect' button works the way it should. ### Simplified Navigation to the Public Part We can add now a button in the header of the admin part to go back to the public part without being forced to get disconnected. ``` TBAdminHeaderComponent >> renderButtonsOn: html + html tbsGlyphIcon iconLogout ] +``` + + +Now we 'Disconnect' button works the way it should. + +### Simplified Navigation to the Public Part + + +We can add now a button in the header of the admin part to go back to the public part without being forced to get disconnected. + +``` +TBAdminHeaderComponent >> renderButtonsOn: html html form: [ self renderDisconnectButtonOn: html. - self renderPublicViewButtonOn: html ] ``` ``` TBAdminHeaderComponent >> renderPublicViewButtonOn: html + self renderPublicViewButtonOn: html ] +``` + + +``` +TBAdminHeaderComponent >> renderPublicViewButtonOn: html self session isLogged ifTrue: [ html tbsNavbarButton tbsPullRight; callback: [ component goToPostListView ]; with: [ html tbsGlyphIcon iconEyeOpen. - html text: ' Public View' ]] ``` Now you can test the naviagtion. It should correspond to the situation depicted by Figure *@SessionNavigation@*. ### Conclusion We put in place an authentication for TinyBlog. We create a reusable modal component. We made the distinction between component displayed when a user is connected or ot not and ease the navigation of a connected user using session. We are now ready for the administration part of the application and we will work on this in the next chapter. We will take advantage of it to show and advanced aspect: the automatic form generation. #### Possible Enhancements You can: - Add the admin logging in the header - Manage multipel admin accounts. \ No newline at end of file + html text: ' Public View' ]] +``` + + +Now you can test the navigation. It should correspond to the situation depicted by Figure *@SessionNavigation@*. + +### Conclusion + + +We put in place an authentication for TinyBlog. We create a reusable modal component. We made the distinction between +component displayed when a user is connected or ot not and ease the navigation of a connected user using session. + +We are now ready for the administration part of the application and we will work on this in the next chapter. +We will take advantage of it to show an advanced aspect: the automatic form generation. + +#### Possible Enhancements + + +You can: +- Add the admin logging in the header +- Manage multiple admin accounts. + diff --git a/Chapters/Chap08-TinyBlog-Admin-EN.md b/Chapters/Chap08-TinyBlog-Admin-EN.md index a52c641..a23d520 100644 --- a/Chapters/Chap08-TinyBlog-Admin-EN.md +++ b/Chapters/Chap08-TinyBlog-Admin-EN.md @@ -1,93 +1,361 @@ -## Administration Web Interface and Automatic Form Generation We will now develop the administration part of TinyBlog. In previous chapter, we define Seaside components that interact together and where each component is responsible for its internal state, behavior and its graphical rendering. In this chapter, we want to show that we can go a step further and generate Seaside components from object descriptions using the Magritte framework. Figure *@RapportNewLookActions2@* shows a part of the result we will obtain, the other part being post edition. ![Post managment.](figures/RapportNewLookActions.png width=75&label=RapportNewLookActions2) Figure *@ApplicationAdmin@* shows a survey of the architecture that we will develop in this chapter. ![Administration components.](figures/ApplicationArchitectureWithAdmin.pdf width=75&label=ApplicationAdmin) ### Describing Domain Data Magritte is a library that allows one to generate various representations once the objects are described. Coupled with Seaside, Magritte generates forms and reports. The Quuve of the Debris Publishing company is a brillant example of Magritte power: all tables and reports are automatically generated \(see [http://www.pharo.org/success](http://www.pharo.org/success)\). Data validation is also done at the Magritte level instead of being dispersed in the user interface code. This chapter will not cover such aspects. Resources on Magritte are a chapter in the Seaside book \([http://book.seaside.st](http://book.seaside.st)\) as well as a tutorial under writing available at [https://github.com/SquareBracketAssociates/Magritte](https://github.com/SquareBracketAssociates/Magritte). A description is an object that specifies information on the datat of our model as well as its type, whether the information is mandatory, if it should be sorted and what is the default value. ### Post Description Let us start to describe the five instance variable of `TBPost` with Magritte. Then we will show how we can get a form generated for us. We will define the five following methods in the protocol 'magritte-descriptions' of the class `TBPost`. Note that the method names are not important but we follow a convention. This is the pragma `` \(method annotation\) that allows Magritte to identify descriptions. The post title is a string of characters that is mandatory. ``` TBPost >> descriptionTitle +## Administration Web Interface and Automatic Form Generation + + +We will now develop the administration part of TinyBlog. +In previous chapter, we define Seaside components that interact together and where each component +is responsible for its internal state, behavior and its graphical rendering. + +In this chapter, we want to show that we can go a step further and generate Seaside components from object descriptions +using the Magritte framework. + +Figure *@RapportNewLookActions2@* shows a part of the result we will obtain, the other part being post-edition. + +![Post managment. %width=75&label=RapportNewLookActions2](figures/RapportNewLookActions.png) + +Figure *@ApplicationAdmin@* shows a survey of the architecture that we will develop in this chapter. + +![Administration components. % width=75&label=ApplicationAdmin](figures/ApplicationArchitectureWithAdmin.pdf) + +### Describing Domain Data + + +Magritte is a library that allows one to generate various representations once the objects are described. +Coupled with Seaside, Magritte generates forms and reports. +The Quuve of the Debris Publishing company +is a brillant example of Magritte power: all tables and reports are automatically generated (see [http://www.pharo.org/success](http://www.pharo.org/success)). + +Data validation is also done at the Magritte level instead of being dispersed in the user interface code. +This chapter will not cover such aspects. Resources on Magritte are a chapter in the Seaside book +([http://book.seaside.st](http://book.seaside.st)). + +A description is an object that specifies information on the data of our model as well as its type, whether the information +is mandatory, if it should be sorted and what is the default value. + +### Post Description + + +Let us start to describe the five instance variables of `TBPost` with Magritte. +Then we will show how we can get a form generated for us. + +We will define the five following methods in the protocol 'magritte-descriptions' of the class `TBPost`. +Note that the method names are not important but we follow a convention. +This is the pragma `` (method annotation) that allows Magritte to identify +descriptions. + +The post title is a string of characters that is mandatory. + +``` +TBPost >> descriptionTitle ^ MAStringDescription new accessor: #title; beRequired; - yourself ``` A post test is a multi-line that is mandatory. ``` TBPost >> descriptionText + yourself +``` + + +A post test is a multi-line that is mandatory. + + +``` +TBPost >> descriptionText ^ MAMemoDescription new accessor: #text; beRequired; - yourself ``` The category is represented as a string and it does not have to be given. In such case the post will be sorted in the 'Unclassified' category. ``` TBPost >> descriptionCategory + yourself +``` + + +The category is represented as a string and it does not have to be given. In such case +the post will be sorted in the 'Unclassified' category. + +``` +TBPost >> descriptionCategory ^ MAStringDescription new accessor: #category; - yourself ``` The post creation time is important since it is used to sort posts. It is then required. ``` TBPost >> descriptionDate + yourself +``` + + +The post creation time is important since it is used to sort posts. +It is then required. + +``` +TBPost >> descriptionDate ^ MADateDescription new accessor: #date; beRequired; - yourself ``` The visible instance variable should be a Boolean and it is required. ``` TBPost >> descriptionVisible + yourself +``` + + +The visible instance variable should be a Boolean and it is required. + +``` +TBPost >> descriptionVisible ^ MABooleanDescription new accessor: #visible; beRequired; - yourself ``` We could enrich the descriptions so that it is not possible to publish a post with a date before the current day. We could change the description of a category to make sure that a category is part of a predefined list of categories. We do not do it to keep it to the main point. ### Automatic Component Creation Once a post described we can generate a Seaside component by sending a message `asComponent` to an post instance. ``` aTBPost asComponent ``` Let us see how we can use this in the following. ### Building a post report We will develop a new component that will be used by the component `TBAdminComponent`. The `TBPostReport` component is a report that will contain all the posts. As we will see below the report Seaside component will be generated automatically from Magritte. We could have develop only one component but we prefer to distinguish it from the admin component for future evolution. #### The PostsReport Component Post list is displayed using a report dynamically generated by Magritte. We will use Magritte to implement the different behaviors of the admin activity \(post list, post creation, edition, delete of a post\). The component `TBPostsReport` is a subclass of `TBSMagritteReport` that manages reports with Bootstrap. ``` TBSMagritteReport subclass: #TBPostsReport + yourself +``` + + +We could enrich the descriptions so that it is not possible to publish a post with a date before the current day. +We could change the description of a category to make sure that a category is part of a predefined list of categories. +We do not do it to keep it to the main point. + + +### Automatic Component Creation + + +Once a post is described we can generate a Seaside component by sending a message `asComponent` to an post instance. + +``` +aTBPost asComponent +``` + + +Let us see how we can use this in the following. + + +### Building a post report + + +We will develop a new component that will be used by the component `TBAdminComponent`. +The `TBPostReport` component is a report that will contain all the posts. +As we will see below the report Seaside component will be generated automatically from Magritte. +We could have develop only one component but we prefer to distinguish it from the admin component for future evolution. + +#### The PostsReport Component + + +Post list is displayed using a report dynamically generated by Magritte. +We will use Magritte to implement the different behaviors of the admin activity (post list, post creation, edition, delete of a post). + +The component `TBPostsReport` is a subclass of +`TBSMagritteReport` that manages reports with Bootstrap. + +``` +TBSMagritteReport subclass: #TBPostsReport instanceVariableNames: '' classVariableNames: '' - package: 'TinyBlog-Components' ``` We add a creation method that takes a blog as argument. ``` TBPostsReport class >> from: aBlog + package: 'TinyBlog-Components' +``` + + +We add a creation method that takes a blog as an argument. + +``` +TBPostsReport class >> from: aBlog | allBlogs | allBlogs := aBlog allBlogPosts. - ^ self rows: allBlogs description: allBlogs anyOne magritteDescription ``` ### AdminComponent Integration with PostsReport Let us now revise our `TBAdminComponent` to display this report. We add an instance variable `report` and its accessors in the class `TBAdminComponent`. ``` TBScreenComponent subclass: #TBAdminComponent + ^ self rows: allBlogs description: allBlogs anyOne magritteDescription +``` + + +### AdminComponent Integration with PostsReport + + +Let us now revise our `TBAdminComponent` to display this report. + +We add an instance variable `report` and its accessors in the class `TBAdminComponent`. + +``` +TBScreenComponent subclass: #TBAdminComponent instanceVariableNames: 'report' classVariableNames: '' - package: 'TinyBlog-Components' ``` ``` TBAdminComponent >> report - ^ report ``` ``` TBAdminComponent >> report: aReport - report := aReport ``` Since the report is a son component of the admin component we should not forget to redefine the method `children`. Note that the collection contains the subcomponents defined in the superclass \(header component\) and those in current class \(report component\). ``` TBAdminComponent >> children - ^ super children copyWith: self report ``` In `initialize` method we instantiate a report by giving it a blog instance. ``` TBAdminComponent >> initialize + package: 'TinyBlog-Components' +``` + + +``` +TBAdminComponent >> report + ^ report +``` + + +``` +TBAdminComponent >> report: aReport + report := aReport +``` + + +Since the report is a son component of the admin component we should not forget to redefine the method `children`. +Note that the collection contains the subcomponents defined in the superclass (header component) and those +in the current class (report component). + +``` +TBAdminComponent >> children + ^ super children copyWith: self report +``` + + +In the `initialize` method we instantiate a report by giving it a blog instance. + +``` +TBAdminComponent >> initialize super initialize. - self report: (TBPostsReport from: self blog) ``` Let us modify the admin part rendering to display the report. ``` TBAdminComponent >> renderContentOn: html + self report: (TBPostsReport from: self blog) +``` + + +Let us modify the admin part rendering to display the report. + +``` +TBAdminComponent >> renderContentOn: html super renderContentOn: html. html tbsContainer: [ html heading: 'Blog Admin'. html horizontalRule. - html render: self report ] ``` You can test this change by refreshing your web browser. ### Filter Columns By default, a report displays the full data of each post. However, some columns are not useful We should filter the columns. Here we only keep the title, category and publication date. We add a class method for the column selection and modifier the method `from:` to use this. ``` TBPostsReport class >> filteredDescriptionsFrom: aBlogPost + html render: self report ] +``` + + +You can test this change by refreshing your web browser. + + +### Filter Columns + + +By default, a report displays the full data of each post. +However, some columns are not useful +We should filter the columns. +Here we only keep the title, category, and publication date. + +We add a class method for the column selection and modifier the method `from:` to use this. + +``` +TBPostsReport class >> filteredDescriptionsFrom: aBlogPost "Filter only some descriptions for the report columns." ^ aBlogPost magritteDescription - select: [ :each | #(title category date) includes: each accessor selector ] ``` ``` TBPostsReport class >> from: aBlog + select: [ :each | #(title category date) includes: each accessor selector ] +``` + + +``` +TBPostsReport class >> from: aBlog | allBlogs | allBlogs := aBlog allBlogPosts. - ^ self rows: allBlogs description: (self filteredDescriptionsFrom: allBlogs anyOne) ``` Figure *@RapportV1@* shows the situation that you should get. ![Magritte report with posts.](figures/RapportMagritteV1.png width=100&label=RapportV1) ### Report Enhancements The previous report is pretty raw. There is no title on columns and the display column order is not fixed. This can change from one instance to the other. To handle this, we modify the description for each instance variable. We specify a priority and a title \(message `label:`\) as follows: ``` TBPost >> descriptionTitle + ^ self rows: allBlogs description: (self filteredDescriptionsFrom: allBlogs anyOne) +``` + + + +Figure *@RapportV1@* shows the situation that you should get. + +![Magritte report with posts. %width=100&label=RapportV1](figures/RapportMagritteV1.png) + + +### Report Enhancements + + +The previous report is pretty raw. +There is no title on columns and the display column order is not fixed. +This can change from one instance to the other. +To handle this, we modify the description for each instance variable. +We specify a priority and a title (message `label:`) as follows: + +``` +TBPost >> descriptionTitle ^ MAStringDescription new label: 'Title'; priority: 100; accessor: #title; beRequired; - yourself ``` ``` TBPost >> descriptionText + yourself +``` + + +``` +TBPost >> descriptionText ^ MAMemoDescription new label: 'Text'; priority: 200; accessor: #text; beRequired; - yourself ``` ``` TBPost >> descriptionCategory + yourself +``` + + +``` +TBPost >> descriptionCategory ^ MAStringDescription new label: 'Category'; priority: 300; accessor: #category; - yourself ``` ``` TBPost >> descriptionDate + yourself +``` + + +``` +TBPost >> descriptionDate ^ MADateDescription new label: 'Date'; priority: 400; accessor: #date; beRequired; - yourself ``` ``` TBPost >> descriptionVisible + yourself +``` + + +``` +TBPost >> descriptionVisible ^ MABooleanDescription new label: 'Visible'; priority: 500; accessor: #visible; beRequired; - yourself ``` You should obtain the situation such as represented by Figure *@adminReportDraft@*. ![Administration Report.](figures/RapportMagritteV2.png width=85&label=adminReportDraft) ### Post Administration We can now put in place a CRUD \(Create Read Update Delete\) allowing to generate posts. For this, we will add a new column \(instance of `MACommandColumn`\) to the report. This column will group the different operations using the `addCommandOn:` message. This method allows one to define a link that will execute a method of the current object. We give access to the blog the report is build for. ``` TBSMagritteReport subclass: #TBPostsReport + yourself +``` + + +You should obtain the situation as represented by Figure *@adminReportDraft@*. +![Administration Report. % width=85&label=adminReportDraft](figures/RapportMagritteV2.png) + + +### Post Administration + + +We can now put in place a CRUD (Create Read Update Delete) allowing to generate posts. +For this, we will add a new column (instance of `MACommandColumn`) to the report. +This column will group the different operations using the `addCommandOn:` message. +This method allows one to define a link that will execute a method of the current object. +We give access to the blog the report is built for. + +``` +TBSMagritteReport subclass: #TBPostsReport instanceVariableNames: 'blog' classVariableNames: '' - package: 'TinyBlog-Components' ``` ``` TBPostsReport >> blog - ^ blog ``` ``` TBPostsReport >> blog: aTBBlog - blog := aTBBlog ``` The method `from:` adds a new column to the report. It groups the different operations. ``` TBPostsReport class >> from: aBlog + package: 'TinyBlog-Components' +``` + + +``` +TBPostsReport >> blog + ^ blog +``` + + +``` +TBPostsReport >> blog: aTBBlog + blog := aTBBlog +``` + + +The method `from:` adds a new column to the report. It groups the different operations. + +``` +TBPostsReport class >> from: aBlog | report blogPosts | blogPosts := aBlog allBlogPosts. report := self rows: blogPosts description: (self filteredDescriptionsFrom: blogPosts anyOne). @@ -96,52 +364,217 @@ addCommandOn: report selector: #viewPost: text: 'View'; yourself; addCommandOn: report selector: #editPost: text: 'Edit'; yourself; addCommandOn: report selector: #deletePost: text: 'Delete'; yourself). - ^ report ``` We will have to define the methods linked to each operation in the following section. In addition this method is a bit lengthly and it does not separate the report definition from the operation definition. A possible solution is to create an instance method named `addCommands` and to call it explicitly. Try to do it to practice. ### Post Addition Addition a post is not associated with a post and we place just before the main report. Since this behavior is then part of the component `TBPostsReport`, we should redefine the method `renderContentOn:` of the component `TBPostsReport` to insert a link `add`. ``` TBPostsReport >> renderContentOn: html + ^ report +``` + + + +We will have to define the methods linked to each operation in the following section. + +In addition, this method is a bit lengthy and it does not separate the report definition from the operation definition. +A possible solution is to create an instance method named `addCommands` and to call it explicitly. +Try to do it to practice. + + + +### Post Addition + +Adding a post is not associated with a post and we place it just before the main report. +Since this behavior is then part of the component `TBPostsReport`, we should redefine the method +`renderContentOn:` of the component `TBPostsReport` to insert a link `add`. + +``` +TBPostsReport >> renderContentOn: html html tbsGlyphIcon iconPencil. html anchor callback: [ self addPost ]; with: 'Add post'. - super renderContentOn: html ``` Login another time and you should get the situation as it is represented in Figure *@RapportNewLookActions@*. ![Post report with links.](figures/RapportNewLookActions.png width=75&label=RapportNewLookActions) ### CRUD Action Implementation Each action \(Create/Read/Update/Delete\) should invoke methods of the instance of `TBPostsReport`. We implement them now. A personalized form is built based on the requested operation \(it is not necessary to have a save butten when the user is just viewing a post\). ### Post Addition Let us begin with post addition. The following method `renderAddPostForm:` iillustres the power of Magritte to generate forms: ``` TBPostsReport >> renderAddPostForm: aPost + super renderContentOn: html +``` + + +Login another time and you should get the situation as it is represented in Figure *@RapportNewLookActions@*. +![Post report with links. % width=75&label=RapportNewLookActions](figures/RapportNewLookActions.png) + + +### CRUD Action Implementation + +Each action (Create/Read/Update/Delete) should invoke methods of the instance of `TBPostsReport`. +We implement them now. +A personalized form is built based on the requested operation (it is not necessary to have a save button when the user is just viewing a post). + + +### Post Addition + +Let us begin with post-addition. +The following method `renderAddPostForm:` illustrates the power of Magritte to generate forms: + +``` +TBPostsReport >> renderAddPostForm: aPost ^ aPost asComponent addDecoration: (TBSMagritteFormDecoration buttons: { #save -> 'Add post' . #cancel -> 'Cancel'}); - yourself ``` Here the message `asComponent`, sent to the object of class `TBPost`, creates directly a component. We add a decoration to this component to manage ok/cancel. The method `addPost` displays the component returned by the method `renderAddPostForm:` and when a new post is created, it is added for the blog. The method `writeBlogPost:` saves the changes the user may do. ``` TBPostsReport >> addPost + yourself +``` + + +Here the message `asComponent`, sent to the object of class `TBPost`, creates directly a component. +We add a decoration to this component to manage ok/cancel. + +The method `addPost` displays the component returned by the method `renderAddPostForm:` and +when a new post is created, it is added to the blog. +The method `writeBlogPost:` saves the changes the user may make. + +``` +TBPostsReport >> addPost | post | post := self call: (self renderAddPostForm: TBPost new). - post ifNotNil: [ blog writeBlogPost: post ] ``` In this method we see another use of the message `call:` to give the control to a component. The link to add a post allows one to display a creation form that we will make better looking later \(See Figure *@AffichePostRaw@*\). ![Basic rendering of a post.](figures/AffichePostRaw.png width=75&label=AffichePostRaw) #### Post Display To display a post in read-only mode, we define two methods similar to the previous. Note that we use the `readonly: true` to indicate that the form is not editable. ``` TBPostsReport >> renderViewPostForm: aPost + post ifNotNil: [ blog writeBlogPost: post ] +``` + + +In this method we see another use of the message `call:` to give the control to a component. +The link to add a post allows one to display a creation form that we will make better looking later (See Figure *@AffichePostRaw@*). + + +![Basic rendering of a post. % width=75&label=AffichePostRaw](figures/AffichePostRaw.png) + + + +#### Post Display + +To display a post in read-only mode, we define two methods similar to the previous. +Note that we use the `readonly: true` to indicate that the form is not editable. + +``` +TBPostsReport >> renderViewPostForm: aPost ^ aPost asComponent addDecoration: (TBSMagritteFormDecoration buttons: { #cancel -> 'Back' }); readonly: true; - yourself ``` Looking at a post does not require any extra action other than rendering it. ``` TBPostsReport >> viewPost: aPost - self call: (self renderViewPostForm: aPost) ``` #### Post Edition To edit a post, we use the same approach. ``` TBPostsReport >> renderEditPostForm: aPost + yourself +``` + + +Looking at a post does not require any extra action other than rendering it. + +``` +TBPostsReport >> viewPost: aPost + self call: (self renderViewPostForm: aPost) +``` + + +#### Post Edition + + +To edit a post, we use the same approach. + +``` +TBPostsReport >> renderEditPostForm: aPost ^ aPost asComponent addDecoration: ( TBSMagritteFormDecoration buttons: { #save -> 'Save post'. #cancel -> 'Cancel'}); - yourself ``` Now the method `editPost:` gets the value of the `call:` message and saves the changes made. ``` TBPostsReport >> editPost: aPost + yourself +``` + + +Now the method `editPost:` gets the value of the `call:` message and saves the changes made. + +``` +TBPostsReport >> editPost: aPost | post | post := self call: (self renderEditPostForm: aPost). - post ifNotNil: [ blog save ] ``` #### Removing a post We must now adding the method `removeBlogPost:` to the class `TBBlog`: ``` TBBlog >> removeBlogPost: aPost + post ifNotNil: [ blog save ] +``` + + +#### Removing a post + + +We must now add the method `removeBlogPost:` to the class `TBBlog`: + +``` +TBBlog >> removeBlogPost: aPost posts remove: aPost ifAbsent: [ ]. - self save. ``` Let us add a unit test: ``` TBBlogTest >> testRemoveBlogPost + self save. +``` + + +Let us add a unit test: + +``` +TBBlogTest >> testRemoveBlogPost self assert: blog size equals: 1. blog removeBlogPost: blog allBlogPosts anyOne. - self assert: blog size equals: 0 ``` To avoid an unwanted operation, we use a modal dialog so that the user confirms the deletion of the post. One the post is displayed, the list of managed posts by `TBPostsReport` is changed and should be refresh. ``` TBPostsReport >> deletePost: aPost + self assert: blog size equals: 0 +``` + + +To avoid an unwanted operation, we use a modal dialog so that the user confirms the deletion of the post. +Once the post is displayed, the list of managed posts by `TBPostsReport` is changed and should be refreshed. + +``` +TBPostsReport >> deletePost: aPost (self confirm: 'Do you want remove this post ?') - ifTrue: [ blog removeBlogPost: aPost ] ``` ### Refreshing Posts The methods `addPost:` and `deletePost:` are working well but the display is not refreshed. We need to refresh the post lists using the expression `self refresh`. ``` TBPostsReport >> refreshReport + ifTrue: [ blog removeBlogPost: aPost ] +``` + + + + +### Refreshing Posts + + +The methods `addPost:` and `deletePost:` are working well but the display is not refreshed. +We need to refresh the post lists using the expression `self refresh`. + +``` +TBPostsReport >> refreshReport self rows: blog allBlogPosts. - self refresh. ``` ``` TBPostsReport >> addPost + self refresh. +``` + + +``` +TBPostsReport >> addPost | post | post := self call: (self renderAddPostForm: TBPost new). post ifNotNil: [ blog writeBlogPost: post. - self refreshReport ] ``` ``` TBPostsReport >> deletePost: aPost + self refreshReport ] +``` + + +``` +TBPostsReport >> deletePost: aPost (self confirm: 'Do you want remove this post ?') ifTrue: [ blog removeBlogPost: aPost. - self refreshReport ] ``` The report is not working and it even manage input constraints: for example, mandatory fields should be filled up. ### Better Form Look To take advantage of Bootstrap, we will modify Magritte definitions. First we specify that the report rendering based on Bootstrap. A container in Magritte is the element that will contain the other components created from descriptions. ``` TBPost >> descriptionContainer + self refreshReport ] +``` + + +The report is not working and it even manages input constraints: for example, mandatory fields should be filled up. + +### Better Form Look + + +To take advantage of Bootstrap, we will modify Magritte definitions. +First we specify that the report rendering is based on Bootstrap. + +A container in Magritte is the element that will contain the other components created from descriptions. + +``` +TBPost >> descriptionContainer ^ super descriptionContainer componentRenderer: TBSMagritteFormRenderer; - yourself ``` We want can now pay attention of the different input fields and improve their appearance. ``` TBPost >> descriptionTitle + yourself +``` + + +We want can now pay attention of the different input fields and improve their appearance. + +``` +TBPost >> descriptionTitle ^ MAStringDescription new label: 'Title'; @@ -151,7 +584,15 @@ comment: 'Please enter a title'; componentClass: TBSMagritteTextInputComponent; beRequired; - yourself ``` ![Post form addition with Bootstrap.](figures/AddAPostBootstrap.png width=85&label=addAPostBootstrap) ``` TBPost >> descriptionText + yourself +``` + + +![Post form addition with Bootstrap. % width=85&label=addAPostBootstrap](figures/AddAPostBootstrap.png) + + +``` +TBPost >> descriptionText ^ MAMemoDescription new label: 'Text'; @@ -161,7 +602,12 @@ requiredErrorMessage: 'A blog post must contain a text.'; comment: 'Please enter a text'; componentClass: TBSMagritteTextAreaComponent; - yourself ``` ``` TBPost >> descriptionCategory + yourself +``` + + +``` +TBPost >> descriptionCategory ^ MAStringDescription new label: 'Category'; @@ -169,7 +615,12 @@ accessor: #category; comment: 'Unclassified if empty'; componentClass: TBSMagritteTextInputComponent; - yourself ``` ``` TBPost >> descriptionVisible + yourself +``` + + +``` +TBPost >> descriptionVisible ^ MABooleanDescription new checkboxLabel: 'Visible'; @@ -177,4 +628,17 @@ accessor: #visible; componentClass: TBSMagritteCheckboxComponent; beRequired; - yourself ``` Based on new Magritte descriptions, forms generated now use Bootstrap. For example, the post form edition should not looks like Figure *@addAPostBootstrap@*. ### Conclusion In this chapter we defined the administration of TinyBlog based on report built out of the posts contained in the current blog. We added links to manage CRUD for each post. What we show is that adding descriptions on post let us generate Seaside components automatically. \ No newline at end of file + yourself +``` + + +Based on new Magritte descriptions, forms generated now use Bootstrap. +For example, the post form edition should not looks like Figure *@addAPostBootstrap@*. + + +### Conclusion + + +In this chapter, we defined the administration of TinyBlog based on a report built out of the posts contained in the current blog. +We added links to manage CRUD for each post. +What we show is that adding descriptions on post let us generate Seaside components automatically. diff --git a/Chapters/Chap13-TinyBlog-Deployment-EN.md b/Chapters/Chap13-TinyBlog-Deployment-EN.md index 7932c08..7c15edf 100644 --- a/Chapters/Chap13-TinyBlog-Deployment-EN.md +++ b/Chapters/Chap13-TinyBlog-Deployment-EN.md @@ -1,4 +1,45 @@ -## Deploying TinyBlog In this chapter we will show you how to deploy your Pharo application. In particular we will show how to deploy on the cloud. ### Deploying in the cloud Now that TinyBlog is ready we will see how we can deploy your application on a server on the web. If you want to deploy your application on a server that you administrate, we suggest reading the last chapter of "Enterprise Pharo: a Web Perspective" \([http://books.pharo.org](http://books.pharo.org)\). In the following we present a simpler solution offered by PharoCloud. ### Login on PharoCloud PharoCloud is hosting Pharo applications and it offers the possibility to freely tests its services \(ephemeric cloud subscription\). Prepare your PharoCloud account: - Create an account on [http://pharocloud.com](http://pharocloud.com) - Activate your account - Connect to this account - Activate "Ephemeric Cloud" to get an id \(**API User ID**\) and password \(**API Auth Token**\) - Click on "Open Cloud Client" and login with the previous ids - Once connected, you should get a web page that allows you to upload a zip archive containing a Pharo image and its companion Pharo changes file. ### Preparing your Pharo image Pharo for PharoCloud #### Get a fresh new image You should - First downaload a fresh PharoWeb image from [http://files.pharo.org/mooc/image/PharoWeb-60.zip](http://files.pharo.org/mooc/image/PharoWeb-60.zip). - Launch this image and now we will configure it. #### Seaside configuration We remove the Seaside demo applications and the development tools ``` "Seaside Deployment configuration" +## Deploying TinyBlog + + +In this chapter, we will show you how to deploy your Pharo application. In particular, we will show how to deploy on the cloud. + +### Deploying in the cloud + + +Now that TinyBlog is ready we will see how we can deploy your application on a server on the web. +If you want to deploy your application on a server that you administrate, we suggest reading the last chapter of + "Enterprise Pharo: a Web Perspective" ([http://books.pharo.org](http://books.pharo.org)). +In the following, we present a simpler solution offered by PharoCloud. + +### Login on PharoCloud + +PharoCloud is hosting Pharo applications and it offers the possibility to freely tests its services \(ephemeric cloud subscription\). + +Prepare your PharoCloud account: +- Create an account on [http://pharocloud.com](http://pharocloud.com) +- Activate your account +- Connect to this account +- Activate "Ephemeric Cloud" to get an id \(**API User ID**\) and password \(**API Auth Token**\) +- Click on "Open Cloud Client" and log in with the previous IDs +- Once connected, you should get a web page that allows you to upload a zip archive containing a Pharo image and its companion Pharo changes file. + + +### Preparing your Pharo image Pharo for PharoCloud + + +#### Get a fresh new image + +You should +- First download a fresh PharoWeb image from [http://files.pharo.org/mooc/image/PharoWeb-60.zip](http://files.pharo.org/mooc/image/PharoWeb-60.zip). +- Launch this image and now we will configure it. + + +#### Seaside configuration + +We remove the Seaside demo applications and the development tools + +``` +"Seaside Deployment configuration" WAAdmin clearAll. WAAdmin applicationDefaults removeParent: WADevelopmentConfiguration instance. WAFileHandler default: WAFileHandler new. @@ -7,7 +48,18 @@ WAFileHandler default put: WAHtmlFileHandlerListing. WAAdmin defaultDispatcher register: WAFileHandler default - at: 'files'. ``` #### Loading TinyBlog We load the latest version of the TinyBlog application. To load the version we propose you can use : ``` "Load TinyBlog" + at: 'files'. +``` + + +#### Loading TinyBlog + + +We load the latest version of the TinyBlog application. +To load the version we propose you can use : + +``` +"Load TinyBlog" Gofer new smalltalkhubUser: 'PharoMooc' project: 'TinyBlog'; package: 'ConfigurationOfTinyBlog'; @@ -15,19 +67,73 @@ Gofer new #ConfigurationOfTinyBlog asClass loadFinalApp. "Create Demo posts if needed" -#TBBlog asClass createDemoPosts. ``` You can also load **your ** TinyBlog code from your Smalltalkhub repository. For example doing: ``` "Load TinyBlog" +#TBBlog asClass createDemoPosts. +``` + + +You can also load **your ** TinyBlog code from your Smalltalkhub repository. +For example doing: + +``` +"Load TinyBlog" Gofer new smalltalkhubUser: 'XXXX' project: 'TinyBlog'; package: 'TinyBlog'; load. "Create Demo posts if needed" -#TBBlog asClass createDemoPosts. ``` #### TinyBlog as Default Seaside Application We now set Tinyblog as the default Seaside application and we run the HTTP webserver: ``` "Tell Seaside to use TinyBlog as default app" +#TBBlog asClass createDemoPosts. +``` + + +#### TinyBlog as Default Seaside Application + +We now set Tinyblog as the default Seaside application and we run the HTTP webserver: + +``` +"Tell Seaside to use TinyBlog as default app" WADispatcher default defaultName: 'TinyBlog'. "Register TinyBlog on Seaside" -#TBApplicationRootComponent asClass initialize. ``` Lancer Seaside : ``` "Start HTTP server" - ZnZincServerAdaptor startOn: 8080. ``` #### Save the Image Save your image \(Pharo Menu > save\) and locally test it in your web browser at: [http://localhost:8080](http://localhost:8080). ### Manually deploying on PharoCloud's Ephemeric Cloud - Create a zip archive that contains the previously saved images and changes files: `PharoWeb.image` et `PharoWeb.changes`. - Drag and drop this zip file on the Ephemeric Cloud and activate the image using the play button as shown in Figure *@activeEphemerics@*. ![Ephemeric Cloud administration Pharo image.](figures/ActiveEphemerics.png width=85&label=activeEphemerics) By clicking on the public URL given by PharoCloud you will be able to display your TinyBlog application as shown by Figure *@tinyBlogOnPharoCloud@*. ![Your TinyBlog Application on PharoCloud.](figures/TinyBlogOnPharoCloud.png width=85&label=tinyBlogOnPharoCloud) ### Automatic Deployment on PharoCloud's Ephemeric Cloud Instead of creating a zip archive and using your web browser, the documentation of PharoCloud \([http://docs.swarm.pharocloud.com/](http://docs.swarm.pharocloud.com/)\) shows how to deploy automatically by executing the following code \(it takes some time\): ``` |client EPHUSER EPHTOKEN| +#TBApplicationRootComponent asClass initialize. +``` + + +Lancer Seaside : + +``` + "Start HTTP server" + ZnZincServerAdaptor startOn: 8080. +``` + + +#### Save the Image + +Save your image (Pharo Menu > save) and locally test it in your web browser at: [http://localhost:8080](http://localhost:8080). + +### Manually deploying on PharoCloud's Ephemeric Cloud + + +- Create a zip archive that contains the previously saved images and changes files: `PharoWeb.image` et `PharoWeb.changes`. + + +- Drag and drop this zip file on the Ephemeric Cloud and activate the image using the play button as shown in Figure *@activeEphemerics@*. + + +![Ephemeric Cloud administration Pharo image. % width=85&label=activeEphemerics](figures/ActiveEphemerics.png) + +By clicking on the public URL given by PharoCloud you will be able to display your TinyBlog application as shown by Figure *@tinyBlogOnPharoCloud@*. + +![Your TinyBlog Application on PharoCloud. % width=85&label=tinyBlogOnPharoCloud](figures/TinyBlogOnPharoCloud.png) + +### Automatic Deployment on PharoCloud's Ephemeric Cloud + + +Instead of creating a zip archive and using your web browser, the documentation of + PharoCloud ([http://docs.swarm.pharocloud.com/](http://docs.swarm.pharocloud.com/)) shows how to deploy automatically by executing the following code (it takes some time): + +``` +|client EPHUSER EPHTOKEN| Metacello new smalltalkhubUser: 'mikefilonov' project: 'EphemericCloudAPI'; @@ -39,4 +145,17 @@ ephToken :=''. client := EphemericCloudClient userID: EPHUSER authToken: EPHTOKEN. (client publishSelfAs: 'glimpse') ifTrue:[ZnZincServerAdaptor startOn: 8080] - ifFalse: [ client lastPublishedInstance hostname ] ``` ### About Dependencies Good development practice in Pharo are to specific explicitly the dependencies on the used packages. This ensures the fact that we can reproduce a software artefact. Such reproducibility supports then the use of an integration server such as Travis or Jenkins. For this a baseline \(a special class\) defines the architecture of a project \(dependencies to other projects as well as structure of your projects\). This is this way that we automatically build the PharoWeb image. In this book we do not cover this point. \ No newline at end of file + ifFalse: [ client lastPublishedInstance hostname ] +``` + + + +### About Dependencies + + +Good development practices in Pharo are to specify explicitly the dependencies on the used packages. +This ensures the fact that we can reproduce a software artifact. +Such reproducibility supports then the use of an integration server such as Travis or Jenkins. +For this a baseline (a special class) defines the architecture of a project \(dependencies to other projects as well as the structure of your projects). +This is this way that we automatically build the PharoWeb image. +In this book we do not cover this point. diff --git a/Chapters/Chap14-TinyBlog-Loading-EN.md b/Chapters/Chap14-TinyBlog-Loading-EN.md index 6d40ad2..400d4a7 100644 --- a/Chapters/Chap14-TinyBlog-Loading-EN.md +++ b/Chapters/Chap14-TinyBlog-Loading-EN.md @@ -1,33 +1,175 @@ -## Loading Chapter Code @cha:loading This chapter contains the expressions to load the code described in each of the chapters. Such expressions can be executed in any Pharo 8.0 \(or above\) image. Nevertheless, using the Pharo MOOC image \(cf. Pharo Launcher\) is usually faster because it already includes several libraries such as: Seaside, Voyage, ... When you start for example the chapter 4, you can load all the code of the previous chapters \(1, 2, and 3\) by following the process described in the following section 'Chapter 4'. Obviously, we believe that this is better that you use you own code but having our code at hand can help you in case you would be stuck. ### Chapter 3: Extending and Testing the Model You can load the correction of the previous chapter as follow: ``` Metacello new +## Loading Chapter Code + +@cha:loading + +This chapter contains the expressions to load the code described in each of the chapters. +Such expressions can be executed in any Pharo 8.0 (or above) image. +Nevertheless, using the Pharo MOOC image (cf. Pharo Launcher) is usually faster because it already includes several libraries such as: Seaside, Voyage, ... + +When you start for example the chapter 4, you can load all the code of the previous Chapters (1, 2, and 3) by following the process described in the following section 'Chapter 4'. + +Obviously, we believe that this is better that you use you own code but having our code at hand can help you in case you would be stuck. + +### Chapter 3: Extending and Testing the Model + + +You can load the correction of the previous chapter as follows: + +``` +Metacello new baseline:'TinyBlog'; repository: 'github://LucFabresse/TinyBlog:chapter2/src'; onConflict: [ :ex | ex useLoaded ]; - load ``` Run the tests! To do so, you can use the TestRunner \(Tools menu > Test Runner\), look for the package TinyBlog-Tests and click on "Run Selected". All tests should be green. ### Chapter 4: Data Persitency using Voyage and Mongo You can load the correction of the previous chapter as follow: ``` Metacello new + load +``` + + +Run the tests! +To do so, you can use the TestRunner (Tools menu > Test Runner), look for the package TinyBlog-Tests, and click on "Run Selected". +All tests should be green. + +### Chapter 4: Data Persistency using Voyage and Mongo + + +You can load the correction of the previous chapter as follows: + +``` +Metacello new baseline:'TinyBlog'; repository: 'github://LucFabresse/TinyBlog:chapter3/src'; onConflict: [ :ex | ex useLoaded ]; - load ``` Once loaded execute the tests. ### Chapter 5: First Steps with Seaside You can load the correction of the previous chapter as follow: ``` Metacello new + load +``` + + +Once loaded execute the tests. + +### Chapter 5: First Steps with Seaside + + +You can load the correction of the previous chapter as follows: + +``` +Metacello new baseline:'TinyBlog'; repository: 'github://LucFabresse/TinyBlog:chapter4/src'; onConflict: [ :ex | ex useLoaded ]; - load ``` Execute the tests. To test the application, start the HTTP server: ``` ZnZincServerAdaptor startOn: 8080. ``` Open your web browser at `http://localhost:8080/TinyBlog` You may need to recreate some posts as follows: ``` TBBlog reset ; createDemoPosts ``` ### Chapitre 6: Web Components for TinyBlog You can load the correction of the previous chapter as follow: ``` Metacello new + load +``` + + +Execute the tests. + +To test the application, start the HTTP server: +``` +ZnZincServerAdaptor startOn: 8080. +``` + + +Open your web browser at `http://localhost:8080/TinyBlog` + +You may need to recreate some posts as follows: + +``` +TBBlog reset ; createDemoPosts +``` + + +### Chapitre 6: Web Components for TinyBlog + + +You can load the correction of the previous chapter as follows: + +``` +Metacello new baseline:'TinyBlog'; repository: 'github://LucFabresse/TinyBlog:chapter5/src'; onConflict: [ :ex | ex useLoaded ]; - load ``` Same process as above. ### Chapitre 7: Managing Categories You can load the correction of the previous chapter as follow: ``` Metacello new + load +``` + + +Same process as above. + +### Chapitre 7: Managing Categories + + +You can load the correction of the previous chapter as follows: + +``` +Metacello new baseline:'TinyBlog'; repository: 'github://LucFabresse/TinyBlog:chapter6/src'; onConflict: [ :ex | ex useLoaded ]; - load ``` To test the application, start the HTTP server: ``` ZnZincServerAdaptor startOn: 8080. ``` Open your web browser at `http://localhost:8080/TinyBlog` You may need to recreate some posts as follows: ``` TBBlog reset ; createDemoPosts ``` ### Chapitre 8: Authentication and Session You can load the correction of the previous chapter as follow: ``` Metacello new + load +``` + + + +To test the application, start the HTTP server: + +``` +ZnZincServerAdaptor startOn: 8080. +``` + + +Open your web browser at `http://localhost:8080/TinyBlog` + + +You may need to recreate some posts as follows: + +``` +TBBlog reset ; createDemoPosts +``` + + + + +### Chapitre 8: Authentication and Session + + +You can load the correction of the previous chapter as follows: + +``` +Metacello new baseline:'TinyBlog'; repository: 'github://LucFabresse/TinyBlog:chapter7/src'; onConflict: [ :ex | ex useLoaded ]; - load ``` To test the application, start the HTTP server: ``` ZnZincServerAdaptor startOn: 8080. ``` ### Chapitre 9: Administration Web Interface and Automatic Form Generation You can load the correction of the previous chapter as follow: ``` Metacello new + load +``` + + +To test the application, start the HTTP server: + +``` +ZnZincServerAdaptor startOn: 8080. +``` + + +### Chapitre 9: Administration Web Interface and Automatic Form Generation + + + +You can load the correction of the previous chapter as follows: + +``` +Metacello new baseline:'TinyBlog'; repository: 'github://LucFabresse/TinyBlog:chapter8/src'; onConflict: [ :ex | ex useLoaded ]; - load ``` ### Latest Version of TinyBlog The most up-to-date version of TinyBlog can be loaded as follow: ``` Metacello new + load +``` + + +### Latest Version of TinyBlog + + +The most up-to-date version of TinyBlog can be loaded as follows: + +``` +Metacello new baseline:'TinyBlog'; repository: 'github://LucFabresse/TinyBlog/src'; onConflict: [ :ex | ex useLoaded ]; - load. ``` \ No newline at end of file + load. +``` diff --git a/Chapters/Chap15-TinyBlog-SavingCode-EN.md b/Chapters/Chap15-TinyBlog-SavingCode-EN.md index 700ecd1..9f0fef0 100644 --- a/Chapters/Chap15-TinyBlog-SavingCode-EN.md +++ b/Chapters/Chap15-TinyBlog-SavingCode-EN.md @@ -1 +1,19 @@ -## Save your code When you save the Pharo image \(Pharo Menu > Save menuentry\), it contains all objects of the system as well as all classes. This solution is useful but fragile. We will show you how Pharoers save their code directly using Iceberg. Iceberg is the Pharo code versionning tools \(introduced in Pharo 7.0\) that directly send code to well-known Web sites such as: github, bitbucket, or gitlab. We suggest you read the chapter in the book "Managing Your Code with Iceberg" \(available at [http://books.pharo.org](http://books.pharo.org)\). We list the key points here: - Create an account on [http://www.github.com](http://www.github.com) or similar. - Create a project on [http://www.github.com](http://www.github.com) or similar. - Add a new project into Iceberg by choosing the option: clone from github. - Create a folder `'src'` with the filelist or using the command line in the folder that you just cloned. - Open your project and add your packages \(Define a baseline to be able to reload your code -- check [https://github.com/pharo-open-documentation/pharo-wiki/blob/master/General/Baselines.md](https://github.com/pharo-open-documentation/pharo-wiki/blob/master/General/Baselines.md)\) - Commit your code. - Push your code on github. \ No newline at end of file +## Save your code + + +When you save the Pharo image (Pharo Menu > Save menu entry), it contains all objects of the system as well as all classes. +This solution is useful but fragile. +We will show you how Pharoers save their code directly using Iceberg. +Iceberg is the Pharo code versioning tools (introduced in Pharo 7.0) that directly send code to well-known Web sites such as: github, bitbucket, or gitlab. + +We suggest you read the chapter in the book "Managing Your Code with Iceberg" \(available at [http://books.pharo.org](http://books.pharo.org)\). + +We list the key points here: + +- Create an account on [http://www.github.com](http://www.github.com) or similar. +- Create a project on [http://www.github.com](http://www.github.com) or similar. +- Add a new project into Iceberg by choosing the option: clone from github. +- Create a folder `'src'` with the filelist or using the command line in the folder that you just cloned. +- Open your project and add your packages (Define a baseline to be able to reload your code -- check [https://github.com/pharo-open-documentation/pharo-wiki/blob/master/General/Baselines.md](https://github.com/pharo-open-documentation/pharo-wiki/blob/master/General/Baselines.md)) +- Commit your code. +- Push your code on github.