From d2e74042ed88429c02b07e0f505e73d24054cdfa Mon Sep 17 00:00:00 2001 From: Matthias Schmidt Date: Thu, 22 Apr 2021 10:43:04 +0200 Subject: [PATCH] Add part 5 of tutorial series --- docs/tutorial/series/overview.md | 1 + docs/tutorial/series/part_5.md | 220 +++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 3 files changed, 222 insertions(+) create mode 100644 docs/tutorial/series/part_5.md diff --git a/docs/tutorial/series/overview.md b/docs/tutorial/series/overview.md index 3f22a394..4f7be8bc 100644 --- a/docs/tutorial/series/overview.md +++ b/docs/tutorial/series/overview.md @@ -10,3 +10,4 @@ Note that in the context of this example, not every added feature might make per - [Part 2: Event and Template Listeners](part_2.md) - [Part 3: Person Page and Comments](part_3.md) - [Part 4: Box and Box Conditions](part_4.md) +- [Part 5: Person Information](part_5.md) diff --git a/docs/tutorial/series/part_5.md b/docs/tutorial/series/part_5.md new file mode 100644 index 00000000..eb753260 --- /dev/null +++ b/docs/tutorial/series/part_5.md @@ -0,0 +1,220 @@ +# Part 5: Person Information + +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. +To make use of those APIs, we need content generated by users in the frontend. + + +## Package Functionality + +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: + +- Users are able to add information on the people in the frontend. +- Users are able to edit and delete the pieces of information they added. +- Moderators are able to edit and delete all pieces of information. + + +## Used Components + +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). + + +## Package Structure + +The package will have the following file structure _excluding_ unchanged files from previous parts: + +``` +├── files +│ ├── acp +│ │ └── database +│ │ └── install_com.woltlab.wcf.people.php +│ ├── js +│ │ └── WoltLabSuite +│ │ └── Core +│ │ └── Controller +│ │ └── Person.js +│ └── lib +│ └── data +│ └── person +│ ├── Person.class.php +│ └── information +│ ├── PersonInformation.class.php +│ ├── PersonInformationAction.class.php +│ ├── PersonInformationEditor.class.php +│ └── PersonInformationList.class.php +├── language +│ ├── de.xml +│ └── en.xml +├── objectType.xml +├── templates +│ ├── person.tpl +│ └── personList.tpl +├── ts +│ └── WoltLabSuite +│ └── Core +│ └── Controller +│ └── Person.ts +└── userGroupOption.xml +``` + +For all changes, please refer to the [source code on GitHub]({jinja{ repo_url }}tree/{jinja{ edit_uri.split("/")[1] }}/snippets/tutorial/tutorial-series/part-5). + + +## Miscellaneous + +Before we focus on the main aspects of this part, we mention some minor aspects that will be used later on: + +- Several new user group options and the relevant language items have been added related to creating, editing, and deleting information: + - `mod.person.canEditInformation` and `mod.person.canDeleteInformation` are moderative permissions to edit and delete any piece of information, regardless of who created it. + - `user.person.canAddInformation` is the permission for users to add new pieces of information. + - `user.person.canEditInformation` and `user.person.canDeleteInformation` are the user permissions to edit and the piece of information they created. +- 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`. +- `personList.tpl` has been adjusted to show the number of pieces of information in the person statistics section. +- We have not updated the person list box to also support sorting by the number of pieces of information added for each person. + + +## Person Information Model + +The PHP file with the database layout has been updated as follows: + +```php +--8<-- "tutorial/tutorial-series/part-5/files/acp/database/install_com.woltlab.wcf.people.php" +``` + +- The number of pieces of information per person is tracked via the new `informationCount` column. +- The `wcf1_person_information` table has been added for the `PersonInformation` model. + The meaning of the different columns is explained in the property documentation part of `PersonInformation`'s documentation (see below). + 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`. + +```php +--8<-- "tutorial/tutorial-series/part-5/files/lib/data/person/information/PersonInformation.class.php" +``` + +`PersonInformation` provides two methods, `canDelete()` and `canEdit()`, to check whether the active user can delete or edit a specific piece of information. +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. + +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()`). +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`. +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. +The most interesting method is `getFormattedInformation()`, which returns the HTML code of the information text meant for output. +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). + +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: + +```php +--8<-- "tutorial/tutorial-series/part-5/files/lib/data/person/information/PersonInformationList.class.php" +``` + + +## Listing and Deleting Person Information + +The `person.tpl` template has been updated to include a block for listing the information at the beginning: + +```smarty +--8<-- "tutorial/tutorial-series/part-5/templates/person.tpl" +``` + +To keep things simple here, we reuse the structure and CSS classes used for comments. +Additionally, we always list all pieces of information. +If there are many pieces of information, a nicer solution would be a pagination or loading more pieces of information with JavaScript. + +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). +In `PersonInformationAction`, we have overridden the default implementations of `validateDelete()` and `delete()` which are called after clicking on a delete button. +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). + +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. + +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. +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. + + +## Creating and Editing Person Information + +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. + +When clicking on the add button or on any of the edit buttons, a dialog opens with the relevant form: + +```TypeScript +--8<-- "tutorial/tutorial-series/part-5/ts/WoltLabSuite/Core/Controller/Person.ts" +``` + +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. +We only have to provide some data during for initializing these objects and call the `open()` function after a button has been clicked. + +Explanation of the initialization arguments for `WoltLabSuite/Core/Form/Builder/Dialog` used here: + +- The first argument is the id of the dialog used to identify it. +- 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. +- The third argument is the name of the method in the referenced PHP class in the previous argument that returns the dialog form. +- The fourth argument contains additional options: + - `actionParameters` are additional parameters send during each AJAX request. + 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. + - `dialog` contains the options for the dialog, see the `DialogOptions` interface. + Here, we only provide the title of the dialog. + - `submitActionName` is the name of the method in the referenced PHP class that is called with the form data after submitting the form. + - `successCallback` is called after the submit AJAX request was successful. + 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. + (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.) + +Next, we focus on `PersonInformationAction`, which actually provides the contents of these dialogs and creates and edits the information: + +```php +--8<-- "tutorial/tutorial-series/part-5/files/lib/data/person/information/PersonInformationAction.class.php" +``` + +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. +In addition to these two methods, the matching validation methods `validateGetAddDialog()` and `validateGetAddDialog()` are also added. +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. +We fire an event in `buildDialog()` so that plugins are able to easily extend the dialog with additional data. + +`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. +The method configured in the `WoltLabSuite/Core/Form/Builder/Dialog` object returning the dialog is expected to return two values: +the id of the form (`formId`) and the contents of form shown in the dialog (`dialog`). +This data is returned by `getAddDialog` using the dialog build previously by `buildDialog()`. + +After the form is submitted, `validateSubmitAddDialog()` has to do the same basic validation as `validateGetAddDialog()` so that `validateGetAddDialog()` is simply called. +Additionally, the form data is read and validated. +In `submitAddDialog()`, we first check if there have been any validation errors: +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. +Otherwise, if the validation succeeded, the form data is used to create the new piece of information. +In addition to the form data, we manually add the id of the person to whom the information belongs to. +Lastly, we could return some data that we could access in the JavaScript callback function after successfully submitting the dialog. +As we will simply be reloading the page, no such data is returned. +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. + +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. +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()`. +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()`. + + +## Username and IP Address Event Listeners + +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: + +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: + + ```php + --8<-- "tutorial//tutorial-series/part-5/files/lib/system/event/listener/PersonUserActionRenameListener.class.php" + ``` +2. If users are merged, all pieces of information need to be assigned to the target user of the merging. + Again, we only have to specify the name of relevant database table if `AbstractUserMergeListener` is extended: + + ```php + --8<-- "tutorial/tutorial-series/part-5/files/lib/system/event/listener/PersonUserMergeListener.class.php" + ``` +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. + 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: + + ```php + --8<-- "tutorial/tutorial-series/part-5/files/lib/system/event/listener/PersonPruneIpAddressesCronjobListener.class.php" + ``` +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: + + ```php + --8<-- "tutorial/tutorial-series/part-5/files/lib/system/event/listener/PersonUserExportGdprListener.class.php" + ``` + +Lastly, we present the updated `eventListener.xml` file with new entries for all of these event listeners: + +```xml +--8<-- "tutorial/tutorial-series/part-5/eventListener.xml" +``` \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index b2e0562e..6d6c88cc 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -135,6 +135,7 @@ nav: - 'Part 2': 'tutorial/series/part_2.md' - 'Part 3': 'tutorial/series/part_3.md' - 'Part 4': 'tutorial/series/part_4.md' + - 'Part 5': 'tutorial/series/part_5.md' plugins: - git-revision-date -- 2.20.1