MediaWiki:Gadget-wrcAddNewIndividual.js

From Meta, a Wikimedia project coordination wiki

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/**
 * Gadget (based on JavaScript) to add new individual (ANI) to 
 * [[Module:Wikimedia Resource Center/Individuals]] on the Wikimedia 
 * Resource Center (WRC).
 */
( function () {
	'use strict';

	if ( mw.config.values.wgPageName.split('/')[0] == 'Connect' ) {
		mw.loader.using( [
			'ext.gadget.luaparse',
			'mediawiki.api',
			'oojs-ui',
			'oojs-ui-core',
			'oojs-ui.styles.icons-editing-core'
		] ).done( function () {
			var addSkillsToUserpage, cleanRawEntry, gadgetMsg,
				getContentIndividuals, getContentModule, getRelevantRawEntry,
				getUserpageData, getWikitextModule, openWindow, userLang;
	
			userLang = mw.config.get( 'wgUserLanguage' );
			/**
			 * Query the I18n Template for keywords to be used in this gadget.
			 * NOTE: The strings must be marked for translation before the keys/value 
			 * pair can works in the gadget.
			 */
			new mw.Api().get( {
				action: 'query',
				list: 'messagecollection',
				mcgroup: 'page-Template:I18n/Wikimedia_Resource_Center',
				mclanguage: userLang
			} ).done( function ( data ) {
				var i, res, key, val, gadgetMsg = {};
				res = data.query.messagecollection;
				for ( i = 0; i < res.length; i++ ) {
					key = res[ i ].key.replace( 'Template:I18n/Wikimedia_Resource_Center/', '' );
					val = res[ i ].translation;
					if ( !val ) {
						// No translation; fall back to English
						val = res[ i ].definition;
					}
					gadgetMsg[ key ] = val;
				}
				
				/**
				 * Provides API parameters for getting the content of the
				 * [[Module:Wikimedia Resource Center/Individuals]] page.
				 *
				 * @return {Object} Lua table
				 */
				getContentIndividuals = function () {
					return {
						action: 'query',
						prop: 'revisions',
						titles: 'Module:Wikimedia Resource Center/Individuals',
						rvprop: 'content',
						rvlimit: 1
					};
				};
				
				/**
				 * Provide parameters to get User page content of a specific 
				 * user in the form [[User:DAlangi_(WMF)]] for example and return.
				 * 
				 * @param {string} username Username of the user
				 * @return {Object} API response (wikitext)
				 */
				getUserpageData = function ( username ) {
					return {
						action: 'query',
						prop: 'revisions',
						titles: 'User:' + username,
						rvprop: 'content',
						rvlimit: 1
					};
				};
				
				/**
				 * Notate the Lua table from [[Module:Wikimedia Resource Center/Individuals]]
				 * page in the form of an Abstract Syntax Tree (AST) for fine manipulation.
				 *
				 * @param {Object} sourceblob The original API return
				 * @return {Object} raw Lua table on an AST
				 */
				getContentModule = function ( sourceblob ) {
					var i, raw, ast;
					for ( i in sourceblob ) {  // should only be one result
						raw = sourceblob[ i ].revisions[ 0 ][ '*' ];
						ast = luaparse.parse( raw );
						return ast.body[ 0 ].arguments[ 0 ].fields;
					}
				};
				
				/**
				 * Get an entire content (wikitext) of a given page
				 *
				 * @param {Object} sourceblob The original API return
				 * @return {Object} raw Entire page content (wikitext)
				 */
				getWikitextModule = function ( sourceblob ) {
					var i, raw;
					for ( i in sourceblob ) {  // should only be one result
						raw = sourceblob[ i ].revisions[ 0 ][ '*' ];
						return raw;
					}
				};
				
				/**
				  * Looks through the abstract syntax tree and returns a specific requested
				  * entry based on a particular name.
				  *
				  * @param {Object} entries The abstract syntax tree
				  * @param {string} name The entry we want to pick out
				  */
				getRelevantRawEntry = function ( entries, name ) {
					var i, j;
					// Look through the individual entries
					for ( i = 0; i < entries.length; i++ ) {
						// Loop through the individual key-value pairs within each entry
						for ( j = 0; j < entries[ i ].value.fields.length; j++ ) {
							if (
								entries[ i ].value.fields[ j ].key.name == 'name' &&
								entries[ i ].value.fields[ j ].value.value == name
							) {
								return entries[ i ].value.fields;
							}
						}
					}
				};
	
				/**
				  * Take a raw entry from the abstract syntax tree and make it an object
				  * that is easier to work with.
				  *
				  * @param {Object} relevantRawEntry the raw entry from the AST
				  * @return {Object} The cleaned up object
				  */
				cleanRawEntry = function ( relevantRawEntry ) {
					var entryData = {},
						i, j;
					for ( i = 0; i < relevantRawEntry.length; i++ ) {
						if ( relevantRawEntry[ i ].key.name == 'skills' ) {
							entryData.skills = [];
							for (
								j = 0;
								j < relevantRawEntry[ i ].value.fields.length;
								j++
							) {
								entryData.skills.push(
									relevantRawEntry[ i ].value.fields[ j ].value.value
								);
							}
						} else {
							entryData[ relevantRawEntry[ i ].key.name ] = relevantRawEntry[ i ].value.value;
						}
					}
					return entryData;
				};
				
				/**
				 * Add user's skills and experiences to their user page
				 * 
				 * @param {string} username The username of the user
				 * @param {array} skills An array of the skills to be added to the user page
				 */
				addSkillsToUserpage = function ( username, skills ) {
					var i, userPageContent, userPageContentWithCategories,
					categories = '', summaryMsg;
					
					// Check the length of the categories to use appropriate edit summary
					if ( skills.length > 1 ) {
						summaryMsg = gadgetMsg[ 'added-categories' ];
					} else {
						summaryMsg = gadgetMsg[ 'added-category' ];
					}
					
					new mw.Api().get( getUserpageData( username ) ).done( function ( data ) {
						categories += "\n<!--Connect Categories-->";
						categories += "\n<!--[DO NOT EDIT BELOW MANUALLY] Please edit on [[Connect]]-->";
						for ( i = 0; i < skills.length; i++ ) {
							categories += "\n[[Category:";
							categories += skills[ i ];
							categories += "]]\n";
						}
						categories += "<!--Edit via form on [[Connect]]-->";
						
						userPageContent = getWikitextModule( data.query.pages );
						// Truncate old categories if possible
						userPageContent = userPageContent.split("<!--Connect Categories-->")[0];
						// Add new categories to the user's page
						userPageContentWithCategories = userPageContent + categories;
						
						// Save the content with the new categories on the user's page
						// This will enable the user to feature on the corresponding categories
						new mw.Api().postWithToken(
							'csrf',
							{
								action: 'edit',
								nocreate: true,
								summary: summaryMsg + skills,
								title: gadgetMsg[ 'user-page' ] + username,
								text: userPageContentWithCategories,
								contentmodel: 'wikitext'
							}
						).done( function () {
							dialog.close();
							// Purge the cache of the page from which the edit was made
							new mw.Api().postWithToken(
								'csrf',
								{ action: 'purge', titles: mw.config.values.wgPageName }
							).done( function () {
								location.reload();
							} );
						} ).fail( function () {
							alert( gadgetMsg[ 'added-category-failure' ] );
							dialog.close();
						} );
					});
				};
	
				/**
				 * Subclass ProcessDialog
				 *
				 * @class WrcAddNewIndividual
				 * @extends OO.ui.ProcessDialog
				 *
				 * @constructor
				 * @param {Object} config
				 */
				function WrcAddNewIndividual( config ) {
					this.type = 'user';
					this.name = '';
					this.description = '';
					this.icon = '';
					this.skills = [];
	
					if ( config.name ) {
						this.name = config.name;
					}
					if ( config.description ) {
						this.description = config.description;
					}
					if ( config.icon ) {
						this.icon = config.icon;
					}
					if ( config.skills ) {
						this.skills = config.skills;
					}
					WrcAddNewIndividual.super.call( this, config );
				}
				OO.inheritClass( WrcAddNewIndividual, OO.ui.ProcessDialog );
	
				WrcAddNewIndividual.static.name = 'wrcAddNewIndividual';
				WrcAddNewIndividual.static.title = gadgetMsg[ 'editor-ani-button' ];
				WrcAddNewIndividual.static.actions = [
					{
						action: 'continue',
						modes: 'edit',
						label: gadgetMsg[ 'editor-save' ],
						flags: [ 'primary', 'constructive' ]
					},
					{
						action: 'cancel',
						modes: 'edit',
						label: gadgetMsg[ 'editor-cancel' ],
						flags: 'safe'
					}
				];
	
				/**
				 * Use the initialize() method to add content to the dialog's $body,
				 * to initialize widgets, and to set up event handlers.
				 */
				WrcAddNewIndividual.prototype.initialize = function () {
					var dialog, i, fieldSkillsToShare, fieldSkillsToShareSelected;
					dialog = this;
	
					WrcAddNewIndividual.super.prototype.initialize.call( this );
					this.content = new OO.ui.PanelLayout( {
						padded: true,
						expanded: false
					} );
					this.fieldType = new OO.ui.TextInputWidget( {
						value: this.type,
						indicator: 'required',
						required: true
					} );
					this.fieldName = new OO.ui.TextInputWidget( {
						value: this.name,
						indicator: 'required',
						required: true
					} );
					this.fieldDescription = new OO.ui.MultilineTextInputWidget( {
						value: this.description,
						rows: 5,
						indicator: 'required',
						required: true
					} );
					this.fieldIcon = new OO.ui.TextInputWidget( {
						value: this.icon
					} );
					fieldSkillsToShareSelected = [];
					for ( i = 0; i < this.skills.length; i++ ) {
						fieldSkillsToShareSelected.push(
							{ data: this.skills[ i ],
								label: gadgetMsg[ this.skills[ i ].toLowerCase().replace( / /g, '-' ) ] }
						);
					}
					this.fieldSkillsToShare = new OO.ui.MenuTagMultiselectWidget( {
						selected: fieldSkillsToShareSelected,
						options: [
							{ data: 'Program design skills',
								label: gadgetMsg[ 'program-design-skills' ] },
							{ data: 'Program evaluation skills',
								label: gadgetMsg[ 'program-evaluation-skills' ] },
							{ data: 'Program planning skills',
								label: gadgetMsg[ 'program-planning-skills' ] },
							{ data: 'Program management skills',
								label: gadgetMsg[ 'program-management-skills' ] },
							{ data: 'Storytelling skills',
								label: gadgetMsg[ 'storytelling-skills' ] },
							{ data: 'Press relations experience',
								label: gadgetMsg[ 'press-relations-experience' ] },
							{ data: 'Annual planning skills',
								label: gadgetMsg[ 'annual-planning-skills' ] },
							{ data: 'Survey skills',
								label: gadgetMsg[ 'survey-skills' ] },
							{ data: 'Fundraising skills',
								label: gadgetMsg[ 'fundraising-skills' ] },
							{ data: 'Financial skills',
								label: gadgetMsg[ 'financial-skills' ] },
							{ data: 'Affiliate collaboration skills',
								label: gadgetMsg[ 'affiliate-collaboration-skills' ] },
							{ data: 'Event organization and planning skills',
								label: gadgetMsg[ 'event-organization-and-planning-skills' ] },
							{ data: 'Event program design skills',
								label: gadgetMsg[ 'event-program-design-skills' ] },
							{ data: 'Facilitation skills',
								label: gadgetMsg[ 'facilitation-skills' ] },
							{ data: 'Online governance experience',
								label: gadgetMsg[ 'online-governance-experience' ] },
							{ data: 'New affiliate proposal experience',
								label: gadgetMsg[ 'new-affiliate-proposal-experience' ] }
						],
						indicator: 'required'
					} );
					
					this.deleteButton = new OO.ui.ButtonWidget( {
						label: gadgetMsg[ 'editor-remove-entry' ],
						icon: 'trash',
						flags: [ 'destructive' ]
					} ).on( 'click', function () {
						new OO.ui.confirm(
							gadgetMsg[ 'editor-remove-confirm' ]
						).done( function ( confirmed ) {
							if ( confirmed ) {
								dialog.saveItem( 'delete' );
							}
						} );
					} );
	
					// Append things to fieldSet
					this.fieldSet = new OO.ui.FieldsetLayout( {
						items: [
							new OO.ui.FieldLayout(
								this.fieldName,
								{
									label: gadgetMsg[ 'editor-ani-fieldname' ],
									align: 'top',
									help: gadgetMsg[ 'editor-ani-fieldname-help' ]
								}
							),
							new OO.ui.FieldLayout(
								this.fieldDescription,
								{
									label: gadgetMsg[ 'editor-ani-fielddescription' ],
									align: 'top',
									help: gadgetMsg[ 'editor-ani-fielddescription-help' ]
								}
							),
							new OO.ui.FieldLayout(
								this.fieldIcon,
								{
									label: gadgetMsg[ 'editor-ani-fieldicon' ],
									align: 'top',
									help: gadgetMsg[ 'editor-ani-fieldicon-help' ]
								}
							),
							new OO.ui.FieldLayout(
								this.fieldSkillsToShare,
								{
									label: gadgetMsg[ 'editor-ani-skills-and-exprience' ],
									align: 'top',
									help: gadgetMsg[ 'editor-ani-skills-and-exprience-help' ]
								}
							),
						]
					} );
					
					if ( this.name ) {
						this.fieldSet.addItems( [
							new OO.ui.FieldLayout(
								this.deleteButton
							)
						] );
					}
					
					// When everything is done
					this.content.$element.append( this.fieldSet.$element );
					this.$body.append( this.content.$element );
				};
	
				/**
				 * Set custom height for the modal window
				 */
				WrcAddNewIndividual.prototype.getBodyHeight = function () {
					return 420; // NOTE: Remember to add height when new fields are added.
				};
	
				/**
				 * In the event "Select" is pressed
				 */
				WrcAddNewIndividual.prototype.getActionProcess = function ( action ) {
					var dialog = this;
					if ( action === 'continue' && dialog.fieldType.getValue() ) {
						return new OO.ui.Process( function () {
							dialog.saveItem();
						} );
					} else {
						return new OO.ui.Process( function () {
							dialog.close();
						} );
					}
					return NewItemDialog.parent.prototype.getActionProcess.call( this, action );
				};
	
				/**
				 * Save the changes to [[Module:Wikimedia Resource Center/Individuals]] page.
				 */
				WrcAddNewIndividual.prototype.saveItem = function ( deleteFlag ) {
					var dialog = this, content;
	
					dialog.pushPending();
	
					new mw.Api().get( getContentIndividuals() ).done( function ( data ) {
						var i, insertInPlace, sanitizeInput, processWorkingEntry,
							editSummary, manifest = [], workingEntry, username,
							generateKeyValuePair, skills, entries, name;
							
						/**
						  * Sanitizes input for saving to wiki
						  *
						  * @param {string} s
						  *
						  * @return {string}
						  */
						sanitizeInput = function ( s ) {
							return s
								.replace( /\\/g, '\\\\' )
							.replace( /\n/g, '<br />' );
						};
						
						/**
						  * Creates Lua-style key-value pairs, including converting the
						  * audiences array into a proper sequential table.
						  *
						  * @param {string} k The key
						  * @param {string} v The value
						  *
						  * @return {string}
						  */
						generateKeyValuePair = function ( k, v ) {
							var res, jsonarray;
							res = '\t\t'.concat( k, ' = ' );
							if ( k == 'skills' ) {
								jsonarray = JSON.stringify( v );
								// Lua uses { } for "arrays"
								jsonarray = jsonarray.replace( '[', '{' );
								jsonarray = jsonarray.replace( ']', '}' );
								// Style changes (single quotes, spaces after commas)
								jsonarray = jsonarray.replace( /\"/g, '\'' );
								jsonarray = jsonarray.replace( /,/g, ', ' );
								// Basic input sanitation
								jsonarray = sanitizeInput( jsonarray );
								res += jsonarray;
							} else {
								v = sanitizeInput( v );
								v = v.replace( /'/g, '\\\'' );
								res += '\'' + v + '\'';
							}
							res += ',\n';
							return res;
						};
						
						/**
						 * Compares a given Wikimedia Resource Center/Individuals entry 
						 * against the edit fields and applies changes where relevant.
						 *
						 * @param {Object} workingEntry the entry being worked on
						 * @return {Object} The same entry but with modifications
						 */
						processWorkingEntry = function ( workingEntry ) {
							workingEntry.type = dialog.fieldType.getValue();
	
							if ( dialog.fieldType.getValue() ) {
								workingEntry.type = dialog.fieldType.getValue();
							} else if ( !dialog.fieldType.getValue() && workingEntry.type ) {
								delete workingEntry.type;
							}
							
							if ( dialog.fieldName.getValue() ) {
								workingEntry.name = dialog.fieldName.getValue();
							} else if ( !dialog.fieldName.getValue() && workingEntry.name ) {
								delete workingEntry.name;
							}
	
							if ( dialog.fieldDescription.getValue() ) {
								workingEntry.description = dialog.fieldDescription.getValue();
							} else if ( !dialog.fieldDescription.getValue() && workingEntry.description ) {
								delete workingEntry.description;
							}
	
							if ( dialog.fieldIcon.getValue() ) {
								workingEntry.icon = dialog.fieldIcon.getValue();
							} else if ( !dialog.fieldIcon.getValue() && workingEntry.icon ) {
								delete workingEntry.icon;
							}
							
							if ( dialog.fieldSkillsToShare.getValue() ) {
								workingEntry.skills = dialog.fieldSkillsToShare.getValue();
							} else if ( !dialog.fieldSkillsToShare.getValue() && workingEntry.skills ) {
								delete workingEntry.skills;
							}
	
							return workingEntry;
						};
						
						// Cycle through existing entries. If we are editing an existing
						// entry, that entry will be modified in place.
						entries = getContentModule( data.query.pages );
	
						for ( i = 0; i < entries.length; i++ ) {
							workingEntry = cleanRawEntry( entries[ i ].value.fields );
							if ( workingEntry.name == dialog.name ) {
								if ( deleteFlag ) {
									editSummary = 'Removing individual: '.concat( workingEntry.name );
								} else {
									workingEntry = processWorkingEntry( workingEntry );
									editSummary = 'Editing individual: '.concat( workingEntry.name );
								}
							}
							if ( workingEntry.name != dialog.name || !deleteFlag ) {
								manifest.push( workingEntry );
							}
						}
						
						// No unique name means this is a new entry
						if ( !dialog.name ) {
							workingEntry = {};
							workingEntry = processWorkingEntry( workingEntry );
							editSummary = gadgetMsg[ 'add-new-individual' ].concat( workingEntry.name );
							manifest.push( workingEntry );
						}
						
						// Re-generate the Lua table based on `manifest`
						insertInPlace = 'return {\n';
						for ( i = 0; i < manifest.length; i++ ) {
							insertInPlace += '\t{\n';
							if ( manifest[ i ].type ) {
								insertInPlace += generateKeyValuePair(
									'type',
									manifest[ i ].type
								);
							}
							if ( manifest[ i ].name ) {
								name = manifest [ i ].name.split(" ").join("_");
								insertInPlace += generateKeyValuePair(
									'name', name
								);
								// Keep track of the username
								username = name;
							}
							if ( manifest[ i ].description ) {
								insertInPlace += generateKeyValuePair(
									'description',
									manifest[ i ].description
								);
							}
							if ( manifest[ i ].icon ) {
								insertInPlace += generateKeyValuePair(
									'icon',
									manifest[ i ].icon
								);
							}
							if ( manifest[ i ].skills ) {
								insertInPlace += generateKeyValuePair(
									'skills',
									manifest[ i ].skills
								);
								skills = manifest[ i ].skills;
							}
							insertInPlace += '\t},\n';
						}
						insertInPlace += '}';
						
						// Add user's skills they can share to their user page
						addSkillsToUserpage( username, skills );
						
						// Add the new user into the Lua table.
						new mw.Api().postWithToken(
							'csrf',
							{
								action: 'edit',
								nocreate: true,
								summary: editSummary,
								pageid: 10588347,  // Module:Wikimedia_Resource_Center/Individuals
								text: insertInPlace,
								contentmodel: 'Scribunto'
							}
						).done( function () {
							dialog.close();
							// Purge the cache of the page from which the edit was made
							new mw.Api().postWithToken(
								'csrf',
								{ action: 'purge', titles: mw.config.values.wgPageName }
							).done( function () {
								location.reload();
							} );
						} ).fail( function () {
							alert( gadgetMsg[ 'editor-ani-failed-alert' ] );
							dialog.close();
						} );
						
					} );
				};
				
				/**
				 * Event handler for when someone clicks on an edit icon/button
				 *
				 * @param {Object} config
				 */
				openWindow = function ( config ) {
					var wrcAddNewIndividual, windowManager;
					config.size = 'large';
					wrcAddNewIndividual = new WrcAddNewIndividual( config );
	
					windowManager = new OO.ui.WindowManager();
					$( 'body' ).append( windowManager.$element );
					windowManager.addWindows( [ wrcAddNewIndividual ] );
					windowManager.openWindow( wrcAddNewIndividual );
				};
				
				// Edit user content via the form
				$( '.user-edit-icon' ).each( function () {
					var $icon = $( this ),
						editButton;
					editButton = new OO.ui.ButtonWidget( {
						framed: false,
						icon: 'edit'
					} ).on( 'click', function () {
						new mw.Api().get( getContentIndividuals() ).done( function ( data ) {
							var entryData, username, content;
							
							username = editButton.$element
								.closest( '.wrc-card' )
								.data( 'wrc-unique-id' );
								
							entryData = cleanRawEntry(
								getRelevantRawEntry(
									getContentModule( data.query.pages ),
									username
								)
							);
							openWindow( entryData );
						} );
					} );
					$icon.css( 'float', 'right' );
					$icon.append( editButton.$element );
				} );
				
				// Open dialog when "Add new individual" is clicked
				$( '.wrc-ai-button' ).each( function () {
					var $addButton = $( this ),
						addButton;
					addButton = new OO.ui.ButtonWidget( {
						icon: 'add',
						label: gadgetMsg[ 'editor-ani-button' ],
						flags: [ 'primary', 'progressive' ]
					} ).on( 'click', function () {
						openWindow( {} );
					} );
					$addButton.css( 'margin', '1.125em' );
					$addButton.append( addButton.$element );
				} );
			} );
		} );
	}
}() );