Skip to content

Commit b3e3174

Browse files
author
Matthias Schmidt
authored
Merge pull request #179 from WoltLab/tutorial6
Add part 6 of tutorial series
2 parents f4bed51 + c4350d4 commit b3e3174

File tree

48 files changed

+2865
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+2865
-0
lines changed

docs/tutorial/series/overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ Note that in the context of this example, not every added feature might make per
1111
- [Part 3: Person Page and Comments](part_3.md)
1212
- [Part 4: Box and Box Conditions](part_4.md)
1313
- [Part 5: Person Information](part_5.md)
14+
- [Part 6: Activity Points and Activity Events](part_6.md)

docs/tutorial/series/part_6.md

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# Part 6: Activity Points and Activity Events
2+
3+
In this part of our tutorial series, we use the person information added in the [previous part](part_5.md) to award activity points to users adding new pieces of information and to also create activity events for these pieces of information.
4+
5+
6+
## Package Functionality
7+
8+
In addition to the existing functions from [part 5](part_5.md), the package will provide the following functionality after this part of the tutorial:
9+
10+
- Users are awarded activity points for adding new pieces of information for people.
11+
- If users add new pieces of information for people, activity events are added which are then shown in the list of recent activities.
12+
13+
14+
## Used Components
15+
16+
In addition to the components used in previous parts, we will use the [user activity points API](../../php/api/user_activity_points.md) and the user activity events API.
17+
18+
19+
## Package Structure
20+
21+
The package will have the following file structure _excluding_ unchanged files from previous parts:
22+
23+
```
24+
├── files
25+
│ └── lib
26+
│ ├── data
27+
│ │ └── person
28+
│ │ ├── PersonAction.class.php
29+
│ │ └── information
30+
│ │ ├── PersonInformation.class.php
31+
│ │ └── PersonInformationAction.class.php
32+
│ └── system
33+
│ ├── user
34+
│ │ └── activity
35+
│ │ └── event
36+
│ │ └── PersonInformationUserActivityEvent.class.php
37+
│ └── worker
38+
│ ├── PersonInformationRebuildDataWorker.class.php
39+
│ └── PersonRebuildDataWorker.class.php
40+
├── eventListener.xml
41+
├── language
42+
│ ├── de.xml
43+
│ └── en.xml
44+
└── objectType.xml
45+
```
46+
47+
For all changes, please refer to the [source code on GitHub]({jinja{ config.repo_url }}tree/{jinja{ config.edit_uri.split("/")[1] }}/snippets/tutorial/tutorial-series/part-6).
48+
49+
50+
## User Activity Points
51+
52+
The first step to support activity points is to register an object type for the `com.woltlab.wcf.user.activityPointEvent` object type definition for created person information and specify the default number of points awarded per piece of information:
53+
54+
{jinja{ codebox(
55+
language="xml",
56+
title="objectType.xml",
57+
contents="""
58+
<type>
59+
<name>com.woltlab.wcf.people.information</name>
60+
<definitionname>com.woltlab.wcf.user.activityPointEvent</definitionname>
61+
<points>2</points>
62+
</type>
63+
"""
64+
) }}
65+
66+
Additionally, the phrase `wcf.user.activityPoint.objectType.com.woltlab.wcf.people.information` (in general: `wcf.user.activityPoint.objectType.{objectType}`) has to be added.
67+
68+
The activity points are awarded when new pieces are created via `PersonInformation::create()` using `UserActivityPointHandler::fireEvent()` and removed in `PersonInformation::create()` via `UserActivityPointHandler::removeEvents()` if pieces of information are deleted.
69+
70+
Lastly, we have to add two components for updating data:
71+
First, we register a new rebuild data worker
72+
73+
{jinja{ codebox(
74+
language="xml",
75+
title="objectType.xml",
76+
contents="""
77+
<type>
78+
<name>com.woltlab.wcf.people.information</name>
79+
<definitionname>com.woltlab.wcf.rebuildData</definitionname>
80+
<classname>wcf\system\worker\PersonInformationRebuildDataWorker</classname>
81+
</type>
82+
"""
83+
) }}
84+
85+
{jinja{ codebox(
86+
title="files/lib/system/worker/PersonInformationRebuildDataWorker.class.php",
87+
language="php",
88+
filepath="tutorial/tutorial-series/part-6/files/lib/system/worker/PersonInformationRebuildDataWorker.class.php"
89+
) }}
90+
91+
which updates the number of instances for which any user received person information activity points.
92+
(This data worker also requires the phrases `wcf.acp.rebuildData.com.woltlab.wcf.people.information` and `wcf.acp.rebuildData.com.woltlab.wcf.people.information.description`).
93+
94+
Second, we add an event listener for `UserActivityPointItemsRebuildDataWorker` to update the total user activity points awarded for person information:
95+
96+
{jinja{ codebox(
97+
language="xml",
98+
title="eventListener.xml",
99+
contents="""
100+
<eventlistener name=\"execute@wcf\system\worker\\UserActivityPointItemsRebuildDataWorker\">
101+
<eventclassname>wcf\system\worker\\UserActivityPointItemsRebuildDataWorker</eventclassname>
102+
<eventname>execute</eventname>
103+
<listenerclassname>wcf\system\event\listener\PersonUserActivityPointItemsRebuildDataWorkerListener</listenerclassname>
104+
<environment>admin</environment>
105+
</eventlistener>
106+
"""
107+
) }}
108+
109+
{jinja{ codebox(
110+
title="files/lib/system/event/listener/PersonUserActivityPointItemsRebuildDataWorkerListener.class.php",
111+
language="php",
112+
filepath="tutorial/tutorial-series/part-6/files/lib/system/event/listener/PersonUserActivityPointItemsRebuildDataWorkerListener.class.php"
113+
) }}
114+
115+
116+
## User Activity Events
117+
118+
To support user activity events, an object type for `com.woltlab.wcf.user.recentActivityEvent` has to be registered with a class implementing `wcf\system\user\activity\event\IUserActivityEvent`:
119+
120+
{jinja{ codebox(
121+
language="xml",
122+
title="objectType.xml",
123+
contents="""
124+
<type>
125+
<name>com.woltlab.wcf.people.information</name>
126+
<definitionname>com.woltlab.wcf.user.recentActivityEvent</definitionname>
127+
<classname>wcf\system\\user\activity\event\PersonInformationUserActivityEvent</classname>
128+
</type>
129+
"""
130+
) }}
131+
132+
{jinja{ codebox(
133+
title="files/lib/system/user/activity/event/PersonInformationUserActivityEvent.class.php",
134+
language="php",
135+
filepath="tutorial/tutorial-series/part-6/files/lib/system/user/activity/event/PersonInformationUserActivityEvent.class.php"
136+
) }}
137+
138+
`PersonInformationUserActivityEvent::prepare()` must check for all events whether the associated piece of information still exists and if it is the case, mark the event as accessible via the `setIsAccessible()` method, set the title of the activity event via `setTitle()`, and set a description of the event via `setDescription()` for which we use the newly added `PersonInformation::getFormattedExcerpt()` method.
139+
140+
Lastly, we have to add the phrase `wcf.user.recentActivity.com.woltlab.wcf.people.information`, which is shown in the list of activity events as the type of activity event.

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ nav:
137137
- 'Part 3': 'tutorial/series/part_3.md'
138138
- 'Part 4': 'tutorial/series/part_4.md'
139139
- 'Part 5': 'tutorial/series/part_5.md'
140+
- 'Part 6': 'tutorial/series/part_6.md'
140141

141142
plugins:
142143
- git-revision-date
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<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/5.4/acpMenu.xsd">
3+
<import>
4+
<acpmenuitem name="wcf.acp.menu.link.person">
5+
<parent>wcf.acp.menu.link.content</parent>
6+
</acpmenuitem>
7+
<acpmenuitem name="wcf.acp.menu.link.person.list">
8+
<controller>wcf\acp\page\PersonListPage</controller>
9+
<parent>wcf.acp.menu.link.person</parent>
10+
<permissions>admin.content.canManagePeople</permissions>
11+
</acpmenuitem>
12+
<acpmenuitem name="wcf.acp.menu.link.person.add">
13+
<controller>wcf\acp\form\PersonAddForm</controller>
14+
<parent>wcf.acp.menu.link.person.list</parent>
15+
<permissions>admin.content.canManagePeople</permissions>
16+
<icon>fa-plus</icon>
17+
</acpmenuitem>
18+
</import>
19+
</data>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{include file='header' pageTitle='wcf.acp.person.'|concat:$action}
2+
3+
<header class="contentHeader">
4+
<div class="contentHeaderTitle">
5+
<h1 class="contentTitle">{lang}wcf.acp.person.{$action}{/lang}</h1>
6+
</div>
7+
8+
<nav class="contentHeaderNavigation">
9+
<ul>
10+
<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>
11+
12+
{event name='contentHeaderNavigation'}
13+
</ul>
14+
</nav>
15+
</header>
16+
17+
{@$form->getHtml()}
18+
19+
{include file='footer'}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
{include file='header' pageTitle='wcf.acp.person.list'}
2+
3+
<header class="contentHeader">
4+
<div class="contentHeaderTitle">
5+
<h1 class="contentTitle">{lang}wcf.acp.person.list{/lang}</h1>
6+
</div>
7+
8+
<nav class="contentHeaderNavigation">
9+
<ul>
10+
<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>
11+
12+
{event name='contentHeaderNavigation'}
13+
</ul>
14+
</nav>
15+
</header>
16+
17+
{hascontent}
18+
<div class="paginationTop">
19+
{content}{pages print=true assign=pagesLinks controller="PersonList" link="pageNo=%d&sortField=$sortField&sortOrder=$sortOrder"}{/content}
20+
</div>
21+
{/hascontent}
22+
23+
{if $objects|count}
24+
<div class="section tabularBox">
25+
<table class="table jsObjectActionContainer" data-object-action-class-name="wcf\data\person\PersonAction">
26+
<thead>
27+
<tr>
28+
<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>
29+
<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>
30+
<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>
31+
32+
{event name='columnHeads'}
33+
</tr>
34+
</thead>
35+
36+
<tbody class="jsReloadPageWhenEmpty">
37+
{foreach from=$objects item=person}
38+
<tr class="jsObjectActionObject" data-object-id="{@$person->getObjectID()}">
39+
<td class="columnIcon">
40+
<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>
41+
{objectAction action="delete" objectTitle=$person->getTitle()}
42+
43+
{event name='rowButtons'}
44+
</td>
45+
<td class="columnID">{#$person->personID}</td>
46+
<td class="columnTitle columnFirstName"><a href="{link controller='PersonEdit' object=$person}{/link}">{$person->firstName}</a></td>
47+
<td class="columnTitle columnLastName"><a href="{link controller='PersonEdit' object=$person}{/link}">{$person->lastName}</a></td>
48+
49+
{event name='columns'}
50+
</tr>
51+
{/foreach}
52+
</tbody>
53+
</table>
54+
</div>
55+
56+
<footer class="contentFooter">
57+
{hascontent}
58+
<div class="paginationBottom">
59+
{content}{@$pagesLinks}{/content}
60+
</div>
61+
{/hascontent}
62+
63+
<nav class="contentFooterNavigation">
64+
<ul>
65+
<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>
66+
67+
{event name='contentFooterNavigation'}
68+
</ul>
69+
</nav>
70+
</footer>
71+
{else}
72+
<p class="info">{lang}wcf.global.noItems{/lang}</p>
73+
{/if}
74+
75+
{include file='footer'}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<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/5.4/eventListener.xsd">
3+
<import>
4+
<eventlistener name="rename@wcf\data\user\UserAction">
5+
<eventclassname>wcf\data\user\UserAction</eventclassname>
6+
<eventname>rename</eventname>
7+
<listenerclassname>wcf\system\event\listener\PersonUserActionRenameListener</listenerclassname>
8+
<environment>all</environment>
9+
</eventlistener>
10+
<eventlistener name="save@wcf\acp\form\UserMergeForm">
11+
<eventclassname>wcf\acp\form\UserMergeForm</eventclassname>
12+
<eventname>save</eventname>
13+
<listenerclassname>wcf\system\event\listener\PersonUserMergeListener</listenerclassname>
14+
<environment>admin</environment>
15+
</eventlistener>
16+
<eventlistener name="execute@wcf\system\cronjob\PruneIpAddressesCronjob">
17+
<eventclassname>wcf\system\cronjob\PruneIpAddressesCronjob</eventclassname>
18+
<eventname>execute</eventname>
19+
<listenerclassname>wcf\system\event\listener\PersonPruneIpAddressesCronjobListener</listenerclassname>
20+
<environment>all</environment>
21+
</eventlistener>
22+
<eventlistener name="export@wcf\acp\action\UserExportGdprAction">
23+
<eventclassname>wcf\acp\action\UserExportGdprAction</eventclassname>
24+
<eventname>export</eventname>
25+
<listenerclassname>wcf\system\event\listener\PersonUserExportGdprListener</listenerclassname>
26+
<environment>admin</environment>
27+
</eventlistener>
28+
<eventlistener name="execute@wcf\system\worker\UserActivityPointItemsRebuildDataWorker">
29+
<eventclassname>wcf\system\worker\UserActivityPointItemsRebuildDataWorker</eventclassname>
30+
<eventname>execute</eventname>
31+
<listenerclassname>wcf\system\event\listener\PersonUserActivityPointItemsRebuildDataWorkerListener</listenerclassname>
32+
<environment>admin</environment>
33+
</eventlistener>
34+
</import>
35+
</data>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
use wcf\system\database\table\column\DefaultTrueBooleanDatabaseTableColumn;
4+
use wcf\system\database\table\column\IntDatabaseTableColumn;
5+
use wcf\system\database\table\column\NotNullInt10DatabaseTableColumn;
6+
use wcf\system\database\table\column\NotNullVarchar255DatabaseTableColumn;
7+
use wcf\system\database\table\column\ObjectIdDatabaseTableColumn;
8+
use wcf\system\database\table\column\SmallintDatabaseTableColumn;
9+
use wcf\system\database\table\column\TextDatabaseTableColumn;
10+
use wcf\system\database\table\column\VarcharDatabaseTableColumn;
11+
use wcf\system\database\table\DatabaseTable;
12+
use wcf\system\database\table\index\DatabaseTableForeignKey;
13+
14+
return [
15+
DatabaseTable::create('wcf1_person')
16+
->columns([
17+
ObjectIdDatabaseTableColumn::create('personID'),
18+
NotNullVarchar255DatabaseTableColumn::create('firstName'),
19+
NotNullVarchar255DatabaseTableColumn::create('lastName'),
20+
NotNullInt10DatabaseTableColumn::create('informationCount')
21+
->defaultValue(0),
22+
SmallintDatabaseTableColumn::create('comments')
23+
->length(5)
24+
->notNull()
25+
->defaultValue(0),
26+
DefaultTrueBooleanDatabaseTableColumn::create('enableComments'),
27+
]),
28+
29+
DatabaseTable::create('wcf1_person_information')
30+
->columns([
31+
ObjectIdDatabaseTableColumn::create('informationID'),
32+
NotNullInt10DatabaseTableColumn::create('personID'),
33+
TextDatabaseTableColumn::create('information'),
34+
IntDatabaseTableColumn::create('userID')
35+
->length(10),
36+
NotNullVarchar255DatabaseTableColumn::create('username'),
37+
VarcharDatabaseTableColumn::create('ipAddress')
38+
->length(39)
39+
->notNull(true)
40+
->defaultValue(''),
41+
NotNullInt10DatabaseTableColumn::create('time'),
42+
])
43+
->foreignKeys([
44+
DatabaseTableForeignKey::create()
45+
->columns(['personID'])
46+
->referencedTable('wcf1_person')
47+
->referencedColumns(['personID'])
48+
->onDelete('CASCADE'),
49+
DatabaseTableForeignKey::create()
50+
->columns(['userID'])
51+
->referencedTable('wcf1_user')
52+
->referencedColumns(['userID'])
53+
->onDelete('SET NULL'),
54+
]),
55+
];

0 commit comments

Comments
 (0)