12f84425be5c01a7c129ed806611bffbf1c327cb
[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 │ └── system
45 │ └── worker
46 │ └── PersonRebuildDataWorker.class.php
47 ├── language
48 │ ├── de.xml
49 │ └── en.xml
50 ├── objectType.xml
51 ├── templates
52 │ ├── person.tpl
53 │ └── personList.tpl
54 ├── ts
55 │ └── WoltLabSuite
56 │ └── Core
57 │ └── Controller
58 │ └── Person.ts
59 └── userGroupOption.xml
60 ```
61
62 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).
63
64
65 ## Miscellaneous
66
67 Before we focus on the main aspects of this part, we mention some minor aspects that will be used later on:
68
69 - Several new user group options and the relevant language items have been added related to creating, editing, and deleting information:
70 - `mod.person.canEditInformation` and `mod.person.canDeleteInformation` are moderative permissions to edit and delete any piece of information, regardless of who created it.
71 - `user.person.canAddInformation` is the permission for users to add new pieces of information.
72 - `user.person.canEditInformation` and `user.person.canDeleteInformation` are the user permissions to edit and the piece of information they created.
73 - 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`.
74 - `personList.tpl` has been adjusted to show the number of pieces of information in the person statistics section.
75 - We have not updated the person list box to also support sorting by the number of pieces of information added for each person.
76
77
78 ## Person Information Model
79
80 The PHP file with the database layout has been updated as follows:
81
82 {jinja{ codebox(
83 title="files/acp/database/install_com.woltlab.wcf.people.php",
84 language="php",
85 filepath="tutorial/tutorial-series/part-5/files/acp/database/install_com.woltlab.wcf.people.php"
86 ) }}
87
88 - The number of pieces of information per person is tracked via the new `informationCount` column.
89 - The `wcf1_person_information` table has been added for the `PersonInformation` model.
90 The meaning of the different columns is explained in the property documentation part of `PersonInformation`'s documentation (see below).
91 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`.
92
93 {jinja{ codebox(
94 title="files/lib/data/person/information/PersonInformation.class.php",
95 language="php",
96 filepath="tutorial/tutorial-series/part-5/files/lib/data/person/information/PersonInformation.class.php"
97 ) }}
98
99 `PersonInformation` provides two methods, `canDelete()` and `canEdit()`, to check whether the active user can delete or edit a specific piece of information.
100 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.
101
102 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()`).
103 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`.
104 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.
105 The most interesting method is `getFormattedInformation()`, which returns the HTML code of the information text meant for output.
106 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).
107
108 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:
109
110 {jinja{ codebox(
111 title="files/lib/data/person/information/PersonInformationList.class.php",
112 language="php",
113 filepath="tutorial/tutorial-series/part-5/files/lib/data/person/information/PersonInformationList.class.php"
114 ) }}
115
116
117 ## Listing and Deleting Person Information
118
119 The `person.tpl` template has been updated to include a block for listing the information at the beginning:
120
121 {jinja{ codebox(
122 title="templates/person.tpl",
123 language="smarty",
124 filepath="tutorial/tutorial-series/part-5/templates/person.tpl"
125 ) }}
126
127 To keep things simple here, we reuse the structure and CSS classes used for comments.
128 Additionally, we always list all pieces of information.
129 If there are many pieces of information, a nicer solution would be a pagination or loading more pieces of information with JavaScript.
130
131 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).
132 In `PersonInformationAction`, we have overridden the default implementations of `validateDelete()` and `delete()` which are called after clicking on a delete button.
133 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).
134
135 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.
136
137 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.
138 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.
139
140
141 ## Creating and Editing Person Information
142
143 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.
144
145 When clicking on the add button or on any of the edit buttons, a dialog opens with the relevant form:
146
147 {jinja{ codebox(
148 title="ts/WoltLabSuite/Core/Controller/Person.ts",
149 language="typescript",
150 filepath="tutorial/tutorial-series/part-5/ts/WoltLabSuite/Core/Controller/Person.ts"
151 ) }}
152
153 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.
154 We only have to provide some data during for initializing these objects and call the `open()` function after a button has been clicked.
155
156 Explanation of the initialization arguments for `WoltLabSuite/Core/Form/Builder/Dialog` used here:
157
158 - The first argument is the id of the dialog used to identify it.
159 - 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.
160 - The third argument is the name of the method in the referenced PHP class in the previous argument that returns the dialog form.
161 - The fourth argument contains additional options:
162 - `actionParameters` are additional parameters send during each AJAX request.
163 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.
164 - `dialog` contains the options for the dialog, see the `DialogOptions` interface.
165 Here, we only provide the title of the dialog.
166 - `submitActionName` is the name of the method in the referenced PHP class that is called with the form data after submitting the form.
167 - `successCallback` is called after the submit AJAX request was successful.
168 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.
169 (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.)
170
171 Next, we focus on `PersonInformationAction`, which actually provides the contents of these dialogs and creates and edits the information:
172
173 {jinja{ codebox(
174 title="files/lib/data/person/information/PersonInformationAction.class.php",
175 language="php",
176 filepath="tutorial/tutorial-series/part-5/files/lib/data/person/information/PersonInformationAction.class.php"
177 ) }}
178
179 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.
180 In addition to these two methods, the matching validation methods `validateGetAddDialog()` and `validateGetAddDialog()` are also added.
181 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.
182 We fire an event in `buildDialog()` so that plugins are able to easily extend the dialog with additional data.
183
184 `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.
185 The method configured in the `WoltLabSuite/Core/Form/Builder/Dialog` object returning the dialog is expected to return two values:
186 the id of the form (`formId`) and the contents of form shown in the dialog (`dialog`).
187 This data is returned by `getAddDialog` using the dialog build previously by `buildDialog()`.
188
189 After the form is submitted, `validateSubmitAddDialog()` has to do the same basic validation as `validateGetAddDialog()` so that `validateGetAddDialog()` is simply called.
190 Additionally, the form data is read and validated.
191 In `submitAddDialog()`, we first check if there have been any validation errors:
192 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.
193 Otherwise, if the validation succeeded, the form data is used to create the new piece of information.
194 In addition to the form data, we manually add the id of the person to whom the information belongs to.
195 Lastly, we could return some data that we could access in the JavaScript callback function after successfully submitting the dialog.
196 As we will simply be reloading the page, no such data is returned.
197 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.
198
199 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.
200 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()`.
201 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()`.
202
203
204 ## Rebuild Data Worker
205
206 To ensure the integrity of the person data, `PersonRebuildDataWorker` updates the `informationCount` counter:
207
208 {jinja{ codebox(
209 title="files/lib/system/worker/PersonRebuildDataWorker.class.php",
210 language="php",
211 filepath="tutorial/tutorial-series/part-5/files/lib/system/worker/PersonRebuildDataWorker.class.php"
212 ) }}
213
214
215 ## Username and IP Address Event Listeners
216
217 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:
218
219 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:
220
221 ```php
222 --8<-- "tutorial//tutorial-series/part-5/files/lib/system/event/listener/PersonUserActionRenameListener.class.php"
223 ```
224 2. If users are merged, all pieces of information need to be assigned to the target user of the merging.
225 Again, we only have to specify the name of relevant database table if `AbstractUserMergeListener` is extended:
226
227 ```php
228 --8<-- "tutorial/tutorial-series/part-5/files/lib/system/event/listener/PersonUserMergeListener.class.php"
229 ```
230 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.
231 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:
232
233 ```php
234 --8<-- "tutorial/tutorial-series/part-5/files/lib/system/event/listener/PersonPruneIpAddressesCronjobListener.class.php"
235 ```
236 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:
237
238 ```php
239 --8<-- "tutorial/tutorial-series/part-5/files/lib/system/event/listener/PersonUserExportGdprListener.class.php"
240 ```
241
242 Lastly, we present the updated `eventListener.xml` file with new entries for all of these event listeners:
243
244 {jinja{ codebox(
245 title="eventListener.xml",
246 language="xml",
247 filepath="tutorial/tutorial-series/part-5/eventListener.xml"
248 ) }}