├── src ├── fonts │ ├── roboto-v20-latin-300.eot │ ├── roboto-v20-latin-300.ttf │ ├── roboto-v20-latin-300.woff │ ├── roboto-v20-latin-300.woff2 │ ├── roboto-v20-latin-regular.eot │ ├── roboto-v20-latin-regular.ttf │ ├── roboto-v20-latin-regular.woff │ ├── roboto-v20-latin-regular.woff2 │ ├── roboto-v20-latin-regular.svg │ └── roboto-v20-latin-300.svg ├── util.js ├── index.js ├── components │ ├── Start.js │ ├── Harvester.js │ └── Form.js └── style.css ├── public ├── landingpage.php ├── index.html ├── gethtshare.php ├── share.js ├── share.css ├── share.php └── tablesorter.js ├── .gitignore ├── README.md ├── package.json └── LICENSE /src/fonts/roboto-v20-latin-300.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubahnverleih/harvesttemplates/master/src/fonts/roboto-v20-latin-300.eot -------------------------------------------------------------------------------- /src/fonts/roboto-v20-latin-300.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubahnverleih/harvesttemplates/master/src/fonts/roboto-v20-latin-300.ttf -------------------------------------------------------------------------------- /src/fonts/roboto-v20-latin-300.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubahnverleih/harvesttemplates/master/src/fonts/roboto-v20-latin-300.woff -------------------------------------------------------------------------------- /src/fonts/roboto-v20-latin-300.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubahnverleih/harvesttemplates/master/src/fonts/roboto-v20-latin-300.woff2 -------------------------------------------------------------------------------- /src/fonts/roboto-v20-latin-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubahnverleih/harvesttemplates/master/src/fonts/roboto-v20-latin-regular.eot -------------------------------------------------------------------------------- /src/fonts/roboto-v20-latin-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubahnverleih/harvesttemplates/master/src/fonts/roboto-v20-latin-regular.ttf -------------------------------------------------------------------------------- /src/fonts/roboto-v20-latin-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubahnverleih/harvesttemplates/master/src/fonts/roboto-v20-latin-regular.woff -------------------------------------------------------------------------------- /src/fonts/roboto-v20-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubahnverleih/harvesttemplates/master/src/fonts/roboto-v20-latin-regular.woff2 -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | export function capitalizeFirstLetter(str) { 2 | return str.charAt(0).toUpperCase() + str.slice(1); 3 | } 4 | 5 | export const backend = "https://plnode.toolforge.org/"; 6 | -------------------------------------------------------------------------------- /public/landingpage.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # production 5 | /build 6 | 7 | # misc 8 | .DS_Store 9 | .env.local 10 | .env.development.local 11 | .env.test.local 12 | .env.production.local 13 | 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | .vscode 18 | *.code-workspace 19 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Harvest Templates 7 | 8 | 9 |
10 |
11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import Form from "./components/Form"; 4 | import Start from "./components/Start"; 5 | 6 | import "./style.css"; 7 | 8 | ReactDOM.render(, document.getElementById("start")); 9 | 10 | ReactDOM.render(
, document.getElementById("form")); 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Wikidata tool to harvest templates of Wikimedia projects 2 | . 3 | 4 | This tool has two parts: front end (this repo) and back end (PLnode). 5 | 6 | 7 | To the extent possible under law, the author(s) have dedicated all copyright 8 | and related and neighboring rights to this software to the public domain 9 | worldwide. This software is distributed without any warranty. 10 | 11 | See for a copy of the 12 | CC0 Public Domain Dedication. 13 | -------------------------------------------------------------------------------- /public/gethtshare.php: -------------------------------------------------------------------------------- 1 | for a copy of the 8 | * CC0 Public Domain Dedication. 9 | **/ 10 | header('Access-Control-Allow-Origin: *'); 11 | header( 'Content-Type: application/json' ); 12 | include('../connect.inc.php'); 13 | 14 | $result = mysqli_query($conn2, 'SELECT para FROM ht_share WHERE id = "'.$_GET['htid'].'"'); 15 | $row = mysqli_fetch_assoc($result); 16 | parse_str($row['para'], $para); 17 | echo json_encode( $para ); 18 | ?> 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ht2", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "/harvesttemplates", 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^4.2.4", 8 | "@testing-library/react": "^9.5.0", 9 | "@testing-library/user-event": "^7.2.1", 10 | "axios": "^0.21.2", 11 | "lodash": "^4.17.20", 12 | "query-string": "^6.12.1", 13 | "react": "^16.13.1", 14 | "react-addons-update": "^15.6.3", 15 | "react-data-table-component": "^6.8.0", 16 | "react-dom": "^16.13.1", 17 | "react-scripts": "3.4.1", 18 | "react-toastify": "^5.5.0", 19 | "styled-components": "^5.1.0", 20 | "universal-cookie": "^4.0.3" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test", 26 | "eject": "react-scripts eject" 27 | }, 28 | "eslintConfig": { 29 | "extends": "react-app" 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /public/share.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | $("#maintable").tablesorter({ headers: { 8: { sorter: false }, 9: { sorter: false } } }); 3 | 4 | $("html").on("click", ".edit", function (e) { 5 | e.preventDefault(); 6 | var tagfield = $(this).parent().find("span"); 7 | var tags = tagfield.html().replace(/
/g, "\n"); 8 | tagfield.html(""); 9 | $(this).attr("src", "https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/OOjs_UI_icon_check.svg/24px-OOjs_UI_icon_check.svg.png"); 10 | $(this).attr("alt", "save"); 11 | $(this).attr("class", "save"); 12 | }); 13 | 14 | $("html").on("click", ".save", function (e) { 15 | e.preventDefault(); 16 | var tagfield = $(this).parent().find("span"); 17 | var newtags = tagfield.find("textarea").val().replace(/\n/g, "
"); 18 | $.get("share.php?action=edit&tags=" + newtags + "&id=" + $(this).parent().parent().attr("data-id")); 19 | tagfield.html(newtags); 20 | $(this).attr( 21 | "src", 22 | "https://upload.wikimedia.org/wikipedia/commons/thumb/8/8a/OOjs_UI_icon_edit-ltr.svg/24px-OOjs_UI_icon_edit-ltr.svg.png" 23 | ); 24 | $(this).attr("alt", "edit"); 25 | $(this).attr("class", "edit"); 26 | }); 27 | 28 | $("html").on("click", ".delete", function (e) { 29 | $(this).load("share.php?action=delete&id=" + $(this).parent().parent().attr("data-id")); 30 | $(this).parent().parent().remove(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /public/share.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Calibri, 'Helvetica Neue', 'Lucida Grande', Tahoma, Verdana, sans-serif; 3 | font-size: 14px; 4 | } 5 | a { 6 | color: #0645AD; 7 | text-decoration: none; 8 | } 9 | 10 | div#main { 11 | width: 95%; 12 | margin: 30px auto; 13 | } 14 | 15 | table{ 16 | width: 100%; 17 | background-color: #F2F2F2; 18 | margin-top: 20px; 19 | } 20 | 21 | tr{ 22 | vertical-align: top; 23 | } 24 | 25 | tr:nth-child(odd) { 26 | background-color: #FFF; 27 | } 28 | 29 | 30 | th{ 31 | font-weight: bold; 32 | text-align: left; 33 | color: #373A3C; 34 | } 35 | 36 | td, th{ 37 | padding: 10px; 38 | } 39 | 40 | .edit:hover, .save:hover, .delete:hover{ 41 | cursor: pointer; 42 | } 43 | 44 | input, textarea{ 45 | margin: 2px; 46 | } 47 | input[type="text"], textarea { 48 | background-color: #FFF; 49 | border: 1px solid #AFAFAF; 50 | border-radius: 3px; 51 | padding: 4px; 52 | width: 250px; 53 | box-sizing: content-box; 54 | -ms-box-sizing: content-box; 55 | -moz-box-sizing: content-box; 56 | -webkit-box-sizing: content-box; 57 | } 58 | 59 | input[type="text"]:focus { 60 | border-color: #808080; 61 | } 62 | 63 | #maintable thead tr .header { 64 | background-image: url(pics/bg.gif); 65 | background-repeat: no-repeat; 66 | background-position: center right; 67 | cursor: pointer; 68 | } 69 | #maintable thead tr .headerSortUp { 70 | background-image: url(pics/asc.gif); 71 | } 72 | #maintable thead tr .headerSortDown { 73 | background-image: url(pics/desc.gif); 74 | } -------------------------------------------------------------------------------- /src/components/Start.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import axios from "axios"; 3 | import Cookies from "universal-cookie"; 4 | import { backend } from "../util.js"; 5 | 6 | class Start extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.onLogin = this.onLogin.bind(this); 10 | this.onLinkClick = this.onLinkClick.bind(this); 11 | this.state = { 12 | username: 0, 13 | }; 14 | } 15 | 16 | onLogin() { 17 | if (this.state.username === 0) { 18 | window.location.href = `${backend}authorize?landingpage=https://pltools.toolforge.org/harvesttemplates/landingpage.php`; 19 | } else { 20 | const cookies = new Cookies(); 21 | window.location.href = `${backend}logout?token=${cookies.get("plnodeJwt")}`; 22 | } 23 | } 24 | 25 | onLinkClick(event) { 26 | let url = event.target.getAttribute("url"); 27 | if (url) { 28 | window.location.href = url; 29 | } 30 | } 31 | 32 | componentDidMount() { 33 | const cookies = new Cookies(); 34 | axios 35 | .get(`${backend}profile`, { 36 | params: { 37 | token: cookies.get("plnodeJwt"), 38 | }, 39 | }) 40 | .then(response => { 41 | this.setState({ 42 | username: response.data.username, 43 | }); 44 | }) 45 | .catch(error => { 46 | console.log(error); 47 | this.setState({ 48 | username: 0, 49 | }); 50 | }); 51 | } 52 | 53 | render() { 54 | return ( 55 | 56 |

Harvest Templates

57 | Tool to tranfer data from Wikimedia projects to Wikidata 58 |
59 | 62 | 65 | 68 | 71 | 74 |
75 | ); 76 | } 77 | } 78 | 79 | export default Start; 80 | -------------------------------------------------------------------------------- /public/share.php: -------------------------------------------------------------------------------- 1 | array( 7 | "header" => "User-Agent: PLtools" 8 | ) 9 | ) 10 | ); 11 | 12 | $username = ''; 13 | if (isset($_COOKIE['plnodeJwt'])){ 14 | $url = "https://plnode.toolforge.org/profile?token=".$_COOKIE['plnodeJwt']; 15 | $response = file_get_contents($url, false, $context); 16 | if ($response !== False){ 17 | $ret = json_decode($response); 18 | $username = $ret->username; 19 | } 20 | } 21 | if (isset($_GET['action']) and $username != ''){ 22 | if ($_GET['action'] == 'savenew'){ 23 | mysqli_query($conn2, 'INSERT INTO ht_share (para, user) VALUES ("'.$_SERVER["QUERY_STRING"].'", "'.$username.'")'); 24 | } else if ($_GET['action'] == 'delete'){ 25 | mysqli_query($conn2, 'DELETE FROM ht_share WHERE id = "'.$_GET['id'].'" AND user = "'.$username.'"'); 26 | return 1; 27 | } else if ($_GET['action'] == 'update' and isset($_GET['htid'])){ 28 | mysqli_query($conn2, 'UPDATE ht_share SET lastrun = now() WHERE id = "'.$_GET['htid'].'"'); 29 | return 1; 30 | } 31 | } 32 | 33 | function apiRequest(array $args) { 34 | $url = "https://www.wikidata.org/w/api.php?".http_build_query($args); 35 | $response = file_get_contents($url, false, $context); 36 | $ret = json_decode($response); 37 | if ($ret === null) { 38 | echo 'Unparsable API response:
' . htmlspecialchars( $ret ) . '
'; 39 | exit(0); 40 | } 41 | return $ret; 42 | } 43 | 44 | function createContent(){ 45 | global $conn2; 46 | global $username; 47 | $content = ''; 48 | $result = mysqli_query($conn2, 'SELECT id, para, user, lastrun FROM ht_share'); 49 | while ($row = mysqli_fetch_assoc($result)){ 50 | parse_str($row['para'], $para); 51 | $content .= sprintf('%s.%s
NS: %d%5$s%6$s', $row['id'], $para['siteid'], $para['project'], $para['namespace'], $para['p'], $para['template']); 52 | if (array_key_exists('aparameter1', $para) and !empty($para['aparameter1'])){ 53 | $content .= sprintf('year: %s
month: %s
day: %s', $para['aparameter1'], $para['aparameter2'], $para['aparameter3']); 54 | } else if (is_array($para['paramters'])){ 55 | $content .= sprintf('%s', implode('
or ', $para['parameter'])); 56 | } else { 57 | $content .= sprintf('%s', $para['parameters']); 58 | } 59 | $additional = ['wikisyntax', 'prefix', 'calendar', 'rel', 'limityear', 'unit', 'decimalmark', 'manuallist', 'set', 'addprefix', 'removeprefix', 'addsuffix', 'removesuffix', 'searchvalue', 'replacevalue', 'category', 'depth']; 60 | $content .= ''; 61 | foreach($additional as $ad){ 62 | if (array_key_exists($ad, $para)){ 63 | $content .= sprintf('%s: %s
', $ad, $para[$ad]); 64 | } 65 | } 66 | $content .= sprintf('%1$s%2$s', $row['user'], $row['lastrun']); 67 | $content .= sprintf('load', $row['id']); 68 | if ($row['user'] == $username){ 69 | $content .= 'delete'; 70 | } else { 71 | $content .= ''; 72 | } 73 | $content .= ''; 74 | } 75 | return $content; 76 | } 77 | ?> 78 | 79 | 87 | 88 | 89 | 90 | PLtools: Harvest Templates Share 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 |
PLtools: Harvest Templates Share
105 | 106 |
107 | 108 | 109 | 110 | 111 | 114 | 115 |
projectpropertytemplateparameteradditional parameterscreated bylast runrun
116 |
117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | /* roboto-300 - latin */ 2 | @font-face { 3 | font-family: 'Roboto'; 4 | font-style: normal; 5 | font-weight: 300; 6 | src: url('fonts/roboto-v20-latin-300.eot'); /* IE9 Compat Modes */ 7 | src: local('Roboto Light'), local('Roboto-Light'), 8 | url('fonts/roboto-v20-latin-300.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 9 | url('fonts/roboto-v20-latin-300.woff2') format('woff2'), /* Super Modern Browsers */ 10 | url('fonts/roboto-v20-latin-300.woff') format('woff'), /* Modern Browsers */ 11 | url('fonts/roboto-v20-latin-300.ttf') format('truetype'), /* Safari, Android, iOS */ 12 | url('fonts/roboto-v20-latin-300.svg#Roboto') format('svg'); /* Legacy iOS */ 13 | } 14 | 15 | /* roboto-regular - latin */ 16 | @font-face { 17 | font-family: 'Roboto'; 18 | font-style: normal; 19 | font-weight: 400; 20 | src: url('fonts/roboto-v20-latin-regular.eot'); /* IE9 Compat Modes */ 21 | src: local('Roboto'), local('Roboto-Regular'), 22 | url('fonts/roboto-v20-latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 23 | url('fonts/roboto-v20-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */ 24 | url('fonts/roboto-v20-latin-regular.woff') format('woff'), /* Modern Browsers */ 25 | url('fonts/roboto-v20-latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */ 26 | url('fonts/roboto-v20-latin-regular.svg#Roboto') format('svg'); /* Legacy iOS */ 27 | } 28 | 29 | 30 | html, body{ 31 | font-family: Roboto; 32 | margin: 0px; 33 | padding: 0px; 34 | } 35 | #start{ 36 | background-color: #009FB7; 37 | box-sizing: border-box; 38 | left: 0px; 39 | padding: 20px; 40 | position: relative; 41 | text-align: left; 42 | top: 0px; 43 | width: 100%; 44 | } 45 | #form{ 46 | background-color: #7DCE82; 47 | box-sizing: border-box; 48 | display: flex; 49 | min-height: 90vh; 50 | padding: 10px; 51 | width: 100%; 52 | } 53 | #harvester{ 54 | background-color: #FADF63; 55 | box-sizing: border-box; 56 | min-height: 10vh; 57 | padding: 10px; 58 | width: 100%; 59 | } 60 | 61 | .rdt_TableHeader, .rdt_Pagination, .rdt_TableHeadRow, .rdt_Table{ 62 | background-color: #FADF63 !important; 63 | } 64 | .flex-container{ 65 | align-content: flex-start; 66 | align-items: flex-start; 67 | display: flex; 68 | display: -ms-flexbox; 69 | display: -webkit-flex; 70 | flex-direction: row; 71 | flex-wrap: wrap; 72 | justify-content: flex-start; 73 | ms-flex-align: start; 74 | ms-flex-direction: row; 75 | ms-flex-line-pack: start; 76 | ms-flex-pack: start; 77 | ms-flex-wrap: wrap; 78 | text-align: left; 79 | webkit-align-content: flex-start; 80 | webkit-align-items: flex-start; 81 | webkit-flex-direction: row; 82 | webkit-flex-wrap: wrap; 83 | webkit-justify-content: flex-start; 84 | } 85 | .flexItem{ 86 | align-self: auto; 87 | flex: 0 1 auto; 88 | margin: 20px; 89 | ms-flex: 0 1 auto; 90 | ms-flex-item-align: auto; 91 | ms-flex-order: 0; 92 | order: 0; 93 | webkit-align-self: auto; 94 | webkit-flex: 0 1 auto; 95 | webkit-order: 0; 96 | } 97 | .row{ 98 | clear: both; 99 | display: table-row; 100 | height: 50px; 101 | width: auto; 102 | } 103 | .col1{ 104 | display: table-cell; 105 | float: left; 106 | height: 50px; 107 | padding-top: 12px; 108 | width: 130px; 109 | } 110 | .col2{ 111 | display: table-cell; 112 | float: left; 113 | padding-top: 2px; 114 | width: 300px; 115 | } 116 | #form input, #form select, #form textarea, #form .formButton{ 117 | background-color: #C8E6C9; 118 | border: none; 119 | border-radius: 2px; 120 | box-sizing: border-box; 121 | margin: 2px; 122 | padding: 12px; 123 | transition: all .2s ease; 124 | webkit-transition: all .2s ease; 125 | width: 100%; 126 | } 127 | #form input[type=checkbox]{ 128 | transform: scale(1.3); 129 | width: 20px; 130 | } 131 | #form input[type=number]{ 132 | width: 30%; 133 | } 134 | #form input.shorter30, #form select.shorter30{ 135 | width: 30%; 136 | } 137 | #form select.shorter55{ 138 | width: 55%; 139 | } 140 | #form .inputerror{ 141 | box-shadow: 0 0 7px red; 142 | } 143 | #form input:focus, #form select:focus, #form textarea:focus, #form .formButton:focus{ 144 | box-shadow: 0 0 2px rgba(0,0,0,.12),0 2px 2px rgba(0,0,0,.2); 145 | } 146 | .linkButton{ 147 | background-color: #C0C0C0; 148 | border: none; 149 | border-radius: 2px; 150 | cursor: pointer; 151 | display: inline-block; 152 | font-size: .875rem; 153 | font-weight: 500; 154 | height: 2.25rem; 155 | letter-spacing: .03rem; 156 | line-height: 1.125rem; 157 | line-height: 2.25rem; 158 | margin: 7px; 159 | padding: 0 1.625rem; 160 | text-align: center; 161 | text-transform: uppercase; 162 | transition: all .2s ease; 163 | vertical-align: middle; 164 | webkit-transition: all .2s ease; 165 | white-space: nowrap; 166 | } 167 | #start .linkButton{ 168 | background-color: #005E6B; 169 | color: #FFF; 170 | } 171 | #form .linkButton{ 172 | background-color: #035809; 173 | color: #FFF; 174 | } 175 | #harvester .linkButton{ 176 | background-color: #FA9D63; 177 | } 178 | #toast .linkButton{ 179 | background-color: #FF998E; 180 | } 181 | 182 | #start .linkButton.deactivated{ 183 | background-color: #008395; 184 | cursor: not-allowed; 185 | } 186 | 187 | #form .linkButton.deactivated{ 188 | background-color: gray; 189 | cursor: not-allowed; 190 | } 191 | #harvester .linkButton.deactivated{ 192 | background-color: #FFEA89; 193 | cursor: not-allowed; 194 | } 195 | 196 | .linkButton:focus, .linkButton:hover{ 197 | box-shadow: 0 0 2px rgba(0,0,0,.12),0 2px 2px rgba(0,0,0,.2); 198 | outline: 0; 199 | text-decoration: none; 200 | } 201 | .linkButton.deactivated:focus, .linkButton.deactivated:hover{ 202 | box-shadow: none; 203 | outline: 0; 204 | text-decoration: none; 205 | } 206 | h1{ 207 | font-family: Roboto; 208 | font-size: 50px; 209 | font-weight: 300; 210 | letter-spacing: -1.5px; 211 | } 212 | h2{ 213 | font-family: Roboto; 214 | font-weight: 300; 215 | letter-spacing: -0.5px; 216 | } 217 | a{ 218 | color: #1976d6; 219 | text-decoration: none; 220 | } 221 | a:hover{ 222 | text-decoration: underline; 223 | } 224 | #harvester a{ 225 | color: #000; 226 | } 227 | .fullwidth{ 228 | width: 100%; 229 | } 230 | #jobstatus{ 231 | margin-top: 20px; 232 | font-size: 1.2em; 233 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | CC0 1.0 Universal 2 | 3 | Statement of Purpose 4 | 5 | The laws of most jurisdictions throughout the world automatically confer 6 | exclusive Copyright and Related Rights (defined below) upon the creator and 7 | subsequent owner(s) (each and all, an "owner") of an original work of 8 | authorship and/or a database (each, a "Work"). 9 | 10 | Certain owners wish to permanently relinquish those rights to a Work for the 11 | purpose of contributing to a commons of creative, cultural and scientific 12 | works ("Commons") that the public can reliably and without fear of later 13 | claims of infringement build upon, modify, incorporate in other works, reuse 14 | and redistribute as freely as possible in any form whatsoever and for any 15 | purposes, including without limitation commercial purposes. These owners may 16 | contribute to the Commons to promote the ideal of a free culture and the 17 | further production of creative, cultural and scientific works, or to gain 18 | reputation or greater distribution for their Work in part through the use and 19 | efforts of others. 20 | 21 | For these and/or other purposes and motivations, and without any expectation 22 | of additional consideration or compensation, the person associating CC0 with a 23 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright 24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work 25 | and publicly distribute the Work under its terms, with knowledge of his or her 26 | Copyright and Related Rights in the Work and the meaning and intended legal 27 | effect of CC0 on those rights. 28 | 29 | 1. Copyright and Related Rights. A Work made available under CC0 may be 30 | protected by copyright and related or neighboring rights ("Copyright and 31 | Related Rights"). Copyright and Related Rights include, but are not limited 32 | to, the following: 33 | 34 | i. the right to reproduce, adapt, distribute, perform, display, communicate, 35 | and translate a Work; 36 | 37 | ii. moral rights retained by the original author(s) and/or performer(s); 38 | 39 | iii. publicity and privacy rights pertaining to a person's image or likeness 40 | depicted in a Work; 41 | 42 | iv. rights protecting against unfair competition in regards to a Work, 43 | subject to the limitations in paragraph 4(a), below; 44 | 45 | v. rights protecting the extraction, dissemination, use and reuse of data in 46 | a Work; 47 | 48 | vi. database rights (such as those arising under Directive 96/9/EC of the 49 | European Parliament and of the Council of 11 March 1996 on the legal 50 | protection of databases, and under any national implementation thereof, 51 | including any amended or successor version of such directive); and 52 | 53 | vii. other similar, equivalent or corresponding rights throughout the world 54 | based on applicable law or treaty, and any national implementations thereof. 55 | 56 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 57 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 58 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright 59 | and Related Rights and associated claims and causes of action, whether now 60 | known or unknown (including existing as well as future claims and causes of 61 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 62 | duration provided by applicable law or treaty (including future time 63 | extensions), (iii) in any current or future medium and for any number of 64 | copies, and (iv) for any purpose whatsoever, including without limitation 65 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes 66 | the Waiver for the benefit of each member of the public at large and to the 67 | detriment of Affirmer's heirs and successors, fully intending that such Waiver 68 | shall not be subject to revocation, rescission, cancellation, termination, or 69 | any other legal or equitable action to disrupt the quiet enjoyment of the Work 70 | by the public as contemplated by Affirmer's express Statement of Purpose. 71 | 72 | 3. Public License Fallback. Should any part of the Waiver for any reason be 73 | judged legally invalid or ineffective under applicable law, then the Waiver 74 | shall be preserved to the maximum extent permitted taking into account 75 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver 76 | is so judged Affirmer hereby grants to each affected person a royalty-free, 77 | non transferable, non sublicensable, non exclusive, irrevocable and 78 | unconditional license to exercise Affirmer's Copyright and Related Rights in 79 | the Work (i) in all territories worldwide, (ii) for the maximum duration 80 | provided by applicable law or treaty (including future time extensions), (iii) 81 | in any current or future medium and for any number of copies, and (iv) for any 82 | purpose whatsoever, including without limitation commercial, advertising or 83 | promotional purposes (the "License"). The License shall be deemed effective as 84 | of the date CC0 was applied by Affirmer to the Work. Should any part of the 85 | License for any reason be judged legally invalid or ineffective under 86 | applicable law, such partial invalidity or ineffectiveness shall not 87 | invalidate the remainder of the License, and in such case Affirmer hereby 88 | affirms that he or she will not (i) exercise any of his or her remaining 89 | Copyright and Related Rights in the Work or (ii) assert any associated claims 90 | and causes of action with respect to the Work, in either case contrary to 91 | Affirmer's express Statement of Purpose. 92 | 93 | 4. Limitations and Disclaimers. 94 | 95 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 96 | surrendered, licensed or otherwise affected by this document. 97 | 98 | b. Affirmer offers the Work as-is and makes no representations or warranties 99 | of any kind concerning the Work, express, implied, statutory or otherwise, 100 | including without limitation warranties of title, merchantability, fitness 101 | for a particular purpose, non infringement, or the absence of latent or 102 | other defects, accuracy, or the present or absence of errors, whether or not 103 | discoverable, all to the greatest extent permissible under applicable law. 104 | 105 | c. Affirmer disclaims responsibility for clearing rights of other persons 106 | that may apply to the Work or any use thereof, including without limitation 107 | any person's Copyright and Related Rights in the Work. Further, Affirmer 108 | disclaims responsibility for obtaining any necessary consents, permissions 109 | or other rights required for any use of the Work. 110 | 111 | d. Affirmer understands and acknowledges that Creative Commons is not a 112 | party to this document and has no duty or obligation with respect to this 113 | CC0 or use of the Work. 114 | 115 | For more information, please see 116 | 117 | -------------------------------------------------------------------------------- /src/components/Harvester.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import axios from "axios"; 3 | import update from "react-addons-update"; 4 | import DataTable from "react-data-table-component"; 5 | import Cookies from "universal-cookie"; 6 | import { toast } from "react-toastify"; 7 | import { backend } from "../util.js"; 8 | 9 | class Harvester extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | this.runHarvester = this.runHarvester.bind(this); 13 | this.convertArrayOfObjectsToCSV = this.convertArrayOfObjectsToCSV.bind(this); 14 | this.handleDownload = this.handleDownload.bind(this); 15 | this.handleStartStop = this.handleStartStop.bind(this); 16 | this.handleSave = this.handleSave.bind(this); 17 | this.handlePermalink = this.handlePermalink.bind(this); 18 | this.createLink = this.createLink.bind(this); 19 | this.onChangeRowsPerPage = this.onChangeRowsPerPage.bind(this); 20 | this.onLogin = this.onLogin.bind(this); 21 | this.cancelJob = this.cancelJob.bind(this); 22 | this.delay = 2000; 23 | this.run = false; 24 | this.stoppedAt = 0; 25 | this.pageNo = 1; 26 | this.rowsPerPage = 20; 27 | this.cancelToken = axios.CancelToken.source(); 28 | this.state = { 29 | data: {}, 30 | jobstatus: "", 31 | }; 32 | this.counts = { total: 0, success: 0, error: 0 }; 33 | const cookies = new Cookies(); 34 | this.Jwt = cookies.get("plnodeJwt"); 35 | } 36 | 37 | onLogin() { 38 | window.location.href = `${backend}authorize?landingpage=https://pltools.toolforge.org/harvesttemplates/landingpage.php`; 39 | } 40 | 41 | runHarvester(i) { 42 | let timeout = false; 43 | if (!this.props.candidates[i]) { 44 | //done 45 | document.title = "DONE - " + document.title; 46 | this.setState({ 47 | jobstatus: `done (${this.counts.success + this.counts.error}/${this.counts.total}, successes: ${this.counts.success}, errors: ${ 48 | this.counts.error 49 | })`, 50 | }); 51 | 52 | if (this.props.job.htid) { 53 | axios.get("share.php", { 54 | params: { 55 | action: "update", 56 | htid: this.props.job.htid, 57 | }, 58 | }); 59 | } 60 | return 1; 61 | } 62 | axios 63 | .post(`${backend}harvester`, { 64 | data: { 65 | candidate: JSON.stringify(this.props.candidates[i]), 66 | job: JSON.stringify(this.props.job), 67 | token: this.Jwt, 68 | }, 69 | cancelToken: this.cancelToken.token, 70 | }) 71 | .then(response => { 72 | if (response.data.status === "success") { 73 | this.counts.success += 1; 74 | } else { 75 | if (response.data.message === "Server lag") { 76 | timeout = true; 77 | } else { 78 | this.counts.error += 1; 79 | } 80 | } 81 | this.setState({ 82 | candidates: update(this.state.candidates, { 83 | [i]: { 84 | parsedvalue: { 85 | $set: response.data.parsedvalue, 86 | }, 87 | rawvalue: { 88 | $set: response.data.rawvalue, 89 | }, 90 | message: { 91 | $set: response.data.message, 92 | }, 93 | status: { 94 | $set: response.data.status, 95 | }, 96 | }, 97 | }), 98 | }); 99 | }) 100 | .catch(error => { 101 | console.log(error); 102 | this.counts.error += 1; 103 | this.setState({ 104 | candidates: update(this.state.candidates, { 105 | [i]: { 106 | message: { 107 | $set: "error", 108 | }, 109 | status: { 110 | $set: "error", 111 | }, 112 | }, 113 | }), 114 | }); 115 | }) 116 | .finally(() => { 117 | i += 1; 118 | if (i % this.rowsPerPage === 0) { 119 | this.pageNo += 1; 120 | } 121 | 122 | if (timeout) { 123 | this.setState({ 124 | jobstatus: `waiting... Server lag (${this.counts.success + this.counts.error}/${this.counts.total}, successes: ${ 125 | this.counts.success 126 | }, errors: ${this.counts.error})`, 127 | }); 128 | } else { 129 | this.setState({ 130 | jobstatus: `doing... (${this.counts.success + this.counts.error}/${this.counts.total}, successes: ${ 131 | this.counts.success 132 | }, errors: ${this.counts.error})`, 133 | }); 134 | } 135 | 136 | setTimeout(() => { 137 | if (this.run === true) { 138 | if (timeout) { 139 | i -= 1; 140 | this.delay = 10000; 141 | } else { 142 | this.delay = 2000; 143 | } 144 | this.runHarvester(i); 145 | } else { 146 | this.stoppedAt = i; 147 | } 148 | }, this.delay); 149 | }); 150 | } 151 | 152 | cancelJob() { 153 | this.stoppedAt = 0; 154 | this.run = false; 155 | document.getElementById("startButton").textContent = "start"; 156 | this.cancelToken.cancel("Operation canceled due to reload"); 157 | this.cancelToken = axios.CancelToken.source(); 158 | } 159 | 160 | componentDidMount() { 161 | this.setState({ 162 | candidates: this.props.candidates, 163 | }); 164 | this.counts.total = this.props.candidates.length; 165 | } 166 | 167 | componentWillUnmount() { 168 | this.cancelJob(); 169 | document.title = document.title.replace("DONE - ", ""); 170 | } 171 | 172 | handleStartStop(event) { 173 | event.preventDefault(); 174 | if (this.run === true) { 175 | event.target.textContent = "start"; 176 | this.run = false; 177 | this.cancelToken.cancel("Operation canceled due to stop button"); 178 | this.cancelToken = axios.CancelToken.source(); 179 | this.setState({ 180 | jobstatus: `stopped (${this.counts.success + this.counts.error}/${this.counts.total}, successes: ${this.counts.success}, errors: ${ 181 | this.counts.error 182 | })`, 183 | }); 184 | } else if (this.Jwt) { 185 | this.run = true; 186 | this.runHarvester(this.stoppedAt); 187 | event.target.textContent = "stop"; 188 | this.setState({ 189 | jobstatus: `doing... (${this.counts.success + this.counts.error}/${this.counts.total}, successes: ${this.counts.success}, errors: ${ 190 | this.counts.error 191 | })`, 192 | }); 193 | } else { 194 | toast.error( 195 | 196 | you are not logged in.{" "} 197 | 200 | , 201 | { position: toast.POSITION.TOP_CENTER } 202 | ); 203 | } 204 | } 205 | 206 | convertArrayOfObjectsToCSV(array) { 207 | const columnDelimiter = ","; 208 | const lineDelimiter = "\n"; 209 | const keys = ["pageid", "title", "qid", "rawvalue", "parsedvalue", "status", "message"]; 210 | let result = ""; 211 | result += keys.join(columnDelimiter); 212 | result += lineDelimiter; 213 | 214 | array.forEach(item => { 215 | let ctr = 0; 216 | keys.forEach(key => { 217 | if (ctr > 0) { 218 | result += columnDelimiter; 219 | } 220 | if (key in item) { 221 | if (String(item[key]).includes(columnDelimiter)) { 222 | result += `"${item[key]}"`; 223 | } else { 224 | result += item[key]; 225 | } 226 | } 227 | ctr++; 228 | }); 229 | result += lineDelimiter; 230 | }); 231 | return result; 232 | } 233 | 234 | createLink() { 235 | let permalink = ""; 236 | let exportParameters = [ 237 | "siteid", 238 | "project", 239 | "namespace", 240 | "p", 241 | "template", 242 | "templateredirects", 243 | "parameters", 244 | "addprefix", 245 | "removeprefix", 246 | "addsuffix", 247 | "removesuffix", 248 | "searchvalue", 249 | "replacevalue", 250 | "category", 251 | "depth", 252 | "constraints", 253 | "alreadyset", 254 | "wikisyntax", 255 | "manuallist", 256 | ]; 257 | if (this.props.job.datatype === "time") { 258 | exportParameters = [...exportParameters, "calendar", "limityear", "rel"]; 259 | } else if (this.props.job.datatype === "quantity") { 260 | exportParameters = [...exportParameters, "unit", "decimalmark"]; 261 | } else if (this.props.job.datatype === "monolingualtext") { 262 | exportParameters = [...exportParameters, "pagetitle", "monolanguage"]; 263 | } 264 | for (let key of exportParameters) { 265 | if (key in this.props.job && this.props.job[key].toString().length > 0) { 266 | if (permalink !== "") { 267 | permalink += "&"; 268 | } 269 | permalink += key + "="; 270 | if (Array.isArray(this.props.job[key])) { 271 | permalink += encodeURIComponent(this.props.job[key].join("|")); 272 | } else if (typeof this.props.job[key] === "boolean") { 273 | permalink += this.props.job[key] ? "1" : "0"; 274 | } else { 275 | permalink += encodeURIComponent(this.props.job[key]); 276 | } 277 | } 278 | } 279 | return permalink; 280 | } 281 | 282 | handlePermalink() { 283 | let querystring = this.createLink(); 284 | let permalink = "https://pltools.toolforge.org/harvesttemplates?" + querystring; 285 | window.open(permalink, "_blank"); 286 | } 287 | 288 | handleSave() { 289 | let querystring = this.createLink(); 290 | let savelink = "https://pltools.toolforge.org/harvesttemplates/share.php?action=savenew&" + querystring; 291 | window.open(savelink, "_blank"); 292 | } 293 | 294 | handleDownload() { 295 | const link = document.createElement("a"); 296 | let array = this.state.candidates ? this.state.candidates : []; 297 | let csv = this.convertArrayOfObjectsToCSV(array); 298 | if (csv == null) return; 299 | const filename = "export.csv"; 300 | if (!csv.match(/^data:text\/csv/i)) { 301 | csv = `data:text/csv;charset=utf-8,${csv}`; 302 | } 303 | csv = csv.replace("#", "%23"); 304 | link.setAttribute("href", encodeURI(csv)); 305 | link.setAttribute("download", filename); 306 | link.click(); 307 | } 308 | 309 | onChangeRowsPerPage(e) { 310 | this.rowsPerPage = e; 311 | } 312 | 313 | render() { 314 | let columns = [ 315 | { 316 | name: "Title", 317 | selector: "title", 318 | sortable: true, 319 | format: row => ( 320 | 321 | {row.title} 322 | 323 | ), 324 | }, 325 | { 326 | name: "Wikidata item", 327 | selector: "qid", 328 | sortable: true, 329 | format: row => ( 330 | 331 | {row.qid} 332 | 333 | ), 334 | }, 335 | { 336 | name: "raw value", 337 | selector: "rawvalue", 338 | sortable: true, 339 | }, 340 | { 341 | name: "parsed value", 342 | selector: "parsedvalue", 343 | sortable: true, 344 | }, 345 | { 346 | name: "message", 347 | selector: "message", 348 | sortable: true, 349 | }, 350 | ]; 351 | 352 | const conditionalRowStyles = [ 353 | { 354 | when: row => row.message !== undefined, 355 | style: { 356 | backgroundColor: "#FF9E9B", 357 | }, 358 | }, 359 | { 360 | when: row => row.message === undefined, 361 | style: { 362 | backgroundColor: "#FADF63", 363 | }, 364 | }, 365 | { 366 | when: row => row.message === "success", 367 | style: { 368 | backgroundColor: "#7DCE82", 369 | }, 370 | }, 371 | ]; 372 | 373 | let mytable = ( 374 | (this.rowsPerPage = e)} 382 | keyField={"pageid"} 383 | conditionalRowStyles={conditionalRowStyles} 384 | noHeader 385 | /> 386 | ); 387 | return ( 388 |
389 | 392 | 395 | 398 | 401 |
{this.state.jobstatus}
402 | {mytable} 403 |
404 | ); 405 | } 406 | } 407 | 408 | export default Harvester; 409 | -------------------------------------------------------------------------------- /public/tablesorter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * TableSorter 2.0 - Client-side table sorting with ease! 4 | * Version 2.0.5b 5 | * @requires jQuery v1.2.3 6 | * 7 | * Copyright (c) 2007 Christian Bach 8 | * Examples and docs at: http://tablesorter.com 9 | * Dual licensed under the MIT and GPL licenses: 10 | * http://www.opensource.org/licenses/mit-license.php 11 | * http://www.gnu.org/licenses/gpl.html 12 | * 13 | */ 14 | /** 15 | * 16 | * @description Create a sortable table with multi-column sorting capabilitys 17 | * 18 | * @example $('table').tablesorter(); 19 | * @desc Create a simple tablesorter interface. 20 | * 21 | * @example $('table').tablesorter({ sortList:[[0,0],[1,0]] }); 22 | * @desc Create a tablesorter interface and sort on the first and secound column column headers. 23 | * 24 | * @example $('table').tablesorter({ headers: { 0: { sorter: false}, 1: {sorter: false} } }); 25 | * 26 | * @desc Create a tablesorter interface and disableing the first and second column headers. 27 | * 28 | * 29 | * @example $('table').tablesorter({ headers: { 0: {sorter:"integer"}, 1: {sorter:"currency"} } }); 30 | * 31 | * @desc Create a tablesorter interface and set a column parser for the first 32 | * and second column. 33 | * 34 | * 35 | * @param Object 36 | * settings An object literal containing key/value pairs to provide 37 | * optional settings. 38 | * 39 | * 40 | * @option String cssHeader (optional) A string of the class name to be appended 41 | * to sortable tr elements in the thead of the table. Default value: 42 | * "header" 43 | * 44 | * @option String cssAsc (optional) A string of the class name to be appended to 45 | * sortable tr elements in the thead on a ascending sort. Default value: 46 | * "headerSortUp" 47 | * 48 | * @option String cssDesc (optional) A string of the class name to be appended 49 | * to sortable tr elements in the thead on a descending sort. Default 50 | * value: "headerSortDown" 51 | * 52 | * @option String sortInitialOrder (optional) A string of the inital sorting 53 | * order can be asc or desc. Default value: "asc" 54 | * 55 | * @option String sortMultisortKey (optional) A string of the multi-column sort 56 | * key. Default value: "shiftKey" 57 | * 58 | * @option String textExtraction (optional) A string of the text-extraction 59 | * method to use. For complex html structures inside td cell set this 60 | * option to "complex", on large tables the complex option can be slow. 61 | * Default value: "simple" 62 | * 63 | * @option Object headers (optional) An object of instructions for per-column 64 | * controls in the format: headers: { 0: { option: setting }, ... }. For 65 | * example, to disable sorting on the first two columns of a table: 66 | * headers: { 0: { sorter: false}, 1: {sorter: false} }. 67 | * Default value: null. 68 | * 69 | * @option Array sortList (optional) An array of instructions for per-column sorting 70 | * and direction in the format: [[columnIndex, sortDirection], ... ] where 71 | * columnIndex is a zero-based index for your columns left-to-right and 72 | * sortDirection is 0 for Ascending and 1 for Descending. A valid argument 73 | * that sorts ascending first by column 1 and then column 2 looks like: 74 | * [[0,0],[1,0]]. Default value: null. 75 | * 76 | * @option Array sortForce (optional) An array containing forced sorting rules. 77 | * Use to add an additional forced sort that will be appended to the dynamic 78 | * selections by the user. For example, can be used to sort people alphabetically 79 | * after some other user-selected sort that results in rows with the same value 80 | * like dates or money due. It can help prevent data from appearing as though it 81 | * has a random secondary sort. Default value: null. 82 | * 83 | * @option Boolean sortLocaleCompare (optional) Boolean flag indicating whatever 84 | * to use String.localeCampare method or not. Default set to true. 85 | * 86 | * 87 | * @option Array sortAppend (optional) An array containing forced sorting rules. 88 | * This option let's you specify a default sorting rule, which is 89 | * appended to user-selected rules. Default value: null 90 | * 91 | * @option Boolean widthFixed (optional) Boolean flag indicating if tablesorter 92 | * should apply fixed widths to the table columns. This is usefull when 93 | * using the pager companion plugin. This options requires the dimension 94 | * jquery plugin. Default value: false 95 | * 96 | * @option Boolean cancelSelection (optional) Boolean flag indicating if 97 | * tablesorter should cancel selection of the table headers text. 98 | * Default value: true 99 | * 100 | * @option Boolean debug (optional) Boolean flag indicating if tablesorter 101 | * should display debuging information usefull for development. 102 | * 103 | * @type jQuery 104 | * 105 | * @name tablesorter 106 | * 107 | * @cat Plugins/Tablesorter 108 | * 109 | * @author Christian Bach/christian.bach@polyester.se 110 | */ 111 | 112 | (function ($) { 113 | $.extend({ 114 | tablesorter: new (function () { 115 | var parsers = [], 116 | widgets = []; 117 | 118 | this.defaults = { 119 | cssHeader: "header", 120 | cssAsc: "headerSortUp", 121 | cssDesc: "headerSortDown", 122 | cssChildRow: "expand-child", 123 | sortInitialOrder: "asc", 124 | sortMultiSortKey: "shiftKey", 125 | sortForce: null, 126 | sortAppend: null, 127 | sortLocaleCompare: true, 128 | textExtraction: "simple", 129 | parsers: {}, 130 | widgets: [], 131 | widgetZebra: { 132 | css: ["even", "odd"], 133 | }, 134 | headers: {}, 135 | widthFixed: false, 136 | cancelSelection: true, 137 | sortList: [], 138 | headerList: [], 139 | dateFormat: "us", 140 | decimal: "/.|,/g", 141 | onRenderHeader: null, 142 | selectorHeaders: "thead th", 143 | debug: false, 144 | }; 145 | 146 | /* debuging utils */ 147 | 148 | function benchmark(s, d) { 149 | log(s + "," + (new Date().getTime() - d.getTime()) + "ms"); 150 | } 151 | 152 | this.benchmark = benchmark; 153 | 154 | function log(s) { 155 | if (typeof console != "undefined" && typeof console.debug != "undefined") { 156 | console.log(s); 157 | } else { 158 | alert(s); 159 | } 160 | } 161 | 162 | /* parsers utils */ 163 | 164 | function buildParserCache(table, $headers) { 165 | if (table.config.debug) { 166 | var parsersDebug = ""; 167 | } 168 | 169 | if (table.tBodies.length == 0) return; // In the case of empty tables 170 | var rows = table.tBodies[0].rows; 171 | 172 | if (rows[0]) { 173 | var list = [], 174 | cells = rows[0].cells, 175 | l = cells.length; 176 | 177 | for (var i = 0; i < l; i++) { 178 | var p = false; 179 | 180 | if ($.metadata && $($headers[i]).metadata() && $($headers[i]).metadata().sorter) { 181 | p = getParserById($($headers[i]).metadata().sorter); 182 | } else if (table.config.headers[i] && table.config.headers[i].sorter) { 183 | p = getParserById(table.config.headers[i].sorter); 184 | } 185 | if (!p) { 186 | p = detectParserForColumn(table, rows, -1, i); 187 | } 188 | 189 | if (table.config.debug) { 190 | parsersDebug += "column:" + i + " parser:" + p.id + "\n"; 191 | } 192 | 193 | list.push(p); 194 | } 195 | } 196 | 197 | if (table.config.debug) { 198 | log(parsersDebug); 199 | } 200 | 201 | return list; 202 | } 203 | 204 | function detectParserForColumn(table, rows, rowIndex, cellIndex) { 205 | var l = parsers.length, 206 | node = false, 207 | nodeValue = false, 208 | keepLooking = true; 209 | while (nodeValue == "" && keepLooking) { 210 | rowIndex++; 211 | if (rows[rowIndex]) { 212 | node = getNodeFromRowAndCellIndex(rows, rowIndex, cellIndex); 213 | nodeValue = trimAndGetNodeText(table.config, node); 214 | if (table.config.debug) { 215 | log("Checking if value was empty on row:" + rowIndex); 216 | } 217 | } else { 218 | keepLooking = false; 219 | } 220 | } 221 | for (var i = 1; i < l; i++) { 222 | if (parsers[i].is(nodeValue, table, node)) { 223 | return parsers[i]; 224 | } 225 | } 226 | // 0 is always the generic parser (text) 227 | return parsers[0]; 228 | } 229 | 230 | function getNodeFromRowAndCellIndex(rows, rowIndex, cellIndex) { 231 | return rows[rowIndex].cells[cellIndex]; 232 | } 233 | 234 | function trimAndGetNodeText(config, node) { 235 | return $.trim(getElementText(config, node)); 236 | } 237 | 238 | function getParserById(name) { 239 | var l = parsers.length; 240 | for (var i = 0; i < l; i++) { 241 | if (parsers[i].id.toLowerCase() == name.toLowerCase()) { 242 | return parsers[i]; 243 | } 244 | } 245 | return false; 246 | } 247 | 248 | /* utils */ 249 | 250 | function buildCache(table) { 251 | if (table.config.debug) { 252 | var cacheTime = new Date(); 253 | } 254 | 255 | var totalRows = (table.tBodies[0] && table.tBodies[0].rows.length) || 0, 256 | totalCells = (table.tBodies[0].rows[0] && table.tBodies[0].rows[0].cells.length) || 0, 257 | parsers = table.config.parsers, 258 | cache = { 259 | row: [], 260 | normalized: [], 261 | }; 262 | 263 | for (var i = 0; i < totalRows; ++i) { 264 | /** Add the table data to main data array */ 265 | var c = $(table.tBodies[0].rows[i]), 266 | cols = []; 267 | 268 | // if this is a child row, add it to the last row's children and 269 | // continue to the next row 270 | if (c.hasClass(table.config.cssChildRow)) { 271 | cache.row[cache.row.length - 1] = cache.row[cache.row.length - 1].add(c); 272 | // go to the next for loop 273 | continue; 274 | } 275 | 276 | cache.row.push(c); 277 | 278 | for (var j = 0; j < totalCells; ++j) { 279 | cols.push(parsers[j].format(getElementText(table.config, c[0].cells[j]), table, c[0].cells[j])); 280 | } 281 | 282 | cols.push(cache.normalized.length); // add position for rowCache 283 | cache.normalized.push(cols); 284 | cols = null; 285 | } 286 | 287 | if (table.config.debug) { 288 | benchmark("Building cache for " + totalRows + " rows:", cacheTime); 289 | } 290 | 291 | return cache; 292 | } 293 | 294 | function getElementText(config, node) { 295 | if (!node) return ""; 296 | 297 | var $node = $(node), 298 | data = $node.attr("data-sort-value"); 299 | if (data !== undefined) return data; 300 | 301 | var text = ""; 302 | 303 | if (!config.supportsTextContent) config.supportsTextContent = node.textContent || false; 304 | 305 | if (config.textExtraction == "simple") { 306 | if (config.supportsTextContent) { 307 | text = node.textContent; 308 | } else { 309 | if (node.childNodes[0] && node.childNodes[0].hasChildNodes()) { 310 | text = node.childNodes[0].innerHTML; 311 | } else { 312 | text = node.innerHTML; 313 | } 314 | } 315 | } else { 316 | if (typeof config.textExtraction == "function") { 317 | text = config.textExtraction(node); 318 | } else { 319 | text = $(node).text(); 320 | } 321 | } 322 | return text; 323 | } 324 | 325 | function appendToTable(table, cache) { 326 | if (table.config.debug) { 327 | var appendTime = new Date(); 328 | } 329 | 330 | var c = cache, 331 | r = c.row, 332 | n = c.normalized, 333 | totalRows = n.length, 334 | checkCell = n[0].length - 1, 335 | tableBody = $(table.tBodies[0]), 336 | rows = []; 337 | 338 | for (var i = 0; i < totalRows; i++) { 339 | var pos = n[i][checkCell]; 340 | 341 | rows.push(r[pos]); 342 | 343 | if (!table.config.appender) { 344 | //var o = ; 345 | var l = r[pos].length; 346 | for (var j = 0; j < l; j++) { 347 | tableBody[0].appendChild(r[pos][j]); 348 | } 349 | 350 | // 351 | } 352 | } 353 | 354 | if (table.config.appender) { 355 | table.config.appender(table, rows); 356 | } 357 | 358 | rows = null; 359 | 360 | if (table.config.debug) { 361 | benchmark("Rebuilt table:", appendTime); 362 | } 363 | 364 | // apply table widgets 365 | applyWidget(table); 366 | 367 | // trigger sortend 368 | setTimeout(function () { 369 | $(table).trigger("sortEnd"); 370 | }, 0); 371 | } 372 | 373 | function buildHeaders(table) { 374 | if (table.config.debug) { 375 | var time = new Date(); 376 | } 377 | 378 | var meta = $.metadata ? true : false; 379 | 380 | var header_index = computeTableHeaderCellIndexes(table); 381 | 382 | var $tableHeaders = $(table.config.selectorHeaders, table).each(function (index) { 383 | this.column = header_index[this.parentNode.rowIndex + "-" + this.cellIndex]; 384 | // this.column = index; 385 | this.order = formatSortingOrder(table.config.sortInitialOrder); 386 | 387 | this.count = this.order; 388 | 389 | if (checkHeaderMetadata(this) || checkHeaderOptions(table, index)) this.sortDisabled = true; 390 | if (checkHeaderOptionsSortingLocked(table, index)) this.order = this.lockedOrder = checkHeaderOptionsSortingLocked(table, index); 391 | 392 | if (!this.sortDisabled) { 393 | var $th = $(this).addClass(table.config.cssHeader); 394 | if (table.config.onRenderHeader) table.config.onRenderHeader.apply($th); 395 | } 396 | 397 | // add cell to headerList 398 | table.config.headerList[index] = this; 399 | }); 400 | 401 | if (table.config.debug) { 402 | benchmark("Built headers:", time); 403 | log($tableHeaders); 404 | } 405 | 406 | return $tableHeaders; 407 | } 408 | 409 | // from: 410 | // http://www.javascripttoolbox.com/lib/table/examples.php 411 | // http://www.javascripttoolbox.com/temp/table_cellindex.html 412 | 413 | function computeTableHeaderCellIndexes(t) { 414 | var matrix = []; 415 | var lookup = {}; 416 | var thead = t.getElementsByTagName("THEAD")[0]; 417 | var trs = thead.getElementsByTagName("TR"); 418 | 419 | for (var i = 0; i < trs.length; i++) { 420 | var cells = trs[i].cells; 421 | for (var j = 0; j < cells.length; j++) { 422 | var c = cells[j]; 423 | 424 | var rowIndex = c.parentNode.rowIndex; 425 | var cellId = rowIndex + "-" + c.cellIndex; 426 | var rowSpan = c.rowSpan || 1; 427 | var colSpan = c.colSpan || 1; 428 | var firstAvailCol; 429 | if (typeof matrix[rowIndex] == "undefined") { 430 | matrix[rowIndex] = []; 431 | } 432 | // Find first available column in the first row 433 | for (var k = 0; k < matrix[rowIndex].length + 1; k++) { 434 | if (typeof matrix[rowIndex][k] == "undefined") { 435 | firstAvailCol = k; 436 | break; 437 | } 438 | } 439 | lookup[cellId] = firstAvailCol; 440 | for (var k = rowIndex; k < rowIndex + rowSpan; k++) { 441 | if (typeof matrix[k] == "undefined") { 442 | matrix[k] = []; 443 | } 444 | var matrixrow = matrix[k]; 445 | for (var l = firstAvailCol; l < firstAvailCol + colSpan; l++) { 446 | matrixrow[l] = "x"; 447 | } 448 | } 449 | } 450 | } 451 | return lookup; 452 | } 453 | 454 | function checkCellColSpan(table, rows, row) { 455 | var arr = [], 456 | r = table.tHead.rows, 457 | c = r[row].cells; 458 | 459 | for (var i = 0; i < c.length; i++) { 460 | var cell = c[i]; 461 | 462 | if (cell.colSpan > 1) { 463 | arr = arr.concat(checkCellColSpan(table, headerArr, row++)); 464 | } else { 465 | if (table.tHead.length == 1 || cell.rowSpan > 1 || !r[row + 1]) { 466 | arr.push(cell); 467 | } 468 | // headerArr[row] = (i+row); 469 | } 470 | } 471 | return arr; 472 | } 473 | 474 | function checkHeaderMetadata(cell) { 475 | if ($.metadata && $(cell).metadata().sorter === false) { 476 | return true; 477 | } 478 | return false; 479 | } 480 | 481 | function checkHeaderOptions(table, i) { 482 | if (table.config.headers[i] && table.config.headers[i].sorter === false) { 483 | return true; 484 | } 485 | return false; 486 | } 487 | 488 | function checkHeaderOptionsSortingLocked(table, i) { 489 | if (table.config.headers[i] && table.config.headers[i].lockedOrder) return table.config.headers[i].lockedOrder; 490 | return false; 491 | } 492 | 493 | function applyWidget(table) { 494 | var c = table.config.widgets; 495 | var l = c.length; 496 | for (var i = 0; i < l; i++) { 497 | getWidgetById(c[i]).format(table); 498 | } 499 | } 500 | 501 | function getWidgetById(name) { 502 | var l = widgets.length; 503 | for (var i = 0; i < l; i++) { 504 | if (widgets[i].id.toLowerCase() == name.toLowerCase()) { 505 | return widgets[i]; 506 | } 507 | } 508 | } 509 | 510 | function formatSortingOrder(v) { 511 | if (typeof v != "Number") { 512 | return v.toLowerCase() == "desc" ? 1 : 0; 513 | } else { 514 | return v == 1 ? 1 : 0; 515 | } 516 | } 517 | 518 | function isValueInArray(v, a) { 519 | var l = a.length; 520 | for (var i = 0; i < l; i++) { 521 | if (a[i][0] == v) { 522 | return true; 523 | } 524 | } 525 | return false; 526 | } 527 | 528 | function setHeadersCss(table, $headers, list, css) { 529 | // remove all header information 530 | $headers.removeClass(css[0]).removeClass(css[1]); 531 | 532 | var h = []; 533 | $headers.each(function (offset) { 534 | if (!this.sortDisabled) { 535 | h[this.column] = $(this); 536 | } 537 | }); 538 | 539 | var l = list.length; 540 | for (var i = 0; i < l; i++) { 541 | h[list[i][0]].addClass(css[list[i][1]]); 542 | } 543 | } 544 | 545 | function fixColumnWidth(table, $headers) { 546 | var c = table.config; 547 | if (c.widthFixed) { 548 | var colgroup = $(""); 549 | $("tr:first td", table.tBodies[0]).each(function () { 550 | colgroup.append($("").css("width", $(this).width())); 551 | }); 552 | $(table).prepend(colgroup); 553 | } 554 | } 555 | 556 | function updateHeaderSortCount(table, sortList) { 557 | var c = table.config, 558 | l = sortList.length; 559 | for (var i = 0; i < l; i++) { 560 | var s = sortList[i], 561 | o = c.headerList[s[0]]; 562 | o.count = s[1]; 563 | o.count++; 564 | } 565 | } 566 | 567 | /* sorting methods */ 568 | 569 | var sortWrapper; 570 | 571 | function multisort(table, sortList, cache) { 572 | if (table.config.debug) { 573 | var sortTime = new Date(); 574 | } 575 | 576 | var dynamicExp = "sortWrapper = function(a,b) {", 577 | l = sortList.length; 578 | 579 | // TODO: inline functions. 580 | for (var i = 0; i < l; i++) { 581 | var c = sortList[i][0]; 582 | var order = sortList[i][1]; 583 | // var s = (getCachedSortType(table.config.parsers,c) == "text") ? 584 | // ((order == 0) ? "sortText" : "sortTextDesc") : ((order == 0) ? 585 | // "sortNumeric" : "sortNumericDesc"); 586 | // var s = (table.config.parsers[c].type == "text") ? ((order == 0) 587 | // ? makeSortText(c) : makeSortTextDesc(c)) : ((order == 0) ? 588 | // makeSortNumeric(c) : makeSortNumericDesc(c)); 589 | var s = 590 | table.config.parsers[c].type == "text" 591 | ? order == 0 592 | ? makeSortFunction("text", "asc", c) 593 | : makeSortFunction("text", "desc", c) 594 | : order == 0 595 | ? makeSortFunction("numeric", "asc", c) 596 | : makeSortFunction("numeric", "desc", c); 597 | var e = "e" + i; 598 | 599 | dynamicExp += "var " + e + " = " + s; // + "(a[" + c + "],b[" + c 600 | // + "]); "; 601 | dynamicExp += "if(" + e + ") { return " + e + "; } "; 602 | dynamicExp += "else { "; 603 | } 604 | 605 | // if value is the same keep orignal order 606 | var orgOrderCol = cache.normalized[0].length - 1; 607 | dynamicExp += "return a[" + orgOrderCol + "]-b[" + orgOrderCol + "];"; 608 | 609 | for (var i = 0; i < l; i++) { 610 | dynamicExp += "}; "; 611 | } 612 | 613 | dynamicExp += "return 0; "; 614 | dynamicExp += "}; "; 615 | 616 | if (table.config.debug) { 617 | benchmark("Evaling expression:" + dynamicExp, new Date()); 618 | } 619 | 620 | eval(dynamicExp); 621 | 622 | cache.normalized.sort(sortWrapper); 623 | 624 | if (table.config.debug) { 625 | benchmark("Sorting on " + sortList.toString() + " and dir " + order + " time:", sortTime); 626 | } 627 | 628 | return cache; 629 | } 630 | 631 | function makeSortFunction(type, direction, index) { 632 | var a = "a[" + index + "]", 633 | b = "b[" + index + "]"; 634 | if (type == "text" && direction == "asc") { 635 | return ( 636 | "(" + 637 | a + 638 | " == " + 639 | b + 640 | " ? 0 : (" + 641 | a + 642 | " === null ? Number.POSITIVE_INFINITY : (" + 643 | b + 644 | " === null ? Number.NEGATIVE_INFINITY : (" + 645 | a + 646 | " < " + 647 | b + 648 | ") ? -1 : 1 )));" 649 | ); 650 | } else if (type == "text" && direction == "desc") { 651 | return ( 652 | "(" + 653 | a + 654 | " == " + 655 | b + 656 | " ? 0 : (" + 657 | a + 658 | " === null ? Number.POSITIVE_INFINITY : (" + 659 | b + 660 | " === null ? Number.NEGATIVE_INFINITY : (" + 661 | b + 662 | " < " + 663 | a + 664 | ") ? -1 : 1 )));" 665 | ); 666 | } else if (type == "numeric" && direction == "asc") { 667 | return ( 668 | "(" + 669 | a + 670 | " === null && " + 671 | b + 672 | " === null) ? 0 :(" + 673 | a + 674 | " === null ? Number.POSITIVE_INFINITY : (" + 675 | b + 676 | " === null ? Number.NEGATIVE_INFINITY : " + 677 | a + 678 | " - " + 679 | b + 680 | "));" 681 | ); 682 | } else if (type == "numeric" && direction == "desc") { 683 | return ( 684 | "(" + 685 | a + 686 | " === null && " + 687 | b + 688 | " === null) ? 0 :(" + 689 | a + 690 | " === null ? Number.POSITIVE_INFINITY : (" + 691 | b + 692 | " === null ? Number.NEGATIVE_INFINITY : " + 693 | b + 694 | " - " + 695 | a + 696 | "));" 697 | ); 698 | } 699 | } 700 | 701 | function makeSortText(i) { 702 | return "((a[" + i + "] < b[" + i + "]) ? -1 : ((a[" + i + "] > b[" + i + "]) ? 1 : 0));"; 703 | } 704 | 705 | function makeSortTextDesc(i) { 706 | return "((b[" + i + "] < a[" + i + "]) ? -1 : ((b[" + i + "] > a[" + i + "]) ? 1 : 0));"; 707 | } 708 | 709 | function makeSortNumeric(i) { 710 | return "a[" + i + "]-b[" + i + "];"; 711 | } 712 | 713 | function makeSortNumericDesc(i) { 714 | return "b[" + i + "]-a[" + i + "];"; 715 | } 716 | 717 | function sortText(a, b) { 718 | if (table.config.sortLocaleCompare) return a.localeCompare(b); 719 | return a < b ? -1 : a > b ? 1 : 0; 720 | } 721 | 722 | function sortTextDesc(a, b) { 723 | if (table.config.sortLocaleCompare) return b.localeCompare(a); 724 | return b < a ? -1 : b > a ? 1 : 0; 725 | } 726 | 727 | function sortNumeric(a, b) { 728 | return a - b; 729 | } 730 | 731 | function sortNumericDesc(a, b) { 732 | return b - a; 733 | } 734 | 735 | function getCachedSortType(parsers, i) { 736 | return parsers[i].type; 737 | } /* public methods */ 738 | this.construct = function (settings) { 739 | return this.each(function () { 740 | // if no thead or tbody quit. 741 | if (!this.tHead || !this.tBodies) return; 742 | // declare 743 | var $this, 744 | $document, 745 | $headers, 746 | cache, 747 | config, 748 | shiftDown = 0, 749 | sortOrder; 750 | // new blank config object 751 | this.config = {}; 752 | // merge and extend. 753 | config = $.extend(this.config, $.tablesorter.defaults, settings); 754 | // store common expression for speed 755 | $this = $(this); 756 | // save the settings where they read 757 | $.data(this, "tablesorter", config); 758 | // build headers 759 | $headers = buildHeaders(this); 760 | // try to auto detect column type, and store in tables config 761 | this.config.parsers = buildParserCache(this, $headers); 762 | // build the cache for the tbody cells 763 | cache = buildCache(this); 764 | // get the css class names, could be done else where. 765 | var sortCSS = [config.cssDesc, config.cssAsc]; 766 | // fixate columns if the users supplies the fixedWidth option 767 | fixColumnWidth(this); 768 | // apply event handling to headers 769 | // this is to big, perhaps break it out? 770 | $headers 771 | .click(function (e) { 772 | var totalRows = ($this[0].tBodies[0] && $this[0].tBodies[0].rows.length) || 0; 773 | if (!this.sortDisabled && totalRows > 0) { 774 | // Only call sortStart if sorting is 775 | // enabled. 776 | $this.trigger("sortStart"); 777 | // store exp, for speed 778 | var $cell = $(this); 779 | // get current column index 780 | var i = this.column; 781 | // get current column sort order 782 | this.order = this.count++ % 2; 783 | // always sort on the locked order. 784 | if (this.lockedOrder) this.order = this.lockedOrder; 785 | 786 | // user only whants to sort on one 787 | // column 788 | if (!e[config.sortMultiSortKey]) { 789 | // flush the sort list 790 | config.sortList = []; 791 | if (config.sortForce != null) { 792 | var a = config.sortForce; 793 | for (var j = 0; j < a.length; j++) { 794 | if (a[j][0] != i) { 795 | config.sortList.push(a[j]); 796 | } 797 | } 798 | } 799 | // add column to sort list 800 | config.sortList.push([i, this.order]); 801 | // multi column sorting 802 | } else { 803 | // the user has clicked on an all 804 | // ready sortet column. 805 | if (isValueInArray(i, config.sortList)) { 806 | // revers the sorting direction 807 | // for all tables. 808 | for (var j = 0; j < config.sortList.length; j++) { 809 | var s = config.sortList[j], 810 | o = config.headerList[s[0]]; 811 | if (s[0] == i) { 812 | o.count = s[1]; 813 | o.count++; 814 | s[1] = o.count % 2; 815 | } 816 | } 817 | } else { 818 | // add column to sort list array 819 | config.sortList.push([i, this.order]); 820 | } 821 | } 822 | setTimeout(function () { 823 | // set css for headers 824 | setHeadersCss($this[0], $headers, config.sortList, sortCSS); 825 | appendToTable($this[0], multisort($this[0], config.sortList, cache)); 826 | }, 1); 827 | // stop normal event by returning false 828 | return false; 829 | } 830 | // cancel selection 831 | }) 832 | .mousedown(function () { 833 | if (config.cancelSelection) { 834 | this.onselectstart = function () { 835 | return false; 836 | }; 837 | return false; 838 | } 839 | }); 840 | // apply easy methods that trigger binded events 841 | $this 842 | .bind("update", function () { 843 | var me = this; 844 | setTimeout(function () { 845 | // rebuild parsers. 846 | me.config.parsers = buildParserCache(me, $headers); 847 | // rebuild the cache map 848 | cache = buildCache(me); 849 | }, 1); 850 | }) 851 | .bind("updateCell", function (e, cell) { 852 | var config = this.config; 853 | // get position from the dom. 854 | var pos = [cell.parentNode.rowIndex - 1, cell.cellIndex]; 855 | // update cache 856 | cache.normalized[pos[0]][pos[1]] = config.parsers[pos[1]].format(getElementText(config, cell), cell); 857 | }) 858 | .bind("sorton", function (e, list) { 859 | $(this).trigger("sortStart"); 860 | config.sortList = list; 861 | // update and store the sortlist 862 | var sortList = config.sortList; 863 | // update header count index 864 | updateHeaderSortCount(this, sortList); 865 | // set css for headers 866 | setHeadersCss(this, $headers, sortList, sortCSS); 867 | // sort the table and append it to the dom 868 | appendToTable(this, multisort(this, sortList, cache)); 869 | }) 870 | .bind("appendCache", function () { 871 | appendToTable(this, cache); 872 | }) 873 | .bind("applyWidgetId", function (e, id) { 874 | getWidgetById(id).format(this); 875 | }) 876 | .bind("applyWidgets", function () { 877 | // apply widgets 878 | applyWidget(this); 879 | }); 880 | if ($.metadata && $(this).metadata() && $(this).metadata().sortlist) { 881 | config.sortList = $(this).metadata().sortlist; 882 | } 883 | // if user has supplied a sort list to constructor. 884 | if (config.sortList.length > 0) { 885 | $this.trigger("sorton", [config.sortList]); 886 | } 887 | // apply widgets 888 | applyWidget(this); 889 | }); 890 | }; 891 | this.addParser = function (parser) { 892 | var l = parsers.length, 893 | a = true; 894 | for (var i = 0; i < l; i++) { 895 | if (parsers[i].id.toLowerCase() == parser.id.toLowerCase()) { 896 | a = false; 897 | } 898 | } 899 | if (a) { 900 | parsers.push(parser); 901 | } 902 | }; 903 | this.addWidget = function (widget) { 904 | widgets.push(widget); 905 | }; 906 | this.formatFloat = function (s) { 907 | var i = parseFloat(s); 908 | return isNaN(i) ? 0 : i; 909 | }; 910 | this.formatInt = function (s) { 911 | var i = parseInt(s); 912 | return isNaN(i) ? 0 : i; 913 | }; 914 | this.isDigit = function (s, config) { 915 | // replace all an wanted chars and match. 916 | return /^[-+]?\d*$/.test($.trim(s.replace(/[,.']/g, ""))); 917 | }; 918 | this.clearTableBody = function (table) { 919 | if ($.browser.msie) { 920 | while (table.tBodies[0].firstChild) { 921 | table.tBodies[0].removeChild(table.tBodies[0].firstChild); 922 | } 923 | } else { 924 | table.tBodies[0].innerHTML = ""; 925 | } 926 | }; 927 | })(), 928 | }); 929 | 930 | // extend plugin scope 931 | $.fn.extend({ 932 | tablesorter: $.tablesorter.construct, 933 | }); 934 | 935 | // make shortcut 936 | var ts = $.tablesorter; 937 | 938 | // add default parsers 939 | ts.addParser({ 940 | id: "text", 941 | is: function (s) { 942 | return true; 943 | }, 944 | format: function (s) { 945 | return $.trim(s.toLocaleLowerCase()); 946 | }, 947 | type: "text", 948 | }); 949 | 950 | ts.addParser({ 951 | id: "digit", 952 | is: function (s, table) { 953 | var c = table.config; 954 | return $.tablesorter.isDigit(s, c); 955 | }, 956 | format: function (s) { 957 | return $.tablesorter.formatFloat(s); 958 | }, 959 | type: "numeric", 960 | }); 961 | 962 | ts.addParser({ 963 | id: "currency", 964 | is: function (s) { 965 | return /^[£$€?.]/.test(s); 966 | }, 967 | format: function (s) { 968 | return $.tablesorter.formatFloat(s.replace(new RegExp(/[£$€]/g), "")); 969 | }, 970 | type: "numeric", 971 | }); 972 | 973 | ts.addParser({ 974 | id: "ipAddress", 975 | is: function (s) { 976 | return /^\d{2,3}[\.]\d{2,3}[\.]\d{2,3}[\.]\d{2,3}$/.test(s); 977 | }, 978 | format: function (s) { 979 | var a = s.split("."), 980 | r = "", 981 | l = a.length; 982 | for (var i = 0; i < l; i++) { 983 | var item = a[i]; 984 | if (item.length == 2) { 985 | r += "0" + item; 986 | } else { 987 | r += item; 988 | } 989 | } 990 | return $.tablesorter.formatFloat(r); 991 | }, 992 | type: "numeric", 993 | }); 994 | 995 | ts.addParser({ 996 | id: "url", 997 | is: function (s) { 998 | return /^(https?|ftp|file):\/\/$/.test(s); 999 | }, 1000 | format: function (s) { 1001 | return jQuery.trim(s.replace(new RegExp(/(https?|ftp|file):\/\//), "")); 1002 | }, 1003 | type: "text", 1004 | }); 1005 | 1006 | ts.addParser({ 1007 | id: "isoDate", 1008 | is: function (s) { 1009 | return /^\d{4}[\/-]\d{1,2}[\/-]\d{1,2}$/.test(s); 1010 | }, 1011 | format: function (s) { 1012 | return $.tablesorter.formatFloat(s != "" ? new Date(s.replace(new RegExp(/-/g), "/")).getTime() : "0"); 1013 | }, 1014 | type: "numeric", 1015 | }); 1016 | 1017 | ts.addParser({ 1018 | id: "percent", 1019 | is: function (s) { 1020 | return /\%$/.test($.trim(s)); 1021 | }, 1022 | format: function (s) { 1023 | return $.tablesorter.formatFloat(s.replace(new RegExp(/%/g), "")); 1024 | }, 1025 | type: "numeric", 1026 | }); 1027 | 1028 | ts.addParser({ 1029 | id: "usLongDate", 1030 | is: function (s) { 1031 | return s.match( 1032 | new RegExp(/^[A-Za-z]{3,10}\.? [0-9]{1,2}, ([0-9]{4}|'?[0-9]{2}) (([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(AM|PM)))$/) 1033 | ); 1034 | }, 1035 | format: function (s) { 1036 | return $.tablesorter.formatFloat(new Date(s).getTime()); 1037 | }, 1038 | type: "numeric", 1039 | }); 1040 | 1041 | ts.addParser({ 1042 | id: "shortDate", 1043 | is: function (s) { 1044 | return /\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}/.test(s); 1045 | }, 1046 | format: function (s, table) { 1047 | var c = table.config; 1048 | s = s.replace(/\-/g, "/"); 1049 | if (c.dateFormat == "us") { 1050 | // reformat the string in ISO format 1051 | s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/, "$3/$1/$2"); 1052 | } 1053 | if (c.dateFormat == "pt") { 1054 | s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/, "$3/$2/$1"); 1055 | } else if (c.dateFormat == "uk") { 1056 | // reformat the string in ISO format 1057 | s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/, "$3/$2/$1"); 1058 | } else if (c.dateFormat == "dd/mm/yy" || c.dateFormat == "dd-mm-yy") { 1059 | s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2})/, "$1/$2/$3"); 1060 | } 1061 | return $.tablesorter.formatFloat(new Date(s).getTime()); 1062 | }, 1063 | type: "numeric", 1064 | }); 1065 | ts.addParser({ 1066 | id: "time", 1067 | is: function (s) { 1068 | return /^(([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(am|pm)))$/.test(s); 1069 | }, 1070 | format: function (s) { 1071 | return $.tablesorter.formatFloat(new Date("2000/01/01 " + s).getTime()); 1072 | }, 1073 | type: "numeric", 1074 | }); 1075 | ts.addParser({ 1076 | id: "metadata", 1077 | is: function (s) { 1078 | return false; 1079 | }, 1080 | format: function (s, table, cell) { 1081 | var c = table.config, 1082 | p = !c.parserMetadataName ? "sortValue" : c.parserMetadataName; 1083 | return $(cell).metadata()[p]; 1084 | }, 1085 | type: "numeric", 1086 | }); 1087 | // add default widgets 1088 | ts.addWidget({ 1089 | id: "zebra", 1090 | format: function (table) { 1091 | if (table.config.debug) { 1092 | var time = new Date(); 1093 | } 1094 | var $tr, 1095 | row = -1, 1096 | odd; 1097 | // loop through the visible rows 1098 | $("tr:visible", table.tBodies[0]).each(function (i) { 1099 | $tr = $(this); 1100 | // style children rows the same way the parent 1101 | // row was styled 1102 | if (!$tr.hasClass(table.config.cssChildRow)) row++; 1103 | odd = row % 2 == 0; 1104 | $tr.removeClass(table.config.widgetZebra.css[odd ? 0 : 1]).addClass(table.config.widgetZebra.css[odd ? 1 : 0]); 1105 | }); 1106 | if (table.config.debug) { 1107 | $.tablesorter.benchmark("Applying Zebra widget", time); 1108 | } 1109 | }, 1110 | }); 1111 | })(jQuery); 1112 | -------------------------------------------------------------------------------- /src/components/Form.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import axios from "axios"; 4 | import _ from "lodash"; 5 | import queryString from "query-string"; 6 | import { ToastContainer, toast } from "react-toastify"; 7 | import "react-toastify/dist/ReactToastify.css"; 8 | import Harvester from "./Harvester"; 9 | import { capitalizeFirstLetter, backend } from "../util"; 10 | 11 | class Form extends React.Component { 12 | constructor(props) { 13 | super(props); 14 | this.state = { 15 | siteid: "en", 16 | project: "wikipedia", 17 | namespace: 0, 18 | p: 1, 19 | template: "", 20 | templateredirects: [], 21 | parameters: [""], 22 | pagetitle: false, 23 | limityear: 1926, 24 | rel: "geq", 25 | decimalmark: ".", 26 | category: "", 27 | depth: 1, 28 | manuallist: "", 29 | alreadyset: true, 30 | wikisyntax: true, 31 | errors: [], 32 | allowedunits: [], 33 | constraints: {}, 34 | addprefix: "", 35 | addsuffix: "", 36 | removeprefix: "", 37 | removesuffix: "", 38 | searchvalue: "", 39 | replacevalue: "", 40 | monolanguage: "", 41 | calendar: "Q1985727", 42 | ready: { 43 | siteinfo: false, 44 | pages: false, 45 | templateredirects: false, 46 | categorymembers: true, 47 | propertyinfo: false, 48 | constraints: false, 49 | itemswithproperty: false, 50 | buttonPressed: false, 51 | }, 52 | }; 53 | this.handleInputChange = this.handleInputChange.bind(this); 54 | this.handleSubmit = this.handleSubmit.bind(this); 55 | this.handleFocus = this.handleFocus.bind(this); 56 | this.loadSiteinfo = this.loadSiteinfo.bind(this); 57 | this.loadPages = this.loadPages.bind(this); 58 | this.loadTemplateredirects = this.loadTemplateredirects.bind(this); 59 | this.loadCategorymembers = this.loadCategorymembers.bind(this); 60 | this.loadPropertyinfo = this.loadPropertyinfo.bind(this); 61 | this.loadItemsWithProperty = this.loadItemsWithProperty.bind(this); 62 | this.loadConstraints = this.loadConstraints.bind(this); 63 | this.loadUnits = this.loadUnits.bind(this); 64 | this.markError = this.markError.bind(this); 65 | this.addAlias = this.addAlias.bind(this); 66 | this.markReady = this.markReady.bind(this); 67 | this.markUnready = this.markUnready.bind(this); 68 | this.prepareHarvester = this.prepareHarvester.bind(this); 69 | this.preloadForm = this.preloadForm.bind(this); 70 | this.oldVals = { p: 1 }; 71 | this.candidates = []; 72 | this.categorymembers = []; 73 | this.itemswithproperty = []; 74 | this.job = {}; 75 | this.tokenC = axios.CancelToken.source(); 76 | this.tokenP = axios.CancelToken.source(); 77 | this.tokenS = axios.CancelToken.source(); 78 | this.tokenT = axios.CancelToken.source(); 79 | } 80 | 81 | preloadForm(params, load) { 82 | let stringvariables = [ 83 | "siteid", 84 | "project", 85 | "namespace", 86 | "template", 87 | "addprefix", 88 | "removeprefix", 89 | "addsuffix", 90 | "removesuffix", 91 | "searchvalue", 92 | "replacevalue", 93 | "category", 94 | "depth", 95 | "calendar", 96 | "limityear", 97 | "rel", 98 | "unit", 99 | "decimalmark", 100 | "manuallist", 101 | "monolanguage", 102 | ]; 103 | let arrayvariables = ["parameters", "templateredirects"]; 104 | let booleanvariables = ["wikisyntax", "alreadyset", "pagetitle"]; 105 | let update = {}; 106 | for (let [key, value] of Object.entries(params)) { 107 | if (key === "p") { 108 | update[key] = value.replace("P", ""); 109 | } else if (key === "constraints") { 110 | update.constraint_temp = value.split("|"); 111 | } else if (key === "templateredirects") { 112 | update.templateredirects_temp = value.split("|"); 113 | } else if (booleanvariables.includes(key)) { 114 | update[key] = value === "1"; 115 | } else if (stringvariables.includes(key)) { 116 | update[key] = value; 117 | } else if (arrayvariables.includes(key)) { 118 | update[key] = value.split("|"); 119 | } 120 | } 121 | this.setState(update, () => { 122 | this.handleFocus(); 123 | if (load) { 124 | this.markReady("buttonPressed"); 125 | } 126 | }); 127 | } 128 | 129 | componentDidMount() { 130 | let params = queryString.parse(window.location.search); 131 | if (Object.keys(params).length > 0) { 132 | if ("htid" in params) { 133 | this.job.htid = params.htid; 134 | axios 135 | .get("https://pltools.toolforge.org/harvesttemplates/gethtshare.php", { 136 | params, 137 | }) 138 | .then(response => { 139 | this.preloadForm(response.data, true); 140 | }) 141 | .catch(error => { 142 | console.log(error); 143 | }); 144 | } else { 145 | this.preloadForm(params, false); 146 | } 147 | } 148 | } 149 | 150 | prepareHarvester() { 151 | let fcandidates = _.differenceBy(this.candidates, this.itemswithproperty, "qid"); 152 | if (this.categorymembers.length > 0) { 153 | fcandidates = fcandidates.filter(c => this.categorymembers.includes(c.title)); 154 | } 155 | if (this.state.manuallist !== "") { 156 | let manuallist = this.state.manuallist.split("\n").map(e => e.replace(/_/g, " ").trim()); 157 | let fcandidates1 = fcandidates.filter(c => manuallist.includes(c.title)); 158 | let fcandidates2 = fcandidates.filter(c => manuallist.includes(c.qid)); 159 | fcandidates = fcandidates1.concat(fcandidates2); 160 | this.job.manuallist = this.state.manuallist; 161 | } 162 | fcandidates.sort((a, b) => a.timestamp - b.timestamp); 163 | this.job.editgroup = Math.floor(Math.random() * Math.pow(2, 48)).toString(16); 164 | this.job.addprefix = this.state.addprefix; 165 | this.job.removeprefix = this.state.removeprefix; 166 | this.job.addsuffix = this.state.addsuffix; 167 | this.job.removesuffix = this.state.removesuffix; 168 | this.job.searchvalue = this.state.searchvalue; 169 | this.job.replacevalue = this.state.replacevalue; 170 | this.job.namespace = this.state.namespace; 171 | this.job.template = capitalizeFirstLetter(this.state.template).replace(/_/g, " "); 172 | this.job.parameters = this.state.parameters; 173 | this.job.pagetitle = this.state.pagetitle; 174 | this.job.calendar = this.state.calendar; 175 | this.job.limityear = this.state.limityear; 176 | this.job.rel = this.state.rel; 177 | this.job.siteid = this.state.siteid; 178 | this.job.project = this.state.project; 179 | this.job.unit = this.state.unit; 180 | this.job.decimalmark = this.state.decimalmark; 181 | this.job.category = this.state.category; 182 | this.job.depth = this.state.depth; 183 | this.job.alreadyset = this.state.alreadyset; 184 | this.job.wikisyntax = this.state.wikisyntax; 185 | this.job.monolanguage = this.state.monolanguage; 186 | this.job.constraints = []; 187 | for (let c of Object.values(this.state.constraints)) { 188 | if (c.value === true) { 189 | this.job.constraints.push(c.qid); 190 | } 191 | } 192 | this.job.templateredirects = []; 193 | for (let c of Object.values(this.state.templateredirects)) { 194 | if (c.value === true) { 195 | this.job.templateredirects.push(c.title); 196 | } 197 | } 198 | 199 | ReactDOM.render(, document.getElementById("harvester")); 200 | 201 | this.markUnready("buttonPressed"); 202 | } 203 | 204 | markReady(value) { 205 | let ready = this.state.ready; 206 | ready[value] = true; 207 | this.setState({ 208 | ready: ready, 209 | }); 210 | for (let val of Object.values(this.state.ready)) { 211 | if (val === false) { 212 | return 0; 213 | } 214 | } 215 | this.prepareHarvester(); 216 | } 217 | 218 | markUnready(value) { 219 | let ready = this.state.ready; 220 | ready[value] = false; 221 | this.setState({ 222 | ready: ready, 223 | }); 224 | } 225 | 226 | markError(field) { 227 | this.markUnready("buttonPressed"); 228 | let newerrors = this.state.errors; 229 | newerrors.push(field); 230 | this.setState({ 231 | errors: newerrors, 232 | }); 233 | } 234 | 235 | loadItemsWithProperty() { 236 | let data = { 237 | query: `SELECT ?item { ?item wdt:P${this.state.p} [] }`, //better but slower: p/ps 238 | format: "json", 239 | }; 240 | axios 241 | .get("https://query.wikidata.org/bigdata/namespace/wdq/sparql?", { 242 | params: data, 243 | }) 244 | .then(response => { 245 | this.itemswithproperty = response.data.results.bindings.map(x => ({ 246 | qid: x.item.value.replace("http://www.wikidata.org/entity/", ""), 247 | })); 248 | this.markReady("itemswithproperty"); 249 | }) 250 | .catch(error => { 251 | console.log("error loadItemsWithProperty", error, data.query); 252 | if (!axios.isCancel(error)) { 253 | this.markError("alreadyset"); 254 | } 255 | }); 256 | } 257 | 258 | loadUnits(allowedunits_id) { 259 | let allowedunits = []; 260 | if (allowedunits_id.length > 0) { 261 | if (allowedunits_id.includes("1")) { 262 | allowedunits.push( 263 | 266 | ); 267 | allowedunits_id = allowedunits_id.filter(item => item !== "1"); 268 | } 269 | let unit = "1"; 270 | let data = { 271 | action: "wbgetentities", 272 | ids: allowedunits_id.join("|"), 273 | props: "labels", 274 | languages: "en", 275 | format: "json", 276 | origin: "*", 277 | }; 278 | axios 279 | .get("https://www.wikidata.org/w/api.php", { 280 | params: data, 281 | cancelToken: this.tokenP.token, 282 | }) 283 | .then(response => { 284 | if ("entities" in response.data) { 285 | for (let q of Object.values(response.data.entities)) { 286 | if (unit === "1") { 287 | unit = q.id; 288 | } 289 | if ("en" in q.labels) { 290 | allowedunits.push( 291 | 294 | ); 295 | } else { 296 | allowedunits.push( 297 | 300 | ); 301 | } 302 | } 303 | } 304 | this.setState({ 305 | allowedunits: allowedunits, 306 | }); 307 | if (this.state.unit === undefined) { 308 | this.setState({ 309 | unit: unit, 310 | }); 311 | } 312 | this.markReady("propertyinfo"); 313 | }) 314 | .catch(error => { 315 | console.log(error); 316 | }); 317 | } else { 318 | toast.error(allowed unit constraint on property page missing, { position: toast.POSITION.TOP_CENTER }); 319 | this.markError("property"); 320 | } 321 | } 322 | 323 | loadConstraints(constraints_id, constraints_mandatory_id) { 324 | let constraints = {}; 325 | let data = { 326 | action: "wbgetentities", 327 | ids: constraints_id.concat(constraints_mandatory_id).join("|"), 328 | props: "labels", 329 | languages: "en", 330 | format: "json", 331 | origin: "*", 332 | }; 333 | axios 334 | .get("https://www.wikidata.org/w/api.php", { 335 | params: data, 336 | cancelToken: this.tokenP.token, 337 | }) 338 | .then(response => { 339 | if ("entities" in response.data) { 340 | for (let q of Object.values(response.data.entities)) { 341 | let label = q.id; 342 | if ("en" in q.labels) { 343 | label = q.labels.en.value; 344 | } 345 | if (constraints_mandatory_id.includes(q.id)) { 346 | constraints[q.id] = { qid: q.id, label: label, disabled: true, value: true }; 347 | } else if (this.state.constraint_temp) { 348 | constraints[q.id] = { qid: q.id, label: label, disabled: false, value: this.state.constraint_temp.includes(q.id) }; 349 | } else { 350 | constraints[q.id] = { qid: q.id, label: label, disabled: false, value: true }; 351 | } 352 | } 353 | } 354 | this.setState({ 355 | constraints: constraints, 356 | }); 357 | this.markReady("constraints"); 358 | }) 359 | .catch(error => { 360 | console.log(error); 361 | }); 362 | } 363 | 364 | loadPropertyinfo() { 365 | this.job.p = `P${this.state.p}`; 366 | let data = { 367 | action: "wbgetentities", 368 | ids: this.job.p, 369 | props: "claims|datatype|labels", 370 | languages: "en", 371 | format: "json", 372 | origin: "*", 373 | }; 374 | axios 375 | .get("https://www.wikidata.org/w/api.php", { 376 | params: data, 377 | cancelToken: this.tokenP.token, 378 | }) 379 | .then(response => { 380 | if (!("missing" in response.data.entities[this.job.p])) { 381 | if ("P31" in response.data.entities[this.job.p].claims) { 382 | for (let claim of response.data.entities[this.job.p].claims.P31) { 383 | let instance = claim.mainsnak.datavalue.value.id; 384 | if (instance === "Q37911748" || instance === "Q18644427") { 385 | toast.error(Property is deprecated, { position: toast.POSITION.TOP_CENTER }); 386 | this.markError("property"); 387 | return 0; 388 | } 389 | } 390 | } 391 | this.job.datatype = response.data.entities[this.job.p].datatype; 392 | const supportedDatatypes = [ 393 | "quantity", 394 | "time", 395 | "wikibase-item", 396 | "url", 397 | "string", 398 | "external-id", 399 | "commonsMedia", 400 | "monolingualtext", 401 | ]; 402 | if (!supportedDatatypes.includes(this.job.datatype)) { 403 | toast.error(not supported datatype: {this.job.datatype}, { position: toast.POSITION.TOP_CENTER }); 404 | this.markError("property"); 405 | return 0; 406 | } 407 | 408 | let label = "en" in response.data.entities[this.job.p].labels ? response.data.entities[this.job.p].labels.en.value : this.job.p; 409 | let propinfofield = ( 410 | 411 | {label} 412 | 413 | ); 414 | this.setState({ 415 | propinfofield: propinfofield, 416 | datatype: this.job.datatype, 417 | }); 418 | let allowedunits_id = []; 419 | let constraints_id = []; 420 | let constraints_mandatory_id = []; 421 | if ("P2302" in response.data.entities[this.job.p].claims) { 422 | for (let claim of response.data.entities[this.job.p].claims.P2302) { 423 | if (claim.mainsnak.datavalue.value.id === "Q21514353") { 424 | for (let c of claim.qualifiers.P2305) { 425 | if (c.snaktype === "novalue") { 426 | allowedunits_id.push("1"); 427 | } else if (c.snaktype === "value") { 428 | allowedunits_id.push(c.datavalue.value.id); 429 | } 430 | } 431 | } 432 | let cstatus = "normal"; 433 | if ("qualifiers" in claim && "P2316" in claim.qualifiers) { 434 | for (let c of claim.qualifiers.P2316) { 435 | if (c.datavalue.value.id === "Q21502408") { 436 | cstatus = "mandatory"; 437 | } else if (c.datavalue.value.id === "Q62026391") { 438 | cstatus = "suggestion"; 439 | } 440 | } 441 | } 442 | if (cstatus === "mandatory") { 443 | constraints_mandatory_id.push(claim.mainsnak.datavalue.value.id); 444 | } else if (cstatus === "normal") { 445 | constraints_id.push(claim.mainsnak.datavalue.value.id); 446 | } 447 | } 448 | } 449 | this.loadConstraints(constraints_id, constraints_mandatory_id); 450 | 451 | if (response.data.entities[this.job.p].datatype === "quantity") { 452 | this.loadUnits(allowedunits_id); 453 | } else { 454 | this.markReady("propertyinfo"); 455 | } 456 | } else { 457 | this.markError("property"); 458 | } 459 | }) 460 | .catch(error => { 461 | if (!axios.isCancel(error)) { 462 | console.log(error); 463 | } 464 | }); 465 | } 466 | 467 | loadCategorymembers() { 468 | const qs = { 469 | category: this.state.category, 470 | depth: this.state.depth, 471 | namespace: this.state.namespace, 472 | servername: `${this.state.siteid}.${this.state.project}.org`, 473 | }; 474 | axios 475 | .get(`${backend}categoryscan`, { params: qs }) 476 | .then(response => { 477 | this.categorymembers = response.data; 478 | this.markReady("categorymembers"); 479 | }) 480 | .catch(error => { 481 | if (!axios.isCancel(error)) { 482 | console.log(error); 483 | this.markError("category"); 484 | } 485 | }); 486 | } 487 | 488 | loadTemplateredirects() { 489 | let templateredirects = {}; 490 | let data = { 491 | action: "query", 492 | prop: "redirects", 493 | titles: `Template:${this.state.template}`, 494 | rdnamespace: 10, 495 | rdlimit: "max", 496 | format: "json", 497 | origin: "*", 498 | }; 499 | axios 500 | .get(`${this.job.site}/w/api.php`, { 501 | params: data, 502 | cancelToken: this.tokenT.token, 503 | }) 504 | .then(response => { 505 | for (let pa of Object.values(response.data.query.pages)) { 506 | if ("redirects" in pa) { 507 | for (let val of pa.redirects) { 508 | val.title = val.title.split(":")[1]; 509 | if (this.state.templateredirects_temp) { 510 | templateredirects[val.pageid] = { 511 | title: val.title, 512 | value: this.state.templateredirects_temp.includes(val.title), 513 | pageid: val.pageid, 514 | }; 515 | } else { 516 | templateredirects[val.pageid] = { title: val.title, value: true, pageid: val.pageid }; 517 | } 518 | } 519 | } 520 | } 521 | this.setState({ 522 | templateredirects: templateredirects, 523 | }); 524 | this.markReady("templateredirects"); 525 | }) 526 | .catch(error => { 527 | if (!axios.isCancel(error)) { 528 | console.log(error); 529 | this.markError("template"); 530 | } 531 | }); 532 | } 533 | 534 | loadPages(cont = 0) { 535 | let templateinfofield; 536 | if (this.state.template !== "") { 537 | templateinfofield = ( 538 | 543 | link 544 | 545 | ); 546 | } else { 547 | templateinfofield = ""; 548 | } 549 | this.setState({ 550 | templateinfofield: templateinfofield, 551 | }); 552 | let data = { 553 | action: "query", 554 | generator: "transcludedin", 555 | titles: `Template:${this.state.template}`, 556 | gtilimit: "max", 557 | gtinamespace: this.state.namespace, 558 | prop: "pageprops|revisions", 559 | rvprop: "timestamp", 560 | ppprop: "wikibase_item", 561 | gticontinue: cont, 562 | format: "json", 563 | origin: "*", 564 | }; 565 | axios 566 | .get(`${this.job.site}/w/api.php`, { 567 | params: data, 568 | cancelToken: this.tokenT.token, 569 | }) 570 | .then(response => { 571 | if ("query" in response.data) { 572 | for (let val of Object.values(response.data.query.pages)) { 573 | if ("pageprops" in val && "wikibase_item" in val.pageprops) { 574 | this.candidates.push({ 575 | pageid: val.pageid, 576 | title: val.title, 577 | qid: val.pageprops.wikibase_item, 578 | lastedit: val.revisions[0].timestamp, 579 | }); 580 | } else { 581 | this.candidates.push({ 582 | pageid: val.pageid, 583 | title: val.title, 584 | lastedit: val.revisions[0].timestamp, 585 | }); 586 | } 587 | } 588 | if ("continue" in response.data) { 589 | let cont = response.data.continue.gticontinue; 590 | this.loadPages(cont); 591 | } else { 592 | this.markReady("pages"); 593 | } 594 | } 595 | }) 596 | .catch(error => { 597 | if (!axios.isCancel(error)) { 598 | console.log(error); 599 | this.markError("template"); 600 | } 601 | }); 602 | } 603 | 604 | loadSiteinfo() { 605 | this.job.site = `https://${this.state.siteid}.${this.state.project}.org`; 606 | let data = { 607 | action: "query", 608 | meta: "siteinfo", 609 | siprop: "general|namespaces|namespacealiases", 610 | format: "json", 611 | origin: "*", 612 | }; 613 | axios 614 | .get(`${this.job.site}/w/api.php`, { 615 | params: data, 616 | cancelToken: this.tokenS.token, 617 | }) 618 | .then(response => { 619 | this.job.lang = response.data.query.general.lang; 620 | this.job.dbname = response.data.query.general.wikiid; 621 | let data = { 622 | query: `SELECT ?wiki { ?wiki wdt:P1800 '${this.job.dbname}'}`, 623 | format: "json", 624 | }; 625 | axios 626 | .get("https://query.wikidata.org/bigdata/namespace/wdq/sparql?", { 627 | params: data, 628 | }) 629 | .then(response => { 630 | if (response.data.results.bindings.length > 0) { 631 | this.job.wbeditionid = response.data.results.bindings[0].wiki.value.replace("http://www.wikidata.org/entity/", ""); 632 | this.markReady("siteinfo"); 633 | } else { 634 | this.markError("siteid"); 635 | this.markError("project"); 636 | } 637 | }) 638 | .catch(error => console.error(error)); 639 | 640 | let namespaces = {}; 641 | for (var ns in response.data.query.namespaces) { 642 | namespaces[ns] = response.data.query.namespaces[ns]["*"]; 643 | } 644 | this.job.fileprefixes = ["File", namespaces[6]]; 645 | this.job.templateprefixes = ["Template", namespaces[10]]; 646 | for (let i in response.data.query.namespacealiases) { 647 | if (response.data.query.namespacealiases[i].id === 6) { 648 | this.job.fileprefixes.push(response.data.query.namespacealiases[i]["*"]); 649 | } else if (response.data.query.namespacealiases[i].id === 10) { 650 | this.job.templateprefixes.push(response.data.query.namespacealiases[i]["*"]); 651 | } 652 | } 653 | }) 654 | .catch(error => { 655 | if (!axios.isCancel(error)) { 656 | this.markError("siteid"); 657 | this.markError("project"); 658 | } 659 | }); 660 | } 661 | 662 | addAlias(event) { 663 | event.preventDefault(); 664 | let newparameters = this.state.parameters; 665 | newparameters.push(""); 666 | this.setState({ 667 | parameters: newparameters, 668 | }); 669 | } 670 | 671 | handleSubmit(event) { 672 | event.preventDefault(); 673 | this.handleFocus(); 674 | this.job.htid = undefined; 675 | if (!this.state.parameters[0] && !this.state.aparameter1 && !this.state.pagetitle) { 676 | this.markError("parameters"); 677 | } else if (this.state.template === "") { 678 | this.markError("template"); 679 | } else if (this.state.siteid === "") { 680 | this.markError("siteid"); 681 | } else if (this.state.p === 1) { 682 | this.markError("property"); 683 | } else { 684 | ReactDOM.render(
, document.getElementById("harvester")); 685 | setTimeout(() => { 686 | this.markReady("buttonPressed"); 687 | }, 1000); 688 | } 689 | } 690 | 691 | handleInputChange(event) { 692 | this.markUnready("buttonPressed"); 693 | const target = event.target; 694 | const name = target.name; 695 | let value; 696 | if (name === "parameters") { 697 | value = this.state.parameters; 698 | value[target.getAttribute("listid")] = target.value; 699 | } else if (name === "templateredirects") { 700 | value = this.state.templateredirects; 701 | value[target.getAttribute("listid")].value = target.checked; 702 | } else if (name === "constraints") { 703 | value = this.state.constraints; 704 | value[target.getAttribute("listid")].value = target.checked; 705 | } else if (target.type === "checkbox") { 706 | value = target.checked; 707 | } else { 708 | value = target.value; 709 | } 710 | let newerrors = this.state.errors.filter(e => e !== name); 711 | if (name === "siteid") { 712 | newerrors = newerrors.filter(e => e !== "project"); 713 | } else if (name === "project") { 714 | newerrors = newerrors.filter(e => e !== "siteid"); 715 | } else if (name === "pagetitle") { 716 | newerrors = newerrors.filter(e => e !== "parameters"); 717 | } 718 | this.setState({ 719 | [name]: value, 720 | errors: newerrors, 721 | }); 722 | } 723 | 724 | handleFocus() { 725 | if (this.oldVals.siteid !== this.state.siteid || this.oldVals.project !== this.state.project) { 726 | this.oldVals.siteid = this.state.siteid; 727 | this.oldVals.project = this.state.project; 728 | this.oldVals.template = ""; //triggers loadPages() 729 | this.oldVals.category = ""; //triggers loadCategorymembers() 730 | this.tokenS.cancel("Operation canceled due to input changes"); 731 | this.tokenS = axios.CancelToken.source(); 732 | this.markUnready("siteinfo"); 733 | this.loadSiteinfo(); 734 | } 735 | if (this.oldVals.template !== this.state.template || this.oldVals.namespace !== this.state.namespace) { 736 | this.candidates = []; 737 | this.oldVals.template = this.state.template; 738 | this.oldVals.namespace = this.state.namespace; 739 | this.tokenT.cancel("Operation canceled due to input changes"); 740 | this.tokenT = axios.CancelToken.source(); 741 | this.markUnready("pages"); 742 | this.markUnready("templateredirects"); 743 | this.loadPages(); 744 | this.loadTemplateredirects(); 745 | } 746 | if (this.oldVals.p !== this.state.p || this.oldVals.alreadyset !== this.state.alreadyset) { 747 | this.itemswithproperty = []; 748 | this.tokenP.cancel("Operation canceled due to input changes"); 749 | this.tokenP = axios.CancelToken.source(); 750 | this.oldVals.alreadyset = this.state.alreadyset; 751 | if (this.oldVals.p !== this.state.p) { 752 | this.oldVals.p = this.state.p; 753 | this.markUnready("constraints"); 754 | this.markUnready("propertyinfo"); 755 | this.loadPropertyinfo(); 756 | } 757 | if (this.state.alreadyset === true) { 758 | this.markUnready("itemswithproperty"); 759 | this.loadItemsWithProperty(); 760 | } else { 761 | this.markReady("itemswithproperty"); 762 | } 763 | } 764 | if (this.oldVals.category !== this.state.category || this.oldVals.depth !== this.state.depth) { 765 | this.categorymembers = []; 766 | this.oldVals.category = this.state.category; 767 | this.oldVals.depth = this.state.depth; 768 | this.tokenC.cancel("Operation canceled due to input changes"); 769 | this.tokenC = axios.CancelToken.source(); 770 | if (this.state.category.length > 0) { 771 | this.markUnready("categorymembers"); 772 | this.loadCategorymembers(); 773 | } 774 | } 775 | } 776 | 777 | render() { 778 | let standardfield = []; 779 | for (let i = 0; i < this.state.parameters.length; i += 1) { 780 | standardfield.push( 781 | 791 | ); 792 | } 793 | let mainfield = ( 794 |
795 | {standardfield} 796 |
797 | 800 |
801 | ); 802 | let parameterfields; 803 | switch (this.state.datatype) { 804 | case "wikibase-item": 805 | parameterfields = ( 806 |
807 | {mainfield} 808 | 815 | try to match target page even without wikisyntax 816 |
817 | ); 818 | break; 819 | case "time": 820 | parameterfields = ( 821 |
822 | {mainfield}or 823 |
824 | {" "} 832 | year 833 |
834 | {" "} 842 | month 843 |
844 | {" "} 852 | day 853 |
854 | {" "} 858 | if year 859 | 863 | 871 |
872 | ); 873 | break; 874 | case "quantity": 875 | parameterfields = ( 876 |
877 | {mainfield} 878 |
879 | unit{" "} 880 | 883 |
884 | decimal mark 885 | 889 |
890 | ); 891 | break; 892 | case "monolingualtext": 893 | parameterfields = ( 894 |
895 | {mainfield} 896 |
897 | 904 | or use page title 905 |
906 | language code 907 | 915 |
916 | ); 917 | break; 918 | default: 919 | parameterfields = mainfield; 920 | } 921 | 922 | let constraintfield; 923 | if (Object.values(this.state.constraints).length > 0) { 924 | let constraintboxes = []; 925 | for (let c of Object.values(this.state.constraints)) { 926 | constraintboxes.push( 927 |
928 |
929 | 939 | {c.label} 940 |
941 |
942 | ); 943 | } 944 | constraintfield = ( 945 |
946 |

Check quality

947 | {constraintboxes} 948 |
949 | ); 950 | } 951 | let templateredirectfield; 952 | if (Object.values(this.state.templateredirects).length > 0) { 953 | let templateredirectboxes = []; 954 | for (let c of Object.values(this.state.templateredirects)) { 955 | templateredirectboxes.push( 956 |
957 | 966 | {c.title} 967 |
968 | ); 969 | } 970 | templateredirectfield = ( 971 |
972 |
include
973 |
{templateredirectboxes}
974 |
975 | ); 976 | } 977 | 978 | return ( 979 |
980 | 981 | 982 |
983 |
984 |

Load pages from

985 |
986 |
Wiki
987 |
988 | 996 | . 997 | 1014 | .org 1015 |
1016 |
1017 | 1018 |
1019 |
Namespace
1020 |
1021 | 1028 |
1029 |
1030 | 1031 |
1032 |

Define import

1033 | 1034 |
1035 |
Property
1036 |
1037 | 1045 | {this.state.propinfofield} 1046 |
1047 |
1048 | 1049 |
1050 |
Template
1051 |
1052 | 1060 | {this.state.templateinfofield} 1061 |
1062 |
1063 | 1064 | {templateredirectfield} 1065 | 1066 |
1067 |
Parameter
1068 |
{parameterfields}
1069 |
1070 |
1071 | 1072 |
1073 |

Modify values

1074 |
1075 |
add prefix
1076 |
1077 | 1084 |
1085 |
1086 | 1087 |
1088 |
remove prefix
1089 |
1090 | 1097 |
1098 |
1099 | 1100 |
1101 |
add suffix
1102 |
1103 | 1110 |
1111 |
1112 | 1113 |
1114 |
remove suffix
1115 |
1116 | 1123 |
1124 |
1125 | 1126 |
1127 |
regex search value
1128 |
1129 | 1136 |
1137 |
1138 | 1139 |
1140 |
regex replace value
1141 |
1142 | 1149 |
1150 |
1151 |
1152 | 1153 |
1154 |

Filter

1155 |
1156 |
Category
1157 |
1158 | 1166 |
1167 |
1168 | 1169 |
1170 |
Category depth
1171 |
1172 | 1173 |
1174 |
1175 | 1176 |
1177 |
Manual list
1178 |
1179 |