This document shows how to add UI skins to the membership lite UI in Grouper 1.6+

Summary

You can make skins on the server side, or users can use attributes and remote config files to manage their own skins (if enabled).  The text, CSS, and (certain) settings can be affected

There are three places where configs and text can be configured: remote URLs, locally, or the default for a membership lite ui.  The resolver will check for each in that order so you can override whichever properties and text you want without having to override all.  Note that this is different than the normal admin UI text so tooltips will not work the same way.

Server-side skinning

If you want to maintain the control of the skins centrally by the grouper admin, the config files can be placed in the UI webapp.  Each group could have more than one skin, based on URL and membershipLiteName.

So the URL to edit a group (in this case by name) used to be:

http://localhost:8090/grouper/grouperUi/appHtml/grouper.html?operation=SimpleMembershipUpdate.init&groupName=aStem:aGroup

But now you add membershipLiteName with a name to the end.  The name must be alphanumeric or underscore

http://localhost:8090/grouper/grouperUi/appHtml/grouper.html?operation=SimpleMembershipUpdate.init&groupName=aStem:aGroup&membershipLiteName=grouperMembershipExample

That name corresponds to a config file.  This is be in one of two places: on the classpath or on the file system

webapp/WEB-INF/classes/membershipLiteName/grouperMembershipExample.properties

of configure this setting in media.properties: simpleMembershipUpdate.confDir to a directory, and put the file in that directory.  e.g.

simpleMembershipUpdate.confDir = c:/temp/membershipLiteName

File is: c:/temp/membershipLiteName/grouperMembershipExample.properties

Attribute-based skinning

If you want distributed control of skinning, then Grouper users can maintain their skins without requiring help from the central Grouper administrator.  Group group type is grouperGroupMembershipSettings and the attribute is grouperGroupMshipSettingsUrl.

You can have grouper auto create this attribute, set this to true in the grouper.properties and restart grouper:

 membershipUpdateLiteTypeAutoCreate=true

Or, here is the GSH to create:

typeAdd("grouperGroupMembershipSettings");
typeAddAttr("grouperGroupMembershipSettings", "grouperGroupMshipSettingsUrl", AccessPrivilege.ADMIN, AccessPrivilege.ADMIN, false);

By default, only wheel users can edit that attribute based on this in the grouper.properties:

 security.types.grouperGroupMembershipSettings.wheelOnly = true

You can change that so that only a certain group is allowed to edit it, or everyone.

The value of the attribute on a group will be a URL which is a properties file that will be retrieved remotely.  This should be stored on a secure server so it isnt tampered with.  e.g. https://school.edu/dept/whatever/someFile.properties.  If you like, you can restrict the IP address of requesters for the properties file and text file to the Grouper UI server(s).  External CSS cannot be protected this way as the request comes from the user's browser.

Membership lite settings

Here is an example config file with the available settings:

## can be subjectId, sourceId, name, description, screenLabel, memberId, or attribute name which is single valued
## comma separated.  will be sorted by sourceId, then the sort field (recommended to be screenName)
#simpleMembershipUpdate.exportAllSubjectFields=sourceId, screenLabel, entityId, name, description
#simpleMembershipUpdate.exportAllSortField=screenLabel

## if by default the screen will delete multiple
## (screen is easier to use but takes longer potentially and defaults to false)
#simpleMembershipUpdate.defaultDeleteMultiple=false

## if by default the screen will show the import by file instead of direct textarea input
## this defaults to true which helps with large imports, there might be memory problem with textarea import
#simpleMembershipUpdate.defaultImportFile=true

#add an extra css to the simple membership update.  use relative or absolute urls, comma separated
#e.g. http://localhost:8090/grouper/membershipLiteExample.css
#note, this is loaded from browser, so if the site is SSL and CSS is not, browser might give a warning to user
#simpleMembershipUpdate.extraCss =

#properties file via url to look for text first (before nav.properties)
#simpleMembershipUpdate.textFromUrl = http://localhost:8090/grouper/membershipLiteTextExample.properties

External text

If the simpleMembershipUpdate.textFromUrl setting is set (in any context), it will use that text (in addition to defaults if applicable) for text on screen.  Note that not *all* text is available, only text in the membership lite module.  E.g. some central text  is not available e.g. text on the paging control.

There is a processor for this text to make it web safe.  This is configured in the media.properties:

#if allowing external text by url, this is the filter to strip invalid html tags.  blank for none
simpleMembershipUpdate.externalUrlTextProperties.grouperHtmlFilter = edu.internet2.middleware.grouper.util.GrouperHtmlFilter

If that is blank, it will not process the text.  You can configure your own to the interface: edu.internet2.middleware.grouper.util.HtmlFilter.  The default will only allow simple HTML tags (with no attributes): b, br, p, i, em, strong, blockquote, ul, li, ol, div, tt, hr, s, span

Here is the available text (uncomment to enable):

#simpleMembershipUpdate.updateTitle=Group membership update lite

#simpleMembershipUpdate.groups.summary.display-extension=Name
#simpleMembershipUpdate.groups.summary.display-name=Path
#simpleMembershipUpdate.groups.summary.description=Description
#simpleMembershipUpdate.groups.summary.extension= ID
#simpleMembershipUpdate.groups.summary.name= ID path
#simpleMembershipUpdate.field.displayName.alternateName=Alternate ID path
#simpleMembershipUpdate.groups.summary.id=UUID

#simpleMembershipUpdate.tooltipTargetted.field.displayName.displayExtension=Name is the label that identifies this group, and might change.
#simpleMembershipUpdate.tooltipTargetted.field.displayName.displayName=Path consists of the name of each enclosing folder plus the group name, separated by colons.
#simpleMembershipUpdate.tooltipTargetted.field.displayName.description=Description contains notes about the group, which could include: <br />what the group represents, why it was created, etc.
#simpleMembershipUpdate.tooltipTargetted.field.displayName.extension=ID is the unique identifier chosen by the group creator for this group.  <br /><br />The ID is unique within this folder, and should rarely change.  It can be used by other systems to refer to this group.
#simpleMembershipUpdate.tooltipTargetted.field.displayName.name=ID Path consists of the unique ID of each enclosing folder plus the group ID, separated by colons.  <br /><br />The ID Path is unique for each group and should rarely change.  It can be used by other systems to refer to this group.
#simpleMembershipUpdate.tooltipTargetted.groups.summary.id=UUID stands for Universal Unique Identifier, a generated key that is distinct from any other UUID in this or any other system.  <br /><br />The UUID does not change, and can be used as an identifier in other systems.
#simpleMembershipUpdate.tooltipTargetted.field.displayName.alternateName=Alternate ID Path allows groups to be searchable using an alternate name.  The format is the same as the format of ID Path.<br /><br />This is especially useful when moving a group, which will add the old ID Path of the group as the Alternate ID Path by default.  Like the ID Path, the Alternate ID Path can be used by other systems to refer to this group.


#infodot.title.simpleMembershipUpdate.updateTitle=This page lets you view and update the memberships of a group.<br /><br />It is meant for only simple operations.

#simpleMembershipUpdate.find.browse.here = Current location is:

#simpleMembershipUpdate.viewInAdminUi=Admin UI

#tooltipTargetted.simpleMembershipUpdate.viewInAdminUi=Switch to the Admin UI for a more complete set of features

#simpleMembershipUpdate.groupSubtitle=Group
#simpleMembershipUpdate.changeLocation=Find a group
#simpleMembershipUpdate.addMemberSubtitle=Add member
#simpleMembershipUpdate.addMemberButton=Add member
#simpleMembershipUpdate.membershipListSubtitle=Membership list
#simpleMembershipUpdate.pagingLabelPrefix=Showing group members:
#simpleMembershipUpdate.noMembersFound=There are no members in this group
#simpleMembershipUpdate.deleteConfirm =Are you sure you want to delete this membership?
#simpleMembershipUpdate.pagingResultPrefix =Result page:
#simpleMembershipUpdate.errorNotEnoughSubjectChars=Enter 2 or more characters
#simpleMembershipUpdate.errorNotEnoughFilterChars=Enter 3 or more characters
#simpleMembershipUpdate.errorNotEnoughFilterCharsAlert=Enter 3 or more characters in the filter field
#simpleMembershipUpdate.successMemberDeleted=Success: the member was deleted: {0}
#simpleMembershipUpdate.errorUserSearchNothingEntered=Enter search criteria into the auto-complete box for an entity to add to the group
#simpleMembershipUpdate.warningSubjectAlreadyMember=Entity already a member: {0}
#simpleMembershipUpdate.successMemberAdded=Success: member added: {0}
#simpleMembershipUpdate.errorSubjectNotFound=Entity not found: {0}
#simpleMembershipUpdate.errorSubjectNotUnique=Entity not unique: {0}
#simpleMembershipUpdate.errorSourceUnavailable=Source unavailable
#simpleMembershipUpdate.errorUserSearchTooManyResults=Too many results, narrow your search
#simpleMembershipUpdate.errorUserSearchNoResults=No results found, change your search criteria
#simpleMembershipUpdate.deleteImageAlt=Delete
#simpleMembershipUpdate.errorTooManyBrowsers=The group you are editing is out of sync in the user interface.  Perhaps you have two browser tabs open editing different groups.  This group will be refreshed, please start over.

#simpleMembershipUpdate.advancedMenuDeleteMultiple=Delete multiple
#simpleMembershipUpdate.advancedMenuDeleteMultipleTooltip=Selecting this option will show checkboxes next to members and a delete button at the bottom
#simpleMembershipUpdate.advancedMenuShowGroupDetails=Show group details
#simpleMembershipUpdate.advancedMenuShowGroupDetailsTooltip=Selecting this option will show more information about the group such as the id.
#simpleMembershipUpdate.advancedMenuShowMemberFilter=Search for member
#simpleMembershipUpdate.advancedMenuShowMemberFilterTooltip=Selecting this option will show a search box above the membership list where you can search for members in this group
#simpleMembershipUpdate.advancedMenuImportExport=Import / export
#simpleMembershipUpdate.advancedMenuImportExportTooltip=Import members from a comma separated values file (CSV) or export to a CSV file
#simpleMembershipUpdate.advancedMenuExport=Export
#simpleMembershipUpdate.advancedMenuExportTooltip=Export members to a comma separated values file (CSV) which can be opened in a spreadsheet program and manipulated
#simpleMembershipUpdate.advancedMenuExportSubjectIds=Export entity IDs
#simpleMembershipUpdate.advancedMenuExportSubjectIdsTooltip=Export entity IDs and source IDs to a comma separated values file (CSV) which can be opened in a spreadsheet program and manipulated
#simpleMembershipUpdate.advancedMenuExportAll=Export all member data
#simpleMembershipUpdate.advancedMenuExportAllTooltip=Export all member data (configured on server which fields), e.g. subject ID, login ID, name, etc to a comma separated values file (CSV) which can be opened in a spreadsheet program and manipulated
#simpleMembershipUpdate.advancedMenuImport=Import
#simpleMembershipUpdate.advancedMenuImportTooltip=Import from a comma separated values (CSV) file

#simpleMembershipUpdate.deleteMultipleButton=Remove selected members
#simpleMembershipUpdate.deleteMultipleTooltip=Select multiple members by checking checkboxes next to the members.  Click this button to remove them from the group.
#simpleMembershipUpdate.deleteAllButton=Remove all members
#simpleMembershipUpdate.deleteAllTooltip=Remove all members in the group, including members not shown on the screen if there are more than the page size.
#simpleMembershipUpdate.errorDeleteCheckboxRequired=Error: select one or more members to remove from the group
#simpleMembershipUpdate.successMembersDeleted=Success: {0} members were deleted
#simpleMembershipUpdate.successAllMembersDeleted=Success: all {0} members were deleted
#simpleMembershipUpdate.advancedButton=Advanced features

#simpleMembershipUpdate.addMemberCombohint=Enter search text to find a member to add
#simpleMembershipUpdate.filterMemberCombohint=Enter search text to find a member in the list

#simpleMembershipUpdate.downloadSubjectIdsLabel=Click the link to download the subjectIds of the members:
#simpleMembershipUpdate.downloadAllLabel=Click the link to download the members information:
#simpleMembershipUpdate.importLabel=Select a comma separated values (CSV) file (e.g. from a spreadsheet program like Excel) which has a column for sourceId, and any of: subjectId, subjectIdentifier, subjectIdOrIdentifier.<br /><br />Alternately, you can upload a text file of subjectIdOrIdentifiers, each on a new line, with the first line labeled as: subjectIdOrIdentifer

#simpleMembershipUpdate.importSubtitle=Import members
#infodot.subtitle.simpleMembershipUpdate.importSubtitle=Upload members from a file.  You check replace existing members, or just add new members.<br /><br />The file must be a comma separated values (CSV) file, e.g. from a spreadsheet program like Microsoft Excel.  There should be a column sourceId, and a column of any of the following: entityId, entityIdentifier, entityIdOrIdentifier.  If there is no sourceId, then all sources will be found (this is less efficient), and if two sources have the same entityId, there will be an error.  Each member is on its own line.  The sourceId is one of a few entity source IDs configured in the Grouper server.<br /><br />The entityId is typically a person ID, the entityIdentifer could be a login ID, and the entityIdOrIdentifier will look by ID, and if not found, look by identifier.<br /><br />Note if you omit the sourceId column, it will be less efficient, but it will still work if you dont have two sources with the same entityId.  This does not need to be CSV, it could be a text file.  e.g. you could upload a text file of entityIdOrIdentifiers, each on a new line, with the first line labeled as: entityIdOrIdentifier<br /><br />Example CSV file 1: <br /><br /><tt>sourceId,entityId<br />someSourceId,12345<br />anotherSourceId,23456</tt><br /><br /><br />Example 2:<br /><br /><tt>sourceId,entityIdentifier<br />someSourceId,sjohnson<br />anotherSourceId,jsmith</tt><br /><br /><br />Example 3:<br /><br /><tt>sourceId,entityIdOrIdentifier<br />someSourceId,45678<br />anotherSourceId,jsmith</tt><br /><br /><br />Example 4:<br /><br /><tt>sourceId,entityId,entityIdentifier<br />someSourceId,45678<br />anotherSourceId,,jsmith</tt><br /><br /><br />Example 5:<br /><br /><tt>entityId<br />56789<br />67890</tt><br /><br /><br />Example 6:<br /><br /><tt>entityIdentifier<br />sjohnson<br />jsmith</tt><br /><br /><br />Example 7:<br /><br /><tt>entityIdOrIdentifier<br />76543<br />jsmith</tt><br /><br /><br />

#simpleMembershipUpdate.importAvailableSourceIds=Available sourceIds:
#simpleMembershipUpdate.importReplaceExistingMembers=Replace existing members:
#simpleMembershipUpdate.importCommaSeparatedValuesFile=Entities file (CSV or list of id's each on new line):
#simpleMembershipUpdate.importCancelButton=Cancel
#simpleMembershipUpdate.importButton=Submit
#simpleMembershipUpdate.importErrorNoWrongFile=Please submit a non-empty CSV file, the file name must end with .csv, or .txt :
#simpleMembershipUpdate.importErrorBlankTextarea=Please enter in the CSV or entity id list in the textarea
#simpleMembershipUpdate.importErrorNoId=Cant find any identifer, need to pass in a entityId or entityIdentifier or entityIdOrIdentifier
#simpleMembershipUpdate.importErrorNoIdCol=Cant find any identifer column header on first line, need to pass in entityId or entityIdentifier or entityIdOrIdentifier
#simpleMembershipUpdate.importErrorSubjectProblems=Error: did not remove any members since there were entity problems in import file
#simpleMembershipUpdate.importSuccessSummary=Success: import completed
#simpleMembershipUpdate.importErrorSummary=Error: import was not successful, see {0} errors below
#simpleMembershipUpdate.importSizeSummary=The immediate membership size was {0} and is now {1}
#simpleMembershipUpdate.importAddsDeletesSummary=There were {0} successful adds, and {1} successful deletes
#simpleMembershipUpdate.importSubjectErrorsLabel=Entity errors
#simpleMembershipUpdate.importAddErrorsLabel=Errors adding members
#simpleMembershipUpdate.importRemoveErrorsLabel=Errors deleting members
#simpleMembershipUpdate.importDirectInput=Entities (CSV or list of id's each on new line)

#simpleMembershipUpdate.membershipLiteImportFileButton=Input members directly on screen
#simpleMembershipUpdate.membershipLiteImportTextfieldButton=Input members from file

#simpleMembershipUpdate.memberMenuDetailsLabel=Member details
#simpleMembershipUpdate.memberMenuDetailsTooltip=View all attributes about this member
#simpleMembershipUpdate.memberMenuEnabledDisabled=Edit start and end dates
#simpleMembershipUpdate.memberMenuEnabledDisabledTooltip=Edit the dates this membership will start or end

#simpleMembershipUpdate.memberMenuAlt=Member menu

#simpleMembershipUpdate.memberDetailsSubtitle=Member details

#simpleMembershipUpdate.enabledDisableSubtitle=Edit start and end dates
#simpleMembershipUpdate.enabledDisableGroupPath=Group path:
#simpleMembershipUpdate.enabledDisableEntity=Entity:
#simpleMembershipUpdate.enabledDisableEntityId=Entity id:
#simpleMembershipUpdate.enabledDisableEntitySource=Entity source:
#simpleMembershipUpdate.enabledDisableStartDate=Start membership on date:
#simpleMembershipUpdate.enabledDisableEndDate=End membership on date:
#simpleMembershipUpdate.enabledDisableOkButton=Submit
#simpleMembershipUpdate.enabledDisableCancelButton=Cancel
#simpleMembershipUpdate.enabledDisableDateMask=(yyyy/mm/dd)
#simpleMembershipUpdate.enabledDisabledSuccess=Success: member was saved

#simpleMembershipUpdate.filterMemberButton=Search for member
#simpleMembershipUpdate.filterLabel=Searching for member:
#simpleMembershipUpdate.clearFilterButton=Clear member search
#simpleMembershipUpdate.errorMemberFilterTooManyResults=Too many results, narrow your search

#simpleMembershipUpdate.disabledPrefix=until



Internal skinned text

You can have text in the nav.properties be specific to a named membership lite updater.  Just do it like this, if the property is: simpleMembershipUpdate.updateTitle, and the name of the skin is: grouperMembershipExample:

membershipLiteName.grouperMembershipExample.simpleMembershipUpdate.updateTitle=Group membership update lite3

You can customize the same text that external can, see list above

Additional CSS

You can append additional CSS to the existing CSS.  You can append multiple CSS (comma separated).  These CSS requests come from the user's browser.  This is configured in media.properties, or a skin file, or a remote skin file

#add an extra css to the simple membership update.  use relative or absolute urls, comma separated
#e.g. http://localhost:8090/grouper/membershipLiteExample.css
#note, this is loaded from browser, so if the site is SSL and CSS is not, browser might give a warning to user
simpleMembershipUpdate.extraCss = http://localhost:8090/grouper/membershipLiteExample.css

Here is an example css file:

.actionheaderContainer td {
  background-color:#f4EFde;
}