Use new titled code box macro
[GitHub/WoltLab/woltlab.github.io.git] / docs / tutorial / series / part_5.md
1 # Part 5: Person Information
2
3 This part of our tutorial series lays the foundation for future parts in which we will be using additional APIs, which we have not used in this series yet.
4 To make use of those APIs, we need content generated by users in the frontend.
5
6
7 ## Package Functionality
8
9 In addition to the existing functions from [part 4](part_4.md), the package will provide the following functionality after this part of the tutorial:
10
11 - Users are able to add information on the people in the frontend.
12 - Users are able to edit and delete the pieces of information they added.
13 - Moderators are able to edit and delete all pieces of information.
14
15
16 ## Used Components
17
18 In addition to the components used in previous parts, we will use the [form builder API](../../php/api/form_builder/overview.md) to create forms shown in dialogs instead of dedicated pages and we will, for the first time, add [TypeScript code](../../javascript/typescript.md).
19
20
21 ## Package Structure
22
23 The package will have the following file structure _excluding_ unchanged files from previous parts:
24
25 ```
26 ├── files
27 │ ├── acp
28 │ │ └── database
29 │ │ └── install_com.woltlab.wcf.people.php
30 │ ├── js
31 │ │ └── WoltLabSuite
32 │ │ └── Core
33 │ │ └── Controller
34 │ │ └── Person.js
35 │ └── lib
36 │ └── data
37 │ └── person
38 │ ├── Person.class.php
39 │ └── information
40 │ ├── PersonInformation.class.php
41 │ ├── PersonInformationAction.class.php
42 │ ├── PersonInformationEditor.class.php
43 │ └── PersonInformationList.class.php
44 ├── language
45 │ ├── de.xml
46 │ └── en.xml
47 ├── objectType.xml
48 ├── templates
49 │ ├── person.tpl
50 │ └── personList.tpl
51 ├── ts
52 │ └── WoltLabSuite
53 │ └── Core
54 │ └── Controller
55 │ └── Person.ts
56 └── userGroupOption.xml
57 ```
58
59 For all changes, please refer to the [source code on GitHub]({jinja{ config.repo_url }}tree/{jinja{ config.edit_uri.split("/")[1] }}/snippets/tutorial/tutorial-series/part-5).
60
61
62 ## Miscellaneous
63
64 Before we focus on the main aspects of this part, we mention some minor aspects that will be used later on:
65
66 - Several new user group options and the relevant language items have been added related to creating, editing, and deleting information:
67 - `mod.person.canEditInformation` and `mod.person.canDeleteInformation` are moderative permissions to edit and delete any piece of information, regardless of who created it.
68 - `user.person.canAddInformation` is the permission for users to add new pieces of information.
69 - `user.person.canEditInformation` and `user.person.canDeleteInformation` are the user permissions to edit and the piece of information they created.
70 - The actual information text will be entered via a WYSIWYG editor, which requires an object type of the definition `com.woltlab.wcf.message`: `com.woltlab.wcf.people.information`.
71 - `personList.tpl` has been adjusted to show the number of pieces of information in the person statistics section.
72 - We have not updated the person list box to also support sorting by the number of pieces of information added for each person.
73
74
75 ## Person Information Model
76
77 The PHP file with the database layout has been updated as follows:
78
79 {jinja{ codebox(
80 "php",
81 "tutorial/tutorial-series/part-5/files/acp/database/install_com.woltlab.wcf.people.php",
82 "files/acp/database/install_com.woltlab.wcf.people.php"
83 ) }}
84
85 - The number of pieces of information per person is tracked via the new `informationCount` column.
86 - The `wcf1_person_information` table has been added for the `PersonInformation` model.
87 The meaning of the different columns is explained in the property documentation part of `PersonInformation`'s documentation (see below).
88 The two foreign keys ensure that if a person is deleted, all of their information is also deleted, and that if a user is deleted, the `userID` column is set to `NULL`.
89
90 {jinja{ codebox(
91 "php",
92 "tutorial/tutorial-series/part-5/files/lib/data/person/information/PersonInformation.class.php",
93 "files/lib/data/person/information/PersonInformation.class.php"
94 ) }}
95
96 `PersonInformation` provides two methods, `canDelete()` and `canEdit()`, to check whether the active user can delete or edit a specific piece of information.
97 In both cases, it is checked if the current user has created the relevant piece of information to check the user-specific permissions or to fall back to the moderator-specific permissions.
98
99 There also two getter methods for the person, the piece of information belongs to (`getPerson()`), and for the user profile of the user who created the information (`getUserProfile()`).
100 In both cases, we use runtime caches, though in `getUserProfile()`, we also have to consider the case of the user who created the information being deleted, i.e. `userID` being `null`.
101 For such a case, we also save the name of the user who created the information in `username`, so that we can return a guest user profile object in this case.
102 The most interesting method is `getFormattedInformation()`, which returns the HTML code of the information text meant for output.
103 To generate such an output, `HtmlOutputProcessor::process()` is used and here is where we first use the associated message object type `com.woltlab.wcf.people.information` mentioned [before](#miscellaneous).
104
105 While `PersonInformationEditor` is simply the default implementation and thus not explicitly shown here, `PersonInformationList::readObjects()` caches the relevant ids of the associated people and users who created the pieces of information using runtime caches:
106
107 {jinja{ codebox(
108 "php",
109 "tutorial/tutorial-series/part-5/files/lib/data/person/information/PersonInformationList.class.php",
110 "files/lib/data/person/information/PersonInformationList.class.php"
111 ) }}
112
113
114 ## Listing and Deleting Person Information
115
116 The `person.tpl` template has been updated to include a block for listing the information at the beginning:
117
118 {jinja{ codebox(
119 "smarty",
120 "tutorial/tutorial-series/part-5/templates/person.tpl",
121 "templates/person.tpl"
122 ) }}
123
124 To keep things simple here, we reuse the structure and CSS classes used for comments.
125 Additionally, we always list all pieces of information.
126 If there are many pieces of information, a nicer solution would be a pagination or loading more pieces of information with JavaScript.
127
128 First, we note the `jsObjectActionContainer` class in combination with the `data-object-action-class-name` attribute, which are needed for the delete button for each piece of information, as explained [here](../../migration/wsc53/javascript.md#wcfactiondelete-and-wcfactiontoggle).
129 In `PersonInformationAction`, we have overridden the default implementations of `validateDelete()` and `delete()` which are called after clicking on a delete button.
130 In `validateDelete()`, we call `PersonInformation::canDelete()` on all pieces of information to be deleted for proper permission validation, and in `delete()`, we update the `informationCount` values of the people the deleted pieces of information belong to (see below).
131
132 The button to add a new piece of information, `#personInformationAddButton`, and the buttons to edit existing pieces of information, `.jsEditInformation`, are controlled with JavaScript code initialized at the very end of the template.
133
134 Lastly, in `create()` we provide default values for the `time`, `userID`, `username`, and `ipAddress` for cases like here when creating a new piece of information, where do not explicitly provide this data.
135 Additionally, we extract the information text from the `information_htmlInputProcessor` parameter provided by the associated WYSIWYG form field and update the number of pieces of information created for the relevant person.
136
137
138 ## Creating and Editing Person Information
139
140 To create new pieces of information or editing existing ones, we do not add new form controllers but instead use dialogs generated by the form builder API so that the user does not have to leave the person page.
141
142 When clicking on the add button or on any of the edit buttons, a dialog opens with the relevant form:
143
144 {jinja{ codebox(
145 "typescript",
146 "tutorial/tutorial-series/part-5/ts/WoltLabSuite/Core/Controller/Person.ts",
147 "ts/WoltLabSuite/Core/Controller/Person.ts"
148 ) }}
149
150 We use the [`WoltLabSuite/Core/Form/Builder/Dialog` module](https://github.com/WoltLab/WCF/blob/master/ts/WoltLabSuite/Core/Form/Builder/Dialog.ts), which takes care of the internal handling with regard to these dialogs.
151 We only have to provide some data during for initializing these objects and call the `open()` function after a button has been clicked.
152
153 Explanation of the initialization arguments for `WoltLabSuite/Core/Form/Builder/Dialog` used here:
154
155 - The first argument is the id of the dialog used to identify it.
156 - The second argument is the PHP class name which provides the contents of the dialog's form and handles the data after the form is submitted.
157 - The third argument is the name of the method in the referenced PHP class in the previous argument that returns the dialog form.
158 - The fourth argument contains additional options:
159 - `actionParameters` are additional parameters send during each AJAX request.
160 Here, we either pass the id of the person for who a new piece of information is added or the id of the edited piece of information.
161 - `dialog` contains the options for the dialog, see the `DialogOptions` interface.
162 Here, we only provide the title of the dialog.
163 - `submitActionName` is the name of the method in the referenced PHP class that is called with the form data after submitting the form.
164 - `successCallback` is called after the submit AJAX request was successful.
165 After adding a new piece of information, we reload the page, and after editing an existing piece of information, we update the existing information text with the updated text.
166 (Dynamically inserting a newly added piece of information instead of reloading the page would also be possible, of course, but for this tutorial series, we kept things simple.)
167
168 Next, we focus on `PersonInformationAction`, which actually provides the contents of these dialogs and creates and edits the information:
169
170 {jinja{ codebox(
171 "php",
172 "tutorial/tutorial-series/part-5/files/lib/data/person/information/PersonInformationAction.class.php",
173 "files/lib/data/person/information/PersonInformationAction.class.php"
174 ) }}
175
176 When setting up the `WoltLabSuite/Core/Form/Builder/Dialog` object for adding new pieces of information, we specified `getAddDialog` and `submitAddDialog` as the names of the dialog getter and submit handler.
177 In addition to these two methods, the matching validation methods `validateGetAddDialog()` and `validateGetAddDialog()` are also added.
178 As the forms for adding and editing pieces of information have the same structure, this form is created in `buildDialog()` using a `DialogFormDocument` object, which is intended for forms in dialogs.
179 We fire an event in `buildDialog()` so that plugins are able to easily extend the dialog with additional data.
180
181 `validateGetAddDialog()` checks if the user has the permission to create new pieces of information and if a valid id for the person, the information will belong to, is given.
182 The method configured in the `WoltLabSuite/Core/Form/Builder/Dialog` object returning the dialog is expected to return two values:
183 the id of the form (`formId`) and the contents of form shown in the dialog (`dialog`).
184 This data is returned by `getAddDialog` using the dialog build previously by `buildDialog()`.
185
186 After the form is submitted, `validateSubmitAddDialog()` has to do the same basic validation as `validateGetAddDialog()` so that `validateGetAddDialog()` is simply called.
187 Additionally, the form data is read and validated.
188 In `submitAddDialog()`, we first check if there have been any validation errors:
189 If any error occured during validation, we return the same data as in `getAddDialog()` so that the dialog is shown again with the erroneous fields marked as such.
190 Otherwise, if the validation succeeded, the form data is used to create the new piece of information.
191 In addition to the form data, we manually add the id of the person to whom the information belongs to.
192 Lastly, we could return some data that we could access in the JavaScript callback function after successfully submitting the dialog.
193 As we will simply be reloading the page, no such data is returned.
194 An alternative to reloading to the page would be dynamically inserting the new piece of information in the list so that we would have to return the rendered list item for the new piece of information.
195
196 The process for getting and submitting the dialog to edit existing pieces of information is similar to the process for adding new pieces of information.
197 Instead of the id of the person, however, we now pass the id of the edited piece of information and in `submitEditDialog()`, we update the edited information instead of creating a new one like in `submitAddDialog()`.
198 After editing a piece of information, we do not reload the page but dynamically update the text of the information in the TypeScript code so that we return the updated rendered information text and id of the edited pieced of information in `submitAddDialog()`.
199
200
201 ## Username and IP Address Event Listeners
202
203 As we store the name of the user who create a new piece of information and store their IP address, we have to add event listeners to properly handle the following scenarios:
204
205 1. If the user is renamed, the value of `username` stored with the person information has to be updated, which can be achieved by a simple event listener that only has to specify the name of relevant database table if `AbstractUserActionRenameListener` is extended:
206
207 ```php
208 --8<-- "tutorial//tutorial-series/part-5/files/lib/system/event/listener/PersonUserActionRenameListener.class.php"
209 ```
210 2. If users are merged, all pieces of information need to be assigned to the target user of the merging.
211 Again, we only have to specify the name of relevant database table if `AbstractUserMergeListener` is extended:
212
213 ```php
214 --8<-- "tutorial/tutorial-series/part-5/files/lib/system/event/listener/PersonUserMergeListener.class.php"
215 ```
216 3. If the option to prune stored ip addresses after a certain period of time is enabled, we also have to prune them in the person information database table.
217 Here we also only have to specify the name of the relevant database table and provide the mapping from the `ipAddress` column to the `time` column:
218
219 ```php
220 --8<-- "tutorial/tutorial-series/part-5/files/lib/system/event/listener/PersonPruneIpAddressesCronjobListener.class.php"
221 ```
222 4. The ip addresses in the person information database table also have to be considered for the user data export which can also be done with minimal effort by providing the name of the relevant database table:
223
224 ```php
225 --8<-- "tutorial/tutorial-series/part-5/files/lib/system/event/listener/PersonUserExportGdprListener.class.php"
226 ```
227
228 Lastly, we present the updated `eventListener.xml` file with new entries for all of these event listeners:
229
230 {jinja{ codebox(
231 "xml",
232 "tutorial/tutorial-series/part-5/eventListener.xml",
233 "eventListener.xml"
234 ) }}