Fix highlighting external files
authorMatthias Schmidt <gravatronics@live.com>
Mon, 28 Dec 2020 15:13:42 +0000 (16:13 +0100)
committerMatthias Schmidt <gravatronics@live.com>
Mon, 28 Dec 2020 15:13:42 +0000 (16:13 +0100)
74 files changed:
docs/migration_wsc-31_form-builder.md
docs/tutorial_tutorial-series_part-1-base-structure.md
docs/tutorial_tutorial-series_part-2-event-listeners-and-template-listeners.md
docs/tutorial_tutorial-series_part-3-person-page-and-comments.md
mkdocs.yml
snippets/migration/wsc-31/formBuilder/PersonAddForm_new.class.php [new file with mode: 0644]
snippets/migration/wsc-31/formBuilder/PersonAddForm_old.class.php [new file with mode: 0644]
snippets/migration/wsc-31/formBuilder/PersonEditForm_new.class.php [new file with mode: 0644]
snippets/migration/wsc-31/formBuilder/PersonEditForm_old.class.php [new file with mode: 0644]
snippets/migration/wsc-31/formBuilder/personAdd_new.tpl [new file with mode: 0644]
snippets/migration/wsc-31/formBuilder/personAdd_old.tpl [new file with mode: 0644]
snippets/tutorial/basic-app/files/global.php [new file with mode: 0644]
snippets/tutorial/basic-app/files/index.php [new file with mode: 0644]
snippets/tutorial/basic-app/files/lib/page/ExamplePage.class.php [new file with mode: 0644]
snippets/tutorial/basic-app/files/lib/system/APPCore.class.php [new file with mode: 0644]
snippets/tutorial/basic-app/menuItem.xml [new file with mode: 0644]
snippets/tutorial/basic-app/package.xml [new file with mode: 0644]
snippets/tutorial/basic-app/page.xml [new file with mode: 0644]
snippets/tutorial/basic-app/templates/example.tpl [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-1/acpMenu.xml [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-1/acptemplates/personAdd.tpl [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-1/acptemplates/personList.tpl [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-1/files/lib/acp/form/PersonAddForm.class.php [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-1/files/lib/acp/form/PersonEditForm.class.php [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-1/files/lib/acp/page/PersonListPage.class.php [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-1/files/lib/data/person/Person.class.php [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-1/files/lib/data/person/PersonAction.class.php [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-1/files/lib/data/person/PersonEditor.class.php [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-1/files/lib/data/person/PersonList.class.php [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-1/files/lib/page/PersonListPage.class.php [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-1/install.sql [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-1/language/de.xml [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-1/language/en.xml [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-1/menuItem.xml [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-1/package.xml [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-1/page.xml [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-1/templates/personList.tpl [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-1/userGroupOption.xml [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-2/acptemplates/__personAddBirthday.tpl [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-2/eventListener.xml [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-2/files/lib/system/event/listener/BirthdayPersonAddFormListener.class.php [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-2/files/lib/system/event/listener/BirthdaySortFieldPersonListPageListener.class.php [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-2/install.sql [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-2/language/de.xml [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-2/language/en.xml [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-2/package.xml [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-2/templateListener.xml [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-2/templates/__personListBirthday.tpl [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-2/templates/__personListBirthdaySortField.tpl [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-3/acpMenu.xml [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-3/acptemplates/personAdd.tpl [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-3/acptemplates/personList.tpl [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-3/files/lib/acp/form/PersonAddForm.class.php [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-3/files/lib/acp/form/PersonEditForm.class.php [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-3/files/lib/acp/page/PersonListPage.class.php [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-3/files/lib/data/person/Person.class.php [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-3/files/lib/data/person/PersonAction.class.php [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-3/files/lib/data/person/PersonEditor.class.php [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-3/files/lib/data/person/PersonList.class.php [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-3/files/lib/page/PersonListPage.class.php [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-3/files/lib/page/PersonPage.class.php [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-3/files/lib/system/cache/runtime/PersonRuntimeCache.class.php [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-3/files/lib/system/comment/manager/PersonCommentManager.class.php [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-3/files/lib/system/page/handler/PersonPageHandler.class.php [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-3/install.sql [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-3/language/de.xml [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-3/language/en.xml [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-3/menuItem.xml [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-3/objectType.xml [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-3/package.xml [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-3/page.xml [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-3/templates/person.tpl [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-3/templates/personList.tpl [new file with mode: 0644]
snippets/tutorial/tutorial-series/part-3/userGroupOption.xml [new file with mode: 0644]

index a15dba62ad5f65eea2f0d07f6dcf716a9ebe0a4d..0c00f5324956ccc5758d7caac2ae0498aa284ce2 100644 (file)
@@ -7,36 +7,36 @@ This form is the perfect first examples as it is very simple with only two text
 
 As a reminder, here are the two relevant PHP files and the relevant template file:
 
-{% highlight php %}
-{% include migration/wsc-31/formBuilder/PersonAddForm_old.class.php %}
-{% endhighlight %}
+```php
+--8<-- "migration/wsc-31/formBuilder/PersonAddForm_old.class.php"
+```
 
-{% highlight php %}
-{% include migration/wsc-31/formBuilder/PersonEditForm_old.class.php %}
-{% endhighlight %}
+```php
+--8<-- "migration/wsc-31/formBuilder/PersonEditForm_old.class.php"
+```
 
-{% highlight php %}
-{% include migration/wsc-31/formBuilder/personAdd_old.tpl %}
-{% endhighlight %}
+```php
+--8<-- "migration/wsc-31/formBuilder/personAdd_old.tpl"
+```
 
 Updating the template is easy as the complete form is replace by a single line of code:
 
-{% highlight php %}
-{% include migration/wsc-31/formBuilder/personAdd_new.tpl %}
-{% endhighlight %}
+```php
+--8<-- "migration/wsc-31/formBuilder/personAdd_new.tpl"
+```
 
 `PersonEditForm` also becomes much simpler:
 only the edited `Person` object must be read:
 
-{% highlight php %}
-{% include migration/wsc-31/formBuilder/PersonEditForm_new.class.php %}
-{% endhighlight %}
+```php
+--8<-- "migration/wsc-31/formBuilder/PersonEditForm_new.class.php"
+```
 
 Most of the work is done in `PersonAddForm`:
 
-{% highlight php %}
-{% include migration/wsc-31/formBuilder/PersonAddForm_new.class.php %}
-{% endhighlight %}
+```php
+--8<-- "migration/wsc-31/formBuilder/PersonAddForm_new.class.php"
+```
 
 But, as you can see, the number of lines almost decreased by half.
 All changes are due to extending `AbstractFormBuilderForm`:
index 7daed09aaca79a4fed19e930b0d68c57ad4917af..6447eb57fe5290b05208225ab83276f3cbc0d4ff 100644 (file)
@@ -82,9 +82,9 @@ Thus, the database table we will store the people in only contains three columns
 
 The first file for our package is the `install.sql` file used to create such a database table during package installation:
 
-{% highlight sql %}
-{% include tutorial/tutorial-series/part-1/install.sql %}
-{% endhighlight %}
+```sql
+--8<-- "tutorial/tutorial-series/part-1/install.sql"
+```
 
 ### Database Object
 
@@ -92,9 +92,9 @@ The first file for our package is the `install.sql` file used to create such a d
 
 In our PHP code, each person will be represented by an object of the following class:
 
-{% highlight php %}
-{% include tutorial/tutorial-series/part-1/files/lib/data/person/Person.class.php %}
-{% endhighlight %}
+```php
+--8<-- "tutorial/tutorial-series/part-1/files/lib/data/person/Person.class.php"
+```
 
 The important thing here is that `Person` extends `DatabaseObject`.
 Additionally, we implement the `IRouteController` interface, which allows us to use `Person` objects to create links, and we implement PHP's magic [__toString()](https://secure.php.net/manual/en/language.oop5.magic.php#object.tostring) method for convenience.
@@ -104,9 +104,9 @@ an action class, an editor class and a list class.
 
 #### `PersonAction`
 
-{% highlight php %}
-{% include tutorial/tutorial-series/part-1/files/lib/data/person/PersonAction.class.php %}
-{% endhighlight %}
+```php
+--8<-- "tutorial/tutorial-series/part-1/files/lib/data/person/PersonAction.class.php"
+```
 
 This implementation of `AbstractDatabaseObjectAction` is very basic and only sets the `$permissionsDelete` and `$requireACP` properties.
 This is done so that later on, when implementing the people list for the ACP, we can delete people simply via AJAX.
@@ -116,18 +116,18 @@ We will later use the [userGroupOption package installation plugin](package_pip_
 
 #### `PersonEditor`
 
-{% highlight php %}
-{% include tutorial/tutorial-series/part-1/files/lib/data/person/PersonEditor.class.php %}
-{% endhighlight %}
+```php
+--8<-- "tutorial/tutorial-series/part-1/files/lib/data/person/PersonEditor.class.php"
+```
 
 This implementation of `DatabaseObjectEditor` fulfills the minimum requirement for a database object editor:
 setting the static `$baseClass` property to the database object class name.
 
 #### `PersonList`
 
-{% highlight php %}
-{% include tutorial/tutorial-series/part-1/files/lib/data/person/PersonList.class.php %}
-{% endhighlight %}
+```php
+--8<-- "tutorial/tutorial-series/part-1/files/lib/data/person/PersonList.class.php"
+```
 
 Due to the default implementation of `DatabaseObjectList`, our `PersonList` class just needs to extend it and everything else is either automatically set by the code of `DatabaseObjectList` or, in the case of properties and methods, provided by that class.
 
@@ -151,9 +151,9 @@ We need to create three menu items:
 1. a third level menu item for the people list page, and
 1. a fourth level menu item for the form to add new people.
 
-{% highlight xml %}
-{% include tutorial/tutorial-series/part-1/acpMenu.xml %}
-{% endhighlight %}
+```xml
+--8<-- "tutorial/tutorial-series/part-1/acpMenu.xml"
+```
 
 We choose `wcf.acp.menu.link.content` as the parent menu item for the first menu item `wcf.acp.menu.link.person` because the people we are managing is just one form of content.
 The fourth level menu item `wcf.acp.menu.link.person.add` will only be shown as an icon and thus needs an additional element `icon` which takes a FontAwesome icon class as value.
@@ -164,9 +164,9 @@ To list the people in the ACP, we need a `PersonListPage` class and a `personLis
 
 #### `PersonListPage`
 
-{% highlight php %}
-{% include tutorial/tutorial-series/part-1/files/lib/acp/page/PersonListPage.class.php %}
-{% endhighlight %}
+```php
+--8<-- "tutorial/tutorial-series/part-1/files/lib/acp/page/PersonListPage.class.php"
+```
 
 As WoltLab Suite Core already provides a powerful default implementation of a sortable page, our work here is minimal:
 
@@ -178,9 +178,9 @@ As WoltLab Suite Core already provides a powerful default implementation of a so
 
 #### `personList.tpl`
 
-{% highlight smarty %}
-{% include tutorial/tutorial-series/part-1/acptemplates/personList.tpl %}
-{% endhighlight %}
+```smarty
+--8<-- "tutorial/tutorial-series/part-1/acptemplates/personList.tpl"
+```
 
 We will go piece by piece through the template code:
 
@@ -212,9 +212,9 @@ Like the person list, the form to add new people requires a controller class and
 
 #### `PersonAddForm`
 
-{% highlight php %}
-{% include tutorial/tutorial-series/part-1/files/lib/acp/form/PersonAddForm.class.php %}
-{% endhighlight %}
+```php
+--8<-- "tutorial/tutorial-series/part-1/files/lib/acp/form/PersonAddForm.class.php"
+```
 
 The properties here consist of two types:
 the “housekeeping” properties `$activeMenuItem` and `$neededPermissions`, which fulfill the same roles as for `PersonListPage`, and the “data” properties `$firstName` and `$lastName`, which will contain the data entered by the user of the person to be created.
@@ -235,9 +235,9 @@ Now, let's go through each method in execution order:
 
 #### `personAdd.tpl`
 
-{% highlight smarty %}
-{% include tutorial/tutorial-series/part-1/acptemplates/personAdd.tpl %}
-{% endhighlight %}
+```smarty
+--8<-- "tutorial/tutorial-series/part-1/acptemplates/personAdd.tpl"
+```
 
 We will now only concentrate on the new parts compared to `personList.tpl`:
 
@@ -266,9 +266,9 @@ As mentioned before, for the form to edit existing people, we only need a new co
 
 #### `PersonEditForm`
 
-{% highlight php %}
-{% include tutorial/tutorial-series/part-1/files/lib/acp/form/PersonEditForm.class.php %}
-{% endhighlight %}
+```php
+--8<-- "tutorial/tutorial-series/part-1/files/lib/acp/form/PersonEditForm.class.php"
+```
 
 In general, edit forms extend the associated add form so that the code to read and to validate the input data is simply inherited.
 
@@ -301,9 +301,9 @@ This page should also be directly linked in the main menu.
 
 First, let us register the page with the system because every front end page or form needs to be explicitly registered using the [page package installation plugin](package_pip_page.md):
 
-{% highlight xml %}
-{% include tutorial/tutorial-series/part-1/page.xml %}
-{% endhighlight %}
+```xml
+--8<-- "tutorial/tutorial-series/part-1/page.xml"
+```
 
 For more information about what each of the elements means, please refer to the [page package installation plugin page](package_pip_page.md).
 
@@ -311,9 +311,9 @@ For more information about what each of the elements means, please refer to the
 
 Next, we register the menu item using the [menuItem package installation plugin](package_pip_menu-item.md):
 
-{% highlight xml %}
-{% include tutorial/tutorial-series/part-1/menuItem.xml %}
-{% endhighlight %}
+```xml
+--8<-- "tutorial/tutorial-series/part-1/menuItem.xml"
+```
 
 Here, the import parts are that we register the menu item for the main menu `com.woltlab.wcf.MainMenu` and link the menu item with the page `com.woltlab.wcf.people.PersonList`, which we just registered.
 
@@ -325,9 +325,9 @@ This is no problem because the qualified names of the classes differ and the fil
 
 #### `PersonListPage`
 
-{% highlight php %}
-{% include tutorial/tutorial-series/part-1/files/lib/page/PersonListPage.class.php %}
-{% endhighlight %}
+```php
+--8<-- "tutorial/tutorial-series/part-1/files/lib/page/PersonListPage.class.php"
+```
 
 This class is almost identical to the ACP version.
 In the front end, we do not need to set the active menu item manually because the system determines the active menu item automatically based on the requested page.
@@ -336,9 +336,9 @@ In the front end, we explicitly set the `$defaultSortField` so that the people l
 
 #### `personList.tpl`
 
-{% highlight smarty %}
-{% include tutorial/tutorial-series/part-1/templates/personList.tpl %}
-{% endhighlight %}
+```smarty
+--8<-- "tutorial/tutorial-series/part-1/templates/personList.tpl"
+```
 
 If you compare this template to the one used in the ACP, you will recognize similar elements like the `.paginationTop` element, the `p.info` element if no people exist, and the `.contentFooter` element.
 Furthermore, we include a template called `header` before actually showing any of the page contents and terminate the template by including the `footer` template.
@@ -362,9 +362,9 @@ Now, let us take a closer look at the differences:
 
 We have already used the `admin.content.canManagePeople` permissions several times, now we need to install it using the [userGroupOption package installation plugin](package_pip_user-group-option.md):
 
-{% highlight xml %}
-{% include tutorial/tutorial-series/part-1/userGroupOption.xml %}
-{% endhighlight %}
+```xml
+--8<-- "tutorial/tutorial-series/part-1/userGroupOption.xml"
+```
 
 We use the existing `admin.content` user group option category for the permission as the people are “content” (similar the the ACP menu item).
 As the permission is for administrators only, we set `defaultvalue` to `0` and `admindefaultvalue` to `1`.
@@ -377,9 +377,9 @@ This is achieved by setting `usersonly` to `1`.
 Lastly, we need to create the `package.xml` file.
 For more information about this kind of file, please refer to [the `package.xml` page](package_package-xml.md).
 
-{% highlight xml %}
-{% include tutorial/tutorial-series/part-1/package.xml %}
-{% endhighlight %}
+```xml
+--8<-- "tutorial/tutorial-series/part-1/package.xml"
+```
 
 As this is a package for WoltLab Suite Core 3, we need to require it using `<requiredpackage>`.
 We require the latest version (when writing this tutorial) `3.0.0 RC 4`.
index 1a5fc498a4d43f30f130466ec216af586d559ba5..443f43d5f9799895751845002a8be8201a97db58 100644 (file)
@@ -66,9 +66,9 @@ The package will have the following file structure:
 The existing model of a person only contains the person’s first name and their last name (in additional to the id used to identify created people).
 To add the birthday to the model, we need to create an additional database table column using the [sql package installation plugin](package_pip_sql.md):
 
-{% highlight sql %}
-{% include tutorial/tutorial-series/part-2/install.sql %}
-{% endhighlight %}
+```sql
+--8<-- "tutorial/tutorial-series/part-2/install.sql"
+```
 
 If we have a [Person object](tutorial_tutorial-series_part-1-base-structure.md#person), this new property can be accessed the same way as the `personID` property, the `firstName` property, or the `lastName` property from the base package: `$person->birthday`.
 
@@ -78,20 +78,20 @@ If we have a [Person object](tutorial_tutorial-series_part-1-base-structure.md#p
 To set the birthday of a person, we need to extend the `personAdd` template to add an additional birthday field.
 This can be achieved using the `dataFields` template event at whose position we inject the following template code:
 
-{% highlight sql %}
-{% include tutorial/tutorial-series/part-2/acptemplates/__personAddBirthday.tpl %}
-{% endhighlight %}
+```sql
+--8<-- "tutorial/tutorial-series/part-2/acptemplates/__personAddBirthday.tpl"
+```
 
 which we store in a `__personAddBirthday.tpl` template file.
 The used language item `wcf.person.birthday` is actually the only new one for this package:
 
-{% highlight sql %}
-{% include tutorial/tutorial-series/part-2/language/de.xml %}
-{% endhighlight %}
+```sql
+--8<-- "tutorial/tutorial-series/part-2/language/de.xml"
+```
 
-{% highlight sql %}
-{% include tutorial/tutorial-series/part-2/language/en.xml %}
-{% endhighlight %}
+```sql
+--8<-- "tutorial/tutorial-series/part-2/language/en.xml"
+```
 
 The template listener needs to be registered using the [templateListener package installation plugin](package_pip_template-listener.md).
 The corresponding complete `templateListener.xml` file is included [below](#templatelistenerxml).
@@ -109,9 +109,9 @@ Before we take a look at the event listener code, we need to identify exactly wh
 
 The following event listeners achieves these requirements:
 
-{% highlight php %}
-{% include tutorial/tutorial-series/part-2/files/lib/system/event/listener/BirthdayPersonAddFormListener.class.php %}
-{% endhighlight %}
+```php
+--8<-- "tutorial/tutorial-series/part-2/files/lib/system/event/listener/BirthdayPersonAddFormListener.class.php"
+```
 
 Some notes on the code:
 
@@ -138,9 +138,9 @@ To add a birthday column to the person list page in the ACP, we need three parts
 
 The first part is a very simple class:
 
-{% highlight php %}
-{% include tutorial/tutorial-series/part-2/files/lib/system/event/listener/BirthdaySortFieldPersonListPageListener.class.php %}
-{% endhighlight %}
+```php
+--8<-- "tutorial/tutorial-series/part-2/files/lib/system/event/listener/BirthdaySortFieldPersonListPageListener.class.php"
+```
 
 !!! info "We use `SortablePage` as a type hint instead of `wcf\acp\page\PersonListPage` because we will be using the same event listener class in the front end to also allow sorting that list by birthday."
 
@@ -165,9 +165,9 @@ In the front end, we also want to make the list sortable by birthday and show th
 To add the birthday as a valid sort field, we use `BirthdaySortFieldPersonListPageListener` just as in the ACP.
 In the front end, we will now use a template (`__personListBirthdaySortField.tpl`) instead of a directly putting the template code in the `templateListener.xml` file:
 
-{% highlight smarty %}
-{% include tutorial/tutorial-series/part-2/templates/__personListBirthdaySortField.tpl %}
-{% endhighlight %}
+```smarty
+--8<-- "tutorial/tutorial-series/part-2/templates/__personListBirthdaySortField.tpl"
+```
 
 !!! info "You might have noticed the two underscores at the beginning of the template file. For templates that are included via template listeners, this is the naming convention we use."
 
@@ -175,18 +175,18 @@ Putting the template code into a file has the advantage that in the administrato
 
 To show the birthday, we use the following template code for the `personStatistics` template event, which again makes sure that the birthday is only shown if it is actually set:
 
-{% highlight smarty %}
-{% include tutorial/tutorial-series/part-2/templates/__personListBirthday.tpl %}
-{% endhighlight %}
+```smarty
+--8<-- "tutorial/tutorial-series/part-2/templates/__personListBirthday.tpl"
+```
 
 
 ## `templateListener.xml`
 
 The following code shows the `templateListener.xml` file used to install all mentioned template listeners:
 
-{% highlight xml %}
-{% include tutorial/tutorial-series/part-2/templateListener.xml %}
-{% endhighlight %}
+```xml
+--8<-- "tutorial/tutorial-series/part-2/templateListener.xml"
+```
 
 In cases where a template is used, we simply use the `include` syntax to load the template.
 
@@ -197,18 +197,18 @@ There are two event listeners, `birthdaySortFieldAdminPersonList` and `birthdayS
 The event listener `birthdayPersonAddFormInherited` takes care of the events that are relevant for both adding and editing people, thus it listens to the `PersonAddForm` class but has `inherit` set to `1` so that it also listens to the events of the `PersonEditForm` class.
 In contrast, reading the existing birthday from a person is only relevant for editing so that the event listener `birthdayPersonEditForm` only listens to that class.
 
-{% highlight xml %}
-{% include tutorial/tutorial-series/part-2/eventListener.xml %}
-{% endhighlight %}
+```xml
+--8<-- "tutorial/tutorial-series/part-2/eventListener.xml"
+```
 
 
 ## `package.xml`
 
 The only relevant difference between the `package.xml` file of the base page from part 1 and the `package.xml` file of this package is that this package requires the base package `com.woltlab.wcf.people` (see `<requiredpackages>`):
 
-{% highlight xml %}
-{% include tutorial/tutorial-series/part-2/package.xml %}
-{% endhighlight %}
+```xml
+--8<-- "tutorial/tutorial-series/part-2/package.xml"
+```
 
 ---
 
index ddd173e7aee2e4d5bb90e73b2067193a661adfb6..fe8c27d0ceefaf220bfaf52bbaf3af751d8f7e49 100644 (file)
@@ -76,9 +76,9 @@ The complete package will have the following file structure (including the files
 
 To reduce the number of database queries when different APIs require person objects, we implement a [runtime cache](php_api_caches_runtime-caches.md) for people:
 
-{% highlight php %}
-{% include tutorial/tutorial-series/part-3/files/lib/system/cache/runtime/PersonRuntimeCache.class.php %}
-{% endhighlight %}
+```php
+--8<-- "tutorial/tutorial-series/part-3/files/lib/system/cache/runtime/PersonRuntimeCache.class.php"
+```
 
 
 ## Comments
@@ -86,15 +86,15 @@ To reduce the number of database queries when different APIs require person obje
 To allow users to comment on people, we need to tell the system that people support comments.
 This is done by registering a `com.woltlab.wcf.comment.commentableContent` object type whose processor implements [ICommentManager](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/comment/manager/ICommentManager.class.php):
 
-{% highlight xml %}
-{% include tutorial/tutorial-series/part-3/objectType.xml %}
-{% endhighlight %}
+```xml
+--8<-- "tutorial/tutorial-series/part-3/objectType.xml"
+```
 
 The `PersonCommentManager` class extended `ICommentManager`’s default implementation [AbstractCommentManager](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/comment/manager/AbstractCommentManager.class.php):
 
-{% highlight php %}
-{% include tutorial/tutorial-series/part-3/files/lib/system/comment/manager/PersonCommentManager.class.php %}
-{% endhighlight %}
+```php
+--8<-- "tutorial/tutorial-series/part-3/files/lib/system/comment/manager/PersonCommentManager.class.php"
+```
 
 - First, the system is told the names of the permissions via the `$permission*` properties.
   More information about comment permissions can be found [here](php_api_comments.md#user-group-options).
@@ -116,9 +116,9 @@ With this option, comments on individual people can be disabled.
 
 ### `PersonPage`
 
-{% highlight php %}
-{% include tutorial/tutorial-series/part-3/files/lib/page/PersonPage.class.php %}
-{% endhighlight %}
+```php
+--8<-- "tutorial/tutorial-series/part-3/files/lib/page/PersonPage.class.php"
+```
 
 The `PersonPage` class is similar to the `PersonEditForm` in the ACP in that it reads the id of the requested person from the request data and validates the id in `readParameters()`.
 The rest of the code only handles fetching the list of comments on the requested person.
@@ -127,9 +127,9 @@ The `assignVariables()` method assigns some additional template variables like `
 
 ### `person.tpl`
 
-{% highlight tpl %}
-{% include tutorial/tutorial-series/part-3/templates/person.tpl %}
-{% endhighlight %}
+```tpl
+--8<-- "tutorial/tutorial-series/part-3/templates/person.tpl"
+```
 
 For now, the `person` template is still very empty and only shows the comments in the content area.
 The template code shown for comments is very generic and used in this form in many locations as it only sets the header of the comment list and the container `ul#personCommentList` element for the comments shown by `commentList` template.
@@ -139,9 +139,9 @@ The attribute `wysiwygSelector` should be the id of the comment list `personComm
 
 ### `page.xml`
 
-{% highlight xml %}
-{% include tutorial/tutorial-series/part-3/page.xml %}
-{% endhighlight %}
+```xml
+--8<-- "tutorial/tutorial-series/part-3/page.xml"
+```
 
 The `page.xml` file has been extended for the new person page with identifier `com.woltlab.wcf.people.Person`.
 Compared to the pre-existing `com.woltlab.wcf.people.PersonList` page, there are four differences:
@@ -155,9 +155,9 @@ Compared to the pre-existing `com.woltlab.wcf.people.PersonList` page, there are
 
 ### `PersonPageHandler`
 
-{% highlight php %}
-{% include tutorial/tutorial-series/part-3/files/lib/system/page/handler/PersonPageHandler.class.php %}
-{% endhighlight %}
+```php
+--8<-- "tutorial/tutorial-series/part-3/files/lib/system/page/handler/PersonPageHandler.class.php"
+```
 
 Like any page handler, the `PersonPageHandler` class has to implement the [IMenuPageHandler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/page/handler/IMenuPageHandler.class.php) interface, which should be done by extending the [AbstractMenuPageHandler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/page/handler/AbstractMenuPageHandler.class.php) class.
 As we want  administrators to link to specific people in menus, for example, we have to also implement the [ILookupPageHandler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/page/handler/ILookupPageHandler.class.php) interface by extending the [AbstractLookupPageHandler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/page/handler/AbstractLookupPageHandler.class.php) class.
index 2502778720032bf731ca276fd4cd8deb8ca1d206..2e40b022b24464d5e1203b8548849d5229ee75d8 100644 (file)
@@ -90,6 +90,8 @@ markdown_extensions:
     - abbr
     - pymdownx.highlight
     - pymdownx.superfences
+    - pymdownx.snippets:
+          base_path: "snippets/"
 
 extra_css:
     - stylesheets/extra.css
diff --git a/snippets/migration/wsc-31/formBuilder/PersonAddForm_new.class.php b/snippets/migration/wsc-31/formBuilder/PersonAddForm_new.class.php
new file mode 100644 (file)
index 0000000..71e0a9e
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+namespace wcf\acp\form;
+use wcf\data\person\PersonAction;
+use wcf\form\AbstractFormBuilderForm;
+use wcf\system\form\builder\container\FormContainer;
+use wcf\system\form\builder\field\TextFormField;
+
+/**
+ * Shows the form to create a new person.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Acp\Form
+ */
+class PersonAddForm extends AbstractFormBuilderForm {
+       /**
+        * @inheritDoc
+        */
+       public $activeMenuItem = 'wcf.acp.menu.link.person.add';
+       
+       /**
+        * @inheritDoc
+        */
+       public $formAction = 'create';
+       
+       /**
+        * @inheritDoc
+        */
+       public $neededPermissions = ['admin.content.canManagePeople'];
+       
+       /**
+        * @inheritDoc
+        */
+       public $objectActionClass = PersonAction::class;
+       
+       /**
+        * @inheritDoc
+        */
+       protected function createForm() {
+               parent::createForm();
+               
+               $dataContainer = FormContainer::create('data')
+                       ->appendChildren([
+                               TextFormField::create('firstName')
+                                       ->label('wcf.person.firstName')
+                                       ->required()
+                                       ->maximumLength(255),
+                               
+                               TextFormField::create('lastName')
+                                       ->label('wcf.person.lastName')
+                                       ->required()
+                                       ->maximumLength(255)
+                       ]);
+               
+               $this->form->appendChild($dataContainer);
+       }
+}
diff --git a/snippets/migration/wsc-31/formBuilder/PersonAddForm_old.class.php b/snippets/migration/wsc-31/formBuilder/PersonAddForm_old.class.php
new file mode 100644 (file)
index 0000000..4a81edd
--- /dev/null
@@ -0,0 +1,109 @@
+<?php
+namespace wcf\acp\form;
+use wcf\data\person\PersonAction;
+use wcf\form\AbstractForm;
+use wcf\system\exception\UserInputException;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * Shows the form to create a new person.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Acp\Form
+ */
+class PersonAddForm extends AbstractForm {
+       /**
+        * @inheritDoc
+        */
+       public $activeMenuItem = 'wcf.acp.menu.link.person.add';
+       
+       /**
+        * first name of the person
+        * @var string
+        */
+       public $firstName = '';
+       
+       /**
+        * last name of the person
+        * @var string
+        */
+       public $lastName = '';
+       
+       /**
+        * @inheritDoc
+        */
+       public $neededPermissions = ['admin.content.canManagePeople'];
+       
+       /**
+        * @inheritDoc
+        */
+       public function assignVariables() {
+               parent::assignVariables();
+               
+               WCF::getTPL()->assign([
+                       'action' => 'add',
+                       'firstName' => $this->firstName,
+                       'lastName' => $this->lastName
+               ]);
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function readFormParameters() {
+               parent::readFormParameters();
+               
+               if (isset($_POST['firstName'])) $this->firstName = StringUtil::trim($_POST['firstName']);
+               if (isset($_POST['lastName'])) $this->lastName = StringUtil::trim($_POST['lastName']);
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function save() {
+               parent::save();
+               
+               $this->objectAction = new PersonAction([], 'create', [
+                       'data' => array_merge($this->additionalFields, [
+                               'firstName' => $this->firstName,
+                               'lastName' => $this->lastName
+                       ])
+               ]);
+               $this->objectAction->executeAction();
+               
+               $this->saved();
+               
+               // reset values
+               $this->firstName = '';
+               $this->lastName = '';
+               
+               // show success message
+               WCF::getTPL()->assign('success', true);
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function validate() {
+               parent::validate();
+               
+               // validate first name
+               if (empty($this->firstName)) {
+                       throw new UserInputException('firstName');
+               }
+               if (mb_strlen($this->firstName) > 255) {
+                       throw new UserInputException('firstName', 'tooLong');
+               }
+               
+               // validate last name
+               if (empty($this->lastName)) {
+                       throw new UserInputException('lastName');
+               }
+               if (mb_strlen($this->lastName) > 255) {
+                       throw new UserInputException('lastName', 'tooLong');
+               }
+       }
+}
diff --git a/snippets/migration/wsc-31/formBuilder/PersonEditForm_new.class.php b/snippets/migration/wsc-31/formBuilder/PersonEditForm_new.class.php
new file mode 100644 (file)
index 0000000..d541a8a
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+namespace wcf\acp\form;
+use wcf\data\person\Person;
+use wcf\system\exception\IllegalLinkException;
+
+/**
+ * Shows the form to edit an existing person.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Acp\Form
+ */
+class PersonEditForm extends PersonAddForm {
+       /**
+        * @inheritDoc
+        */
+       public $activeMenuItem = 'wcf.acp.menu.link.person';
+       
+       /**
+        * @inheritDoc
+        */
+       public function readParameters() {
+               parent::readParameters();
+               
+               if (isset($_REQUEST['id'])) {
+                       $this->formObject = new Person(intval($_REQUEST['id']));
+                       if (!$this->formObject->personID) {
+                               throw new IllegalLinkException();
+                       }
+               }
+       }
+}
diff --git a/snippets/migration/wsc-31/formBuilder/PersonEditForm_old.class.php b/snippets/migration/wsc-31/formBuilder/PersonEditForm_old.class.php
new file mode 100644 (file)
index 0000000..ad8f86a
--- /dev/null
@@ -0,0 +1,91 @@
+<?php
+namespace wcf\acp\form;
+use wcf\data\person\Person;
+use wcf\data\person\PersonAction;
+use wcf\form\AbstractForm;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\WCF;
+
+/**
+ * Shows the form to edit an existing person.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Acp\Form
+ */
+class PersonEditForm extends PersonAddForm {
+       /**
+        * @inheritDoc
+        */
+       public $activeMenuItem = 'wcf.acp.menu.link.person';
+       
+       /**
+        * edited person object
+        * @var Person
+        */
+       public $person = null;
+       
+       /**
+        * id of the edited person
+        * @var integer
+        */
+       public $personID = 0;
+       
+       /**
+        * @inheritDoc
+        */
+       public function assignVariables() {
+               parent::assignVariables();
+               
+               WCF::getTPL()->assign([
+                       'action' => 'edit',
+                       'person' => $this->person
+               ]);
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function readData() {
+               parent::readData();
+               
+               if (empty($_POST)) {
+                       $this->firstName = $this->person->firstName;
+                       $this->lastName = $this->person->lastName;
+               }
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function readParameters() {
+               parent::readParameters();
+               
+               if (isset($_REQUEST['id'])) $this->personID = intval($_REQUEST['id']);
+               $this->person = new Person($this->personID);
+               if (!$this->person->personID) {
+                       throw new IllegalLinkException();
+               }
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function save() {
+               AbstractForm::save();
+               
+               $this->objectAction = new PersonAction([$this->person], 'update', [
+                       'data' => array_merge($this->additionalFields, [
+                               'firstName' => $this->firstName,
+                               'lastName' => $this->lastName
+                       ])
+               ]);
+               $this->objectAction->executeAction();
+               
+               $this->saved();
+               
+               // show success message
+               WCF::getTPL()->assign('success', true);
+       }
+}
diff --git a/snippets/migration/wsc-31/formBuilder/personAdd_new.tpl b/snippets/migration/wsc-31/formBuilder/personAdd_new.tpl
new file mode 100644 (file)
index 0000000..4cf2ec4
--- /dev/null
@@ -0,0 +1,19 @@
+{include file='header' pageTitle='wcf.acp.person.'|concat:$action}
+
+<header class="contentHeader">
+       <div class="contentHeaderTitle">
+               <h1 class="contentTitle">{lang}wcf.acp.person.{$action}{/lang}</h1>
+       </div>
+       
+       <nav class="contentHeaderNavigation">
+               <ul>
+                       <li><a href="{link controller='PersonList'}{/link}" class="button"><span class="icon icon16 fa-list"></span> <span>{lang}wcf.acp.menu.link.person.list{/lang}</span></a></li>
+                       
+                       {event name='contentHeaderNavigation'}
+               </ul>
+       </nav>
+</header>
+
+{@$form->getHtml()}
+
+{include file='footer'}
diff --git a/snippets/migration/wsc-31/formBuilder/personAdd_old.tpl b/snippets/migration/wsc-31/formBuilder/personAdd_old.tpl
new file mode 100644 (file)
index 0000000..d9b357c
--- /dev/null
@@ -0,0 +1,68 @@
+{include file='header' pageTitle='wcf.acp.person.'|concat:$action}
+
+<header class="contentHeader">
+       <div class="contentHeaderTitle">
+               <h1 class="contentTitle">{lang}wcf.acp.person.{$action}{/lang}</h1>
+       </div>
+       
+       <nav class="contentHeaderNavigation">
+               <ul>
+                       <li><a href="{link controller='PersonList'}{/link}" class="button"><span class="icon icon16 fa-list"></span> <span>{lang}wcf.acp.menu.link.person.list{/lang}</span></a></li>
+                       
+                       {event name='contentHeaderNavigation'}
+               </ul>
+       </nav>
+</header>
+
+{include file='formError'}
+
+{if $success|isset}
+       <p class="success">{lang}wcf.global.success.{$action}{/lang}</p>
+{/if}
+
+<form method="post" action="{if $action == 'add'}{link controller='PersonAdd'}{/link}{else}{link controller='PersonEdit' object=$person}{/link}{/if}">
+       <div class="section">
+               <dl{if $errorField == 'firstName'} class="formError"{/if}>
+                       <dt><label for="firstName">{lang}wcf.person.firstName{/lang}</label></dt>
+                       <dd>
+                               <input type="text" id="firstName" name="firstName" value="{$firstName}" required autofocus maxlength="255" class="long">
+                               {if $errorField == 'firstName'}
+                                       <small class="innerError">
+                                               {if $errorType == 'empty'}
+                                                       {lang}wcf.global.form.error.empty{/lang}
+                                               {else}
+                                                       {lang}wcf.acp.person.firstName.error.{$errorType}{/lang}
+                                               {/if}
+                                       </small>
+                               {/if}
+                       </dd>
+               </dl>
+               
+               <dl{if $errorField == 'lastName'} class="formError"{/if}>
+                       <dt><label for="lastName">{lang}wcf.person.lastName{/lang}</label></dt>
+                       <dd>
+                               <input type="text" id="lastName" name="lastName" value="{$lastName}" required maxlength="255" class="long">
+                               {if $errorField == 'lastName'}
+                                       <small class="innerError">
+                                               {if $errorType == 'empty'}
+                                                       {lang}wcf.global.form.error.empty{/lang}
+                                               {else}
+                                                       {lang}wcf.acp.person.lastName.error.{$errorType}{/lang}
+                                               {/if}
+                                       </small>
+                               {/if}
+                       </dd>
+               </dl>
+               
+               {event name='dataFields'}
+       </div>
+       
+       {event name='sections'}
+       
+       <div class="formSubmit">
+               <input type="submit" value="{lang}wcf.global.button.submit{/lang}" accesskey="s">
+               {@SECURITY_TOKEN_INPUT_TAG}
+       </div>
+</form>
+
+{include file='footer'}
diff --git a/snippets/tutorial/basic-app/files/global.php b/snippets/tutorial/basic-app/files/global.php
new file mode 100644 (file)
index 0000000..197e695
--- /dev/null
@@ -0,0 +1,8 @@
+<?php
+// include config
+/** @noinspection PhpIncludeInspection */
+require_once(dirname(__FILE__).'/config.inc.php');
+
+// include wcf
+/** @noinspection PhpIncludeInspection */
+require_once(RELATIVE_WCF_DIR.'global.php');
diff --git a/snippets/tutorial/basic-app/files/index.php b/snippets/tutorial/basic-app/files/index.php
new file mode 100644 (file)
index 0000000..73c4c67
--- /dev/null
@@ -0,0 +1,3 @@
+<?php
+require_once('./global.php');
+wcf\system\request\RequestHandler::getInstance()->handle('app');
diff --git a/snippets/tutorial/basic-app/files/lib/page/ExamplePage.class.php b/snippets/tutorial/basic-app/files/lib/page/ExamplePage.class.php
new file mode 100644 (file)
index 0000000..3af931c
--- /dev/null
@@ -0,0 +1,5 @@
+<?php
+namespace app\page;
+use wcf\page\AbstractPage;
+
+class ExamplePage extends AbstractPage {}
diff --git a/snippets/tutorial/basic-app/files/lib/system/APPCore.class.php b/snippets/tutorial/basic-app/files/lib/system/APPCore.class.php
new file mode 100644 (file)
index 0000000..1500fa4
--- /dev/null
@@ -0,0 +1,11 @@
+<?php
+namespace app\system;
+use app\page\ExamplePage;
+use wcf\system\application\AbstractApplication;
+
+class APPCore extends AbstractApplication {
+       /**
+        * @inheritDoc
+        */
+       protected $primaryController = ExamplePage::class;
+}
diff --git a/snippets/tutorial/basic-app/menuItem.xml b/snippets/tutorial/basic-app/menuItem.xml
new file mode 100644 (file)
index 0000000..6bea03f
--- /dev/null
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/menuItem.xsd">
+       <import>
+               <item identifier="com.example.app.Example">
+                       <menu>com.woltlab.wcf.MainMenu</menu>
+                       <title language="de">Beispiel-Seite</title>
+                       <title language="en">Example Page</title>
+                       <page>com.example.app.Example</page>
+               </item>
+       </import>
+</data>
diff --git a/snippets/tutorial/basic-app/package.xml b/snippets/tutorial/basic-app/package.xml
new file mode 100644 (file)
index 0000000..b4754e6
--- /dev/null
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package name="com.example.app" xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/package.xsd">
+       <packageinformation>
+               <packagename>Example App</packagename>
+               <packagedescription>A very basic example of an app.</packagedescription>
+               <isapplication>1</isapplication>
+               <version>3.1.0</version>
+               <date>2018-03-29</date>
+       </packageinformation>
+       
+       <authorinformation>
+               <author>Example Author</author>
+               <authorurl>https://www.example.com</authorurl>
+       </authorinformation>
+       
+       <requiredpackages>
+               <requiredpackage minversion="3.1.0">com.woltlab.wcf</requiredpackage>
+       </requiredpackages>
+       
+       <compatibility>
+               <api version="2018" />
+       </compatibility>
+       
+       <instructions type="install">
+               <instruction type="file" />
+               <instruction type="template" />
+               
+               <instruction type="page" />
+               <instruction type="menuItem" />
+       </instructions>
+</package>
diff --git a/snippets/tutorial/basic-app/page.xml b/snippets/tutorial/basic-app/page.xml
new file mode 100644 (file)
index 0000000..2b00a90
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/page.xsd">
+       <import>
+               <page identifier="com.example.app.Example">
+                       <pageType>system</pageType>
+                       <controller>app\page\ExamplePage</controller>
+                       <name language="de">Beispiel-Seite</name>
+                       <name language="en">Example App</name>
+                       <allowSpidersToIndex>1</allowSpidersToIndex>
+                       
+                       <content language="en">
+                               <title>Hello World</title>
+                       </content>
+                       <content language="de">
+                               <title>Hello World</title>
+                       </content>
+               </page>
+       </import>
+</data>
diff --git a/snippets/tutorial/basic-app/templates/example.tpl b/snippets/tutorial/basic-app/templates/example.tpl
new file mode 100644 (file)
index 0000000..f40cce3
--- /dev/null
@@ -0,0 +1,7 @@
+{include file='header'}
+
+<div class="section">
+       <p>Example Text</p>
+</div>
+
+{include file='footer'}
diff --git a/snippets/tutorial/tutorial-series/part-1/acpMenu.xml b/snippets/tutorial/tutorial-series/part-1/acpMenu.xml
new file mode 100644 (file)
index 0000000..8ad2af2
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/acpMenu.xsd">
+       <import>
+               <acpmenuitem name="wcf.acp.menu.link.person">
+                       <parent>wcf.acp.menu.link.content</parent>
+               </acpmenuitem>
+               <acpmenuitem name="wcf.acp.menu.link.person.list">
+                       <controller>wcf\acp\page\PersonListPage</controller>
+                       <parent>wcf.acp.menu.link.person</parent>
+                       <permissions>admin.content.canManagePeople</permissions>
+               </acpmenuitem>
+               <acpmenuitem name="wcf.acp.menu.link.person.add">
+                       <controller>wcf\acp\form\PersonAddForm</controller>
+                       <parent>wcf.acp.menu.link.person.list</parent>
+                       <permissions>admin.content.canManagePeople</permissions>
+                       <icon>fa-plus</icon>
+               </acpmenuitem>
+       </import>
+</data>
diff --git a/snippets/tutorial/tutorial-series/part-1/acptemplates/personAdd.tpl b/snippets/tutorial/tutorial-series/part-1/acptemplates/personAdd.tpl
new file mode 100644 (file)
index 0000000..a19603a
--- /dev/null
@@ -0,0 +1,64 @@
+{include file='header' pageTitle='wcf.acp.person.'|concat:$action}
+
+<header class="contentHeader">
+       <div class="contentHeaderTitle">
+               <h1 class="contentTitle">{lang}wcf.acp.person.{$action}{/lang}</h1>
+       </div>
+       
+       <nav class="contentHeaderNavigation">
+               <ul>
+                       <li><a href="{link controller='PersonList'}{/link}" class="button"><span class="icon icon16 fa-list"></span> <span>{lang}wcf.acp.menu.link.person.list{/lang}</span></a></li>
+                       
+                       {event name='contentHeaderNavigation'}
+               </ul>
+       </nav>
+</header>
+
+{include file='formNotice'}
+
+<form method="post" action="{if $action == 'add'}{link controller='PersonAdd'}{/link}{else}{link controller='PersonEdit' object=$person}{/link}{/if}">
+       <div class="section">
+               <dl{if $errorField == 'firstName'} class="formError"{/if}>
+                       <dt><label for="firstName">{lang}wcf.person.firstName{/lang}</label></dt>
+                       <dd>
+                               <input type="text" id="firstName" name="firstName" value="{$firstName}" required autofocus maxlength="255" class="long">
+                               {if $errorField == 'firstName'}
+                                       <small class="innerError">
+                                               {if $errorType == 'empty'}
+                                                       {lang}wcf.global.form.error.empty{/lang}
+                                               {else}
+                                                       {lang}wcf.acp.person.firstName.error.{$errorType}{/lang}
+                                               {/if}
+                                       </small>
+                               {/if}
+                       </dd>
+               </dl>
+               
+               <dl{if $errorField == 'lastName'} class="formError"{/if}>
+                       <dt><label for="lastName">{lang}wcf.person.lastName{/lang}</label></dt>
+                       <dd>
+                               <input type="text" id="lastName" name="lastName" value="{$lastName}" required maxlength="255" class="long">
+                               {if $errorField == 'lastName'}
+                                       <small class="innerError">
+                                               {if $errorType == 'empty'}
+                                                       {lang}wcf.global.form.error.empty{/lang}
+                                               {else}
+                                                       {lang}wcf.acp.person.lastName.error.{$errorType}{/lang}
+                                               {/if}
+                                       </small>
+                               {/if}
+                       </dd>
+               </dl>
+               
+               {event name='dataFields'}
+       </div>
+       
+       {event name='sections'}
+       
+       <div class="formSubmit">
+               <input type="submit" value="{lang}wcf.global.button.submit{/lang}" accesskey="s">
+               {csrfToken}
+       </div>
+</form>
+
+{include file='footer'}
diff --git a/snippets/tutorial/tutorial-series/part-1/acptemplates/personList.tpl b/snippets/tutorial/tutorial-series/part-1/acptemplates/personList.tpl
new file mode 100644 (file)
index 0000000..39507ec
--- /dev/null
@@ -0,0 +1,93 @@
+{include file='header' pageTitle='wcf.acp.person.list'}
+
+<header class="contentHeader">
+       <div class="contentHeaderTitle">
+               <h1 class="contentTitle">{lang}wcf.acp.person.list{/lang}</h1>
+       </div>
+       
+       <nav class="contentHeaderNavigation">
+               <ul>
+                       <li><a href="{link controller='PersonAdd'}{/link}" class="button"><span class="icon icon16 fa-plus"></span> <span>{lang}wcf.acp.menu.link.person.add{/lang}</span></a></li>
+                       
+                       {event name='contentHeaderNavigation'}
+               </ul>
+       </nav>
+</header>
+
+{hascontent}
+       <div class="paginationTop">
+               {content}{pages print=true assign=pagesLinks controller="PersonList" link="pageNo=%d&sortField=$sortField&sortOrder=$sortOrder"}{/content}
+       </div>
+{/hascontent}
+
+{if $objects|count}
+       <div class="section tabularBox" id="personTableContainer">
+               <table class="table">
+                       <thead>
+                               <tr>
+                                       <th class="columnID columnPersonID{if $sortField == 'personID'} active {@$sortOrder}{/if}" colspan="2"><a href="{link controller='PersonList'}pageNo={@$pageNo}&sortField=personID&sortOrder={if $sortField == 'personID' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.global.objectID{/lang}</a></th>
+                                       <th class="columnTitle columnFirstName{if $sortField == 'firstName'} active {@$sortOrder}{/if}"><a href="{link controller='PersonList'}pageNo={@$pageNo}&sortField=firstName&sortOrder={if $sortField == 'firstName' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.person.firstName{/lang}</a></th>
+                                       <th class="columnTitle columnLastName{if $sortField == 'lastName'} active {@$sortOrder}{/if}"><a href="{link controller='PersonList'}pageNo={@$pageNo}&sortField=lastName&sortOrder={if $sortField == 'lastName' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.person.lastName{/lang}</a></th>
+                                       
+                                       {event name='columnHeads'}
+                               </tr>
+                       </thead>
+                       
+                       <tbody>
+                               {foreach from=$objects item=person}
+                                       <tr class="jsPersonRow">
+                                               <td class="columnIcon">
+                                                       <a href="{link controller='PersonEdit' object=$person}{/link}" title="{lang}wcf.global.button.edit{/lang}" class="jsTooltip"><span class="icon icon16 fa-pencil"></span></a>
+                                                       <span class="icon icon16 fa-times jsDeleteButton jsTooltip pointer" title="{lang}wcf.global.button.delete{/lang}" data-object-id="{@$person->personID}" data-confirm-message-html="{lang __encode=true}wcf.acp.person.delete.confirmMessage{/lang}"></span>
+                                                       
+                                                       {event name='rowButtons'}
+                                               </td>
+                                               <td class="columnID">{#$person->personID}</td>
+                                               <td class="columnTitle columnFirstName"><a href="{link controller='PersonEdit' object=$person}{/link}">{$person->firstName}</a></td>
+                                               <td class="columnTitle columnLastName"><a href="{link controller='PersonEdit' object=$person}{/link}">{$person->lastName}</a></td>
+                                               
+                                               {event name='columns'}
+                                       </tr>
+                               {/foreach}
+                       </tbody>
+               </table>
+       </div>
+       
+       <footer class="contentFooter">
+               {hascontent}
+                       <div class="paginationBottom">
+                               {content}{@$pagesLinks}{/content}
+                       </div>
+               {/hascontent}
+               
+               <nav class="contentFooterNavigation">
+                       <ul>
+                               <li><a href="{link controller='PersonAdd'}{/link}" class="button"><span class="icon icon16 fa-plus"></span> <span>{lang}wcf.acp.menu.link.person.add{/lang}</span></a></li>
+                               
+                               {event name='contentFooterNavigation'}
+                       </ul>
+               </nav>
+       </footer>
+{else}
+       <p class="info">{lang}wcf.global.noItems{/lang}</p>
+{/if}
+
+<script data-relocate="true">
+       $(function() {
+               new WCF.Action.Delete('wcf\\data\\person\\PersonAction', '.jsPersonRow');
+               
+               var options = { };
+               {if $pages > 1}
+                       options.refreshPage = true;
+                       {if $pages == $pageNo}
+                               options.updatePageNumber = -1;
+                       {/if}
+               {else}
+                       options.emptyMessage = '{lang}wcf.global.noItems{/lang}';
+               {/if}
+               
+               new WCF.Table.EmptyTableHandler($('#personTableContainer'), 'jsPersonRow', options);
+       });
+</script>
+
+{include file='footer'}
diff --git a/snippets/tutorial/tutorial-series/part-1/files/lib/acp/form/PersonAddForm.class.php b/snippets/tutorial/tutorial-series/part-1/files/lib/acp/form/PersonAddForm.class.php
new file mode 100644 (file)
index 0000000..fd3beeb
--- /dev/null
@@ -0,0 +1,113 @@
+<?php
+namespace wcf\acp\form;
+use wcf\data\person\PersonAction;
+use wcf\form\AbstractForm;
+use wcf\system\exception\UserInputException;
+use wcf\system\request\LinkHandler;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * Shows the form to create a new person.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Acp\Form
+ */
+class PersonAddForm extends AbstractForm {
+       /**
+        * @inheritDoc
+        */
+       public $activeMenuItem = 'wcf.acp.menu.link.person.add';
+       
+       /**
+        * first name of the person
+        * @var string
+        */
+       public $firstName = '';
+       
+       /**
+        * last name of the person
+        * @var string
+        */
+       public $lastName = '';
+       
+       /**
+        * @inheritDoc
+        */
+       public $neededPermissions = ['admin.content.canManagePeople'];
+       
+       /**
+        * @inheritDoc
+        */
+       public function assignVariables() {
+               parent::assignVariables();
+               
+               WCF::getTPL()->assign([
+                       'action' => 'add',
+                       'firstName' => $this->firstName,
+                       'lastName' => $this->lastName
+               ]);
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function readFormParameters() {
+               parent::readFormParameters();
+               
+               if (isset($_POST['firstName'])) $this->firstName = StringUtil::trim($_POST['firstName']);
+               if (isset($_POST['lastName'])) $this->lastName = StringUtil::trim($_POST['lastName']);
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function save() {
+               parent::save();
+               
+               $this->objectAction = new PersonAction([], 'create', [
+                       'data' => array_merge($this->additionalFields, [
+                               'firstName' => $this->firstName,
+                               'lastName' => $this->lastName
+                       ])
+               ]);
+               $returnValues = $this->objectAction->executeAction();
+               
+               $this->saved();
+               
+               // reset values
+               $this->firstName = '';
+               $this->lastName = '';
+               
+               // show success message
+               WCF::getTPL()->assign([
+                       'success' => true,
+                       'objectEditLink' => LinkHandler::getInstance()->getControllerLink(PersonEditForm::class, ['id' => $returnValues['returnValues']->personID]),
+               ]);
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function validate() {
+               parent::validate();
+               
+               // validate first name
+               if (empty($this->firstName)) {
+                       throw new UserInputException('firstName');
+               }
+               if (mb_strlen($this->firstName) > 255) {
+                       throw new UserInputException('firstName', 'tooLong');
+               }
+               
+               // validate last name
+               if (empty($this->lastName)) {
+                       throw new UserInputException('lastName');
+               }
+               if (mb_strlen($this->lastName) > 255) {
+                       throw new UserInputException('lastName', 'tooLong');
+               }
+       }
+}
diff --git a/snippets/tutorial/tutorial-series/part-1/files/lib/acp/form/PersonEditForm.class.php b/snippets/tutorial/tutorial-series/part-1/files/lib/acp/form/PersonEditForm.class.php
new file mode 100644 (file)
index 0000000..ad8f86a
--- /dev/null
@@ -0,0 +1,91 @@
+<?php
+namespace wcf\acp\form;
+use wcf\data\person\Person;
+use wcf\data\person\PersonAction;
+use wcf\form\AbstractForm;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\WCF;
+
+/**
+ * Shows the form to edit an existing person.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Acp\Form
+ */
+class PersonEditForm extends PersonAddForm {
+       /**
+        * @inheritDoc
+        */
+       public $activeMenuItem = 'wcf.acp.menu.link.person';
+       
+       /**
+        * edited person object
+        * @var Person
+        */
+       public $person = null;
+       
+       /**
+        * id of the edited person
+        * @var integer
+        */
+       public $personID = 0;
+       
+       /**
+        * @inheritDoc
+        */
+       public function assignVariables() {
+               parent::assignVariables();
+               
+               WCF::getTPL()->assign([
+                       'action' => 'edit',
+                       'person' => $this->person
+               ]);
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function readData() {
+               parent::readData();
+               
+               if (empty($_POST)) {
+                       $this->firstName = $this->person->firstName;
+                       $this->lastName = $this->person->lastName;
+               }
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function readParameters() {
+               parent::readParameters();
+               
+               if (isset($_REQUEST['id'])) $this->personID = intval($_REQUEST['id']);
+               $this->person = new Person($this->personID);
+               if (!$this->person->personID) {
+                       throw new IllegalLinkException();
+               }
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function save() {
+               AbstractForm::save();
+               
+               $this->objectAction = new PersonAction([$this->person], 'update', [
+                       'data' => array_merge($this->additionalFields, [
+                               'firstName' => $this->firstName,
+                               'lastName' => $this->lastName
+                       ])
+               ]);
+               $this->objectAction->executeAction();
+               
+               $this->saved();
+               
+               // show success message
+               WCF::getTPL()->assign('success', true);
+       }
+}
diff --git a/snippets/tutorial/tutorial-series/part-1/files/lib/acp/page/PersonListPage.class.php b/snippets/tutorial/tutorial-series/part-1/files/lib/acp/page/PersonListPage.class.php
new file mode 100644 (file)
index 0000000..a16f28d
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+namespace wcf\acp\page;
+use wcf\data\person\PersonList;
+use wcf\page\SortablePage;
+
+/**
+ * Shows the list of people.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Acp\Page
+ */
+class PersonListPage extends SortablePage {
+       /**
+        * @inheritDoc
+        */
+       public $activeMenuItem = 'wcf.acp.menu.link.person.list';
+       
+       /**
+        * @inheritDoc
+        */
+       public $neededPermissions = ['admin.content.canManagePeople'];
+       
+       /**
+        * @inheritDoc
+        */
+       public $objectListClassName = PersonList::class;
+       
+       /**
+        * @inheritDoc
+        */
+       public $validSortFields = ['personID', 'firstName', 'lastName'];
+}
diff --git a/snippets/tutorial/tutorial-series/part-1/files/lib/data/person/Person.class.php b/snippets/tutorial/tutorial-series/part-1/files/lib/data/person/Person.class.php
new file mode 100644 (file)
index 0000000..fe30af4
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+namespace wcf\data\person;
+use wcf\data\DatabaseObject;
+use wcf\system\request\IRouteController;
+
+/**
+ * Represents a person.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Data\Person
+ * 
+ * @property-read      integer         $personID       unique id of the person
+ * @property-read      string          $firstName      first name of the person
+ * @property-read      string          $lastName       last name of the person
+ */
+class Person extends DatabaseObject implements IRouteController {
+       /**
+        * Returns the first and last name of the person if a person object is treated as a string.
+        * 
+        * @return      string
+        */
+       public function __toString() {
+               return $this->getTitle();
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getTitle() {
+               return $this->firstName . ' ' . $this->lastName;
+       }
+}
diff --git a/snippets/tutorial/tutorial-series/part-1/files/lib/data/person/PersonAction.class.php b/snippets/tutorial/tutorial-series/part-1/files/lib/data/person/PersonAction.class.php
new file mode 100644 (file)
index 0000000..d76b926
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+namespace wcf\data\person;
+use wcf\data\AbstractDatabaseObjectAction;
+
+/**
+ * Executes person-related actions.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Data\Person
+ *
+ * @method     Person          create()
+ * @method     PersonEditor[]  getObjects()
+ * @method     PersonEditor    getSingleObject()
+ */
+class PersonAction extends AbstractDatabaseObjectAction {
+       /**
+        * @inheritDoc
+        */
+       protected $permissionsDelete = ['admin.content.canManagePeople'];
+       
+       /**
+        * @inheritDoc
+        */
+       protected $requireACP = ['delete'];
+}
diff --git a/snippets/tutorial/tutorial-series/part-1/files/lib/data/person/PersonEditor.class.php b/snippets/tutorial/tutorial-series/part-1/files/lib/data/person/PersonEditor.class.php
new file mode 100644 (file)
index 0000000..3890bc1
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+namespace wcf\data\person;
+use wcf\data\DatabaseObjectEditor;
+
+/**
+ * Provides functions to edit people.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Data\Person
+ * 
+ * @method static      Person  create(array $parameters = [])
+ * @method             Person  getDecoratedObject()
+ * @mixin              Person
+ */
+class PersonEditor extends DatabaseObjectEditor {
+       /**
+        * @inheritDoc
+        */
+       protected static $baseClass = Person::class;
+}
diff --git a/snippets/tutorial/tutorial-series/part-1/files/lib/data/person/PersonList.class.php b/snippets/tutorial/tutorial-series/part-1/files/lib/data/person/PersonList.class.php
new file mode 100644 (file)
index 0000000..bedf543
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+namespace wcf\data\person;
+use wcf\data\DatabaseObjectList;
+
+/**
+ * Represents a list of people.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Data\Person
+ * 
+ * @method     Person          current()
+ * @method     Person[]        getObjects()
+ * @method     Person|null     search($objectID)
+ * @property   Person[]        $objects
+ */
+class PersonList extends DatabaseObjectList {}
diff --git a/snippets/tutorial/tutorial-series/part-1/files/lib/page/PersonListPage.class.php b/snippets/tutorial/tutorial-series/part-1/files/lib/page/PersonListPage.class.php
new file mode 100644 (file)
index 0000000..839b3e2
--- /dev/null
@@ -0,0 +1,28 @@
+<?php
+namespace wcf\page;
+use wcf\data\person\PersonList;
+
+/**
+ * Shows the list of people.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Page
+ */
+class PersonListPage extends SortablePage {
+       /**
+        * @inheritDoc
+        */
+       public $defaultSortField = 'lastName';
+       
+       /**
+        * @inheritDoc
+        */
+       public $objectListClassName = PersonList::class;
+       
+       /**
+        * @inheritDoc
+        */
+       public $validSortFields = ['personID', 'firstName', 'lastName'];
+}
diff --git a/snippets/tutorial/tutorial-series/part-1/install.sql b/snippets/tutorial/tutorial-series/part-1/install.sql
new file mode 100644 (file)
index 0000000..111a0f8
--- /dev/null
@@ -0,0 +1,6 @@
+DROP TABLE IF EXISTS wcf1_person;
+CREATE TABLE wcf1_person (
+       personID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+       firstName VARCHAR(255) NOT NULL,
+       lastName VARCHAR(255) NOT NULL
+);
diff --git a/snippets/tutorial/tutorial-series/part-1/language/de.xml b/snippets/tutorial/tutorial-series/part-1/language/de.xml
new file mode 100644 (file)
index 0000000..c046856
--- /dev/null
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<language xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/language.xsd" languagecode="de">
+       <category name="wcf.acp.group">
+               <item name="wcf.acp.group.option.admin.content.canManagePeople"><![CDATA[Kann Personen verwalten]]></item>
+       </category>
+       
+       <category name="wcf.acp.menu">
+               <item name="wcf.acp.menu.link.person"><![CDATA[Personen]]></item>
+               <item name="wcf.acp.menu.link.person.add"><![CDATA[Person hinzufügen]]></item>
+               <item name="wcf.acp.menu.link.person.list"><![CDATA[Personen]]></item>
+       </category>
+       
+       <category name="wcf.acp.person">
+               <item name="wcf.acp.person.add"><![CDATA[Person hinzufügen]]></item>
+               <item name="wcf.acp.person.delete.confirmMessage"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} die Person <span class="confirmationObject">{$person}</span> wirklich löschen?]]></item>
+               <item name="wcf.acp.person.edit"><![CDATA[Person bearbeiten]]></item>
+               <item name="wcf.acp.person.firstName.error.tooLong"><![CDATA[Der Vorname darf nicht länger als 255 Zeichen sein.]]></item>
+               <item name="wcf.acp.person.lastName.error.tooLong"><![CDATA[Der Nachname darf nicht länger als 255 Zeichen sein.]]></item>
+               <item name="wcf.acp.person.list"><![CDATA[Personen]]></item>
+       </category>
+       
+       <category name="wcf.person">
+               <item name="wcf.person.firstName"><![CDATA[Vorname]]></item>
+               <item name="wcf.person.lastName"><![CDATA[Nachname]]></item>
+               <item name="wcf.person.list"><![CDATA[Personen]]></item>
+       </category>
+</language>
diff --git a/snippets/tutorial/tutorial-series/part-1/language/en.xml b/snippets/tutorial/tutorial-series/part-1/language/en.xml
new file mode 100644 (file)
index 0000000..bff83cf
--- /dev/null
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<language xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/language.xsd" languagecode="en">
+       <category name="wcf.acp.group">
+               <item name="wcf.acp.group.option.admin.content.canManagePeople"><![CDATA[Can manage people]]></item>
+       </category>
+
+       <category name="wcf.acp.menu">
+               <item name="wcf.acp.menu.link.person"><![CDATA[People]]></item>
+               <item name="wcf.acp.menu.link.person.add"><![CDATA[Add Person]]></item>
+               <item name="wcf.acp.menu.link.person.list"><![CDATA[People]]></item>
+       </category>
+       
+       <category name="wcf.acp.person">
+               <item name="wcf.acp.person.add"><![CDATA[Add Person]]></item>
+               <item name="wcf.acp.person.delete.confirmMessage"><![CDATA[Do you really want to delete the person <span class="confirmationObject">{$person}</span>?]]></item>
+               <item name="wcf.acp.person.edit"><![CDATA[Edit Person]]></item>
+               <item name="wcf.acp.person.firstName.error.tooLong"><![CDATA[The first name must not be longer than 255 characters.]]></item>
+               <item name="wcf.acp.person.lastName.error.tooLong"><![CDATA[The last name must not be longer than 255 characters.]]></item>
+               <item name="wcf.acp.person.list"><![CDATA[People]]></item>
+       </category>
+       
+       <category name="wcf.person">
+               <item name="wcf.person.firstName"><![CDATA[First Name]]></item>
+               <item name="wcf.person.lastName"><![CDATA[Last Name]]></item>
+               <item name="wcf.person.list"><![CDATA[People]]></item>
+       </category>
+</language>
diff --git a/snippets/tutorial/tutorial-series/part-1/menuItem.xml b/snippets/tutorial/tutorial-series/part-1/menuItem.xml
new file mode 100644 (file)
index 0000000..378a297
--- /dev/null
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/menuItem.xsd">
+       <import>
+               <item identifier="com.woltlab.wcf.people.PersonList">
+                       <menu>com.woltlab.wcf.MainMenu</menu>
+                       <title language="de">Personen</title>
+                       <title language="en">People</title>
+                       <page>com.woltlab.wcf.people.PersonList</page>
+               </item>
+       </import>
+</data>
diff --git a/snippets/tutorial/tutorial-series/part-1/package.xml b/snippets/tutorial/tutorial-series/part-1/package.xml
new file mode 100644 (file)
index 0000000..5bedded
--- /dev/null
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package name="com.woltlab.wcf.people" xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/package.xsd">
+       <packageinformation>
+               <packagename>WoltLab Suite Core Tutorial: People</packagename>
+               <packagedescription>Adds a simple management system for people as part of a tutorial to create packages.</packagedescription>
+               <version>3.1.0</version>
+               <date>2018-03-30</date>
+       </packageinformation>
+       
+       <authorinformation>
+               <author>WoltLab GmbH</author>
+               <authorurl>http://www.woltlab.com</authorurl>
+       </authorinformation>
+       
+       <requiredpackages>
+               <requiredpackage minversion="3.1.0">com.woltlab.wcf</requiredpackage>
+       </requiredpackages>
+       
+       <excludedpackages>
+               <excludedpackage version="3.2.0 Alpha 1">com.woltlab.wcf</excludedpackage>
+       </excludedpackages>
+
+       <compatibility>
+               <api version="2018" />
+       </compatibility>
+       
+       <instructions type="install">
+               <instruction type="acpTemplate" />
+               <instruction type="file" />
+               <instruction type="sql" />
+               <instruction type="template" />
+               <instruction type="language" />
+               
+               <instruction type="acpMenu" />
+               <instruction type="page" />
+               <instruction type="menuItem" />
+               <instruction type="userGroupOption" />
+       </instructions>
+</package>
diff --git a/snippets/tutorial/tutorial-series/part-1/page.xml b/snippets/tutorial/tutorial-series/part-1/page.xml
new file mode 100644 (file)
index 0000000..3f80ef8
--- /dev/null
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/page.xsd">
+       <import>
+               <page identifier="com.woltlab.wcf.people.PersonList">
+                       <pageType>system</pageType>
+                       <controller>wcf\page\PersonListPage</controller>
+                       <name language="de">Personen-Liste</name>
+                       <name language="en">Person List</name>
+                       
+                       <content language="de">
+                               <title>Personen</title>
+                       </content>
+                       <content language="en">
+                               <title>People</title>
+                       </content>
+               </page>
+       </import>
+</data>
diff --git a/snippets/tutorial/tutorial-series/part-1/templates/personList.tpl b/snippets/tutorial/tutorial-series/part-1/templates/personList.tpl
new file mode 100644 (file)
index 0000000..6cad747
--- /dev/null
@@ -0,0 +1,102 @@
+{capture assign='contentTitle'}{lang}wcf.person.list{/lang} <span class="badge">{#$items}</span>{/capture}
+
+{capture assign='headContent'}
+       {if $pageNo < $pages}
+               <link rel="next" href="{link controller='PersonList'}pageNo={@$pageNo+1}{/link}">
+       {/if}
+       {if $pageNo > 1}
+               <link rel="prev" href="{link controller='PersonList'}{if $pageNo > 2}pageNo={@$pageNo-1}{/if}{/link}">
+       {/if}
+       <link rel="canonical" href="{link controller='PersonList'}{if $pageNo > 1}pageNo={@$pageNo}{/if}{/link}">
+{/capture}
+
+{capture assign='sidebarRight'}
+       <section class="box">
+               <form method="post" action="{link controller='PersonList'}{/link}">
+                       <h2 class="boxTitle">{lang}wcf.global.sorting{/lang}</h2>
+                       
+                       <div class="boxContent">
+                               <dl>
+                                       <dt></dt>
+                                       <dd>
+                                               <select id="sortField" name="sortField">
+                                                       <option value="firstName"{if $sortField == 'firstName'} selected{/if}>{lang}wcf.person.firstName{/lang}</option>
+                                                       <option value="lastName"{if $sortField == 'lastName'} selected{/if}>{lang}wcf.person.lastName{/lang}</option>
+                                                       {event name='sortField'}
+                                               </select>
+                                               <select name="sortOrder">
+                                                       <option value="ASC"{if $sortOrder == 'ASC'} selected{/if}>{lang}wcf.global.sortOrder.ascending{/lang}</option>
+                                                       <option value="DESC"{if $sortOrder == 'DESC'} selected{/if}>{lang}wcf.global.sortOrder.descending{/lang}</option>
+                                               </select>
+                                       </dd>
+                               </dl>
+                               
+                               <div class="formSubmit">
+                                       <input type="submit" value="{lang}wcf.global.button.submit{/lang}" accesskey="s">
+                               </div>
+                       </div>
+               </form>
+       </section>
+{/capture}
+
+{include file='header'}
+
+{hascontent}
+       <div class="paginationTop">
+               {content}
+                       {pages print=true assign=pagesLinks controller='PersonList' link="pageNo=%d&sortField=$sortField&sortOrder=$sortOrder"}
+               {/content}
+       </div>
+{/hascontent}
+
+{if $items}
+       <div class="section sectionContainerList">
+               <ol class="containerList personList">
+                       {foreach from=$objects item=person}
+                               <li>
+                                       <div class="box48">
+                                               <span class="icon icon48 fa-user"></span>
+                                               
+                                               <div class="details personInformation">
+                                                       <div class="containerHeadline">
+                                                               <h3>{$person}</h3>
+                                                       </div>
+                                                       
+                                                       {hascontent}
+                                                               <ul class="inlineList commaSeparated">
+                                                                       {content}{event name='personData'}{/content}
+                                                               </ul>
+                                                       {/hascontent}
+                                                       
+                                                       {hascontent}
+                                                               <dl class="plain inlineDataList small">
+                                                                       {content}{event name='personStatistics'}{/content}
+                                                               </dl>
+                                                       {/hascontent}
+                                               </div>
+                                       </div>
+                               </li>
+                       {/foreach}
+               </ol>
+       </div>
+{else}
+       <p class="info">{lang}wcf.global.noItems{/lang}</p>
+{/if}
+
+<footer class="contentFooter">
+       {hascontent}
+               <div class="paginationBottom">
+                       {content}{@$pagesLinks}{/content}
+               </div>
+       {/hascontent}
+       
+       {hascontent}
+               <nav class="contentFooterNavigation">
+                       <ul>
+                               {content}{event name='contentFooterNavigation'}{/content}
+                       </ul>
+               </nav>
+       {/hascontent}
+</footer>
+
+{include file='footer'}
diff --git a/snippets/tutorial/tutorial-series/part-1/userGroupOption.xml b/snippets/tutorial/tutorial-series/part-1/userGroupOption.xml
new file mode 100644 (file)
index 0000000..2e041d3
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/userGroupOption.xsd">
+       <import>
+               <options>
+                       <option name="admin.content.canManagePeople">
+                               <categoryname>admin.content</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue>0</defaultvalue>
+                               <admindefaultvalue>1</admindefaultvalue>
+                               <usersonly>1</usersonly>
+                       </option>
+               </options>
+       </import>
+</data>
diff --git a/snippets/tutorial/tutorial-series/part-2/acptemplates/__personAddBirthday.tpl b/snippets/tutorial/tutorial-series/part-2/acptemplates/__personAddBirthday.tpl
new file mode 100644 (file)
index 0000000..451466a
--- /dev/null
@@ -0,0 +1,15 @@
+<dl{if $errorField == 'birthday'} class="formError"{/if}>
+       <dt><label for="birthday">{lang}wcf.person.birthday{/lang}</label></dt>
+       <dd>
+               <input type="date" id="birthday" name="birthday" value="{$birthday}">
+               {if $errorField == 'birthday'}
+                       <small class="innerError">
+                               {if $errorType == 'noValidSelection'}
+                                       {lang}wcf.global.form.error.noValidSelection{/lang}
+                               {else}
+                                       {lang}wcf.acp.person.birthday.error.{$errorType}{/lang}
+                               {/if}
+                       </small>
+               {/if}
+       </dd>
+</dl>
diff --git a/snippets/tutorial/tutorial-series/part-2/eventListener.xml b/snippets/tutorial/tutorial-series/part-2/eventListener.xml
new file mode 100644 (file)
index 0000000..fb8631d
--- /dev/null
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/eventListener.xsd">
+       <import>
+               <!-- admin -->
+               <eventlistener name="birthdaySortFieldAdminPersonList">
+                       <environment>admin</environment>
+                       <eventclassname>wcf\acp\page\PersonListPage</eventclassname>
+                       <eventname>validateSortField</eventname>
+                       <listenerclassname>wcf\system\event\listener\BirthdaySortFieldPersonListPageListener</listenerclassname>
+               </eventlistener>
+               <eventlistener name="birthdayPersonAddForm">
+                       <environment>admin</environment>
+                       <eventclassname>wcf\acp\form\PersonAddForm</eventclassname>
+                       <eventname>saved</eventname>
+                       <listenerclassname>wcf\system\event\listener\BirthdayPersonAddFormListener</listenerclassname>
+               </eventlistener>
+               <eventlistener name="birthdayPersonAddFormInherited">
+                       <environment>admin</environment>
+                       <eventclassname>wcf\acp\form\PersonAddForm</eventclassname>
+                       <eventname>assignVariables,readFormParameters,save,validate</eventname>
+                       <listenerclassname>wcf\system\event\listener\BirthdayPersonAddFormListener</listenerclassname>
+                       <inherit>1</inherit>
+               </eventlistener>
+               <eventlistener name="birthdayPersonEditForm">
+                       <environment>admin</environment>
+                       <eventclassname>wcf\acp\form\PersonEditForm</eventclassname>
+                       <eventname>readData</eventname>
+                       <listenerclassname>wcf\system\event\listener\BirthdayPersonAddFormListener</listenerclassname>
+               </eventlistener>
+               <!-- /admin -->
+               
+               <!-- user -->
+               <eventlistener name="birthdaySortFieldPersonList">
+                       <environment>user</environment>
+                       <eventclassname>wcf\page\PersonListPage</eventclassname>
+                       <eventname>validateSortField</eventname>
+                       <listenerclassname>wcf\system\event\listener\BirthdaySortFieldPersonListPageListener</listenerclassname>
+               </eventlistener>
+               <!-- /user -->
+       </import>
+</data>
diff --git a/snippets/tutorial/tutorial-series/part-2/files/lib/system/event/listener/BirthdayPersonAddFormListener.class.php b/snippets/tutorial/tutorial-series/part-2/files/lib/system/event/listener/BirthdayPersonAddFormListener.class.php
new file mode 100644 (file)
index 0000000..df26e49
--- /dev/null
@@ -0,0 +1,90 @@
+<?php
+namespace wcf\system\event\listener;
+use wcf\acp\form\PersonAddForm;
+use wcf\acp\form\PersonEditForm;
+use wcf\form\IForm;
+use wcf\page\IPage;
+use wcf\system\exception\UserInputException;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * Handles setting the birthday when adding and editing people.
+ *
+ * @author     Matthias Schmidt
+ * @copyright  2001-2020 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Event\Listener
+ */
+class BirthdayPersonAddFormListener extends AbstractEventListener {
+       /**
+        * birthday of the created or edited person
+        * @var string
+        */
+       protected $birthday = '';
+       
+       /**
+        * @see IPage::assignVariables()
+        */
+       protected function onAssignVariables() {
+               WCF::getTPL()->assign('birthday', $this->birthday);
+       }
+       
+       /**
+        * @see IPage::readData()
+        */
+       protected function onReadData(PersonEditForm $form) {
+               if (empty($_POST)) {
+                       $this->birthday = $form->person->birthday;
+                       
+                       if ($this->birthday === '0000-00-00') {
+                               $this->birthday = '';
+                       }
+               }
+       }
+       
+       /**
+        * @see IForm::readFormParameters()
+        */
+       protected function onReadFormParameters() {
+               if (isset($_POST['birthday'])) {
+                       $this->birthday = StringUtil::trim($_POST['birthday']);
+               }
+       }
+       
+       /**
+        * @see IForm::save()
+        */
+       protected function onSave(PersonAddForm $form) {
+               if ($this->birthday) {
+                       $form->additionalFields['birthday'] = $this->birthday;
+               }
+               else {
+                       $form->additionalFields['birthday'] = '0000-00-00';
+               }
+       }
+       
+       /**
+        * @see IForm::saved()
+        */
+       protected function onSaved() {
+               $this->birthday = '';
+       }
+       
+       /**
+        * @see IForm::validate()
+        */
+       protected function onValidate() {
+               if (empty($this->birthday)) {
+                       return;
+               }
+               
+               if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $this->birthday, $match)) {
+                       throw new UserInputException('birthday', 'noValidSelection');
+               }
+               
+               if (!checkdate(intval($match[2]), intval($match[3]), intval($match[1]))) {
+                       throw new UserInputException('birthday', 'noValidSelection');
+               }
+       }
+}
diff --git a/snippets/tutorial/tutorial-series/part-2/files/lib/system/event/listener/BirthdaySortFieldPersonListPageListener.class.php b/snippets/tutorial/tutorial-series/part-2/files/lib/system/event/listener/BirthdaySortFieldPersonListPageListener.class.php
new file mode 100644 (file)
index 0000000..c704048
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+namespace wcf\system\event\listener;
+use wcf\page\SortablePage;
+
+/**
+ * Makes people's birthday a valid sort field in the ACP and the front end.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Event\Listener
+ */
+class BirthdaySortFieldPersonListPageListener implements IParameterizedEventListener {
+       /**
+        * @inheritDoc
+        */
+       public function execute($eventObj, $className, $eventName, array &$parameters) {
+               /** @var SortablePage $eventObj */
+               
+               $eventObj->validSortFields[] = 'birthday';
+       }
+}
diff --git a/snippets/tutorial/tutorial-series/part-2/install.sql b/snippets/tutorial/tutorial-series/part-2/install.sql
new file mode 100644 (file)
index 0000000..5e121fc
--- /dev/null
@@ -0,0 +1 @@
+ALTER TABLE wcf1_person ADD birthday DATE NOT NULL;
diff --git a/snippets/tutorial/tutorial-series/part-2/language/de.xml b/snippets/tutorial/tutorial-series/part-2/language/de.xml
new file mode 100644 (file)
index 0000000..4e49b14
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<language xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/language.xsd" languagecode="de">
+       <category name="wcf.person">
+               <item name="wcf.person.birthday"><![CDATA[Geburtstag]]></item>
+       </category>
+</language>
diff --git a/snippets/tutorial/tutorial-series/part-2/language/en.xml b/snippets/tutorial/tutorial-series/part-2/language/en.xml
new file mode 100644 (file)
index 0000000..133e822
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<language xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/language.xsd" languagecode="en">
+       <category name="wcf.person">
+               <item name="wcf.person.birthday"><![CDATA[Birthday]]></item>
+       </category>
+</language>
diff --git a/snippets/tutorial/tutorial-series/part-2/package.xml b/snippets/tutorial/tutorial-series/part-2/package.xml
new file mode 100644 (file)
index 0000000..463253c
--- /dev/null
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package name="com.woltlab.wcf.people.birthday" xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/package.xsd">
+       <packageinformation>
+               <packagename>WoltLab Suite Core Tutorial: People (Birthday)</packagename>
+               <packagedescription>Adds a birthday field to the people management system as part of a tutorial to create packages.</packagedescription>
+               <version>3.1.0</version>
+               <date>2018-03-30</date>
+       </packageinformation>
+       
+       <authorinformation>
+               <author>WoltLab GmbH</author>
+               <authorurl>http://www.woltlab.com</authorurl>
+       </authorinformation>
+       
+       <requiredpackages>
+               <requiredpackage minversion="3.1.0">com.woltlab.wcf</requiredpackage>
+               <requiredpackage minversion="3.1.0">com.woltlab.wcf.people</requiredpackage>
+       </requiredpackages>
+       
+       <excludedpackages>
+               <excludedpackage version="3.2.0 Alpha 1">com.woltlab.wcf</excludedpackage>
+       </excludedpackages>
+       
+       <compatibility>
+               <api version="2018" />
+       </compatibility>
+       
+       <instructions type="install">
+               <instruction type="acpTemplate" />
+               <instruction type="file" />
+               <instruction type="sql" />
+               <instruction type="template" />
+               <instruction type="language" />
+               
+               <instruction type="eventListener" />
+               <instruction type="templateListener" />
+       </instructions>
+</package>
diff --git a/snippets/tutorial/tutorial-series/part-2/templateListener.xml b/snippets/tutorial/tutorial-series/part-2/templateListener.xml
new file mode 100644 (file)
index 0000000..565310e
--- /dev/null
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/tornado/XSD/templateListener.xsd">
+       <import>
+               <!-- admin -->
+               <templatelistener name="personListBirthdayColumnHead">
+                       <eventname>columnHeads</eventname>
+                       <environment>admin</environment>
+                       <templatecode><![CDATA[<th class="columnDate columnBirthday{if $sortField == 'birthday'} active {@$sortOrder}{/if}"><a href="{link controller='PersonList'}pageNo={@$pageNo}&sortField=birthday&sortOrder={if $sortField == 'birthday' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.person.birthday{/lang}</a></th>]]></templatecode>
+                       <templatename>personList</templatename>
+               </templatelistener>
+               <templatelistener name="personListBirthdayColumn">
+                       <eventname>columns</eventname>
+                       <environment>admin</environment>
+                       <templatecode><![CDATA[<td class="columnDate columnBirthday">{if $person->birthday !== '0000-00-00'}{@$person->birthday|strtotime|date}{/if}</td>]]></templatecode>
+                       <templatename>personList</templatename>
+               </templatelistener>
+               <templatelistener name="personAddBirthday">
+                       <eventname>dataFields</eventname>
+                       <environment>admin</environment>
+                       <templatecode><![CDATA[{include file='__personAddBirthday'}]]></templatecode>
+                       <templatename>personAdd</templatename>
+               </templatelistener>
+               <!-- /admin -->
+               
+               <!-- user -->
+               <templatelistener name="personListBirthday">
+                       <eventname>personStatistics</eventname>
+                       <environment>user</environment>
+                       <templatecode><![CDATA[{include file='__personListBirthday'}]]></templatecode>
+                       <templatename>personList</templatename>
+               </templatelistener>
+               <templatelistener name="personListBirthdaySortField">
+                       <eventname>sortField</eventname>
+                       <environment>user</environment>
+                       <templatecode><![CDATA[{include file='__personListBirthdaySortField'}]]></templatecode>
+                       <templatename>personList</templatename>
+               </templatelistener>
+               <!-- /user -->
+       </import>
+</data>
diff --git a/snippets/tutorial/tutorial-series/part-2/templates/__personListBirthday.tpl b/snippets/tutorial/tutorial-series/part-2/templates/__personListBirthday.tpl
new file mode 100644 (file)
index 0000000..cb393bb
--- /dev/null
@@ -0,0 +1,4 @@
+{if $person->birthday !== '0000-00-00'}
+       <dt>{lang}wcf.person.birthday{/lang}</dt>
+       <dd>{@$person->birthday|strtotime|date}</dd>
+{/if}
diff --git a/snippets/tutorial/tutorial-series/part-2/templates/__personListBirthdaySortField.tpl b/snippets/tutorial/tutorial-series/part-2/templates/__personListBirthdaySortField.tpl
new file mode 100644 (file)
index 0000000..9ce0acd
--- /dev/null
@@ -0,0 +1 @@
+<option value="birthday"{if $sortField == 'birthday'} selected{/if}>{lang}wcf.person.birthday{/lang}</option>
diff --git a/snippets/tutorial/tutorial-series/part-3/acpMenu.xml b/snippets/tutorial/tutorial-series/part-3/acpMenu.xml
new file mode 100644 (file)
index 0000000..8ad2af2
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/acpMenu.xsd">
+       <import>
+               <acpmenuitem name="wcf.acp.menu.link.person">
+                       <parent>wcf.acp.menu.link.content</parent>
+               </acpmenuitem>
+               <acpmenuitem name="wcf.acp.menu.link.person.list">
+                       <controller>wcf\acp\page\PersonListPage</controller>
+                       <parent>wcf.acp.menu.link.person</parent>
+                       <permissions>admin.content.canManagePeople</permissions>
+               </acpmenuitem>
+               <acpmenuitem name="wcf.acp.menu.link.person.add">
+                       <controller>wcf\acp\form\PersonAddForm</controller>
+                       <parent>wcf.acp.menu.link.person.list</parent>
+                       <permissions>admin.content.canManagePeople</permissions>
+                       <icon>fa-plus</icon>
+               </acpmenuitem>
+       </import>
+</data>
diff --git a/snippets/tutorial/tutorial-series/part-3/acptemplates/personAdd.tpl b/snippets/tutorial/tutorial-series/part-3/acptemplates/personAdd.tpl
new file mode 100644 (file)
index 0000000..0e36907
--- /dev/null
@@ -0,0 +1,72 @@
+{include file='header' pageTitle='wcf.acp.person.'|concat:$action}
+
+<header class="contentHeader">
+       <div class="contentHeaderTitle">
+               <h1 class="contentTitle">{lang}wcf.acp.person.{$action}{/lang}</h1>
+       </div>
+       
+       <nav class="contentHeaderNavigation">
+               <ul>
+                       <li><a href="{link controller='PersonList'}{/link}" class="button"><span class="icon icon16 fa-list"></span> <span>{lang}wcf.acp.menu.link.person.list{/lang}</span></a></li>
+                       
+                       {event name='contentHeaderNavigation'}
+               </ul>
+       </nav>
+</header>
+
+{include file='formNotice'}
+
+<form method="post" action="{if $action == 'add'}{link controller='PersonAdd'}{/link}{else}{link controller='PersonEdit' object=$person}{/link}{/if}">
+       <div class="section">
+               <dl{if $errorField == 'firstName'} class="formError"{/if}>
+                       <dt><label for="firstName">{lang}wcf.person.firstName{/lang}</label></dt>
+                       <dd>
+                               <input type="text" id="firstName" name="firstName" value="{$firstName}" required autofocus maxlength="255" class="long">
+                               {if $errorField == 'firstName'}
+                                       <small class="innerError">
+                                               {if $errorType == 'empty'}
+                                                       {lang}wcf.global.form.error.empty{/lang}
+                                               {else}
+                                                       {lang}wcf.acp.person.firstName.error.{$errorType}{/lang}
+                                               {/if}
+                                       </small>
+                               {/if}
+                       </dd>
+               </dl>
+               
+               <dl{if $errorField == 'lastName'} class="formError"{/if}>
+                       <dt><label for="lastName">{lang}wcf.person.lastName{/lang}</label></dt>
+                       <dd>
+                               <input type="text" id="lastName" name="lastName" value="{$lastName}" required maxlength="255" class="long">
+                               {if $errorField == 'lastName'}
+                                       <small class="innerError">
+                                               {if $errorType == 'empty'}
+                                                       {lang}wcf.global.form.error.empty{/lang}
+                                               {else}
+                                                       {lang}wcf.acp.person.lastName.error.{$errorType}{/lang}
+                                               {/if}
+                                       </small>
+                               {/if}
+                       </dd>
+               </dl>
+               
+               <dl>
+                       <dt></dt>
+                       <dd>
+                               <label><input name="enableComments" type="checkbox" value="1"{if $enableComments} checked{/if}> {lang}wcf.person.enableComments{/lang}</label>
+                               <small>{lang}wcf.person.enableComments.description{/lang}</small>
+                       </dd>
+               </dl>
+               
+               {event name='dataFields'}
+       </div>
+       
+       {event name='sections'}
+       
+       <div class="formSubmit">
+               <input type="submit" value="{lang}wcf.global.button.submit{/lang}" accesskey="s">
+               {csrfToken}
+       </div>
+</form>
+
+{include file='footer'}
diff --git a/snippets/tutorial/tutorial-series/part-3/acptemplates/personList.tpl b/snippets/tutorial/tutorial-series/part-3/acptemplates/personList.tpl
new file mode 100644 (file)
index 0000000..39507ec
--- /dev/null
@@ -0,0 +1,93 @@
+{include file='header' pageTitle='wcf.acp.person.list'}
+
+<header class="contentHeader">
+       <div class="contentHeaderTitle">
+               <h1 class="contentTitle">{lang}wcf.acp.person.list{/lang}</h1>
+       </div>
+       
+       <nav class="contentHeaderNavigation">
+               <ul>
+                       <li><a href="{link controller='PersonAdd'}{/link}" class="button"><span class="icon icon16 fa-plus"></span> <span>{lang}wcf.acp.menu.link.person.add{/lang}</span></a></li>
+                       
+                       {event name='contentHeaderNavigation'}
+               </ul>
+       </nav>
+</header>
+
+{hascontent}
+       <div class="paginationTop">
+               {content}{pages print=true assign=pagesLinks controller="PersonList" link="pageNo=%d&sortField=$sortField&sortOrder=$sortOrder"}{/content}
+       </div>
+{/hascontent}
+
+{if $objects|count}
+       <div class="section tabularBox" id="personTableContainer">
+               <table class="table">
+                       <thead>
+                               <tr>
+                                       <th class="columnID columnPersonID{if $sortField == 'personID'} active {@$sortOrder}{/if}" colspan="2"><a href="{link controller='PersonList'}pageNo={@$pageNo}&sortField=personID&sortOrder={if $sortField == 'personID' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.global.objectID{/lang}</a></th>
+                                       <th class="columnTitle columnFirstName{if $sortField == 'firstName'} active {@$sortOrder}{/if}"><a href="{link controller='PersonList'}pageNo={@$pageNo}&sortField=firstName&sortOrder={if $sortField == 'firstName' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.person.firstName{/lang}</a></th>
+                                       <th class="columnTitle columnLastName{if $sortField == 'lastName'} active {@$sortOrder}{/if}"><a href="{link controller='PersonList'}pageNo={@$pageNo}&sortField=lastName&sortOrder={if $sortField == 'lastName' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.person.lastName{/lang}</a></th>
+                                       
+                                       {event name='columnHeads'}
+                               </tr>
+                       </thead>
+                       
+                       <tbody>
+                               {foreach from=$objects item=person}
+                                       <tr class="jsPersonRow">
+                                               <td class="columnIcon">
+                                                       <a href="{link controller='PersonEdit' object=$person}{/link}" title="{lang}wcf.global.button.edit{/lang}" class="jsTooltip"><span class="icon icon16 fa-pencil"></span></a>
+                                                       <span class="icon icon16 fa-times jsDeleteButton jsTooltip pointer" title="{lang}wcf.global.button.delete{/lang}" data-object-id="{@$person->personID}" data-confirm-message-html="{lang __encode=true}wcf.acp.person.delete.confirmMessage{/lang}"></span>
+                                                       
+                                                       {event name='rowButtons'}
+                                               </td>
+                                               <td class="columnID">{#$person->personID}</td>
+                                               <td class="columnTitle columnFirstName"><a href="{link controller='PersonEdit' object=$person}{/link}">{$person->firstName}</a></td>
+                                               <td class="columnTitle columnLastName"><a href="{link controller='PersonEdit' object=$person}{/link}">{$person->lastName}</a></td>
+                                               
+                                               {event name='columns'}
+                                       </tr>
+                               {/foreach}
+                       </tbody>
+               </table>
+       </div>
+       
+       <footer class="contentFooter">
+               {hascontent}
+                       <div class="paginationBottom">
+                               {content}{@$pagesLinks}{/content}
+                       </div>
+               {/hascontent}
+               
+               <nav class="contentFooterNavigation">
+                       <ul>
+                               <li><a href="{link controller='PersonAdd'}{/link}" class="button"><span class="icon icon16 fa-plus"></span> <span>{lang}wcf.acp.menu.link.person.add{/lang}</span></a></li>
+                               
+                               {event name='contentFooterNavigation'}
+                       </ul>
+               </nav>
+       </footer>
+{else}
+       <p class="info">{lang}wcf.global.noItems{/lang}</p>
+{/if}
+
+<script data-relocate="true">
+       $(function() {
+               new WCF.Action.Delete('wcf\\data\\person\\PersonAction', '.jsPersonRow');
+               
+               var options = { };
+               {if $pages > 1}
+                       options.refreshPage = true;
+                       {if $pages == $pageNo}
+                               options.updatePageNumber = -1;
+                       {/if}
+               {else}
+                       options.emptyMessage = '{lang}wcf.global.noItems{/lang}';
+               {/if}
+               
+               new WCF.Table.EmptyTableHandler($('#personTableContainer'), 'jsPersonRow', options);
+       });
+</script>
+
+{include file='footer'}
diff --git a/snippets/tutorial/tutorial-series/part-3/files/lib/acp/form/PersonAddForm.class.php b/snippets/tutorial/tutorial-series/part-3/files/lib/acp/form/PersonAddForm.class.php
new file mode 100644 (file)
index 0000000..7e6de52
--- /dev/null
@@ -0,0 +1,123 @@
+<?php
+namespace wcf\acp\form;
+use wcf\data\person\PersonAction;
+use wcf\form\AbstractForm;
+use wcf\system\exception\UserInputException;
+use wcf\system\request\LinkHandler;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * Shows the form to create a new person.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Acp\Form
+ */
+class PersonAddForm extends AbstractForm {
+       /**
+        * @inheritDoc
+        */
+       public $activeMenuItem = 'wcf.acp.menu.link.person.add';
+       
+       /**
+        * is `1` if comments are enabled for the person, otherwise `0`
+        * @var integer
+        */
+       public $enableComments = 1;
+       
+       /**
+        * first name of the person
+        * @var string
+        */
+       public $firstName = '';
+       
+       /**
+        * last name of the person
+        * @var string
+        */
+       public $lastName = '';
+       
+       /**
+        * @inheritDoc
+        */
+       public $neededPermissions = ['admin.content.canManagePeople'];
+       
+       /**
+        * @inheritDoc
+        */
+       public function assignVariables() {
+               parent::assignVariables();
+               
+               WCF::getTPL()->assign([
+                       'action' => 'add',
+                       'enableComments' => $this->enableComments,
+                       'firstName' => $this->firstName,
+                       'lastName' => $this->lastName
+               ]);
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function readFormParameters() {
+               parent::readFormParameters();
+               
+               $this->enableComments = isset($_POST['enableComments']) ? 1 : 0;
+               if (isset($_POST['firstName'])) $this->firstName = StringUtil::trim($_POST['firstName']);
+               if (isset($_POST['lastName'])) $this->lastName = StringUtil::trim($_POST['lastName']);
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function save() {
+               parent::save();
+               
+               $this->objectAction = new PersonAction([], 'create', [
+                       'data' => array_merge($this->additionalFields, [
+                               'enableComments' => $this->enableComments,
+                               'firstName' => $this->firstName,
+                               'lastName' => $this->lastName
+                       ])
+               ]);
+               $returnValues = $this->objectAction->executeAction();
+               
+               $this->saved();
+               
+               // reset values
+               $this->enableComments = 1;
+               $this->firstName = '';
+               $this->lastName = '';
+               
+               // show success message
+               WCF::getTPL()->assign([
+                       'success' => true,
+                       'objectEditLink' => LinkHandler::getInstance()->getControllerLink(PersonEditForm::class, ['id' => $returnValues['returnValues']->personID]),
+               ]);
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function validate() {
+               parent::validate();
+               
+               // validate first name
+               if (empty($this->firstName)) {
+                       throw new UserInputException('firstName');
+               }
+               if (mb_strlen($this->firstName) > 255) {
+                       throw new UserInputException('firstName', 'tooLong');
+               }
+               
+               // validate last name
+               if (empty($this->lastName)) {
+                       throw new UserInputException('lastName');
+               }
+               if (mb_strlen($this->lastName) > 255) {
+                       throw new UserInputException('lastName', 'tooLong');
+               }
+       }
+}
diff --git a/snippets/tutorial/tutorial-series/part-3/files/lib/acp/form/PersonEditForm.class.php b/snippets/tutorial/tutorial-series/part-3/files/lib/acp/form/PersonEditForm.class.php
new file mode 100644 (file)
index 0000000..1cb6333
--- /dev/null
@@ -0,0 +1,93 @@
+<?php
+namespace wcf\acp\form;
+use wcf\data\person\Person;
+use wcf\data\person\PersonAction;
+use wcf\form\AbstractForm;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\WCF;
+
+/**
+ * Shows the form to edit an existing person.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Acp\Form
+ */
+class PersonEditForm extends PersonAddForm {
+       /**
+        * @inheritDoc
+        */
+       public $activeMenuItem = 'wcf.acp.menu.link.person';
+       
+       /**
+        * edited person object
+        * @var Person
+        */
+       public $person = null;
+       
+       /**
+        * id of the edited person
+        * @var integer
+        */
+       public $personID = 0;
+       
+       /**
+        * @inheritDoc
+        */
+       public function assignVariables() {
+               parent::assignVariables();
+               
+               WCF::getTPL()->assign([
+                       'action' => 'edit',
+                       'person' => $this->person
+               ]);
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function readData() {
+               parent::readData();
+               
+               if (empty($_POST)) {
+                       $this->enableComments = $this->person->enableComments;
+                       $this->firstName = $this->person->firstName;
+                       $this->lastName = $this->person->lastName;
+               }
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function readParameters() {
+               parent::readParameters();
+               
+               if (isset($_REQUEST['id'])) $this->personID = intval($_REQUEST['id']);
+               $this->person = new Person($this->personID);
+               if (!$this->person->personID) {
+                       throw new IllegalLinkException();
+               }
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function save() {
+               AbstractForm::save();
+               
+               $this->objectAction = new PersonAction([$this->person], 'update', [
+                       'data' => array_merge($this->additionalFields, [
+                               'enableComments' => $this->enableComments,
+                               'firstName' => $this->firstName,
+                               'lastName' => $this->lastName
+                       ])
+               ]);
+               $this->objectAction->executeAction();
+               
+               $this->saved();
+               
+               // show success message
+               WCF::getTPL()->assign('success', true);
+       }
+}
diff --git a/snippets/tutorial/tutorial-series/part-3/files/lib/acp/page/PersonListPage.class.php b/snippets/tutorial/tutorial-series/part-3/files/lib/acp/page/PersonListPage.class.php
new file mode 100644 (file)
index 0000000..a16f28d
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+namespace wcf\acp\page;
+use wcf\data\person\PersonList;
+use wcf\page\SortablePage;
+
+/**
+ * Shows the list of people.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Acp\Page
+ */
+class PersonListPage extends SortablePage {
+       /**
+        * @inheritDoc
+        */
+       public $activeMenuItem = 'wcf.acp.menu.link.person.list';
+       
+       /**
+        * @inheritDoc
+        */
+       public $neededPermissions = ['admin.content.canManagePeople'];
+       
+       /**
+        * @inheritDoc
+        */
+       public $objectListClassName = PersonList::class;
+       
+       /**
+        * @inheritDoc
+        */
+       public $validSortFields = ['personID', 'firstName', 'lastName'];
+}
diff --git a/snippets/tutorial/tutorial-series/part-3/files/lib/data/person/Person.class.php b/snippets/tutorial/tutorial-series/part-3/files/lib/data/person/Person.class.php
new file mode 100644 (file)
index 0000000..eaeb108
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+namespace wcf\data\person;
+use wcf\data\DatabaseObject;
+use wcf\data\ILinkableObject;
+use wcf\system\request\IRouteController;
+use wcf\system\request\LinkHandler;
+
+/**
+ * Represents a person.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Data\Person
+ * 
+ * @property-read      integer         $personID               unique id of the person
+ * @property-read      string          $firstName              first name of the person
+ * @property-read      string          $lastName               last name of the person
+ * @property-read      integer         $comments               number of comments on the person
+ * @property-read      integer         $enableComments         is `1` if comments are enabled for the person, otherwise `0`
+ */
+class Person extends DatabaseObject implements ILinkableObject, IRouteController {
+       /**
+        * Returns the first and last name of the person if a person object is treated as a string.
+        * 
+        * @return      string
+        */
+       public function __toString() {
+               return $this->getTitle();
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getLink() {
+               return LinkHandler::getInstance()->getLink('Person', [
+                       'forceFrontend' => true,
+                       'object' => $this
+               ]);
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getTitle() {
+               return $this->firstName . ' ' . $this->lastName;
+       }
+}
diff --git a/snippets/tutorial/tutorial-series/part-3/files/lib/data/person/PersonAction.class.php b/snippets/tutorial/tutorial-series/part-3/files/lib/data/person/PersonAction.class.php
new file mode 100644 (file)
index 0000000..d76b926
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+namespace wcf\data\person;
+use wcf\data\AbstractDatabaseObjectAction;
+
+/**
+ * Executes person-related actions.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Data\Person
+ *
+ * @method     Person          create()
+ * @method     PersonEditor[]  getObjects()
+ * @method     PersonEditor    getSingleObject()
+ */
+class PersonAction extends AbstractDatabaseObjectAction {
+       /**
+        * @inheritDoc
+        */
+       protected $permissionsDelete = ['admin.content.canManagePeople'];
+       
+       /**
+        * @inheritDoc
+        */
+       protected $requireACP = ['delete'];
+}
diff --git a/snippets/tutorial/tutorial-series/part-3/files/lib/data/person/PersonEditor.class.php b/snippets/tutorial/tutorial-series/part-3/files/lib/data/person/PersonEditor.class.php
new file mode 100644 (file)
index 0000000..3890bc1
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+namespace wcf\data\person;
+use wcf\data\DatabaseObjectEditor;
+
+/**
+ * Provides functions to edit people.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Data\Person
+ * 
+ * @method static      Person  create(array $parameters = [])
+ * @method             Person  getDecoratedObject()
+ * @mixin              Person
+ */
+class PersonEditor extends DatabaseObjectEditor {
+       /**
+        * @inheritDoc
+        */
+       protected static $baseClass = Person::class;
+}
diff --git a/snippets/tutorial/tutorial-series/part-3/files/lib/data/person/PersonList.class.php b/snippets/tutorial/tutorial-series/part-3/files/lib/data/person/PersonList.class.php
new file mode 100644 (file)
index 0000000..bedf543
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+namespace wcf\data\person;
+use wcf\data\DatabaseObjectList;
+
+/**
+ * Represents a list of people.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Data\Person
+ * 
+ * @method     Person          current()
+ * @method     Person[]        getObjects()
+ * @method     Person|null     search($objectID)
+ * @property   Person[]        $objects
+ */
+class PersonList extends DatabaseObjectList {}
diff --git a/snippets/tutorial/tutorial-series/part-3/files/lib/page/PersonListPage.class.php b/snippets/tutorial/tutorial-series/part-3/files/lib/page/PersonListPage.class.php
new file mode 100644 (file)
index 0000000..839b3e2
--- /dev/null
@@ -0,0 +1,28 @@
+<?php
+namespace wcf\page;
+use wcf\data\person\PersonList;
+
+/**
+ * Shows the list of people.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Page
+ */
+class PersonListPage extends SortablePage {
+       /**
+        * @inheritDoc
+        */
+       public $defaultSortField = 'lastName';
+       
+       /**
+        * @inheritDoc
+        */
+       public $objectListClassName = PersonList::class;
+       
+       /**
+        * @inheritDoc
+        */
+       public $validSortFields = ['personID', 'firstName', 'lastName'];
+}
diff --git a/snippets/tutorial/tutorial-series/part-3/files/lib/page/PersonPage.class.php b/snippets/tutorial/tutorial-series/part-3/files/lib/page/PersonPage.class.php
new file mode 100644 (file)
index 0000000..d68c0d7
--- /dev/null
@@ -0,0 +1,89 @@
+<?php
+namespace wcf\page;
+use wcf\data\person\Person;
+use wcf\system\comment\CommentHandler;
+use wcf\system\comment\manager\PersonCommentManager;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\WCF;
+
+/**
+ * Shows the details of a certain person.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Page
+ */
+class PersonPage extends AbstractPage {
+       /**
+        * list of comments
+        * @var StructuredCommentList
+        */
+       public $commentList;
+       
+       /**
+        * person comment manager object
+        * @var PersonCommentManager
+        */
+       public $commentManager;
+       
+       /**
+        * id of the person comment object type
+        * @var integer
+        */
+       public $commentObjectTypeID = 0;
+       
+       /**
+        * shown person
+        * @var Person
+        */
+       public $person;
+       
+       /**
+        * id of the shown person
+        * @var integer
+        */
+       public $personID = 0;
+       
+       /**
+        * @inheritDoc
+        */
+       public function assignVariables() {
+               parent::assignVariables();
+               
+               WCF::getTPL()->assign([
+                       'commentCanAdd' => WCF::getSession()->getPermission('user.person.canAddComment'),
+                       'commentList' => $this->commentList,
+                       'commentObjectTypeID' => $this->commentObjectTypeID,
+                       'lastCommentTime' => $this->commentList ? $this->commentList->getMinCommentTime() : 0,
+                       'likeData' => (MODULE_LIKE && $this->commentList) ? $this->commentList->getLikeData() : [],
+                       'person' => $this->person
+               ]);
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function readData() {
+               parent::readData();
+               
+               if ($this->person->enableComments) {
+                       $this->commentObjectTypeID = CommentHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.person.personComment');
+                       $this->commentManager = CommentHandler::getInstance()->getObjectType($this->commentObjectTypeID)->getProcessor();
+                       $this->commentList = CommentHandler::getInstance()->getCommentList($this->commentManager, $this->commentObjectTypeID, $this->person->personID);
+               }
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function readParameters() {
+               parent::readParameters();
+               
+               if (isset($_REQUEST['id'])) $this->personID = intval($_REQUEST['id']);
+               $this->person = new Person($this->personID);
+               if (!$this->person->personID) {
+                       throw new IllegalLinkException();
+               }
+       }
+}
diff --git a/snippets/tutorial/tutorial-series/part-3/files/lib/system/cache/runtime/PersonRuntimeCache.class.php b/snippets/tutorial/tutorial-series/part-3/files/lib/system/cache/runtime/PersonRuntimeCache.class.php
new file mode 100644 (file)
index 0000000..db63fde
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+namespace wcf\system\cache\runtime;
+use wcf\data\person\Person;
+use wcf\data\person\PersonList;
+
+/**
+ * Runtime cache implementation for people.
+ *
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Cache\Runtime
+ * @since      3.0
+ *
+ * @method     Person[]        getCachedObjects()
+ * @method     Person          getObject($objectID)
+ * @method     Person[]        getObjects(array $objectIDs)
+ */
+class PersonRuntimeCache extends AbstractRuntimeCache {
+       /**
+        * @inheritDoc
+        */
+       protected $listClassName = PersonList::class;
+}
diff --git a/snippets/tutorial/tutorial-series/part-3/files/lib/system/comment/manager/PersonCommentManager.class.php b/snippets/tutorial/tutorial-series/part-3/files/lib/system/comment/manager/PersonCommentManager.class.php
new file mode 100644 (file)
index 0000000..0b35af2
--- /dev/null
@@ -0,0 +1,83 @@
+<?php
+namespace wcf\system\comment\manager;
+use wcf\data\person\Person;
+use wcf\data\person\PersonEditor;
+use wcf\system\cache\runtime\PersonRuntimeCache;
+use wcf\system\WCF;
+
+/**
+ * Comment manager implementation for people.
+ *
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Comment\Manager
+ */
+class PersonCommentManager extends AbstractCommentManager {
+       /**
+        * @inheritDoc
+        */
+       protected $permissionAdd = 'user.person.canAddComment';
+       
+       /**
+        * @inheritDoc
+        */
+       protected $permissionAddWithoutModeration = 'user.person.canAddCommentWithoutModeration';
+       
+       /**
+        * @inheritDoc
+        */
+       protected $permissionCanModerate = 'mod.person.canModerateComment';
+       
+       /**
+        * @inheritDoc
+        */
+       protected $permissionDelete = 'user.person.canDeleteComment';
+       
+       /**
+        * @inheritDoc
+        */
+       protected $permissionEdit = 'user.person.canEditComment';
+       
+       /**
+        * @inheritDoc
+        */
+       protected $permissionModDelete = 'mod.person.canDeleteComment';
+       
+       /**
+        * @inheritDoc
+        */
+       protected $permissionModEdit = 'mod.person.canEditComment';
+       
+       /**
+        * @inheritDoc
+        */
+       public function getLink($objectTypeID, $objectID) {
+               return PersonRuntimeCache::getInstance()->getObject($objectID)->getLink();
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function isAccessible($objectID, $validateWritePermission = false) {
+               return PersonRuntimeCache::getInstance()->getObject($objectID) !== null;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getTitle($objectTypeID, $objectID, $isResponse = false) {
+               if ($isResponse) {
+                       return WCF::getLanguage()->get('wcf.person.commentResponse');
+               }
+               
+               return WCF::getLanguage()->getDynamicVariable('wcf.person.comment');
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function updateCounter($objectID, $value) {
+               (new PersonEditor(new Person($objectID)))->updateCounters(['comments' => $value]);
+       }
+}
diff --git a/snippets/tutorial/tutorial-series/part-3/files/lib/system/page/handler/PersonPageHandler.class.php b/snippets/tutorial/tutorial-series/part-3/files/lib/system/page/handler/PersonPageHandler.class.php
new file mode 100644 (file)
index 0000000..58819b6
--- /dev/null
@@ -0,0 +1,96 @@
+<?php
+namespace wcf\system\page\handler;
+use wcf\data\page\Page;
+use wcf\data\person\PersonList;
+use wcf\data\user\online\UserOnline;
+use wcf\system\cache\runtime\PersonRuntimeCache;
+use wcf\system\database\util\PreparedStatementConditionBuilder;
+use wcf\system\WCF;
+
+/**
+ * Page handler implementation for person page.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Page\Handler
+ */
+class PersonPageHandler extends AbstractLookupPageHandler implements IOnlineLocationPageHandler {
+       use TOnlineLocationPageHandler;
+       
+       /**
+        * @inheritDoc
+        */
+       public function getLink($objectID) {
+               return PersonRuntimeCache::getInstance()->getObject($objectID)->getLink();
+       }
+       
+       /**
+        * Returns the textual description if a user is currently online viewing this page.
+        *
+        * @see IOnlineLocationPageHandler::getOnlineLocation()
+        *
+        * @param       Page            $page           visited page
+        * @param       UserOnline      $user           user online object with request data
+        * @return      string
+        */
+       public function getOnlineLocation(Page $page, UserOnline $user) {
+               if ($user->pageObjectID === null) {
+                       return '';
+               }
+               
+               $person = PersonRuntimeCache::getInstance()->getObject($user->pageObjectID);
+               if ($person === null) {
+                       return '';
+               }
+               
+               return WCF::getLanguage()->getDynamicVariable('wcf.page.onlineLocation.'.$page->identifier, ['person' => $person]);
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function isValid($objectID = null) {
+               return PersonRuntimeCache::getInstance()->getObject($objectID) !== null;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function lookup($searchString) {
+               $conditionBuilder = new PreparedStatementConditionBuilder(false, 'OR');
+               $conditionBuilder->add('person.firstName LIKE ?', ['%' . $searchString . '%']);
+               $conditionBuilder->add('person.lastName LIKE ?', ['%' . $searchString . '%']);
+               
+               $personList = new PersonList();
+               $personList->getConditionBuilder()->add($conditionBuilder, $conditionBuilder->getParameters());
+               $personList->readObjects();
+               
+               $results = [];
+               foreach ($personList as $person) {
+                       $results[] = [
+                               'image' => 'fa-user',
+                               'link' => $person->getLink(),
+                               'objectID' => $person->personID,
+                               'title' => $person->getTitle()
+                       ];
+               }
+               
+               return $results;
+       }
+       
+       /**
+        * Prepares fetching all necessary data for the textual description if a user is currently online
+        * viewing this page.
+        * 
+        * @see IOnlineLocationPageHandler::prepareOnlineLocation()
+        *
+        * @param       Page            $page           visited page
+        * @param       UserOnline      $user           user online object with request data
+        */
+       public function prepareOnlineLocation(/** @noinspection PhpUnusedParameterInspection */Page $page, UserOnline $user) {
+               if ($user->pageObjectID !== null) {
+                       PersonRuntimeCache::getInstance()->cacheObjectID($user->pageObjectID);
+               }
+       }
+}
diff --git a/snippets/tutorial/tutorial-series/part-3/install.sql b/snippets/tutorial/tutorial-series/part-3/install.sql
new file mode 100644 (file)
index 0000000..af5dfcb
--- /dev/null
@@ -0,0 +1,8 @@
+DROP TABLE IF EXISTS wcf1_person;
+CREATE TABLE wcf1_person (
+       personID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+       firstName VARCHAR(255) NOT NULL,
+       lastName VARCHAR(255) NOT NULL,
+       comments SMALLINT(5) NOT NULL DEFAULT 0,
+       enableComments TINYINT(1) NOT NULL DEFAULT 1
+);
diff --git a/snippets/tutorial/tutorial-series/part-3/language/de.xml b/snippets/tutorial/tutorial-series/part-3/language/de.xml
new file mode 100644 (file)
index 0000000..1cf1af3
--- /dev/null
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<language xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/language.xsd" languagecode="de">
+       <category name="wcf.acp.group">
+               <item name="wcf.acp.group.option.admin.content.canManagePeople"><![CDATA[Kann Personen verwalten]]></item>
+               
+               <item name="wcf.acp.group.option.category.mod.person"><![CDATA[Personen]]></item>
+               <item name="wcf.acp.group.option.category.user.person"><![CDATA[Personen]]></item>
+               
+               <item name="wcf.acp.group.option.mod.person.canDeleteComment"><![CDATA[Kann Kommentare löschen]]></item>
+               <item name="wcf.acp.group.option.mod.person.canEditComment"><![CDATA[Kann Kommentare bearbeiten]]></item>
+               <item name="wcf.acp.group.option.mod.person.canModerateComment"><![CDATA[Kann Kommentare moderieren]]></item>
+               
+               <item name="wcf.acp.group.option.user.person.canAddComment"><![CDATA[Kann Kommentare erstellen]]></item>
+               <item name="wcf.acp.group.option.user.person.canDeleteComment"><![CDATA[Kann eigene Kommentare löschen]]></item>
+               <item name="wcf.acp.group.option.user.person.canEditComment"><![CDATA[Kann eigene Kommentare bearbeiten]]></item>
+       </category>
+       
+       <category name="wcf.acp.menu">
+               <item name="wcf.acp.menu.link.person"><![CDATA[Personen]]></item>
+               <item name="wcf.acp.menu.link.person.add"><![CDATA[Person hinzufügen]]></item>
+               <item name="wcf.acp.menu.link.person.list"><![CDATA[Personen]]></item>
+       </category>
+       
+       <category name="wcf.acp.person">
+               <item name="wcf.acp.person.add"><![CDATA[Person hinzufügen]]></item>
+               <item name="wcf.acp.person.delete.confirmMessage"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} die Person <span class="confirmationObject">{$person}</span> wirklich löschen?]]></item>
+               <item name="wcf.acp.person.edit"><![CDATA[Person bearbeiten]]></item>
+               <item name="wcf.acp.person.firstName.error.tooLong"><![CDATA[Der Vorname darf nicht länger als 255 Zeichen sein.]]></item>
+               <item name="wcf.acp.person.lastName.error.tooLong"><![CDATA[Der Nachname darf nicht länger als 255 Zeichen sein.]]></item>
+               <item name="wcf.acp.person.list"><![CDATA[Personen]]></item>
+       </category>
+       
+       <category name="wcf.page">
+               <item name="wcf.page.onlineLocation.com.woltlab.wcf.people.Person"><![CDATA[Person <a href="{$person->getLink()}">{$person}</a>]]></item>
+       </category>
+       
+       <category name="wcf.person">
+               <item name="wcf.person.comment"><![CDATA[Person-Kommentar]]></item>
+               <item name="wcf.person.commentResponse"><![CDATA[Antwort auf Person-Kommentar]]></item>
+               <item name="wcf.person.comments"><![CDATA[Kommentare]]></item>
+               <item name="wcf.person.enableComments"><![CDATA[Kommentare aktivieren]]></item>
+               <item name="wcf.person.enableComments.description"><![CDATA[Erlaubt es Benutzern diese Person zu kommentieren.]]></item>
+               <item name="wcf.person.firstName"><![CDATA[Vorname]]></item>
+               <item name="wcf.person.lastName"><![CDATA[Nachname]]></item>
+               <item name="wcf.person.list"><![CDATA[Personen]]></item>
+       </category>
+</language>
diff --git a/snippets/tutorial/tutorial-series/part-3/language/en.xml b/snippets/tutorial/tutorial-series/part-3/language/en.xml
new file mode 100644 (file)
index 0000000..a7155b5
--- /dev/null
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<language xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/language.xsd" languagecode="en">
+       <category name="wcf.acp.group">
+               <item name="wcf.acp.group.option.admin.content.canManagePeople"><![CDATA[Can manage people]]></item>
+               
+               <item name="wcf.acp.group.option.category.mod.person"><![CDATA[People]]></item>
+               <item name="wcf.acp.group.option.category.user.person"><![CDATA[People]]></item>
+               
+               <item name="wcf.acp.group.option.mod.person.canDeleteComment"><![CDATA[Can delete comments]]></item>
+               <item name="wcf.acp.group.option.mod.person.canEditComment"><![CDATA[Can edit comments]]></item>
+               <item name="wcf.acp.group.option.mod.person.canModerateComment"><![CDATA[Can moderate comments]]></item>
+               
+               <item name="wcf.acp.group.option.user.person.canAddComment"><![CDATA[Can create comments]]></item>
+               <item name="wcf.acp.group.option.user.person.canDeleteComment"><![CDATA[Can delete their comments]]></item>
+               <item name="wcf.acp.group.option.user.person.canEditComment"><![CDATA[Can edit their comments]]></item>
+       </category>
+       
+       <category name="wcf.acp.menu">
+               <item name="wcf.acp.menu.link.person"><![CDATA[People]]></item>
+               <item name="wcf.acp.menu.link.person.add"><![CDATA[Add Person]]></item>
+               <item name="wcf.acp.menu.link.person.list"><![CDATA[People]]></item>
+       </category>
+       
+       <category name="wcf.acp.person">
+               <item name="wcf.acp.person.add"><![CDATA[Add Person]]></item>
+               <item name="wcf.acp.person.delete.confirmMessage"><![CDATA[Do you really want to delete the person <span class="confirmationObject">{$person}</span>?]]></item>
+               <item name="wcf.acp.person.edit"><![CDATA[Edit Person]]></item>
+               <item name="wcf.acp.person.firstName.error.tooLong"><![CDATA[The first name must not be longer than 255 characters.]]></item>
+               <item name="wcf.acp.person.lastName.error.tooLong"><![CDATA[The last name must not be longer than 255 characters.]]></item>
+               <item name="wcf.acp.person.list"><![CDATA[People]]></item>
+       </category>
+       
+       <category name="wcf.page">
+               <item name="wcf.page.onlineLocation.com.woltlab.wcf.people.Person"><![CDATA[Person <a href="{$person->getLink()}">{$person}</a>]]></item>
+       </category>
+       
+       <category name="wcf.person">
+               <item name="wcf.person.comment"><![CDATA[Person Comment]]></item>
+               <item name="wcf.person.commentResponse"><![CDATA[Reply to Person Comment]]></item>
+               <item name="wcf.person.comments"><![CDATA[Comments]]></item>
+               <item name="wcf.person.enableComments"><![CDATA[Allow Comments]]></item>
+               <item name="wcf.person.enableComments.description"><![CDATA[Allow users to comment on this person.]]></item>
+               <item name="wcf.person.firstName"><![CDATA[First Name]]></item>
+               <item name="wcf.person.lastName"><![CDATA[Last Name]]></item>
+               <item name="wcf.person.list"><![CDATA[People]]></item>
+       </category>
+</language>
diff --git a/snippets/tutorial/tutorial-series/part-3/menuItem.xml b/snippets/tutorial/tutorial-series/part-3/menuItem.xml
new file mode 100644 (file)
index 0000000..378a297
--- /dev/null
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/menuItem.xsd">
+       <import>
+               <item identifier="com.woltlab.wcf.people.PersonList">
+                       <menu>com.woltlab.wcf.MainMenu</menu>
+                       <title language="de">Personen</title>
+                       <title language="en">People</title>
+                       <page>com.woltlab.wcf.people.PersonList</page>
+               </item>
+       </import>
+</data>
diff --git a/snippets/tutorial/tutorial-series/part-3/objectType.xml b/snippets/tutorial/tutorial-series/part-3/objectType.xml
new file mode 100644 (file)
index 0000000..acce431
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/objectType.xsd">
+       <import>
+               <type>
+                       <name>com.woltlab.wcf.person.personComment</name>
+                       <definitionname>com.woltlab.wcf.comment.commentableContent</definitionname>
+                       <classname>wcf\system\comment\manager\PersonCommentManager</classname>
+               </type>
+       </import>
+</data>
diff --git a/snippets/tutorial/tutorial-series/part-3/package.xml b/snippets/tutorial/tutorial-series/part-3/package.xml
new file mode 100644 (file)
index 0000000..6519115
--- /dev/null
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package name="com.woltlab.wcf.people" xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/package.xsd">
+       <packageinformation>
+               <packagename>WoltLab Suite Core Tutorial: People</packagename>
+               <packagedescription>Adds a simple management system for people as part of a tutorial to create packages.</packagedescription>
+               <version>3.1.0</version>
+               <date>2018-03-30</date>
+       </packageinformation>
+       
+       <authorinformation>
+               <author>WoltLab GmbH</author>
+               <authorurl>http://www.woltlab.com</authorurl>
+       </authorinformation>
+       
+       <requiredpackages>
+               <requiredpackage minversion="3.1.0">com.woltlab.wcf</requiredpackage>
+       </requiredpackages>
+       
+       <excludedpackages>
+               <excludedpackage version="3.2.0 Alpha 1">com.woltlab.wcf</excludedpackage>
+       </excludedpackages>
+       
+       <compatibility>
+               <api version="2018" />
+       </compatibility>
+       
+       <instructions type="install">
+               <instruction type="acpTemplate" />
+               <instruction type="file" />
+               <instruction type="sql" />
+               <instruction type="template" />
+               <instruction type="language" />
+               <instruction type="objectType" />
+               
+               <instruction type="acpMenu" />
+               <instruction type="page" />
+               <instruction type="menuItem" />
+               <instruction type="userGroupOption" />
+       </instructions>
+</package>
diff --git a/snippets/tutorial/tutorial-series/part-3/page.xml b/snippets/tutorial/tutorial-series/part-3/page.xml
new file mode 100644 (file)
index 0000000..0e1ea88
--- /dev/null
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/page.xsd">
+       <import>
+               <page identifier="com.woltlab.wcf.people.PersonList">
+                       <pageType>system</pageType>
+                       <controller>wcf\page\PersonListPage</controller>
+                       <name language="de">Personen-Liste</name>
+                       <name language="en">Person List</name>
+                       
+                       <content language="de">
+                               <title>Personen</title>
+                       </content>
+                       <content language="en">
+                               <title>People</title>
+                       </content>
+               </page>
+               <page identifier="com.woltlab.wcf.people.Person">
+                       <pageType>system</pageType>
+                       <controller>wcf\page\PersonPage</controller>
+                       <handler>wcf\system\page\handler\PersonPageHandler</handler>
+                       <name language="de">Person</name>
+                       <name language="en">Person</name>
+                       <requireObjectID>1</requireObjectID>
+                       <parent>com.woltlab.wcf.people.PersonList</parent>
+               </page>
+       </import>
+</data>
diff --git a/snippets/tutorial/tutorial-series/part-3/templates/person.tpl b/snippets/tutorial/tutorial-series/part-3/templates/person.tpl
new file mode 100644 (file)
index 0000000..8ccd8f9
--- /dev/null
@@ -0,0 +1,42 @@
+{capture assign='pageTitle'}{$person} - {lang}wcf.person.list{/lang}{/capture}
+
+{capture assign='contentTitle'}{$person}{/capture}
+
+{include file='header'}
+
+{if $person->enableComments}
+       {if $commentList|count || $commentCanAdd}
+               <section id="comments" class="section sectionContainerList">
+                       <header class="sectionHeader">
+                               <h2 class="sectionTitle">{lang}wcf.person.comments{/lang}{if $person->comments} <span class="badge">{#$person->comments}</span>{/if}</h2>
+                       </header>
+                       
+                       {include file='__commentJavaScript' commentContainerID='personCommentList'}
+                       
+                       <div class="personComments">
+                               <ul id="personCommentList" class="commentList containerList"
+                                       data-can-add="{if $commentCanAdd}true{else}false{/if}" 
+                                       data-object-id="{@$person->personID}"
+                                       data-object-type-id="{@$commentObjectTypeID}"
+                                       data-comments="{if $person->comments}{@$commentList->countObjects()}{else}0{/if}"
+                                       data-last-comment-time="{@$lastCommentTime}"
+                               >
+                                       {include file='commentListAddComment' wysiwygSelector='personCommentListAddComment'}
+                                       {include file='commentList'}
+                               </ul>
+                       </div>
+               </section>
+       {/if}
+{/if}
+
+<footer class="contentFooter">
+       {hascontent}
+               <nav class="contentFooterNavigation">
+                       <ul>
+                               {content}{event name='contentFooterNavigation'}{/content}
+                       </ul>
+               </nav>
+       {/hascontent}
+</footer>
+
+{include file='footer'}
diff --git a/snippets/tutorial/tutorial-series/part-3/templates/personList.tpl b/snippets/tutorial/tutorial-series/part-3/templates/personList.tpl
new file mode 100644 (file)
index 0000000..eb193f1
--- /dev/null
@@ -0,0 +1,109 @@
+{capture assign='contentTitle'}{lang}wcf.person.list{/lang} <span class="badge">{#$items}</span>{/capture}
+
+{capture assign='headContent'}
+       {if $pageNo < $pages}
+               <link rel="next" href="{link controller='PersonList'}pageNo={@$pageNo+1}{/link}">
+       {/if}
+       {if $pageNo > 1}
+               <link rel="prev" href="{link controller='PersonList'}{if $pageNo > 2}pageNo={@$pageNo-1}{/if}{/link}">
+       {/if}
+       <link rel="canonical" href="{link controller='PersonList'}{if $pageNo > 1}pageNo={@$pageNo}{/if}{/link}">
+{/capture}
+
+{capture assign='sidebarRight'}
+       <section class="box">
+               <form method="post" action="{link controller='PersonList'}{/link}">
+                       <h2 class="boxTitle">{lang}wcf.global.sorting{/lang}</h2>
+                       
+                       <div class="boxContent">
+                               <dl>
+                                       <dt></dt>
+                                       <dd>
+                                               <select id="sortField" name="sortField">
+                                                       <option value="firstName"{if $sortField == 'firstName'} selected{/if}>{lang}wcf.person.firstName{/lang}</option>
+                                                       <option value="lastName"{if $sortField == 'lastName'} selected{/if}>{lang}wcf.person.lastName{/lang}</option>
+                                                       {event name='sortField'}
+                                               </select>
+                                               <select name="sortOrder">
+                                                       <option value="ASC"{if $sortOrder == 'ASC'} selected{/if}>{lang}wcf.global.sortOrder.ascending{/lang}</option>
+                                                       <option value="DESC"{if $sortOrder == 'DESC'} selected{/if}>{lang}wcf.global.sortOrder.descending{/lang}</option>
+                                               </select>
+                                       </dd>
+                               </dl>
+                               
+                               <div class="formSubmit">
+                                       <input type="submit" value="{lang}wcf.global.button.submit{/lang}" accesskey="s">
+                               </div>
+                       </div>
+               </form>
+       </section>
+{/capture}
+
+{include file='header'}
+
+{hascontent}
+       <div class="paginationTop">
+               {content}
+                       {pages print=true assign=pagesLinks controller='PersonList' link="pageNo=%d&sortField=$sortField&sortOrder=$sortOrder"}
+               {/content}
+       </div>
+{/hascontent}
+
+{if $items}
+       <div class="section sectionContainerList">
+               <ol class="containerList personList">
+                       {foreach from=$objects item=person}
+                               <li>
+                                       <div class="box48">
+                                               <span class="icon icon48 fa-user"></span>
+                                               
+                                               <div class="details personInformation">
+                                                       <div class="containerHeadline">
+                                                               <h3><a href="{$person->getLink()}">{$person}</a></h3>
+                                                       </div>
+                                                       
+                                                       {hascontent}
+                                                               <ul class="inlineList commaSeparated">
+                                                                       {content}{event name='personData'}{/content}
+                                                               </ul>
+                                                       {/hascontent}
+                                                       
+                                                       {hascontent}
+                                                               <dl class="plain inlineDataList small">
+                                                                       {content}
+                                                                               {if $person->enableComments}
+                                                                                       <dt>{lang}wcf.person.comments{/lang}</dt>
+                                                                                       <dd>{#$person->comments}</dd>
+                                                                               {/if}
+                                                                               
+                                                                               {event name='personStatistics'}
+                                                                       {/content}
+                                                               </dl>
+                                                       {/hascontent}
+                                               </div>
+                                       </div>
+                               </li>
+                       {/foreach}
+               </ol>
+       </div>
+{else}
+       <p class="info">{lang}wcf.global.noItems{/lang}</p>
+{/if}
+
+<footer class="contentFooter">
+       {hascontent}
+               <div class="paginationBottom">
+                       {content}{@$pagesLinks}{/content}
+               </div>
+       {/hascontent}
+       
+       {hascontent}
+               <nav class="contentFooterNavigation">
+                       <ul>
+                               {content}{event name='contentFooterNavigation'}{/content}
+                       </ul>
+               </nav>
+       {/hascontent}
+</footer>
+
+{include file='footer'}
diff --git a/snippets/tutorial/tutorial-series/part-3/userGroupOption.xml b/snippets/tutorial/tutorial-series/part-3/userGroupOption.xml
new file mode 100644 (file)
index 0000000..044394a
--- /dev/null
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/userGroupOption.xsd">
+       <import>
+               <categories>
+                       <category name="mod.person">
+                               <parent>mod</parent>
+                       </category>
+                       
+                       <category name="user.person">
+                               <parent>user</parent>
+                       </category>
+               </categories>
+               
+               <options>
+                       <!-- admin.content -->
+                       <option name="admin.content.canManagePeople">
+                               <categoryname>admin.content</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue>0</defaultvalue>
+                               <admindefaultvalue>1</admindefaultvalue>
+                               <usersonly>1</usersonly>
+                       </option>
+                       <!-- /admin.content -->
+                       
+                       <!-- mod.person -->
+                       <option name="mod.person.canModerateComment">
+                               <categoryname>mod.person</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue>0</defaultvalue>
+                               <moddefaultvalue>1</moddefaultvalue>
+                               <usersonly>1</usersonly>
+                       </option>
+                       <option name="mod.person.canEditComment">
+                               <categoryname>mod.person</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue>0</defaultvalue>
+                               <moddefaultvalue>1</moddefaultvalue>
+                               <usersonly>1</usersonly>
+                       </option>
+                       <option name="mod.person.canDeleteComment">
+                               <categoryname>mod.person</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue>0</defaultvalue>
+                               <moddefaultvalue>1</moddefaultvalue>
+                               <usersonly>1</usersonly>
+                       </option>
+                       <!-- /mod.person -->
+                       
+                       <!-- user.person -->
+                       <option name="user.person.canAddComment">
+                               <categoryname>user.person</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue>1</defaultvalue>
+                               <enableoptions>user.person.canAddCommentWithoutModeration</enableoptions>
+                               <excludedInTinyBuild>1</excludedInTinyBuild>
+                       </option>
+                       <option name="user.person.canAddCommentWithoutModeration">
+                               <categoryname>user.person</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue>1</defaultvalue>
+                               <excludedInTinyBuild>1</excludedInTinyBuild>
+                       </option>
+                       <option name="user.person.canEditComment">
+                               <categoryname>user.person</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue>1</defaultvalue>
+                               <usersonly>1</usersonly>
+                       </option>
+                       <option name="user.person.canDeleteComment">
+                               <categoryname>user.person</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue>0</defaultvalue>
+                               <admindefaultvalue>1</admindefaultvalue>
+                               <usersonly>1</usersonly>
+                       </option>
+                       <!-- /user.person -->
+               </options>
+       </import>
+</data>