Explanation of how to build a application
Here I've tried to explain how to build a application in XWiki.
It's goal is to explain how to work with data in Objects from Classes.
In depth study of the XWiki User application
In this page I would like to explain how to make a XWiki application that will have a database for fields, forms to maintain data and how to create a new record for that.
XWiki applications are Single-class oriented, but it is possible to create Parent-Class with multiple child-classes. That aspect will be out of scope for this page!
We will focus on single-class applications.
For this case I will take the current project focus; User profiles.
Which classes are involved in our project User-profile?
To find them navigate to XWikiClasses.
In the search box (Location) type USER.
the result will be two classes:
- Users
- XWikiUsers -> is one of the classes involved
Then type PROFILE in search box.
- UserProfileSectionClass -> somewhat related, but out of scope for this page
- waihonaPediaUserProfileClass -> is the second class involved
- CdLS_ATD_UserProfileClass -> Depricated, is the old class that at the end of the project must be migrated to the WaihonaCode version. Note that properties are almost similar
- UserProfileSectionsClass -> somewhat related, but out of scope for this page
then type PERSON in search
- personToSyndromePersonRelationClass -> is the fourth class involved
- personWithSyndromeClass -> is the third class involved
- personHealthAttentionClass
We have identified the 4 classes involved.
A quick defenition of these classes.
- XWikiUsers Is default XWiki, can be seen as USERACCOUNT. Is automatically created when a user registered. Note that all properties are visible on the internet (public) as a object of this class is required to login. (properties are somewhat secured from public view)
- waihonaPediaUserProfileClass only for approved user, holds the more privat data for the user account, specific for WaihonaPedia
- personWithSyndromeClass If the user is related to a person with a syndrome/decease, a object of this holds the basic data for such a person
- personToSyndromePersonRelationClass is a connecting class to connect a user with a person with a syndrome. It also specifies the role of the connection: Father, Doctor,etcetera.
A bit of information of how a XWiki application is structured:
- A class, represented by an object for each user
- A template (you can have 1 and more templates)
- 1 or more Sheets
All the 3 above elements are basicly normal XWiki pages, each having a way to edit them
When you look at one of the classes of the previous section you experience 2 of these in action.
SHEETS
The class is somewhat hidden in a page; you can normally only see it when you click EDIT-Class-editor. But when you navigate and just want to view the class without editing you would normally not see anything. This is where a 'Sheet' kicks in. A sheet can take objects and 'display' them to the viewer. The page I reviewed (ProfileSheet are in fact a SHEET. Thus a sheet contains CODE (Velocity or Groovy and HTML. They depend on CSS for styling.
So the relationship between a CLASS and a SHEET is to VIEW objects of that Class.
A default sheet will normally support two MODES; VIEW and EDIT. The one reviewed supports (untill now) only VIEW. When you EDIT a CLASS XWiki also uses another sheet as compared to VIEWING a class: for more complex applications this is quite normal to have different sheets for viewing and editing.
As a example: the out-of-the-box XWiki User profile uses 6 SHEETS of which 5 support both MODES: VIEW and EDIT.
TEMPLATE
A way to VIEW and EDIT objects is nice, but we miss one important aspect! CREATE!
There is not much value if we build a nice Class and some SHEETS but depend on the object EDITOR to ask a novice user to create his own USER-PROFILE.
This is what TEMPLATES do.
It is basicly a page with a object of the Class that you can visit and modify with your Sheets. In fact all pages having a object of that class could be used as a TEMPLATE, but it would be strange to see your new page already populated with data of another record!
So we keep the TEMPLATE's object empty of data, except if we want to set some default data!
So the lifecycle of a record could be described as following;
- create the record by making a copy of the TEMPLATE
- navigate to the new page and EDIT with a SHEET (note that the edit button has the mode FORM (which basicly tells the wiki, USE A SHEET IF YOU CAN FIND ONE)
- enter your data
- SAVE and VIEW the data with a SHEET (can be the same sheet as when EDITING)
- If other users go to the Page make sure a SHEET is automatically loaded
Automatic SHEET loading
When we do nothing the XWiki server has no knowledge of which SHEETS, TEMPLATES and CLASSES belong together as a application! Sometimes this is GOOD as it increases security and maybe if we want to create a screen for Admin's only that is what we want.
In such cases we need to 'run-time' make a application by using request.parameters
Request Sheets
To apply a sheet on a document upon request, you can use the URL sheet query string parameter as follows:
/xwiki/bin/view/Main/UserPage?sheet=XWiki.XWikiUserSheet. The sheet specified in the request is applied only to the requested document and it doesn't work recursively.
We could create HTML buttons that link to such a URL.
The following URL would edit the page Main.UserPage using the sheet XWikiUserSheet
/xwiki/bin/edit/Main/UserPage?sheet=XWiki.XWikiUserSheet
But most of the time we want a more convenient approach because the URL parameters can be easily typed wrongly.
To see what sheets are linked to a class use this piece of code:
{{velocity}}
#set($className = 'WaihonaCode.waihonaPediaUserProfileClass')
The list of class sheets bound to $className:
#set($classDocument = $xwiki.getDocument($className))
#foreach($sheetReference in $services.sheet.getClassSheets($classDocument))
* $services.model.serialize($sheetReference)
#end
{{/velocity}}
To see the sheets that will be replaced by this project testSheetService
How is this done?;
- On the class page EDIT the page using the OBJECT editor
- Notice a object of class: XWiki.DocumentSheetBinding
- In that object the sheet is specified, if multiple sheets notice each is represented by a object (see as example: personToSyndromePersonRelationClass
- On each sheet page we could add a object for which action (VIEW or EDIT or other action like PRINT) the sheet is intended by a object XWiki.SheetDescriptorClass. This step is optional and only when needed.
How to create a new record (
page with object) using a Template==
create a standard wiki document for your template
see the following HTML to allow the creation of a document from your template:
NOTE: THE CODE IS FOR XWIKI OLDER VERSION, the line with 'webname' needs to be discussed, experimented on
<form action="" id="newdoc">
<input type="hidden" name="parent" value="${doc.fullName}" />
<input type="hidden" name="template" value="XWiki.YourTemplateName" />
<input type="hidden" name="webname" value="Main" size="8" />
Title: <input type="text" name="name" value="Name of your document" />
<br />
<br />
<input type="button" value="Create this document" onclick='if (updateName(this.form.name)) {this.form.action="../../inline/" + this.form.webname.value + "/" + this.form.name.value; this.form.submit(); }'>
</form>
The project context
The 'user profile' project is replacing 4 sheets (+5 sheet module pages). With replacing we normally do not change the original sheets, but making a copy and change it to our new Design.
It is good to discuss the BEST Sheet that we want to replace as it is build by XWiki and thus holds all the best-practises we could need.
XWiki.XWikiUserSheet
When we look at the code with our editor (Best practise is not to EDIT but use the vertical ... button and 'view source')
Styling and Javascript
## CSS & Javascripts
#########################
#set($discard = $xwiki.ssx.use("XWiki.XWikiUserSheet"))
#set($discard = $xwiki.jsx.use("XWiki.XWikiUserSheet"))
Notice that the page is using specific styling and javascript. I recommend you inspect the CSS and Javascript (as i'm no good at explaining that part anyway ;-)
Categories (or TABS)
Next the sheet defines some categories. In this use-case categories are the TABS on the screen which in XWiki default are below the user Avatar. Notice that this could also be done by using a UIExtension list. The advantage of this way is that the categories are defined in the code and easy to find. A disadvantage of this way is that it is hardly re-use. But the argument here is that the TABS are very specific to this application
7: #########################
8: ## Setting categories
9: #########################
10: #set($categories = [])
11: #set($discard = $categories.add({'id':'profile', 'sheet':'XWiki.XWikiUserProfileSheet', 'glyphicon': 'user'}))
12: #set($isMyProfile = ($services.model.resolveDocument($xcontext.user) == $doc.documentReference))
13: #if($isMyProfile || $hasAdmin)
14: #set($discard = $categories.add({'id':'preferences', 'sheet':'XWiki.XWikiUserPreferencesSheet', 'glyphicon': 'wrench'}))
15: #end
16: ## TODO: add APIs to be able to display users watchlists to admins
17: #if($isMyProfile && $hasWatch)
18: #set($discard = $categories.add({'id':'watchlist', 'sheet':'XWiki.XWikiUserWatchListSheet', 'glyphicon': 'eye-open'}))
19: #end
20: #if($isMyProfile)
21: #set($discard = $categories.add({'id':'network', 'sheet':'XWiki.XWikiUserNetworkSheet', 'glyphicon': 'globe'}))
22: #end
23: #set($userWikiSheet = 'WikiManager.UserWikiSheet')
24: #if($xcontext.isMainWiki() && $xwiki.exists($userWikiSheet))
25: #set($discard = $categories.add({
26: 'id': 'wikis',
27: 'name': $services.localization.render('platform.wiki.menu.userwikis'),
28: 'sheet': $userWikiSheet,
29: 'glyphicon': 'list'
30: }))
31: #end
32: #if($isMyProfile && $hasDashboard)
33: #set($discard = $categories.add({'id':'dashboard', 'sheet':'Dashboard.XWikiUserDashboardSheet', 'glyphicon': 'th'}))
34: #end
10: this creates a empty array for categories.
11, 14, 18, 21, 25 and 32 ADD elements to the category array all having a ID:
- profile -> 'glyphicon': 'user'
- preferences -> 'glyphicon': 'wrench'
- watchlist -> 'glyphicon': 'eye-open'
- network -> 'glyphicon': 'globe'
- wikis -> 'glyphicon': 'list'
- dashboard -> 'glyphicon': 'th'
Notice that they use a '$services.localization.render('platform.wiki.menu.userwikis') to get a translated label: NICE...
Notice the Sheet part, The code of this page is relatively short as they use SHEET-PARTS organized in different pages. That will be used to render the Right side of the screen for the respective category...
- XWikiUserProfileSheet
- XWikiUserPreferencesSheet
- XWikiUserWatchListSheet
- XWikiUserNetworkSheet
- UserWikiSheet
- XWikiUserDashboardSheet
In the respective #if clauses we learn nice inspections
12: This line is setting the $isMyProfile variable that will tell us:
- Is the viewing user ($xcontext.user) the user owning this page or is the page viewed by another user.
Note that the page-name ($doc.documentReference) is tightly linked to the account; e.g. my account name is XWiki.GerritjanKoekkoek and my data is in the page XWiki.GerritjanKoekkoek. Ergo: I should be made difficult/impossible to change the page-name or move the page. This is not the case in the normal XWiki software.
13: #if($isMyProfile || $hasAdmin)
Use the variable in a OR clause
$hasAdmin is a obvious system variable
Notice that we have a custom variable implemented $isApproved (only in the custom skin)
24: igore this as we have a single WIKI setup
Meaning our solution should drop this functionality
32: igore this as we have a very different DASHBOARD implementation
Meaning our solution should drop this functionality, and replace it with linking to our WaihonaPedia Dashboard.
Current category or TAB
#########################
36: ## Current category
37: #########################
38: #set($currentCategory = "$!request.category")
39: #if($currentCategory == "")
40: #set($currentCategory = $categories[0].get('id'))
41: #end
38: A request variable is used: This comes from the url after the '?' symbol in the url. Al request variables are seperated by '&'
40: the velocity way of reading the array CATEGORIES.
so url http://some url?category=preferences would get that line from the Array
Creating a vertical Menu
#########################
43: ## Creating vertical menu
44: #########################
45: #set($userMenu = [{
46: " classname="XWiki.XWikiUsers" object="$obj.number" property="avatar" savemode="direct" defaultValue="XWiki.XWikiUserSheet@noavatar.png" width="180" alternateText="$xwiki.getUserName($doc.fullName, false)" buttontext="$services.localization.render('platform.core.profile.changePhoto')" displayImage="true" filter="png,jpg,gif"/}}
78: #end
79:
62:
..:
the line closing the 'div' is not in our code above, but will be in our next code block
This is WIKI syntax (not to be confused by Velocity)!!!
When the Wiki syntax is transformed to HTML it will result in
<div id="user-menu-col">
...
</div>
Note that the programmer in this sheet does not use the HTML macro and only use WIKI syntax combined with velocity. I think that in our project we could add the HTML macro
67, 68 and 79 will create a div for the avatar
69 if a request parameter xpage == 'edituser'
The first part seems to relate to the state when the UI of the Avatar selection is open. for now we only explain ELSE part.
77: the {{attachmentSelector}} macro is used
read full documentation for this macro here
{{attachmentSelector
classname="XWiki.XWikiUsers"
object="$obj.number"
property="avatar"
#if ($isMyProfile) savemode="direct" #end
defaultValue="XWiki.XWikiUserSheet@noavatar.png"
width="180"
alternateText="$xwiki.getUserName($doc.fullName, false)"
buttontext="$services.localization.render('platform.core.profile.changePhoto')"
displayImage="true"
filter="png,jpg,gif"/}}
As you can see the macro is a nice starting point for our drag-drop replacement variant.
The code for this macro is here use the EDIT?editor=object
Display the left menu continued
81: ## Menu
82: ##########
83: (% id="user-vertical-menu" %)
84: (((
85: #verticalNavigation($userMenu, {'translationPrefix' : 'platform.core.profile.category.', 'crtItemId' : $currentCategory, 'cssClass' : 'profile-menu'})
86: )))
87: )))
83,84 and 86 create a enclosing div
85: Typical XWiki way of surprising us with a velocity macro that is not easy to find
But i'm sure it will create a menu structure in HTML. Notice that it gets 2 parameters that both are of type Object (e.g. we have seen $userMenu was a object, but the second is enclosed by {}, the velocity notation for a object. Notice it looks very much like a JSON object which certainly is a best parctise in XWiki.
display the right side of the screen
89: ## Display the page content
90: #########################
91: (% id="user-page-content" %)
92: (((
93: #foreach($category in $userMenu)
94: #foreach($subcategory in $category.get('children'))
95: #set($tabKey = $subcategory.get('id'))
96: (% id="${tabKey}Pane" class="user-page-pane#if($tabKey != $currentCategory) hidden#end" %)
97: (((
98: #set($tabInclude = $subcategory.get('sheet'))
99: {{include reference="${tabInclude}" /}}
100: )))
101: #end
102: #end
103: )))
Note that the full page is generated and all categories are there.
95: Each selector needs a unique ID ($tabKey)
96: Div's with this id ${tabKey}Pane are generated.
notice the velocity inside (velocity can be chained making inline statements possible)
In the code below we 'undo' the chaining for better readability
hidden
#end"
it will result in the following two class specifications
- class="user-page-pane" -> for the selected category
- class="user-page-pane hidden"-> for all other categories
specific code for EDIT mode, both normal and INLINE
108: #if($xcontext.action == 'edit' || $xcontext.action == 'inline')
109: <input type='hidden' name='category' value="$!{escapetool.xml($currentCategory)}" />
110: #end
111: <div class="clearfloats"> </div>
112: #if($request.get('xpage'))
113: <script type="text/javascript">
114: document.fire('lightbox:userprofile:loaded');
115: </script>
116: #end
117: {{/html}}
107: If your HTML is wrapped inside other HTML it is good practise to use 'clean="false"'
108: This is how you can detect if the MODE is EDIT
109: A hidden input field is added, in this case the current category (profile, preferences, watchlist, network, wikis or dashboard)
112: Think this is related to the UI of the AVATAR selection (the macro)
appendix
Example of link behind the EDIT AVATAR button (the little pencil)
http://cdlsworld.devxwiki.com/xwiki/bin/view/XWiki/AttachmentSelector?
docname
XWiki.GerritjanKoekkoek
& classname=XWiki.XWikiUsers
& property=avatar
& object=0
& savemode=direct
& defaultValue=XWiki.XWikiUserSheet%40noavatar.png
& filter=png,jpg,gif
& displayImage=true
Example of the button-link that will save the photo already uploaded to the profile
http://cdlsworld.devxwiki.com/xwiki/bin/save/XWiki/GerritjanKoekkoek?
XWiki.XWikiUsers_0_avatar=Gerritjan.png
& form_token=iCCW4ymGixW6x2zkMQ7PtA