From 745f8aabb9741958d57d204823a380e1b861e8b6 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Thu, 22 Aug 2024 10:54:58 -0300 Subject: [PATCH 01/75] Moving static files to static directory --- {images => static}/Desenho_01.png | Bin {images => static}/Logo_Branco.svg | 0 {images => static}/Logo_Preto_Tagline.svg | 0 {bin => static}/authorship.js | 0 {bin => static}/copyvios.js | 0 {bin => static}/diff.css | 0 {images => static}/folder.svg | 0 {bin => static}/rtl.css | 0 {bin => static}/w3.css | 2 +- 9 files changed, 1 insertion(+), 1 deletion(-) rename {images => static}/Desenho_01.png (100%) rename {images => static}/Logo_Branco.svg (100%) rename {images => static}/Logo_Preto_Tagline.svg (100%) rename {bin => static}/authorship.js (100%) rename {bin => static}/copyvios.js (100%) rename {bin => static}/diff.css (100%) rename {images => static}/folder.svg (100%) rename {bin => static}/rtl.css (100%) rename {bin => static}/w3.css (99%) diff --git a/images/Desenho_01.png b/static/Desenho_01.png similarity index 100% rename from images/Desenho_01.png rename to static/Desenho_01.png diff --git a/images/Logo_Branco.svg b/static/Logo_Branco.svg similarity index 100% rename from images/Logo_Branco.svg rename to static/Logo_Branco.svg diff --git a/images/Logo_Preto_Tagline.svg b/static/Logo_Preto_Tagline.svg similarity index 100% rename from images/Logo_Preto_Tagline.svg rename to static/Logo_Preto_Tagline.svg diff --git a/bin/authorship.js b/static/authorship.js similarity index 100% rename from bin/authorship.js rename to static/authorship.js diff --git a/bin/copyvios.js b/static/copyvios.js similarity index 100% rename from bin/copyvios.js rename to static/copyvios.js diff --git a/bin/diff.css b/static/diff.css similarity index 100% rename from bin/diff.css rename to static/diff.css diff --git a/images/folder.svg b/static/folder.svg similarity index 100% rename from images/folder.svg rename to static/folder.svg diff --git a/bin/rtl.css b/static/rtl.css similarity index 100% rename from bin/rtl.css rename to static/rtl.css diff --git a/bin/w3.css b/static/w3.css similarity index 99% rename from bin/w3.css rename to static/w3.css index cbd40cf..41445f3 100644 --- a/bin/w3.css +++ b/static/w3.css @@ -256,4 +256,4 @@ h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin .w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important} .w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important} .w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important} -.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important} +.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important} \ No newline at end of file From 28a8344672b9c402482e2f963e2d6a2ad94a8abc Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Thu, 22 Aug 2024 11:18:57 -0300 Subject: [PATCH 02/75] Migrating from PHP to Python (initial) --- .github/workflows/build.yml | 21 - .github/workflows/translations.yml | 98 -- .gitignore | 18 + badge.php | 57 -- bin/backtrack.php | 148 --- bin/color.php | 26 - bin/compare.php | 508 ---------- bin/connect.php | 74 -- bin/counter.php | 284 ------ bin/credentials-lib.php | 152 --- bin/edits.php | 234 ----- bin/evaluators.php | 319 ------ bin/graph.php | 202 ---- bin/languages.php | 89 -- bin/load_edits.php | 235 ----- bin/load_reverts.php | 72 -- bin/load_users.php | 173 ---- bin/login.php | 369 ------- bin/manage_contests.php | 966 ------------------- bin/manage_login.php | 63 -- bin/modify.php | 360 ------- bin/password.php | 92 -- bin/protect.php | 23 - bin/recover.php | 220 ----- bin/sidebar.php | 115 --- bin/stats.php | 190 ---- bin/triage.php | 878 ----------------- contests/__init__.py | 0 contests/admin.py | 12 + contests/apps.py | 6 + contests/management/__init__.py | 0 contests/management/commands/__init__.py | 0 contests/management/commands/load_edits.py | 153 +++ contests/management/commands/load_reverts.py | 69 ++ contests/management/commands/load_users.py | 218 +++++ contests/management/commands/translate.py | 65 ++ contests/management/commands/update.py | 52 + contests/migrations/0001_initial.py | 126 +++ contests/migrations/0002_initial.py | 75 ++ contests/migrations/__init__.py | 0 contests/models.py | 135 +++ contests/templates/base.html | 124 +++ contests/templates/contest.html | 159 +++ contests/templates/home.html | 177 ++++ contests/templates/triage.html | 477 +++++++++ contests/tests.py | 3 + contests/triage.py | 356 +++++++ contests/urls.py | 9 + contests/views.py | 197 ++++ credentials/__init__.py | 0 credentials/admin.py | 10 + credentials/apps.py | 6 + credentials/migrations/0001_initial.py | 40 + credentials/migrations/__init__.py | 0 credentials/models.py | 38 + credentials/pipeline.py | 20 + credentials/tests.py | 3 + credentials/urls.py | 8 + credentials/views.py | 16 + font/GPL.txt | 343 ------- font/LICENCE.txt | 7 - font/LinLibertine_Re-4.7.3.otf | Bin 439388 -> 0 bytes font/OFL.txt | 98 -- git.sh | 61 -- index.php | 275 ------ manage.py | 22 + requirements.txt | 8 + sonar-project.properties | 12 - update.php | 102 -- wikiscore/__init__.py | 0 wikiscore/asgi.py | 16 + wikiscore/settings.py | 162 ++++ wikiscore/urls.py | 27 + wikiscore/wsgi.py | 16 + 74 files changed, 2823 insertions(+), 6866 deletions(-) delete mode 100644 .github/workflows/build.yml delete mode 100644 .github/workflows/translations.yml create mode 100644 .gitignore delete mode 100644 badge.php delete mode 100644 bin/backtrack.php delete mode 100644 bin/color.php delete mode 100644 bin/compare.php delete mode 100644 bin/connect.php delete mode 100644 bin/counter.php delete mode 100644 bin/credentials-lib.php delete mode 100644 bin/edits.php delete mode 100644 bin/evaluators.php delete mode 100644 bin/graph.php delete mode 100644 bin/languages.php delete mode 100644 bin/load_edits.php delete mode 100644 bin/load_reverts.php delete mode 100644 bin/load_users.php delete mode 100644 bin/login.php delete mode 100644 bin/manage_contests.php delete mode 100644 bin/manage_login.php delete mode 100644 bin/modify.php delete mode 100644 bin/password.php delete mode 100644 bin/protect.php delete mode 100644 bin/recover.php delete mode 100644 bin/sidebar.php delete mode 100644 bin/stats.php delete mode 100644 bin/triage.php create mode 100644 contests/__init__.py create mode 100644 contests/admin.py create mode 100644 contests/apps.py create mode 100644 contests/management/__init__.py create mode 100644 contests/management/commands/__init__.py create mode 100644 contests/management/commands/load_edits.py create mode 100644 contests/management/commands/load_reverts.py create mode 100644 contests/management/commands/load_users.py create mode 100644 contests/management/commands/translate.py create mode 100644 contests/management/commands/update.py create mode 100644 contests/migrations/0001_initial.py create mode 100644 contests/migrations/0002_initial.py create mode 100644 contests/migrations/__init__.py create mode 100644 contests/models.py create mode 100644 contests/templates/base.html create mode 100644 contests/templates/contest.html create mode 100644 contests/templates/home.html create mode 100644 contests/templates/triage.html create mode 100644 contests/tests.py create mode 100644 contests/triage.py create mode 100644 contests/urls.py create mode 100644 contests/views.py create mode 100644 credentials/__init__.py create mode 100644 credentials/admin.py create mode 100644 credentials/apps.py create mode 100644 credentials/migrations/0001_initial.py create mode 100644 credentials/migrations/__init__.py create mode 100644 credentials/models.py create mode 100644 credentials/pipeline.py create mode 100644 credentials/tests.py create mode 100644 credentials/urls.py create mode 100644 credentials/views.py delete mode 100644 font/GPL.txt delete mode 100644 font/LICENCE.txt delete mode 100644 font/LinLibertine_Re-4.7.3.otf delete mode 100644 font/OFL.txt delete mode 100644 git.sh delete mode 100644 index.php create mode 100644 manage.py create mode 100644 requirements.txt delete mode 100644 sonar-project.properties delete mode 100644 update.php create mode 100644 wikiscore/__init__.py create mode 100644 wikiscore/asgi.py create mode 100644 wikiscore/settings.py create mode 100644 wikiscore/urls.py create mode 100644 wikiscore/wsgi.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 3f5702f..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Build -on: - push: - branches: - - stable - - develop - pull_request: - types: [opened, synchronize, reopened] -jobs: - sonarcloud: - name: SonarCloud - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - - name: SonarCloud Scan - uses: SonarSource/sonarcloud-github-action@master - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/translations.yml b/.github/workflows/translations.yml deleted file mode 100644 index dbbcc2f..0000000 --- a/.github/workflows/translations.yml +++ /dev/null @@ -1,98 +0,0 @@ -name: Translation - -on: push - -jobs: - regex-json-verification: - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@v3 - - - name: Verify Regex and JSON - run: | - # Run regex verification - source_keys=() - while IFS= read -r match - do - # Extract the desired part from the match using awk - clean_match=$(echo "$match" | awk -F "['\"]" '{print $2}') - source_keys+=("$clean_match") - done < <(grep -r -P -o "(?<=§\()['\"]([\w-]+)['\"]" .) - - if [ ${#source_keys[@]} -gt 0 ] - then - echo "Regex pattern found in code." - else - echo "Regex pattern not found." - exit 1 - fi - - # Verify JSON keys - json_keys=($(jq -r 'keys[]' translations/en.json)) - if [ ${#json_keys[@]} -gt 0 ] - then - echo "JSON source found." - else - echo "JSON source not found." - exit 1 - fi - - #Matching keys - MISMATCH_FOUND=false - for source_key in "${source_keys[@]}" - do - KEY_FOUND=false - for json_key in "${json_keys[@]}" - do - if [ "$json_key" = "$source_key" ] - then - KEY_FOUND=true - break - fi - done - - if [ "$KEY_FOUND" = false ] - then - echo "Key ${source_key} is not defined" - MISMATCH_FOUND=true - fi - done - - if [ "$MISMATCH_FOUND" = true ] - then - echo "Some asked JSON keys were not found." - exit 1 - else - echo "All asked JSON keys were found." - fi - - #Inverse matching - UNUSED_KEYS=false - for json_key in "${json_keys[@]}" - do - KEY_FOUND=false - for source_key in "${source_keys[@]}" - do - if [ "$json_key" = "$source_key" ] - then - KEY_FOUND=true - break - fi - done - - if [ "$KEY_FOUND" = false ] - then - echo "Key ${json_key} is not used" - UNUSED_KEYS=true - fi - done - - if [ "$UNUSED_KEYS" = true ] - then - echo "Some JSON keys were not been used." - exit 1 - else - echo "All JSON keys were been used." - fi \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4373817 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +.idea/ +.env +__pycache__/ +*/__pycache__ +*.pyc +venv/ +.venv/ +Lib/ +Scripts/ +*.sqlite3 +*.pkl +*.coverage +htmlcov/ +media/ +.vscode/ +pytest.ini +coverage.xml +static/ \ No newline at end of file diff --git a/badge.php b/badge.php deleted file mode 100644 index 04e55ff..0000000 --- a/badge.php +++ /dev/null @@ -1,57 +0,0 @@ - "query", - "format" => "php", - "meta" => "messagegroupstats", - "formatversion" => "2", - "mgsgroup" => "wikiscore", - "mgssuppressempty" => "1", -]; - -// Decode API data -$data = file_get_contents("https://translatewiki.net/w/api.php?" . http_build_query($params)); -$data = unserialize($data); - -// Count the number of elements in "messagegroupstats" -$numElements = count($data['query']['messagegroupstats']); - -// Extract "translated" values and calculate sum -$translatedValues = array_column($data['query']['messagegroupstats'], 'translated'); -$sumTranslated = array_sum($translatedValues); - -// Extract the first "total" value -$firstTotal = $data['query']['messagegroupstats'][0]['total']; - -// Calculate percentage -$percentage = round( ( $sumTranslated / ( $firstTotal * $numElements ) ) * 100 ); - -// Pick color -switch (true) { - case $percentage == 100: - $color = 'green'; - break; - - case $percentage > 90: - $color = 'blue'; - break; - - case $percentage > 75: - $color = 'orange'; - break; - - default: - $color = 'red'; - break; -} - -// Prepare result array -$result = [ - 'label' => 'translated', - 'message' => $percentage . '%', - 'color' => $color, -]; - -// Convert result to JSON and output -echo json_encode($result, JSON_PRETTY_PRINT); diff --git a/bin/backtrack.php b/bin/backtrack.php deleted file mode 100644 index cb37e54..0000000 --- a/bin/backtrack.php +++ /dev/null @@ -1,148 +0,0 @@ - $edit['diff'], - "bytes" => $edit['bytes'], - 'timestamp' => $edit['edit_timestamp'] - ]; -} - -//Processa informações caso formulário tenha sido submetido -if (isset($_POST['diff'])) { - - //Monta query para atualizar banco de dados - $now = date('Y-m-d H:i:s'); - $update_statement = " - UPDATE - `{$contest['name_id']}__edits` - SET - `valid_user` = '1', - `obs` = CONCAT( - IFNULL(`obs`, ''), - 'Backtrack: ', - ?, - ' em ', - ?, - '\n' - ) - WHERE - `diff` = ? AND - `valid_user` IS NULL - "; - $update_query = mysqli_prepare($con, $update_statement); - mysqli_stmt_bind_param( - $update_query, - "ssi", - $_SESSION['user']['user_name'], - $now, - $_POST['diff'] - ); - - //Executa query - mysqli_stmt_execute($update_query); - if (mysqli_stmt_affected_rows($update_query) != 0) { - $output['success']['diff'] = addslashes($_POST['diff']); - } - mysqli_stmt_close($update_query); - mysqli_close($con); -} - -//Exibe página -?> - - - - <?=§('backtrack')?> - <?=$contest['name'];?> - - - - - - -
-
-
-

- -

-
-
- $case): ?> -
-
'>

-
- -
-
' style='filter: hue-rotate(180deg);'> -
{$case['enrollment_timestamp']}")?>
-
-
- -
- - - - - - - - - diff --git a/bin/color.php b/bin/color.php deleted file mode 100644 index 0eb3819..0000000 --- a/bin/color.php +++ /dev/null @@ -1,26 +0,0 @@ - - -.w3-color, -.w3-hover-color:hover { - color: #fff !important; - background-color: # !important -} - -.w3-text-color, -.w3-hover-text-color:hover { - color: # !important -} - -.w3-border-color, -.w3-hover-border-color:hover { - border-color: # !important -} diff --git a/bin/compare.php b/bin/compare.php deleted file mode 100644 index 410aa04..0000000 --- a/bin/compare.php +++ /dev/null @@ -1,508 +0,0 @@ -".§('compare-error')); - } else { - $update = true; - } -} - -if (isset($_POST['diff'])) { - $fix_query = mysqli_prepare( - $con, - "DELETE FROM - `{$contest['name_id']}__edits` - WHERE - `article` NOT IN ( - SELECT - `articleID` - FROM - `{$contest['name_id']}__articles` - ) AND - `diff` = ?" - ); - mysqli_stmt_bind_param($fix_query, "i", $_POST['diff']); - mysqli_stmt_execute($fix_query); - if (mysqli_stmt_affected_rows($fix_query) == 0) { - die("
".§('compare-inconsistency-error')); - } else { - $fixed = true; - } -} - - -//Verifica se a lista oficial e a categoria foram definidas -if (isset($contest['official_list_pageid']) && isset($contest['category_pageid'])) { - - //Define arrays - $list_cat = array(); - $list_official = array(); - - //Coleta lista de artigos na página do concurso - $list_api_params = [ - "action" => "query", - "format" => "php", - "generator" => "links", - "pageids" => $contest['official_list_pageid'], - "gplnamespace" => "0", - "gpllimit" => "max" - ]; - - $list_api = unserialize(file_get_contents($contest['api_endpoint']."?".http_build_query($list_api_params))); - $listmembers = $list_api['query']['pages'] ?? []; - foreach ($listmembers as $pagetitle) { - if (isset($pagetitle['missing'])) { continue; } - $list_official[] = $pagetitle['title']; - } - - //Coleta segunda página da lista, caso exista - while (isset($list_api['continue'])) { - $list_api_params = [ - "action" => "query", - "format" => "php", - "generator" => "links", - "pageids" => $contest['official_list_pageid'], - "gplnamespace" => "0", - "gpllimit" => "max", - "gplcontinue" => $list_api['continue']['gplcontinue'] - ]; - $list_api = unserialize( - file_get_contents($contest['api_endpoint']."?".http_build_query($list_api_params)) - ); - $listmembers = $list_api['query']['pages']; - foreach ($listmembers as $pagetitle) { - if (isset($pagetitle['missing'])) { continue; } - $list_official[] = $pagetitle['title']; - } - } - - //Coleta lista de artigos na categoria - $categorymembers_api_params = [ - "action" => "query", - "format" => "php", - "prop" => "pageprops", - "generator" => "categorymembers", - "ppprop" => "wikibase_item", - "cmnamespace" => "0", - "gcmpageid" => $contest['category_pageid'], - "gcmprop" => "title", - "gcmlimit" => "max" - ]; - $categorymembers_api = unserialize( - file_get_contents($contest['api_endpoint']."?".http_build_query($categorymembers_api_params)) - ); - foreach ($categorymembers_api['query']['pages'] ?? [] as $pageid) { - $list_cat[] = $pageid['title']; - if (!isset($pageid['pageprops']['wikibase_item'])) { - $list_wd[] = $pageid['title']; - } - } - - //Coleta segunda página da categoria, caso exista - while (isset($categorymembers_api['continue'])) { - $categorymembers_api_params = [ - "action" => "query", - "format" => "php", - "prop" => "pageprops", - "generator" => "categorymembers", - "ppprop" => "wikibase_item", - "cmnamespace" => "0", - "gcmpageid" => $contest['category_pageid'], - "gcmprop" => "title", - "gcmcontinue" => $categorymembers_api['continue']['gcmcontinue'] - ]; - $categorymembers_api = unserialize( - file_get_contents($contest['api_endpoint']."?".http_build_query($categorymembers_api_params)) - ); - foreach ($categorymembers_api['query']['pages'] as $pageid) { - $list_cat[] = $pageid['title']; - if (!isset($pageid['pageprops']['wikibase_item'])) { - $list_wd[] = $pageid['title']; - } - } - } - - //Processa listagens - $adicionar = array_diff($list_cat, $list_official); - asort($adicionar); - $remover = array_diff($list_official, $list_cat); - asort($remover); - -} else { - - //Define variáveis como em branco, para evitar erro - $adicionar = array(); - $remover = array(); -} - -//Seleciona categoria de páginas pendentes de eliminação -if ($contest['api_endpoint'] == 'https://pt.wikipedia.org/w/api.php') { - $ec_curid = '1001045'; - $other_dels = [ '3501865' , '2419924' , '5857294' ]; - - //Coleta páginas em EC - $ec_list_params = [ - "action" => "query", - "format" => "php", - "list" => "categorymembers", - "cmpageid" => $ec_curid, - "cmnamespace" => "4", - "cmlimit" => "max", - "cmprop" => "title" - ]; - $ec_list = unserialize( - file_get_contents($contest['api_endpoint']."?".http_build_query($ec_list_params)) - )['query']['categorymembers']; - foreach ($ec_list as $page) { - $deletion[] = substr($page['title'], 34); - } - - //Coleta outras páginas - foreach ($other_dels as $del_curid) { - $list_params = [ - "action" => "query", - "format" => "php", - "list" => "categorymembers", - "cmpageid" => $del_curid, - "cmnamespace" => "0", - "cmlimit" => "max", - "cmprop" => "title" - ]; - $list = unserialize( - file_get_contents($contest['api_endpoint']."?".http_build_query($list_params)) - )['query']['categorymembers'] ?? []; - foreach ($list as $page) { - $deletion[] = $page['title']; - } - } - - - //Processa listagem - $eliminar = array_intersect($deletion, $list_cat); - asort($eliminar); -} - -//Coleta lista de diffs inconsistentes -$inconsistency_query = mysqli_prepare( - $con, - "SELECT - `diff`, - `valid_edit`, - `timestamp` - FROM - `{$contest['name_id']}__edits` - WHERE - `article` NOT IN ( - SELECT - `articleID` - FROM - `{$contest['name_id']}__articles` - ) - AND `valid_user` = '1' - AND `reverted` IS NULL - ORDER BY `timestamp` ASC" -); -mysqli_stmt_execute($inconsistency_query); -$inconsistency_result = mysqli_stmt_get_result($inconsistency_query); - -//Coleta lista de diffs revertidos e validados -$reverted_query = mysqli_prepare( - $con, - "SELECT - `diff`, - `timestamp` - FROM - `{$contest['name_id']}__edits` - WHERE - `valid_edit` = '1' - AND `reverted` IS NOT NULL - ORDER BY `timestamp` ASC" -); -mysqli_stmt_execute($reverted_query); -$reverted_result = mysqli_stmt_get_result($reverted_query); - - -//Calcula contagem regressiva para atualização do banco de dados -if ($contest['end_time'] + 172800 < time()) { - $countdown = false; -} elseif ($contest['next_update'] > time() && !isset($update)) { - $countdown = $contest['next_update'] - time(); -} else { - $countdown = 0; -} -?> - - - - - <?=§('compare')?> - <?=$contest['name'];?> - - - - - - - - - - -
- -
- -

- -

-
-

- -

-
- -
- -
-

...

-
-

- - - - -

-
-
- -
-
-
-
-
-
-

-
- -
-
-
-
-
-

-
- -
-
-
-
-
-

-
- -
-
-
-
-
-
-
-

-
- -
-
-
-
-
-

-
-
    -
  • - -
  • - -
  • '> - -
    " - )'> - - '> -
    -
  • - -
-
-
-
-
-
-

-
-
    -
  • - -
  • - -
  • - - -
  • - -
-
-
-
- - - - - diff --git a/bin/connect.php b/bin/connect.php deleted file mode 100644 index ea812ba..0000000 --- a/bin/connect.php +++ /dev/null @@ -1,74 +0,0 @@ - {$contest['max_bytes_per_article']} - THEN {$contest['max_bytes_per_article']} - ELSE SUM(`{$contest['name_id']}__edits`.`bytes`) - END AS `bytes`, - COUNT(`{$contest['name_id']}__edits`.`valid_edit`) AS `valid_edits` - FROM - `{$contest['name_id']}__edits` - WHERE - `{$contest['name_id']}__edits`.`valid_edit` = '1' AND - `{$contest['name_id']}__edits`.`timestamp` < ( - CASE - WHEN '{$time_sql}' = '0' - THEN NOW() - ELSE '{$time_sql}' - END - ) - GROUP BY - `user_id`, - `article` - ORDER BY - NULL - ) AS edits_ruled - GROUP BY - edits_ruled.`user_id` - ORDER BY - NULL - ) AS t1 - LEFT JOIN ( - SELECT - `distinct`.`user_id`, - `distinct`.`article`, - SUM(`distinct`.`pictures`) AS `total pictures`, - CASE - WHEN {$contest['pictures_per_points']} = 0 - THEN 0 - ELSE FLOOR(SUM(`distinct`.`pictures`) / {$contest['pictures_per_points']}) - END AS `pictures points` - FROM - ( - SELECT - `{$contest['name_id']}__edits`.`user_id`, - `{$contest['name_id']}__edits`.`article`, - `{$contest['name_id']}__edits`.`pictures`, - `{$contest['name_id']}__edits`.`n` - FROM - `{$contest['name_id']}__edits` - WHERE - `{$contest['name_id']}__edits`.`pictures` IS NOT NULL AND - `{$contest['name_id']}__edits`.`timestamp` < ( - CASE WHEN '{$time_sql}' = '0' THEN NOW() ELSE '{$time_sql}' END - ) - GROUP BY - CASE - WHEN {$contest['pictures_mode']} = 0 - THEN `{$contest['name_id']}__edits`.`user_id` - END, - CASE - WHEN {$contest['pictures_mode']} = 0 - THEN `{$contest['name_id']}__edits`.`article` - END, - CASE - WHEN {$contest['pictures_mode']} = 0 - THEN `{$contest['name_id']}__edits`.`pictures` - ELSE `{$contest['name_id']}__edits`.`n` - END - ) AS `distinct` - GROUP BY - `distinct`.`user_id` - ORDER BY - NULL - ) AS t2 ON t1.`user_id` = t2.`user_id` - ) AS `points` ON `user_table`.`user_id` = `points`.`user_id` - ORDER BY - `points`.`total points` DESC, - `points`.`sum` DESC, - `user_table`.`user` ASC - ;" -); - - - - -?> - - - - - <?=§('counter')?> - <?=$contest['name'];?> - - - - - - - - - -
-
-

- - -

-
-
-
- - - - - -
-
-
- $contest['start_time']) : ?> - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
" - )'> - - - type='submit' - class='w3-btn w3-' - value=''> -
-
-
- -
-

-

-

-
- - - - - - diff --git a/bin/credentials-lib.php b/bin/credentials-lib.php deleted file mode 100644 index f29f0c2..0000000 --- a/bin/credentials-lib.php +++ /dev/null @@ -1,152 +0,0 @@ -pdo = new PDO( - "mysql:host=".DB_HOST.";dbname=".DB_NAME.";charset=".DB_CHARSET, - DB_USER, - DB_PASSWORD, - [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC ] - ); - } catch (Exception $ex) { exit($ex->getMessage()); } - } - - // (B) DESTRUCTOR - CLOSE CONNECTION - public function __destruct() - { - if ($this->stmt !== null) { $this->stmt = null; } - if ($this->pdo !== null) { $this->pdo = null; } - } - - // (C) GET USER BY EMAIL - public function getByEmail($email) - { - $this->stmt = $this->pdo->prepare("SELECT * FROM `".CONTEST."__credentials` WHERE `user_email`=?"); - $this->stmt->execute([$email]); - return $this->stmt->fetch(); - } - - // (D) VERIFY EMAIL PASSWORD - // SESSION MUST BE STARTED! - public function login($email, $password) - { - // (D1) ALREADY SIGNED IN - if (isset($_SESSION['user'])) { return true; } - - // (D2) GET USER - // (D3) USER STATUS - // (D4) VERIFY PASSWORD + REGISTER SESSION - $user = $this->getByEmail($email); - if ( - is_array($user) && - $user['user_status']!="P" && - password_verify($password, $user['user_password']) - ) { - $_SESSION['user'] = []; - foreach ($user as $k => $v) { - if ($k!="user_password") { $_SESSION['user'][$k] = $v; } - } - $_SESSION['user']['contest'] = CONTEST; - return true; - } - - // (D5) AUTH FAIL - return false; - } - - // (E) VERIFY USER PASSWORD - public function verify($email, $password) - { - $user = $this->getByEmail($email); - if ( - is_array($user) && - password_verify($password, $user['user_password']) - ) { - return true; - } - return false; - } - - // (F) SAVE USER - public function save($email, $pass, $id=null) - { - $name = strstr($email, "@", true); - if (!$name) { return false; } - $name = trim($name, "@"); - $contestid = CONTEST; - - if ($id===null) { - $sql = " - INSERT INTO `{$contestid}__credentials` - (`user_name`, `user_email`, `user_password`, `user_status`) - VALUES - (?,?,?,'A')"; - $data = [$name, $email, password_hash($pass, PASSWORD_DEFAULT)]; - } else { - $sql = " - UPDATE `{$contestid}__credentials` - SET `user_name`=?, `user_email`=?, `user_password`=? - WHERE `user_id`=? - "; - $data = [$name, $email, password_hash($pass, PASSWORD_DEFAULT), $id]; - } - try { - $this->stmt = $this->pdo->prepare($sql); - $this->stmt->execute($data); - return true; - } catch (Exception $ex) { - $this->error = $ex->getMessage(); - return false; - } - } -} - -// (G) DATABASE SETTINGS -define('DB_HOST', $db_host); -define('DB_NAME', $database); -define('DB_CHARSET', 'utf8'); -define('DB_USER', $db_user); -define('DB_PASSWORD', $db_pass); -define('CONTEST', $contest['name_id']); - -// (H) CREATE USER OBJECT -$USR = new Credentials(); diff --git a/bin/edits.php b/bin/edits.php deleted file mode 100644 index fb802d3..0000000 --- a/bin/edits.php +++ /dev/null @@ -1,234 +0,0 @@ - - - - - <?=§('triage-evaluated')?> - <?=$contest['name'];?> - - - - - - - - - - - - - - - -
-
-
-
-
- - -
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - ✓"; - } elseif ($x === 0) { - return ""; - } else { - return $x; - } - } - ?> - - - - - - - - - - - - - - - - - - - -
- -
-
-
- - diff --git a/bin/evaluators.php b/bin/evaluators.php deleted file mode 100644 index b1a2f62..0000000 --- a/bin/evaluators.php +++ /dev/null @@ -1,319 +0,0 @@ - $row['user_email'], - "status" => $row['user_status'], - "evaluated" => $row['evaluated'] - ]; -} - - -//Processa submissão de formulário -if ($_POST) { - - //Encerra script caso usuário não seja gestor - if ($_SESSION['user']['user_status'] != 'G') { - die(§('evaluators-denied')); - } - - //Cria acesso para novo avaliador - if (isset($_POST['email'])) { - - $email = filter_var($_POST['email'], FILTER_VALIDATE_EMAIL); - if (!$email) { - echo §("recover-notemail"); - die(); - } - - $password = bin2hex(random_bytes(14)); - - require_once "credentials-lib.php"; - $USR->save($email, $password); - - //Cria corpo do e-mail para avaliador - $message = "Oi!\n"; - $message .= "Seu e-mail foi cadastrado como avaliador do Wikiconcurso {$contest['name']}.\n"; - $message .= "Para acessar, utilize seu e-mail e a seguinte senha: {$password}\n"; - $message .= "Caso queira, a senha pode ser alterada ao clicar em 'Esqueci a senha' na tela de login.\n"; - $message .= "Para mais detalhes, consulte nosso manual na wiki do GitHub.\n\n"; - $message .= "Atenciosamente,\nWikiScore"; - $emailFile = fopen("php://temp", 'w+'); - $subject = "Wikiconcurso {$contest['name']} - Novo avaliador cadastrado"; - fwrite($emailFile, "Subject: " . $subject . "\n" . $message); - rewind($emailFile); - $fstat = fstat($emailFile); - $size = $fstat['size']; - - //Envia e-mail ao gestor - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, 'smtp://mail.tools.wmflabs.org:587'); - curl_setopt($ch, CURLOPT_MAIL_FROM, get_current_user()."@tools.wmflabs.org"); - curl_setopt($ch, CURLOPT_MAIL_RCPT, array($email)); - curl_setopt($ch, CURLOPT_INFILE, $emailFile); - curl_setopt($ch, CURLOPT_INFILESIZE, $size); - curl_setopt($ch, CURLOPT_UPLOAD, true); - curl_exec($ch); - fclose($emailFile); - curl_close($ch); - - //Retorna mensagem final - echo ""; - exit(); - } - - //Escapa nome de usuário submetido no formulário, ou encerra script caso nenhum nome tenha sido submetido - if ( - !isset($_POST['user']) && ( - !isset($_POST['on']) || - !isset($_POST['off'])|| - !isset($_POST['reset']) - ) - ) { - die(§('evaluators-missing')); - } - - //Processa query - $update_query = mysqli_prepare( - $con, - "UPDATE - `{$contest['name_id']}__credentials` - SET - `user_status` = ? - WHERE - `user_status` = ? - AND `user_name` = ?" - ); - mysqli_stmt_bind_param($update_query, "sss", $after, $before, $_POST['user']); - if (isset($_POST['off'])) { - $before = 'A'; - $after = 'P'; - } elseif (isset($_POST['on'])) { - $before = 'P'; - $after = 'A'; - } - mysqli_stmt_execute($update_query); - if (mysqli_stmt_affected_rows($update_query) != 0) { $output['success'] = true; } - - if (isset($_POST['reset'])) { - $reset_query = mysqli_prepare( - $con, - "DELETE FROM - `{$contest['name_id']}__edits` - WHERE - `by` = ?" - ); - mysqli_stmt_bind_param($reset_query, "s", $_POST['user']); - mysqli_stmt_execute($reset_query); - if (mysqli_stmt_affected_rows($reset_query) != 0) { - $refresh_query = mysqli_prepare( - $con, - "UPDATE - `manage__contests` - SET - `next_update` = NOW() - WHERE - `name_id` = ?" - ); - mysqli_stmt_bind_param($refresh_query, "s", $contest['name_id']); - mysqli_stmt_execute($refresh_query); - if (mysqli_stmt_affected_rows($refresh_query) != 0) { - $output['reseted'] = true; - } - } - } -} - -//Exibe página -?> - - - - <?=§('evaluators')?> - <?=$contest['name'];?> - - - - - - - -
-
-
-

-
-
- -
-
'> -

-
-
-
    -
  • - -
    - - -
    -
  • -
-
-
- -
-
'> -

-
-
-
    - $data): ?> -
  • - -
    -
    -
    - -
    - -
    - - - -
    - -
  • - -
-
-
-
-
'> -

-
-
-
    - $data): ?> -
  • - -
    -
    -
    - -
    - -
    - - - -
    -
    - - - -
    - -
  • - -
-
-
-
-
'> -

-
-
-
    - $data): ?> -
  • - -
    -
    -
    - -
    - -
    - - - -
    -
    - - - -
    - -
  • - -
-
-
-
- - - - - - - - - - - diff --git a/bin/graph.php b/bin/graph.php deleted file mode 100644 index e615d2a..0000000 --- a/bin/graph.php +++ /dev/null @@ -1,202 +0,0 @@ - {$contest['max_bytes_per_article']} THEN {$contest['max_bytes_per_article']} ELSE SUM(`{$contest['name_id']}__edits`.`bytes`) END AS `bytes`, COUNT(`{$contest['name_id']}__edits`.`valid_edit`) AS `valid_edits` FROM `{$contest['name_id']}__edits` WHERE `{$contest['name_id']}__edits`.`valid_edit` = '1' AND `{$contest['name_id']}__edits`.`timestamp` < ( CASE WHEN '{$time_sql}' = '0' THEN NOW() ELSE '{$time_sql}' END ) GROUP BY `user_id`, `article` ORDER BY NULL ) AS edits_ruled GROUP BY edits_ruled.`user_id` ORDER BY NULL ) AS t1 LEFT JOIN ( SELECT `distinct`.`user_id`, `distinct`.`article`, SUM(`distinct`.`pictures`) AS `total pictures`, CASE WHEN {$contest['pictures_per_points']} = 0 THEN 0 ELSE FLOOR(SUM(`distinct`.`pictures`) / {$contest['pictures_per_points']}) END AS `pictures points` FROM ( SELECT `{$contest['name_id']}__edits`.`user_id`, `{$contest['name_id']}__edits`.`article`, `{$contest['name_id']}__edits`.`pictures`, `{$contest['name_id']}__edits`.`n` FROM `{$contest['name_id']}__edits` WHERE `{$contest['name_id']}__edits`.`pictures` IS NOT NULL AND `{$contest['name_id']}__edits`.`timestamp` < ( CASE WHEN '{$time_sql}' = '0' THEN NOW() ELSE '{$time_sql}' END ) GROUP BY CASE WHEN {$contest['pictures_mode']} = 0 THEN `{$contest['name_id']}__edits`.`user_id` END, CASE WHEN {$contest['pictures_mode']} = 0 THEN `{$contest['name_id']}__edits`.`article` END, CASE WHEN {$contest['pictures_mode']} = 0 THEN `{$contest['name_id']}__edits`.`pictures` ELSE `{$contest['name_id']}__edits`.`n` END ) AS `distinct` GROUP BY `distinct`.`user_id` ORDER BY NULL ) AS t2 ON t1.`user_id` = t2.`user_id` ) AS `points` ON `user_table`.`user_id` = `points`.`user_id` ORDER BY `points`.`total points` DESC, `points`.`sum` DESC, `user_table`.`user` ASC;"; - - $run = mysqli_query($con, $query); - - while ($row = mysqli_fetch_assoc($run)) { - $data[$row["user"]] = $row["total points"]; - } - - return $data ?? false; -} - - -//Calcula dia de início do wikiconcurso -$start_day = date('Y-m-d', $contest['start_time']); - -//Caso o concurso ainda não tenha iniciado -if (time() < $contest['start_time']) { - - $datasets_graph = false; - -} else { - - //Calcula dia de término do wikiconcurso, ou dia atual caso o wikiconcurso ainda esteja ocorrendo - if (time() > $contest['end_time']) { - $end_time = date('Y-m-d', ($contest['end_time'] + 86400)); - $finished = true; - } else { - $end_time = date('Y-m-d'); - $finished = false; - } - - //Monta lista de dias em uma array - function generateDateRange($startDay, $endDate) { - $start = new DateTime($startDay); - $end = new DateTime($endDate); - - // Include the end date in the range - $end->modify('+1 day'); - - $interval = new DateInterval('P1D'); // 1 day interval - $dateRange = new DatePeriod($start, $interval, $end); - - $result = []; - foreach ($dateRange as $date) { - $result[] = $date->format('Y-m-d'); - } - - return $result; - } - $days = generateDateRange($start_day, $end_time); - - //Separa último dia da lista - $last_day = array_pop($days); - - //Coleta pontuação dos demais dias via query - foreach ($days as $time_sql) { - $data_graph[$time_sql] = querypoints($time_sql); - } - - //Coleta pontuação do último dia do último dia do wikiconcurso, caso ainda esteja ocorrendo - if ($finished) { - $data_graph[$last_day] = querypoints($last_day); - } - - //Coleta lista dos 9 primeiros colocados do último dia via query - $last_day = querypoints($last_day); - if (!$last_day) { - die("Erro durante consulta. Há edições avaliadas?"); - } - $last_day = array_keys($last_day); - $user_list = array_slice($last_day, 0, 9); - - //Designa 9 cores para as linhas do gráfico - $colors = [ - "#fd7f6f", - "#7eb0d5", - "#b2e061", - "#bd7ebe", - "#ffb55a", - "#ffee65", - "#beb9db", - "#fdcce5", - "#8bd3c7" - ]; - - //Loop para geração da série de pontos de cada usuário - foreach ($user_list as $user) { - - //Converte matriz Dia > Usuário > Pontos para Usuário > Dia > Pontos - foreach ($data_graph as $day_points) { - $user_points[] = $day_points[$user]; - } - - //Converte array de pontos do particante em uma string - $user_points = implode(', ', $user_points); - - //Coleta a primera cor da lista e remove de sua array - $color = array_shift($colors); - - //Gera dataset para uso no gráfico - $datasets_graph[] = " - { - label: '{$user}', - data: [ {$user_points} ], - fill: false, - borderColor: '{$color}', - tension: 0.1 - }"; - - //Apaga variável para uso no próximo loop - unset($user_points); - } - - //Converte array de datasets em uma única string - $datasets_graph = implode(',', $datasets_graph); - - //Calcula número total de dias do wikiconcurso e monta eixo X dos gráficos - $elapsed_days = floor((time() - $contest['start_time']) / 60 / 60 / 24); - $total_days = ceil(($contest['end_time'] - $contest['start_time']) / 60 / 60 / 24) + 2; - $all_days = array(); - for ($i=1; $i < $total_days; $i++) { array_push($all_days, $i); } - $all_days = implode(", ", $all_days); -} - -?> - - - - - <?=§('graph')?> - <?=$contest['name'];?> - - - - - - -
-

-

-
-
-
-
-
-

-
-
-
- - - - - - - - - -
-
- - diff --git a/bin/languages.php b/bin/languages.php deleted file mode 100644 index 18d3474..0000000 --- a/bin/languages.php +++ /dev/null @@ -1,89 +0,0 @@ - $quality) { - if ($quality === '') { - $languagesWithQuality[$language] = 1; - } - } - - // Sort languages by quality in descending order - arsort($languagesWithQuality, SORT_NUMERIC); - } - - // Find the first accepted language from the sorted list - foreach (array_keys($languagesWithQuality) as $acceptedLang) { - $acceptedLang = strtolower($acceptedLang); - if (in_array($acceptedLang, $acceptedLanguages)) { - $lang = $acceptedLang; - break; - } - } -} - -// Set a default language if none is determined -if (!isset($lang)) { - $lang = 'en'; -} - -// Load translation files -if ($lang != 'qqx') { - $translationFile = './translations/' . $lang . '.json'; - $trans = file_exists($translationFile) ? json_decode(file_get_contents($translationFile), true) : []; - $original = json_decode(file_get_contents('./translations/en.json'), true); -} - -// Main function -function §($item, ...$args) { - global $trans; - global $original; - global $lang; - - if ($lang == 'qqx') { - return $item; - } - $translatedString = $trans[$item] ?? $original[$item] ?? "$item"; - - // Replace placeholders ($1, $2, $3, etc.) with corresponding arguments - for ($i = 1; $i <= count($args); $i++) { - $placeholder = '$' . $i; - $translatedString = str_replace($placeholder, $args[$i - 1], $translatedString); - } - - return $translatedString; -} - -//RTL conversion -$rtl = ['he', 'skr-arab']; -$right = 'right'; -$left = 'left'; -if (in_array($lang, $rtl)) { - echo ''; - $right = 'left'; - $left = 'right'; -} \ No newline at end of file diff --git a/bin/load_edits.php b/bin/load_edits.php deleted file mode 100644 index fb70cd0..0000000 --- a/bin/load_edits.php +++ /dev/null @@ -1,235 +0,0 @@ -"; - -//Aumenta tempo limite de execução do script -set_time_limit(1200); - -//Verifica se PSID do PetScan foi fornecido ao invés de uma categoria comum -if (isset($contest['category_petscan'])) { - - //Recupera lista do PetScan - $petscan_list = json_decode( - file_get_contents("https://petscan.wmflabs.org/?format=json&psid=".$contest['category_petscan']), - true - ); - $petscan_list = $petscan_list['*']['0']['a']['*']; - - //Insere lista em uma array - $list = array(); - foreach ($petscan_list as $petscan_id) { - $list[] = [ - "id" => $petscan_id['id'], - "title" => $petscan_id['title'] - ]; - } - -} else { - - //Coleta lista de artigos na categoria - $categorymembers_api_params = [ - "action" => "query", - "format" => "php", - "list" => "categorymembers", - "cmnamespace" => "0", - "cmpageid" => $contest['category_pageid'], - "cmprop" => "ids|title", - "cmlimit" => "max" - ]; - $categorymembers_api = unserialize( - file_get_contents($contest['api_endpoint']."?".http_build_query($categorymembers_api_params)) - ); - - //Insere lista em uma array - $list = array(); - foreach ($categorymembers_api['query']['categorymembers'] as $pageid) { - $list[] = [ - "id" => $pageid['pageid'], - "title" => $pageid['title'] - ]; - } - - //Coleta segunda página da lista, caso exista - while (isset($categorymembers_api['continue'])) { - $categorymembers_api_params = [ - "action" => "query", - "format" => "php", - "list" => "categorymembers", - "cmnamespace" => "0", - "cmpageid" => $contest['category_pageid'], - "cmprop" => "ids|title", - "cmlimit" => "max", - "cmcontinue" => $categorymembers_api['continue']['cmcontinue'] - ]; - $categorymembers_api = unserialize( - file_get_contents($contest['api_endpoint']."?".http_build_query($categorymembers_api_params)) - ); - foreach ($categorymembers_api['query']['categorymembers'] as $pageid) { - $list[] = [ - "id" => $pageid['pageid'], - "title" => $pageid['title'] - ]; - } - } -} - -//Realiza manutenção do banco de dados -mysqli_query($con, "ALTER TABLE `{$contest['name_id']}__articles` ADD COLUMN IF NOT EXISTS `title` VARCHAR(100) NOT NULL AFTER `articleID`;"); -mysqli_query($con, "ALTER TABLE `{$contest['name_id']}__edits` ADD COLUMN IF NOT EXISTS `orig_bytes` INT(11) DEFAULT NULL AFTER `bytes`;"); -mysqli_query($con, "UPDATE `{$contest['name_id']}__edits` SET `orig_bytes` = CASE - WHEN `obs` REGEXP 'Aval: de (-?[0-9]+) para (-?[0-9]+)' THEN CAST(REGEXP_SUBSTR(`obs`, 'Aval: de \\\\K-?[0-9]*') AS SIGNED) - WHEN `obs` REGEXP 'bytes: (-?[0-9]+) -> (-?[0-9]+)' THEN CAST(REGEXP_SUBSTR(`obs`, 'bytes: \\\\K-?[0-9]*') AS SIGNED) - ELSE `bytes` END WHERE `orig_bytes` IS NULL;"); - -//Monta e executa query para atualização da tabela de artigos -mysqli_query($con, "TRUNCATE `{$contest['name_id']}__articles`;"); -$addarticle_statement = " - INSERT INTO - `{$contest['name_id']}__articles` (`articleID`, `title`) - VALUES - (?, ?) -"; -$addarticle_query = mysqli_prepare($con, $addarticle_statement); -mysqli_stmt_bind_param($addarticle_query, "is", $articleID, $title); -foreach ($list as $add_title) { - $articleID = $add_title['id']; - $title = $add_title['title']; - mysqli_stmt_execute($addarticle_query); -} -mysqli_stmt_close($addarticle_query); - -//Coleta lista de artigos -$articles_query = mysqli_prepare($con, "SELECT * FROM `{$contest['name_id']}__articles`;"); -mysqli_stmt_execute($articles_query); -$articles_result = mysqli_stmt_get_result($articles_query); -mysqli_stmt_close($articles_query); -if (mysqli_num_rows($articles_result) == 0) { die("No articles"); } - -//Coleta revisões já inseridas no banco de dados -$revision_list = array(); -$revision_query = mysqli_query( - $con, - "SELECT - `diff` - FROM - `{$contest['name_id']}__edits` - ORDER BY - `diff` - ;" -); -foreach (mysqli_fetch_all($revision_query, MYSQLI_ASSOC) as $diff) { $revision_list[] = $diff['diff']; } - -//Loop para análise de cada artigo -while ($row = mysqli_fetch_assoc($articles_result)) { - - //Coleta revisões do artigo - echo "\nCurID: ".$row["articleID"]; - $revisions_api_params = [ - "action" => "query", - "format" => "php", - "prop" => "revisions", - "rvprop" => "ids", - "rvlimit" => "max", - "rvstart" => date('Y-m-d\TH:i:s.000\Z', $contest['end_time']), - "rvend" => date('Y-m-d\TH:i:s.000\Z', $contest['start_time']), - "pageids" => $row["articleID"] - ]; - - $revisions_api = unserialize( - file_get_contents($contest['api_endpoint']."?".http_build_query($revisions_api_params)) - ); - $revisions_api = $revisions_api['query']['pages'][$row['articleID']]; - - //Verifica se artigo possui revisões dentro dos parâmetros escolhidos - if (!isset($revisions_api['revisions'])) { continue; } - - //Prepara query para atualizações no banco de dados - $addedit_statement = " - INSERT IGNORE INTO - `{$contest['name_id']}__edits` ( - `diff`, - `article`, - `timestamp`, - `user_id`, - `bytes`, - `orig_bytes`, - `new_page` - ) - VALUES - ( ? , ? , ? , ? , ? , ? , ? ) - "; - $addedit_query = mysqli_prepare($con, $addedit_statement); - mysqli_stmt_bind_param( - $addedit_query, - "iisiiii", - $addedit_diff, - $addedit_article, - $addedit_timestamp, - $addedit_user_id, - $addedit_bytes, - $addedit_bytes, - $addedit_newpage - ); - - //Loop para cada revisão do artigo - foreach ($revisions_api['revisions'] as $revision) { - - //Verifica se revisão ainda não existe no banco de dados - echo "\n- Diff: ".$revision['revid']; - if (in_array($revision['revid'], $revision_list)) { continue; } - echo " -> inserindo"; - - //Coleta dados de diferenciais da revisão - $compare_api_params = [ - "action" => "compare", - "format" => "php", - "torelative"=> "prev", - "prop" => "diffsize|size|title|user|timestamp", - "fromrev" => $revision['revid'] - ]; - $compare_api = unserialize( - file_get_contents( - $contest['api_endpoint']."?".http_build_query($compare_api_params) - ) - ); - - //Verifica se edição foi ocultada. Caso sim, define valores da edição como nulos - if (!isset($compare_api['compare'])) { - $compare_api['touserid'] = null; - $compare_api['tosize'] = null; - $compare_api['new_page'] = null; - $compare_api['totimestamp'] = null; - } else { - - //Acessa subarray - $compare_api = $compare_api['compare']; - - //Verifica se página é nova. - //Caso sim, mantem tamanho atual do artigo - //Caso não, subtrai tamanho anterior do artigo - if (!isset($compare_api['fromsize'])) { - $compare_api['new_page'] = 1; - } else { - $compare_api['new_page'] = null; - $compare_api['tosize'] = $compare_api['tosize'] - $compare_api['fromsize']; - } - - } - - //Executa query - $addedit_diff = $revision['revid']; - $addedit_article = $row["articleID"]; - $addedit_timestamp = $compare_api['totimestamp']; - $addedit_user_id = $compare_api['touserid']; - $addedit_bytes = $compare_api['tosize']; - $addedit_newpage = $compare_api['new_page']; - mysqli_stmt_execute($addedit_query); - if (mysqli_stmt_affected_rows($addedit_query) != 0) { - echo " -> feito!"; - } - } -} - -//Encerra conexão -mysqli_close($con); -echo "
Concluido! (1/3)
"; -echo "Próxima etapa, clique aqui."; diff --git a/bin/load_reverts.php b/bin/load_reverts.php deleted file mode 100644 index 98f7336..0000000 --- a/bin/load_reverts.php +++ /dev/null @@ -1,72 +0,0 @@ -"; - -//Aumenta tempo limite de execução do script -set_time_limit(1200); - -//Coleta lista de edições -$edits_statement = " - SELECT - `diff` - FROM - `{$contest['name_id']}__edits` - WHERE - `valid_user` IS NOT NULL AND - `reverted` IS NULL -"; -$edits_query = mysqli_prepare($con, $edits_statement); -mysqli_stmt_execute($edits_query); -$edits_result = mysqli_stmt_get_result($edits_query); - -//Verifica se existem edições cadastradas no banco de dados -$rows = mysqli_num_rows($edits_result); -if ($rows == 0) { die("No edits"); } - -//Prepara query para atualizações no banco de dados -$update_statement = " - UPDATE - `{$contest['name_id']}__edits` - SET - `reverted` = '1' - WHERE - `diff` = ? -"; -$update_query = mysqli_prepare($con, $update_statement); -mysqli_stmt_bind_param($update_query, "i", $diff); - -//Loop para análise de cada edição -while ($row = mysqli_fetch_assoc($edits_result)) { - - //Coleta tags da revisão - $revisions_api_params = [ - "action" => "query", - "format" => "php", - "prop" => "revisions", - "rvprop" => "sha1|tags", - "revids" => $row["diff"] - ]; - $revisions_api = file_get_contents($contest['api_endpoint']."?".http_build_query($revisions_api_params)); - $revisions_api = unserialize($revisions_api)['query']; - if (isset($revisions_api['pages'])) { - $revision = end($revisions_api['pages']); - $revision = $revision['revisions']['0'] ?? false; - } - - //Marca edição caso tenha sido revertida ou eliminada - if ( - isset($revisions_api['badrevids']) - || isset($revision['sha1hidden']) - || in_array('mw-reverted', $revision['tags']) - ) { - $diff = $row["diff"]; - mysqli_stmt_execute($update_query); - if (mysqli_stmt_affected_rows($update_query) != 0) { - echo "Marcada edição {$row["diff"]} como revertida.
"; - } - } -} - -//Encerra conexão -mysqli_stmt_close($update_query); -mysqli_close($con); -echo "Concluido! (3/3)"; diff --git a/bin/load_users.php b/bin/load_users.php deleted file mode 100644 index af932cd..0000000 --- a/bin/load_users.php +++ /dev/null @@ -1,173 +0,0 @@ -"; - -//Coleta planilha com usuarios inscritos -$outreach_params = [ - "course" => $contest['outreach_name'] -]; -$csv = file_get_contents( - 'https://outreachdashboard.wmflabs.org/course_students_csv?'.http_build_query($outreach_params) -); -if (is_null($csv)) { - die("Não foi possível encontrar a lista de usuários no Outreach."); -} - -//Converte csv em uma array -$csv_lines = str_getcsv($csv, "\n"); -foreach ($csv_lines as &$row) { - $row = str_getcsv($row, ","); -} -unset($row); - -//Separa a linha de cabeçalho da array -$csv_head = array_shift($csv_lines); - -//Conta a quantidade de colunas -$csv_num_rows = count($csv_head); - -//Consolida informações em uma array própria -$enrollments = array(); -foreach ($csv_lines as $csv_line) { - - //Caso existam linhas com menos colunas que o cabeçalho - if (count($csv_line) < $csv_num_rows) { - die("Erro! Uma das linhas possui menos colunas que o cabeçalho."); - } - - //Caso existam linhas com mais colunas que o cabeçalho - //Ocorre essencialmente com usernames contendo vírgulas que foram divididas em colunas diferentes - //Nesse caso, ele concatena as primeiras colunas até alcançar a quantidade correta de colunas - if (count($csv_line) > $csv_num_rows) { - $csv_line_extra_columns = 1 + count($csv_line) - $csv_num_rows; - $csv_line_first_column = array_splice($csv_line, 0, $csv_line_extra_columns); - $csv_line_first_column = implode(',', $csv_line_first_column); - array_unshift($csv_line, $csv_line_first_column); - } - - //Combina cabeçalho e linha na array própria - $enrollments[] = array_combine($csv_head, $csv_line); -} - -//Prepara lista de CentralUser IDs -foreach ($enrollments as $enrollment) { - $global_ids[] = $enrollment['global_id']; -} -$bindClause = implode(',', array_fill(0, count($global_ids), '?')); -$bindString = str_repeat('s', count($global_ids) + 1); - -//Coleta ID da wiki -$wiki_id_params = [ - "action" => "query", - "format" => "php", - "meta" => "siteinfo" -]; -$wiki_id = file_get_contents( - $contest['api_endpoint'] . "?" . http_build_query($wiki_id_params) -); -$wiki_id = unserialize($wiki_id)["query"]["general"]["wikiid"]; - -//Conecta ao banco de dados do CentralAuth -$con_centralauth = mysqli_connect('centralauth.analytics.db.svc.wikimedia.cloud', $db_user, $db_pass, 'centralauth_p'); -if (mysqli_connect_errno()) { - echo "Failed to connect to MySQL: " . mysqli_connect_error(); - exit(); -} - -//Prepara consulta SQL -$centralauth_query = mysqli_prepare( - $con_centralauth, - "SELECT - `lu_name`, `lu_local_id`, `lu_attached_timestamp`, `lu_global_id` - FROM - `localuser` - WHERE - `lu_wiki` = ? - AND `lu_global_id` IN ({$bindClause})" -); - -//Executa consulta e coleta os resultados -mysqli_stmt_bind_param($centralauth_query, $bindString, $wiki_id, ...$global_ids); -mysqli_stmt_execute($centralauth_query); -$centralauth_result = mysqli_stmt_get_result($centralauth_query); -mysqli_stmt_close($centralauth_query); - -while ($lu = mysqli_fetch_assoc($centralauth_result)) { - $centralauth_users[$lu["lu_global_id"]] = [ - "lu_name" => $lu['lu_name'], - "lu_local_id" => $lu['lu_local_id'], - "lu_attached" => $lu['lu_attached_timestamp'] - ]; -} - - - -//Limpa tabela de usuários -mysqli_query($con, "TRUNCATE `{$contest['name_id']}__users`;"); - -//Prepara query de inserção de usuários -$adduser_statement = " - INSERT INTO - `{$contest['name_id']}__users` (`user`, `timestamp`, `global_id`, `local_id`, `attached`) - VALUES - (?, ?, ?, ?, ?) -"; -$adduser_query = mysqli_prepare($con, $adduser_statement); -mysqli_stmt_bind_param($adduser_query, "ssiis", $row_user, $row_timestamp, $row_global_id, $row_local_id, $row_attached); - -//Prepara query de validação de edições -$validedit_statement = " - UPDATE - `{$contest['name_id']}__edits` - SET - `valid_user`='1' - WHERE - `user_id` = ? AND `timestamp` >= ? -"; -$validedit_query = mysqli_prepare($con, $validedit_statement); -mysqli_stmt_bind_param($validedit_query, "ss", $row_local_id, $row_timestamp); - -//Loop de execução das queries -foreach ($enrollments as $enrollment) { - $row_global_id = $enrollment['global_id']; - $row_timestamp = strftime('%Y-%m-%d %H:%M:%S', strtotime($enrollment['enrollment_timestamp'])); - $row_local_id = $centralauth_users[$enrollment['global_id']]['lu_local_id'] ?? null; - $row_user = $centralauth_users[$enrollment['global_id']]['lu_name'] ?? $enrollment['username'] ?? null; - if (isset($centralauth_users[$enrollment['global_id']]['lu_attached'])) { - $row_attached = strftime('%Y-%m-%d %H:%M:%S', strtotime($centralauth_users[$enrollment['global_id']]['lu_attached'])); - } else { - $row_attached = null; - } - - mysqli_stmt_execute($adduser_query); - mysqli_stmt_execute($validedit_query); - - if (mysqli_stmt_affected_rows($adduser_query) != 0) { - echo "\nInserido participante {$row_user}"; - } - - if (mysqli_stmt_affected_rows($validedit_query) != 0) { - echo "\n- Ativando ".mysqli_stmt_affected_rows($validedit_query)." edições"; - } -} - -//Encerra queries -mysqli_stmt_close($adduser_query); -mysqli_stmt_close($validedit_query); - -//Destrava edições que porventura ainda estejam travadas -mysqli_query( - $con, - "UPDATE - `{$contest['name_id']}__edits` - SET - `by` = NULL, - `when` = NULL - WHERE - `by` LIKE 'hold-%' OR - `by` LIKE 'skip-%';" -); - -//Encerra conexão -mysqli_close($con); -echo "
Concluido! (2/3)
"; -echo "Próxima etapa, clique aqui."; diff --git a/bin/login.php b/bin/login.php deleted file mode 100644 index 80e428b..0000000 --- a/bin/login.php +++ /dev/null @@ -1,369 +0,0 @@ -login($_POST['email'], $_POST['password']); -} - -// (B) REDIRECT USER IF SIGNED IN -if (isset($_SESSION['user'])) { - header("Location: index.php?lang=$lang&contest=".$_GET['contest']."&page=triage"); - exit(); -} - -// (C) SHOW LOGIN FORM OTHERWISE - -//Calcula número total de dias do wikiconcurso e monta eixo X dos gráficos -$elapsed_days = floor((time() - $contest['start_time']) / 60 / 60 / 24); -$total_days = ceil(($contest['end_time'] - $contest['start_time']) / 60 / 60 / 24) + 2; -for ($i=1; $i < $total_days; $i++) { - $all_days[] = $i; -} -$all_days = implode(", ", $all_days); - -//Define faixa de dias para queries dos gráficos -$start_day = date('Y-m-d', $contest['start_time']); -$end_day = date('Y-m-d', $contest['end_time']); -mysqli_query($con, "SET @date_min = '{$start_day}';"); -mysqli_query($con, "SET @date_max = '{$end_day}';"); - -//Processa queries para gráficos -$date_generator = - "SELECT - DATE_ADD(@date_min, INTERVAL(@i:= @i + 1) - 1 DAY) AS `date` - FROM - information_schema.columns, ( - SELECT @i:= 0 - ) gen_sub - WHERE - DATE_ADD(@date_min, INTERVAL @i DAY) BETWEEN @date_min AND @date_max"; - -$total_edits = mysqli_query( - $con, - "SELECT - date_generator.date as the_date, - IFNULL(COUNT(`{$contest['name_id']}__edits`.n), 0) as count - from ( {$date_generator} ) date_generator - left join `{$contest['name_id']}__edits` on DATE(`timestamp`) = date_generator.date - GROUP BY date; -"); -if ($total_edits != false) { - while ($row = mysqli_fetch_assoc($total_edits)) $total_edits_rows[] = $row['count']; - array_splice($total_edits_rows, $elapsed_days); - $total_edits_rows = implode(", ", $total_edits_rows); -} else { - $total_edits_rows = ''; -} - - -$valid_edits = mysqli_query($con, " - SELECT - date_generator.date as the_date, - IFNULL(COUNT(`queried`.n), 0) as count - from ( {$date_generator} ) date_generator - left join (SELECT `n`, DATE(`timestamp`) AS date_timestamp from `{$contest['name_id']}__edits` - WHERE `valid_edit` = 1) AS queried on date_timestamp = date_generator.date - GROUP BY date; -"); -if ($valid_edits != false) { - while ($row = mysqli_fetch_assoc($valid_edits)) $valid_edits_rows[] = $row['count']; - array_splice($valid_edits_rows, $elapsed_days); - $valid_edits_rows = implode(", ", $valid_edits_rows); -} else { - $valid_edits_rows = ''; -} - -$new_articles = mysqli_query($con, " - SELECT - date_generator.date as the_date, - IFNULL(COUNT(`queried`.n), 0) as count - from ( {$date_generator} ) date_generator - left join (SELECT `n`, DATE(`timestamp`) AS date_timestamp from `{$contest['name_id']}__edits` - WHERE `new_page` = 1) AS queried on date_timestamp = date_generator.date - GROUP BY date; -"); -if ($new_articles != false) { - while ($row = mysqli_fetch_assoc($new_articles)) $new_articles_rows[] = $row['count']; - array_splice($new_articles_rows, $elapsed_days); - $new_articles_rows = implode(", ", $new_articles_rows); -} else { - $new_articles_rows = ''; -} - -$new_participants = mysqli_query($con, " - SELECT - date_generator.date as the_date, - IFNULL(COUNT(`queried`.n), 0) as count - from ( {$date_generator} ) date_generator - left join (SELECT `n`, DATE(`timestamp`) AS date_timestamp from `{$contest['name_id']}__users` - ) AS queried on date_timestamp = date_generator.date - GROUP BY date; -"); -if ($new_participants) { - while ($row = mysqli_fetch_assoc($new_participants)) { - $new_participants_rows[] = $row['count']; - } - array_splice($new_participants_rows, $elapsed_days); - $new_participants_rows = implode(", ", $new_participants_rows); -} else { - $new_participants_rows = ''; -} - -$new_bytes = mysqli_query($con, " - SELECT - date_generator.date as the_date, - IFNULL(SUM(`queried`.`bytes`) / 1024, 0) as count - from ( {$date_generator} ) date_generator - left join (SELECT `n`, DATE(`timestamp`) AS date_timestamp, `bytes`, `valid_edit` from `{$contest['name_id']}__edits` - WHERE `bytes` > 0) as `queried` on `queried`.date_timestamp = date_generator.date - GROUP BY date; -"); -if ($new_bytes != false) { - while ($row = mysqli_fetch_assoc($new_bytes)) $new_bytes_rows[] = $row['count']; - array_splice($new_bytes_rows, $elapsed_days); - $new_bytes_rows = implode(", ", $new_bytes_rows); -} else { - $new_bytes_rows = ''; -} - -$valid_bytes = mysqli_query($con, " - SELECT - date_generator.date as the_date, - IFNULL(SUM(`queried`.`bytes`) / 1024, 0) as count - from ( {$date_generator} ) date_generator - left join (SELECT `n`, DATE(`timestamp`) AS date_timestamp, `bytes`, `valid_edit` from `{$contest['name_id']}__edits` - WHERE `valid_edit` = 1) as `queried` on `queried`.date_timestamp = date_generator.date - GROUP BY date; -"); -if ($valid_bytes != false) { - while ($row = mysqli_fetch_assoc($valid_bytes)) $valid_bytes_rows[] = $row['count']; - array_splice($valid_bytes_rows, $elapsed_days); - $valid_bytes_rows = implode(", ", $valid_bytes_rows); -} else { - $valid_bytes_rows = ''; -} -?> - - - - - <?=$contest['name'];?> - - - - - - - - - - - - -
- logo - - - -   - - - - - - -
-
- - -
- $contest['start_time']) { - require_once "stats.php"; - } ?> -
-
-
- -
-
-
-
- -
-
-
-
- -
-
-
-
-
-
-
- - - - - - -
-
-
- - - - - -
-
-
- -
-
-
- - - diff --git a/bin/manage_contests.php b/bin/manage_contests.php deleted file mode 100644 index c0c3faf..0000000 --- a/bin/manage_contests.php +++ /dev/null @@ -1,966 +0,0 @@ -alert('{$finalMessage}');window.location.href = window.location.href;"; - -//Processo para reiniciar/apagar concurso -} elseif (isset($_POST['do_restart']) || isset($_POST['do_delete']) || isset($_POST['do_edit'])) { - - //Valida código interno submetido - preg_match('/^[a-z_]{1,30}$/', $_POST['name_id'], $name_id); - if (!isset($name_id['0'])) die(§('manage-invalidcode')); - $name_id = $name_id['0']; - - //Valida se usuário pertence ao grupo relacionado ao concurso - if (!isset($contests_array[$name_id])) die(§('manage-notfound')); - if ( - $_SESSION['user']["user_group"] !== "ALL" && - $_SESSION['user']["user_group"] !== $contests_array[$name_id]['group'] - ) die(§('manage-unauthorized')); - - //Reinicia concurso - if (isset($_POST['do_restart'])) { - - //Reinicia tabelas do concurso, mas mantem a tabela de credenciais - mysqli_query($con, "TRUNCATE TABLE `{$name_id}__edits`;"); - mysqli_query($con, "TRUNCATE TABLE `{$name_id}__users`;"); - mysqli_query($con, "TRUNCATE TABLE `{$name_id}__articles`;"); - - //Retorna mensagem final - echo ""; - - //Apaga concurso - } elseif (isset($_POST['do_delete'])) { - - //Apaga tabelas do concurso - mysqli_query($con, "DROP TABLE `{$name_id}__edits`, `{$name_id}__users`, `{$name_id}__articles`, `{$name_id}__credentials`;"); - - //Apaga registro na tabela de concursos - $delete_statement = - "DELETE FROM - `manage__contests` - WHERE - `name_id` = ? - LIMIT 1"; - - $delete_query = mysqli_prepare($con, $delete_statement); - mysqli_stmt_bind_param( - $delete_query, - "s", - $name_id - ); - mysqli_stmt_execute($delete_query); - - //Retorna mensagem final - echo ""; - - } elseif (isset($_POST['do_edit'])) { - - //Valida informações submetidas via formulário - processFormData(); - - //Prepara e executa query - $update_statement = - "UPDATE - `manage__contests` - SET - `start_time` = ?, - `end_time` = ?, - `name` = ?, - `group` = ?, - `revert_time` = ?, - `official_list_pageid` = ?, - `category_pageid` = ?, - `category_petscan` = ?, - `endpoint` = ?, - `api_endpoint` = ?, - `outreach_name` = ?, - `bytes_per_points` = ?, - `max_bytes_per_article` = ?, - `minimum_bytes` = ?, - `pictures_per_points` = ?, - `pictures_mode` = ?, - `max_pic_per_article` = ?, - `theme` = ?, - `color` = ? - WHERE - `name_id` = ?"; - $update_query = mysqli_prepare($con, $update_statement); - mysqli_stmt_bind_param( - $update_query, - "ssssiiiisssiiiiiisss", - $_POST['start_time'], - $_POST['end_time'], - $_POST['name'], - $_POST['group'], - $_POST['revert_time'], - $_POST['official_list_pageid'], - $_POST['category_pageid'], - $_POST['category_petscan'], - $_POST['endpoint'], - $_POST['api_endpoint'], - $_POST['outreach_name'], - $_POST['bytes_per_points'], - $_POST['max_bytes_per_article'], - $_POST['minimum_bytes'], - $_POST['pictures_per_points'], - $_POST['pictures_mode'], - $_POST['max_pic_per_article'], - $_POST['theme'], - $_POST['color'], - $_POST['name_id'] - ); - mysqli_stmt_execute($update_query); - - //Verifica se linha foi inserida com sucesso - if (mysqli_stmt_affected_rows($update_query) != 1) { - printf("Erro: %s.\n", mysqli_stmt_error($update_query)); - die(); - } else { - //Retorna mensagem final - echo ""; - } - } -} - -//Adiciona novo item na array para criar campo em branco para cadastro -$contests_array[]['name'] = null; - -?> - - - - <?=§('manage-title')?> - - - - - -
-

-
-
-
-
-
-

-
-
- $contest_info): ?> - -
-
' - style='color: #fff; background-color: #' - > -

-
-
-
-
- - - > - -
-
- - - > - -
-
- - - " - > - -
-
- - - > - - - > - - - > - - - > - -
-
- - - > - - - - -
-
- - - > - - - > - -
-
- - - > - -
-
- - - > - - - > - - - > - -
-
- - - > - - - > - - - - -
-
- -
-
- - -
-
- - > -
-
- - - - > - - - -
-
- - -
-
- -
-
-
-
- -
-
- -
-
- -
-
-
-
-

- - - -
-
- -
-
- -
-
-
-
-
-
- -
- - diff --git a/bin/manage_login.php b/bin/manage_login.php deleted file mode 100644 index 9a3d529..0000000 --- a/bin/manage_login.php +++ /dev/null @@ -1,63 +0,0 @@ -login($_POST['email'], $_POST['password']); -} - -// (B) REDIRECT USER IF SIGNED IN -if (isset($_SESSION['user'])) { - header("Location: index.php?lang=$lang&manage=true&page=contests"); - exit(); -} - -// (C) SHOW LOGIN FORM OTHERWISE -if (isset($_POST['do_login'])) { - echo ""; -} -?> - - - - - <?=§('login-manage')?> - - - - -
-

-
-
-
-
-
- - - - - - -
-
-
- - - - - -
-
-
- -
-
-
- - - \ No newline at end of file diff --git a/bin/modify.php b/bin/modify.php deleted file mode 100644 index e9c0109..0000000 --- a/bin/modify.php +++ /dev/null @@ -1,360 +0,0 @@ - "compare", - "prop" => "title|diff|comment|user", - "format" => "php", - "fromrev" => $_GET['diff'], - "torelative"=> "prev" - ]; - $output['compare'] = unserialize( - file_get_contents($contest['api_endpoint']."?".http_build_query($compare_api_params)) - )['compare']; - } else { - $output['compare'] = false; - } -} - -//Encerra conexão -mysqli_close($con); - -//Exibe edição e formulário de avaliação -?> - - - - <?=§('modify')?> - <?=$contest['name'];?> - - - - - - - - - -
-
-
-

- > - -

- - -

-

- -

-
-
-

- -
-

- -
- -

-
-
-

- - -

- - -
- -

- -
-

- -
- - -

-
-
-

-
    -

  • - - - -
  • -

  • - - - -
  • -

  • -

  • -

  • -

  •  
  • -

  • -

  • -

  • -

  • -

  • -

  • -

  • -

  •  
  • -
-
-
-
-
-

- - - -
-
- - - - - - - - - - -
-
- - diff --git a/bin/password.php b/bin/password.php deleted file mode 100644 index db835d7..0000000 --- a/bin/password.php +++ /dev/null @@ -1,92 +0,0 @@ -verify($_SESSION['user']['user_email'], $_POST['oldpass'])) { - - //Troca senha - $USR->save($_SESSION['user']['user_email'], $_POST['newpass'], $_SESSION['user']['user_id']); - - //Gera resultado - $status = §('recover-success'); - - } else { - - //Gera erro - $status = §('password-wrongpass'); - } -} else { - - //Formulário inicial - $status = false; -} - -?> - - - - <?=§('recover-reset')?> - <?=$contest['name'];?> - - - - - - -
-
-
-

-
-
-
-
-
-
- - - - - - - - - - -
-
-
-
-
- - - - - - diff --git a/bin/protect.php b/bin/protect.php deleted file mode 100644 index 6e7232c..0000000 --- a/bin/protect.php +++ /dev/null @@ -1,23 +0,0 @@ - time(), - 'token' => $token - ) - ); - - //Processa query - $update_query = mysqli_prepare( - $con, - "UPDATE - `{$contest['name_id']}__credentials` - SET - `user_data` = ? - WHERE - `user_email` = ?" - ); - mysqli_stmt_bind_param($update_query, "ss", $user_data, $email); - mysqli_stmt_execute($update_query); - - //Verifica se houve alteração (se e-mail foi encontrado, principalmente) - if (mysqli_stmt_affected_rows($update_query) != 0) { - - //Cria corpo do e-mail - $message = §('recover-greeting'); - $message .= "\n"; - $message .= §('recover-instruction'); - $message .= "\n{$token}\n\n\n"; - $message .= §('recover-signature'); - $emailFile = fopen("php://temp", 'w+'); - $subject = §('recover-subject'); - fwrite($emailFile, "Subject: " . $subject . "\n" . $message); - rewind($emailFile); - $fstat = fstat($emailFile); - $size = $fstat['size']; - - //Envia e-mail - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, 'smtp://mail.tools.wmflabs.org:587'); - curl_setopt($ch, CURLOPT_MAIL_FROM, get_current_user()."@tools.wmflabs.org"); - curl_setopt($ch, CURLOPT_MAIL_RCPT, array($email)); - curl_setopt($ch, CURLOPT_INFILE, $emailFile); - curl_setopt($ch, CURLOPT_INFILESIZE, $size); - curl_setopt($ch, CURLOPT_UPLOAD, true); - curl_exec($ch); - fclose($emailFile); - curl_close($ch); - - //Gera resultado - $status = §('recover-sent'); - $input['token'] = true; - $input['password'] = true; - - } else { - - //Gera erro - $status = §('recover-notfound'); - } - - //Pedido com token - } else { - - //Processa query - $verify_query = mysqli_prepare( - $con, - "SELECT - `user_id`, - `user_data` - FROM - `{$contest['name_id']}__credentials` - WHERE - `user_email` = ? AND `user_data` IS NOT NULL" - ); - mysqli_stmt_bind_param($verify_query, "s", $email); - mysqli_stmt_execute($verify_query); - $verify_result = mysqli_stmt_get_result($verify_query); - - //Verifica se avaliador existe - if (mysqli_num_rows($verify_result) != 0) { - - //Abre código seriado e coloca informações em uma array - $verify_result = mysqli_fetch_assoc($verify_result); - $user_id = $verify_result['user_id']; - $verify_result = unserialize($verify_result['user_data']); - - //Verifica se token ainda é válido (prazo de 900 segundos) - if ($verify_result['timestamp'] > (time() - 900)) { - - //Verifica se token é igual - if ($verify_result['token'] == trim($_POST['token'])) { - - //Grava nova senha - session_start(); - require_once "credentials-lib.php"; - $USR->save($email, $_POST['password'], $user_id); - - //Gera resultado - $status = §('recover-success'); - $reload = true; - } else { - - //Gera erro - $status = §('recover-invalid'); - $input['token'] = true; - } - } else { - - //Gera erro - $status = §('recover-expired'); - } - } else { - - //Gera erro - $status = §('recover-notrequested'); - } - } -} else { - - //Formulário inicial - $status = false; -} - - - -?> - - - - <?=§('recover-reset')?> - <?=$contest['name'];?> - - - - - -
-

-
-
-
-
-
-

-
-
-
-
-
-
- - - - - > - - - > - - -
-
-
-
-
- - - - - - - - - diff --git a/bin/sidebar.php b/bin/sidebar.php deleted file mode 100644 index 378adac..0000000 --- a/bin/sidebar.php +++ /dev/null @@ -1,115 +0,0 @@ - - -
- - - - - -   - - - - -
- - \ No newline at end of file diff --git a/bin/stats.php b/bin/stats.php deleted file mode 100644 index c4429f1..0000000 --- a/bin/stats.php +++ /dev/null @@ -1,190 +0,0 @@ - 0 - ) AS `all_bytes`, - ( - SELECT - COUNT(*) - FROM - `{$contest['name_id']}__users` - WHERE - `timestamp` != '0' - ) AS `all_users` - FROM - `{$contest['name_id']}__edits`) AS edits_summary;" -); -mysqli_stmt_execute($stats_query); -$stats = mysqli_fetch_assoc(mysqli_stmt_get_result($stats_query)); - -//Coleta lista de artigos na página do concurso -$list_api_params = [ - "action" => "query", - "format" => "php", - "generator" => "links", - "pageids" => $contest['official_list_pageid'], - "gplnamespace" => "0", - "gpllimit" => "max" -]; - -$list_api = unserialize(file_get_contents($contest['api_endpoint']."?".http_build_query($list_api_params))); -$stats['listed_articles'] = count($list_api['query']['pages'] ?? []); - -//Coleta segunda página da lista, caso exista -while (isset($list_api['continue'])) { - $list_api_params = [ - "action" => "query", - "format" => "php", - "generator" => "links", - "pageids" => $contest['official_list_pageid'], - "gplnamespace" => "0", - "gpllimit" => "max", - "gplcontinue" => $list_api['continue']['gplcontinue'] - ]; - $list_api = unserialize( - file_get_contents($contest['api_endpoint']."?".http_build_query($list_api_params)) - ); - $stats['listed_articles'] += count($list_api['query']['pages']); -} - -?> - -
-
-

-
-
-
-
-
-

-
-
-
-

-
-
-
-

-
-
-
-
-
-

-
-
-
-

-
-
-
-

-
-
-
-
-
-
-
-
-
-
-
-
-

- - - -
- -

-
-
-
-
-
-
-
-
-
-

- - - -
- -

-
-
-
-
-
-
-
-
-
-

- - - -
- -

-
-
-
-
-
diff --git a/bin/triage.php b/bin/triage.php deleted file mode 100644 index 079549b..0000000 --- a/bin/triage.php +++ /dev/null @@ -1,878 +0,0 @@ -Erro ao pular edição. Atualize a página para tentar novamente."); - } else { - $output['success']['skip'] = true; - } - - //Libera edições puladas - } elseif(isset($_POST['release'])) { - $release_query = mysqli_prepare( - $con, - "UPDATE - `{$contest['name_id']}__edits` - SET - `by` = NULL - WHERE - `by` = CONCAT('skip-',?) - "); - mysqli_stmt_bind_param( - $release_query, - "s", - $_SESSION['user']['user_name'] - ); - mysqli_stmt_execute($release_query); - if (mysqli_stmt_affected_rows($release_query) == 0) { - die("
Erro ao liberar edição. Atualize a página para tentar novamente."); - } else { - $output['success']['release'] = true; - } - - //Libera edições em avaliação (apenas gestores) - } elseif(isset($_POST['unhold'])) { - if ($_SESSION['user']['user_status'] != 'G') { - die(§('evaluators-denied')); - } - - $release_query = mysqli_prepare( - $con, - "UPDATE - `{$contest['name_id']}__edits` - SET - `by` = NULL - WHERE - `by` LIKE 'skip-%' OR `by` LIKE 'hold-%' - "); - mysqli_stmt_execute($release_query); - if (mysqli_stmt_affected_rows($release_query) == 0) { - die("
Erro ao liberar edição. Atualize a página para tentar novamente."); - } else { - $output['success']['release'] = true; - } - - //Salva avaliação da edição - } else { - - //Processa validade da edição, de acordo com o avaliador - if ($_POST['valid'] == 'sim') { - $post['valid'] = 1; - } else { - $post['valid'] = 0; - } - - //Verifica se/quantas imagem(es) foi(ram) inserida(s), de acordo com o avaliador - if ($contest['pictures_mode'] == 2) { - $post['pic'] = addslashes($_POST['pic']); - } else { - if ($_POST['pic'] == 'sim') { - $post['pic'] = 1; - } else { - $post['pic'] = 0; - } - } - - //Processa alteração do número de bytes, caso informação tenha sido editada pelo avaliador - if (isset($_POST['overwrite']) && is_numeric($_POST['overwrite'])) { - - //Busca número de bytes no banco de dados - $eval_query = mysqli_prepare( - $con, - "SELECT - `bytes` - FROM - `{$contest['name_id']}__edits` - WHERE - `diff` = ? - LIMIT 1" - ); - mysqli_stmt_bind_param($eval_query, "i", $_POST['diff']); - mysqli_stmt_execute($eval_query); - $query = mysqli_fetch_assoc(mysqli_stmt_get_result($eval_query)); - - //Verifica se há diferença. Caso sim, altera o número de bytes e adiciona comentário - if ($query['bytes'] != $post['overwrite']) { - $overwrite_query = mysqli_prepare( - $con, - "UPDATE - `{$contest['name_id']}__edits` - SET - `bytes` = ? - WHERE - `diff` = ?" - ); - mysqli_stmt_bind_param($overwrite_query, "ii", $post['overwrite'], $_POST['diff']); - mysqli_stmt_execute($overwrite_query); - $post['overwrited'] = TRUE; - } - } - - //Processa observação inserida no formulário - if (!isset($_POST['obs']) OR $_POST['obs'] == '') { - $post['obs'] = ''; - } elseif (isset($post['overwrited'])) { - $obs = addslashes($_POST['obs']); - $post['obs'] = "Aval: de {$query['bytes']} para {$post['overwrite']} com justificativa \"{$obs}\"\n"; - } else { - $obs = addslashes($_POST['obs']); - $post['obs'] = "Aval: \"{$obs}\"\n"; - } - - //Prepara query - $when = date('Y-m-d H:i:s'); - $update_statement = " - UPDATE - `{$contest['name_id']}__edits` - SET - `valid_edit` = ?, - `pictures` = ?, - `by` = ?, - `when` = ?, - `obs` = CONCAT(IFNULL(`obs`, ''), ?) - WHERE `diff` = ?"; - $update_query = mysqli_prepare($con, $update_statement); - mysqli_stmt_bind_param( - $update_query, - "iisssi", - $post['valid'], - $post['pic'], - $_SESSION['user']['user_name'], - $when, - $post['obs'], - $_POST['diff'] - ); - - //Executa query e retorna o resultado para o avaliador - mysqli_stmt_execute($update_query); - if (mysqli_stmt_affected_rows($update_query) != 0) { - - $output['success']['diff'] = htmlspecialchars($_POST['diff']); - $output['success']['valid'] = $post['valid']; - $output['success']['pic'] = $post['pic']; - - } - } -} - -//Define número mínimo de bytes necessários de acordo com com configuração do concurso -$bytes = 0; -if (isset($contest['minimum_bytes'])) { - $bytes = $contest['minimum_bytes']; -} - -//Converte prazo de reversão para formato compatível com SQL -$revert_time = date('Y-m-d H:i:s', strtotime("-{$contest['revert_time']} hours")); - -//Coleta edição para avaliação -$revision_query = mysqli_prepare( - $con, - "SELECT - `diff`, - `bytes`, - `article`, - `timestamp` - FROM - `{$contest['name_id']}__edits` - WHERE - `reverted` IS null AND - `valid_edit` IS null AND - `valid_user` IS NOT null AND - CASE - WHEN ? = '-1' - THEN `bytes` IS NOT null - ELSE `bytes` > ? - END AND - `timestamp` < ? AND - ( - `by` IS null OR - `by` = CONCAT('hold-', ?) - ) - ORDER BY - `by` DESC, - `timestamp` ASC - LIMIT 1" -); -mysqli_stmt_bind_param($revision_query, "iiss", $bytes, $bytes, $revert_time, $_SESSION['user']['user_name']); -mysqli_stmt_execute($revision_query); -$output['revision'] = mysqli_fetch_assoc(mysqli_stmt_get_result($revision_query)); - -//Evita avaliação durante atualização do banco de dados -if ($contest['started_update'] > $contest['finished_update']) { - $output['revision'] = null; - $output['updating'] = true; -} - -//Trava edição para evitar que dois avaliadores avaliem a mesma edição ao mesmo tempo -if ($output['revision'] != null) { - $hold_query = mysqli_prepare( - $con, - "UPDATE - `{$contest['name_id']}__edits` - SET - `by` = CONCAT('hold-', ?), - `when` = NOW() - WHERE - `diff`= ?" - ); - mysqli_stmt_bind_param($hold_query, "si", $_SESSION['user']['user_name'], $output['revision']['diff']); - mysqli_stmt_execute($hold_query); - if (mysqli_stmt_affected_rows($hold_query) == 0) { - die("
Erro ao travar edição. Atualize a página para tentar novamente."); - } - - //Coleta informações da edição via API do MediaWiki - $compare_api_params = [ - "action" => "compare", - "prop" => "title|diff|comment|user|ids", - "format" => "php", - "fromrev" => $output['revision']['diff'], - "torelative"=> "prev" - ]; - $output['compare'] = unserialize( - file_get_contents($contest['api_endpoint']."?".http_build_query($compare_api_params)) - )['compare'] ?? false; - - //Interrope script caso artigo tenha sido eliminado - if (!$output['compare']) { - $reverted_statment = " - UPDATE - `{$contest['name_id']}__edits` - SET - `reverted` = '1' - WHERE - `diff` = ? - "; - $reverted_query = mysqli_prepare($con, $reverted_statment); - mysqli_stmt_bind_param($reverted_query, "i", $output['revision']['diff']); - mysqli_stmt_execute($reverted_query); - if (mysqli_stmt_affected_rows($reverted_query) != 0) { - echo " - - "; - } - exit(); - } - - //Coleta informações da edição via API do MediaWiki versão inline - $compare_api_params["difftype"] = "inline"; - $output['compare_mobile'] = unserialize( - file_get_contents($contest['api_endpoint']."?".http_build_query($compare_api_params)) - )['compare']; - - //Coleta histórico recente do artigo até o início do concurso - $history_params = [ - "action" => "query", - "format" => "php", - "prop" => "revisions", - "pageids" => $output['revision']['article'], - "rvprop" => "timestamp|user|size|ids", - "rvlimit" => "max", - "rvend" => date('Y-m-d\TH:i:s.000\Z', $contest['start_time']) - ]; - $history = unserialize( - file_get_contents($contest['api_endpoint']."?".http_build_query($history_params)) - )["query"]["pages"]; - - //Verifica situação da primeira edição. - //Se for a primeira edição, insere pseudo-edição anterior com tamanho zero - //Se existir edição anterior, busca na API o tamanho da edição - //imediatamente anterior e insere pseudo-edição com tamanho correspondente - $history = end($history)["revisions"]; - if (end($history)["parentid"] == 0) { - $history[] = [ - "size" => "0", - "timestamp" => "1970-01-01T00:00:00", - "user" => "None", - "revid" => "0" - ]; - } else { - $lastdiff_params = [ - "action" => "compare", - "format" => "php", - "fromrev" => end($history)["revid"], - "torelative" => "prev", - "prop" => "size" - ]; - $lastdiff = unserialize( - file_get_contents($contest['api_endpoint']."?".http_build_query($lastdiff_params)) - )["compare"]; - $history[] = [ - "size" => $lastdiff["fromsize"], - "timestamp" => "1970-01-01T00:00:00", - "user" => "None", - "revid" => "0" - ]; - } - - //Loop para retornar o código HTML do histórico da página - foreach ($history as $i => $edit) { - - //Calcula quantidade de bytes e formata timestamp - $delta = $history[$i]['size'] - @$history[$i+1]['size']; - $timestamp = date($utc_format, strtotime($edit['timestamp'])); - - //Define cor para número de bytes - $delta_color = "grey"; - if ($delta < 0) { - $delta_color = "red"; - } elseif ($delta > 0) { - $delta_color = "green"; - } - - //Insere estilo no parágrafo para destacar edição em avaliação - $history_class = 'w3-small'; - if ($edit['revid'] == $output['revision']['diff']) { - $history_class = "w3-small w3-${left}bar w3-border-grey w3-padding-small"; - } - - //Monta código da edição - $output['history'][] = [ - "class" => $history_class, - "user" => $edit['user'], - "timestamp" => $timestamp, - "color" => $delta_color, - "bytes" => $delta, - "revid" => $edit['revid'] - ]; - } - - //Remove pseudo-edição - array_pop($output['history']); - -} - -//Conta edições faltantes -$count_query = mysqli_prepare( - $con, - "SELECT - IFNULL(SUM(CASE WHEN `by` IS null THEN 1 ELSE 0 END), 0) AS `onqueue`, - IFNULL(SUM(CASE WHEN `timestamp` > ? THEN 1 ELSE 0 END), 0) AS `onwait`, - IFNULL(SUM(CASE WHEN `by` LIKE 'skip-%' THEN 1 ELSE 0 END), 0) AS `onskip`, - IFNULL(SUM(CASE WHEN `by` LIKE 'hold-%' THEN 1 ELSE 0 END), 0) AS `onhold` - FROM - `{$contest['name_id']}__edits` - WHERE - `reverted` IS null AND - `valid_edit` IS null AND - `valid_user` IS NOT null AND - CASE - WHEN ? = '-1' - THEN `bytes` IS NOT null - ELSE `bytes` > ? - END" -); -mysqli_stmt_bind_param($count_query, "sii", $revert_time, $bytes, $bytes); -mysqli_stmt_execute($count_query); -$count_result = mysqli_fetch_assoc(mysqli_stmt_get_result($count_query)); -$output['onwait'] = $count_result['onwait']; -$output['onskip'] = $count_result['onskip']; -$output['onhold'] = $count_result['onhold']; -$output['onqueue'] = max(0, $count_result['onqueue'] - $count_result['onwait']); - -//Encerra conexão -mysqli_close($con); - -//Exibe edição e formulário de avaliação -?> - - - - <?=$contest['name'];?> - - - - - - - - - - - - - - -
-
- -
-

-

- : -

-

- : -

-

- : -

-

- -

-
- -
-

-
- -
-

- -
- -

-
-
- -

- -
- -

- -
- -

- -
-

- -
- -

- - -
-
-
- -
-
-
- - - -
-
-
-
-
-

-
-
-
-

-
-
-
-

-
-
-
-
-
-

-
-
-
-

-
-
-

-

- - -
-

- -

-

- - -
-

- -
-
-
- -
-

-

- -

-
- -
-

-

-

-
- -
-

-
- - -
- - -
- - - -
- - (UTC) -
-
- - bytes -
- : - -
- : - - -
- - -
-
- -
-

- - - - - - - - - - - - -
-
- -
-
-
-

- -

'> - -
- -
- '> bytes -

- -
-
-

-

- -
- -

-

- -
- -

-

- -
- -

-

- -
- -

-

- -
- -

-

- -
- -

-

- -
- -

-

- -
- - / - -

-

- -
- -

-

- -
- -

-

- -
- -

-

- -
- -

-
-
-
- - \ No newline at end of file diff --git a/contests/__init__.py b/contests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/contests/admin.py b/contests/admin.py new file mode 100644 index 0000000..c14fa07 --- /dev/null +++ b/contests/admin.py @@ -0,0 +1,12 @@ +from django.contrib import admin +from contests.models import Contest, Group, Article, Participant, Edit, Qualification, Evaluation, Evaluator + +# Register your models here. +admin.site.register(Contest) +admin.site.register(Group) +admin.site.register(Article) +admin.site.register(Participant) +admin.site.register(Edit) +admin.site.register(Qualification) +admin.site.register(Evaluation) +admin.site.register(Evaluator) \ No newline at end of file diff --git a/contests/apps.py b/contests/apps.py new file mode 100644 index 0000000..d8b7671 --- /dev/null +++ b/contests/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ContestsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'contests' diff --git a/contests/management/__init__.py b/contests/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/contests/management/commands/__init__.py b/contests/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/contests/management/commands/load_edits.py b/contests/management/commands/load_edits.py new file mode 100644 index 0000000..3426f5f --- /dev/null +++ b/contests/management/commands/load_edits.py @@ -0,0 +1,153 @@ +import requests +import time +from datetime import datetime +from django.core.management.base import BaseCommand +from django.db import connection +from contests.models import Contest, Article, Edit, Participant + +class Command(BaseCommand): + help = "Carrega edições para o concurso." + + def add_arguments(self, parser): + parser.add_argument('contest', type=str, help="Nome ID do concurso") + + def handle(self, *args, **options): + contest_name_id = options.get('contest') + contest = Contest.objects.get(name_id=contest_name_id) + + # Coleta lista de artigos na categoria ou via PetScan + if contest.category_petscan: + # Recupera lista do PetScan + petscan_list = requests.get(f"https://petscan.wmflabs.org/?format=json&psid={contest.category_petscan}").json() + list_ = [ + {"id": item['id'], "title": item['title']} + for item in petscan_list['*'][0]['a']['*'] + ] + else: + # Coleta lista de artigos na categoria + list_ = self.get_category_articles(contest) + + # Desativa lista de artigos já existentes + Article.objects.filter(contest=contest).update(active=False) + + # Insere lista de artigos na tabela + # Se já existir, apenas ativa + for item in list_: + article, created = Article.objects.get_or_create( + contest=contest, + articleID=item['id'], + title=item['title'], + ) + if not created: + article.active = True + article.save(update_fields=['active']) + + # Coleta lista de revisões já inseridas no banco de dados + existing_revisions = Edit.objects.filter(contest=contest).values_list('diff', flat=True) + + # Loop para análise de cada artigo + for article in Article.objects.filter(contest=contest): + self.stdout.write(f"CurID: {article.articleID}") + + # Coleta revisões do artigo + revisions = self.get_article_revisions(article, contest) + + # Verifica se o artigo possui revisões dentro dos parâmetros escolhidos + if not revisions: + continue + + # Loop para cada revisão do artigo + for revision in revisions: + self.stdout.write(f"- Diff: {revision['revid']}") + if revision['revid'] in existing_revisions: + continue + self.stdout.write(" -> inserindo") + + # Coleta dados de diferenciais da revisão + compare_data = self.get_revision_compare(revision, contest) + + # Executa inserção no banco de dados + Edit.objects.create( + diff=revision['revid'], + article=article, + timestamp=compare_data.get('timestamp'), + user_id=compare_data.get('user_id'), + orig_bytes=compare_data.get('bytes'), + new_page=compare_data.get('new_page'), + contest=contest + ) + + self.stdout.write(" -> feito!") + + self.stdout.write("
Concluido! (1/3)
") + + def get_category_articles(self, contest): + """Coleta lista de artigos na categoria.""" + list_ = [] + categorymembers_api_params = { + "action": "query", + "format": "json", + "list": "categorymembers", + "cmnamespace": "0", + "cmpageid": contest.category_pageid, + "cmprop": "ids|title", + "cmlimit": "max" + } + response = requests.get(contest.api_endpoint, params=categorymembers_api_params).json() + list_.extend(response['query']['categorymembers']) + + # Coleta segunda página da lista, caso exista + while 'continue' in response: + categorymembers_api_params['cmcontinue'] = response['continue']['cmcontinue'] + response = requests.get(contest.api_endpoint, params=categorymembers_api_params).json() + list_.extend(response['query']['categorymembers']) + + return list_ + + def get_article_revisions(self, article, contest): + """Coleta revisões do artigo.""" + revisions_api_params = { + "action": "query", + "format": "json", + "prop": "revisions", + "rvprop": "ids", + "rvlimit": "max", + "rvstart": contest.end_time.strftime('%Y-%m-%dT%H:%M:%S.000Z'), + "rvend": contest.start_time.strftime('%Y-%m-%dT%H:%M:%S.000Z'), + "pageids": article.articleID + } + revisions_api = requests.get(contest.api_endpoint, params=revisions_api_params).json() + revisions_api = revisions_api.get('query', {}).get('pages', {}).get(str(article.articleID), {}) + + return revisions_api.get('revisions', []) + + def get_revision_compare(self, revision, contest): + """Coleta dados de diferenciais da revisão.""" + compare_api_params = { + "action": "compare", + "format": "json", + "torelative": "prev", + "prop": "diffsize|size|title|user|timestamp", + "fromrev": revision['revid'] + } + compare_api = requests.get(contest.api_endpoint, params=compare_api_params).json() + + if 'compare' not in compare_api: + return {"timestamp": None, "user_id": None, "bytes": None, "new_page": None} + + compare_api = compare_api['compare'] + + # Verifica se página é nova + if 'fromsize' not in compare_api: + compare_api['new_page'] = True + else: + compare_api['new_page'] = False + compare_api['tosize'] = compare_api['tosize'] - compare_api['fromsize'] + + return { + "timestamp": compare_api.get('totimestamp'), + "user_id": compare_api.get('touserid'), + "bytes": compare_api.get('tosize'), + "new_page": compare_api.get('new_page') + } + diff --git a/contests/management/commands/load_reverts.py b/contests/management/commands/load_reverts.py new file mode 100644 index 0000000..db30fae --- /dev/null +++ b/contests/management/commands/load_reverts.py @@ -0,0 +1,69 @@ +import requests +import time +from datetime import datetime +from django.core.management.base import BaseCommand +from django.db import connection +from contests.models import Contest, Article, Edit, Qualification +from django.db.models import OuterRef, Subquery + +class Command(BaseCommand): + help = 'Update reverts' + + def add_arguments(self, parser): + parser.add_argument('contest', type=str, help="Nome ID do concurso") + + def handle(self, *args, **options): + contest_name_id = options.get('contest') + contest = Contest.objects.get(name_id=contest_name_id) + + # Coleta lista de edições + subquery = Qualification.objects.filter( + contest=contest, + diff=OuterRef('diff') + ).order_by('-when').values('pk')[:1] + + # Get the latest enrollment for each user, then filter by enrolled=True + already_reverted = Qualification.objects.filter( + contest=contest, + pk__in=Subquery(subquery), + status=0 + ).values_list('diff__diff', flat=True) + edits = Edit.objects.filter(contest=contest).exclude(participant=None).values_list('diff', flat=True) + + diffs = list(set(edits) - set(already_reverted)) + + # Loop para análise de cada edição + for diff in diffs: + + # Coleta tags da revisão + revisions_api_params = { + "action": "query", + "format": "json", + "prop": "revisions", + "rvprop": "sha1|tags", + "revids": diff, + } + revisions_api = requests.get(contest.api_endpoint, params=revisions_api_params).json() + revisions_api_query = revisions_api.get('query', {}) + revision = None + + if 'pages' in revisions_api_query: + self.stdout.write(f"Revisão: {diff}") + revision_page = next(iter(revisions_api_query['pages'].values()), None) + revision = revision_page['revisions'][0] if revision_page and 'revisions' in revision_page else None + + # Marca edição caso tenha sido revertida ou eliminada + if ( + 'badrevids' in revisions_api_query + or (revision and 'sha1hidden' in revision) + or (revision and 'mw-reverted' in revision['tags']) + ): + Qualification.objects.create( + contest=contest, + diff=Edit.objects.get(diff=diff), + status=0, + ) + self.stdout.write(f"Marcada edição {diff} como revertida.") + + # Encerra script + self.stdout.write("Concluído! (3/3)") diff --git a/contests/management/commands/load_users.py b/contests/management/commands/load_users.py new file mode 100644 index 0000000..c2ad9ff --- /dev/null +++ b/contests/management/commands/load_users.py @@ -0,0 +1,218 @@ +import csv +import requests +from django.db import connection, models +from django.db.models import OuterRef, Subquery +from datetime import datetime +from dateutil import parser +from contests.models import Contest, Edit, Participant, ParticipantEnrollment, Qualification, Evaluation +from django.core.management.base import BaseCommand + +class Command(BaseCommand): + help = "Carrega usuários inscritos no concurso." + + def add_arguments(self, parser): + parser.add_argument('contest', type=str, help="Nome ID do concurso") + + def handle(self, *args, **options): + contest_name_id = options.get('contest') + contest = self.get_contest(contest_name_id) + + # Coleta planilha com usuários inscritos + if contest.campaign_event_id: + self.stdout.write("Este concurso possui um evento de campanha.") + event_id = contest.campaign_event_id + api = f"https://meta.wikimedia.org/w/rest.php/campaignevents/v0/event_registration/{event_id}/participants" + response = requests.get(api).json() + enrollments = self.parse_event(response) + else: + csv_content = self.fetch_csv_data(contest) + enrollments = self.parse_csv(csv_content) + self.stdout.write(f"Lista de usuários coletada. ({len(enrollments)} usuários encontrados)") + + # Coleta ID da wiki + wiki_id = self.fetch_wiki_id(contest) + self.stdout.write(f"Wiki ID: {wiki_id}") + + # Processa os usuários inscritos + self.process_enrollments(enrollments, contest, wiki_id) + + # Destrava edições bloqueadas + self.unlock_edits(contest) + self.stdout.write("Concluído! (2/3)") + + def get_contest(self, contest_name_id): + """Fetches the contest instance.""" + return Contest.objects.get(name_id=contest_name_id) + + def fetch_csv_data(self, contest): + """Fetches the CSV data of enrolled users from Outreach.""" + self.stdout.write("Coletando lista de usuários inscritos...") + csv_params = {"course": contest.outreach_name} + csv_url = 'https://outreachdashboard.wmflabs.org/course_students_csv' + response = requests.get(csv_url, params=csv_params, timeout=15) + response.encoding = 'utf-8' + if not response.text: + raise ValueError("Não foi possível encontrar a lista de usuários no Outreach.") + return response.text + + def parse_event(self, response): + """Parses the event response into a list of enrollments.""" + enrollments = [] + for participant in response: + enrollments.append({ + 'global_id': participant['user_id'], + 'username': participant['user_name'], + 'enrollment_timestamp': parser.parse(participant['user_registered_at']) + }) + return enrollments + + def parse_csv(self, csv_content): + """Parses the CSV content into a list of enrollments.""" + csv_lines = list(csv.reader(csv_content.splitlines())) + csv_head = csv_lines.pop(0) + csv_num_rows = len(csv_head) + + enrollments = [] + for csv_line in csv_lines: + if len(csv_line) != csv_num_rows: + csv_line = self.fix_csv_line_length(csv_line, csv_num_rows) + enrollments.append(dict(zip(csv_head, csv_line))) + + return enrollments + + def fix_csv_line_length(self, csv_line, csv_num_rows): + """Fixes the CSV line length in case of issues.""" + if len(csv_line) < csv_num_rows: + raise ValueError("Erro! Uma das linhas possui menos colunas que o cabeçalho.") + extra_columns = len(csv_line) - csv_num_rows + 1 + csv_line_first_column = ",".join(csv_line[:extra_columns]) + return [csv_line_first_column] + csv_line[extra_columns:] + + def fetch_wiki_id(self, contest): + """Fetches the wiki ID from the contest API.""" + self.stdout.write("Coletando ID da wiki...") + params = {"action": "query", "format": "json", "meta": "siteinfo"} + response = requests.get(contest.api_endpoint, params=params).json() + return response['query']['general']['wikiid'] + + def process_enrollments(self, enrollments, contest, wiki_id): + """Processes each enrollment, updating or inserting users.""" + # Get all participant enrollments for the specific contest + latest_enrollment_subquery = ParticipantEnrollment.objects.filter( + contest=contest, + user=OuterRef('user') + ).order_by('-when').values('pk')[:1] + + # Get the latest enrollment for each user, then filter by enrolled=True + already_enrolled = ParticipantEnrollment.objects.filter( + contest=contest, + pk__in=Subquery(latest_enrollment_subquery), + enrolled=True + ).values_list('user__global_id', flat=True) + + missing_enrollments = [enrollment for enrollment in already_enrolled if enrollment not in enrollments] + ParticipantEnrollment.objects.bulk_create([ + ParticipantEnrollment(contest=contest, enrolled=False, user=Participant.objects.get(global_id=enrollment, contest=contest)) for enrollment in missing_enrollments + ]) + + for enrollment in enrollments: + global_id = enrollment['global_id'] + username = enrollment['username'] + timestamp = parser.parse(enrollment['enrollment_timestamp']) + self.stdout.write(f"Coletando informações do usuário {username} ({global_id})...") + + if not global_id: + self.stdout.write("Usuário sem ID global. Ignorando...") + continue + + self.insert_or_update_user(global_id, username, contest, wiki_id, timestamp) + + def insert_or_update_user(self, global_id, username, contest, wiki_id, timestamp): + """Inserts or updates the user in the Participant table.""" + if Participant.objects.filter(global_id=global_id, contest=contest).exists(): + Participant.objects.filter(global_id=global_id, contest=contest).update(user=username) + local_id = Participant.objects.get(global_id=global_id, contest=contest).local_id + self.stdout.write(f"Usuário {username} já está na tabela. Ignorando...") + else: + local_id = self.add_user_contest(global_id, contest, wiki_id, timestamp) + + if not local_id: + self.stdout.write(f"Usuário {username} não encontrado. Ignorando...") + return None + else: + self.stdout.write(f"Usuário {username} inserido com sucesso!") + self.update_user_edits(local_id, contest, timestamp) + + def add_user_contest(self, global_id, contest, wiki_id, timestamp): + """Adds a user to the contest.""" + centralauth_response = self.fetch_user_data(global_id, contest) + centralauth_merged = centralauth_response['query']['globaluserinfo']['merged'] + local_id = next((merged['id'] for merged in centralauth_merged if merged['wiki'] == wiki_id), None) + user = centralauth_response['query']['globaluserinfo']['name'] + attached = centralauth_response['query']['globaluserinfo']['registration'] + + if not local_id: + return None + else: + Participant.objects.create( + contest=contest, + user=user, + timestamp=timestamp, + global_id=global_id, + local_id=local_id, + attached=attached, + ) + return local_id + + def fetch_user_data(self, global_id, contest): + """Fetches user data from the contest API.""" + params = { + "action": "query", + "format": "json", + "meta": "globaluserinfo", + "guiprop": "merged", + "formatversion": "2", + "guiid": global_id + } + return requests.get(contest.api_endpoint, params=params).json() + + def update_user_edits(self, local_id, contest, timestamp): + """Updates user edits in the Edit table.""" + self.stdout.write(f"Atualizando edições do usuário com ID local {local_id}...") + participant = Participant.objects.get(local_id=local_id, contest=contest) + + try: + already_enrolled = ParticipantEnrollment.objects.filter(contest=contest, user=participant).latest('when').enrolled + except ParticipantEnrollment.DoesNotExist: + already_enrolled = False + + if already_enrolled: + self.stdout.write("Usuário já está inscrito. Ignorando...") + else: + ParticipantEnrollment.objects.create(contest=contest, user=participant) + + Edit.objects.filter(user_id=local_id, contest=contest, participant=None).update(participant=participant) + + edits = Edit.objects.filter(user_id=local_id, timestamp__gte=timestamp, contest=contest) + Qualification.objects.bulk_create([ + Qualification(contest=contest, diff=edit) for edit in edits + ]) + + def unlock_edits(self, contest): + """Unlocks any remaining locked edits in the Edit table.""" + self.stdout.write("Destravando edições...") + + subquery = Evaluation.objects.filter( + contest=contest, + edit=OuterRef('edit') + ).order_by('-when').values('pk')[:1] + + lockeds = Evaluation.objects.filter( + contest=contest, + pk__in=Subquery(subquery), + status__in=['2', '3'] + ) + + Evaluation.objects.bulk_create([ + Evaluation(contest=self.contest, edit=locked.edit) for locked in lockeds + ]) diff --git a/contests/management/commands/translate.py b/contests/management/commands/translate.py new file mode 100644 index 0000000..20a3557 --- /dev/null +++ b/contests/management/commands/translate.py @@ -0,0 +1,65 @@ +import os +import json +import re +from django.core.management.base import BaseCommand +from django.conf import settings +from polib import POFile, POEntry + +class Command(BaseCommand): + help = "Convert JSON translations to PO files." + + def handle(self, *args, **options): + json_dir = os.path.join(settings.BASE_DIR, 'translations') + for filename in os.listdir(json_dir): + if filename.endswith('.json'): + filepath = os.path.join(json_dir, filename) + translations = self.load_translations(filepath) + language = filename.split('.')[0] + po = self.convert_to_po(translations, language) + po_path = os.path.join(settings.BASE_DIR, 'locale', language, 'LC_MESSAGES', 'django.po') + self.save_po_file(po, po_path) + + def load_translations(self, filepath): + with open(filepath, 'r', encoding='utf-8') as f: + return json.load(f) + + def convert_to_po(self, translations, language): + po = POFile(encoding='utf-8') + po.metadata = { + 'Content-Type': 'text/plain; charset=utf-8', + 'Content-Transfer-Encoding': '8bit', + 'Language': language, + } + for key, value in translations.items(): + if isinstance(value, str): + # Count the number of placeholders in the string + count = len(re.findall(r'\$\d+', value)) + if count > 0: + # If key is "an-example" and value is "This: $1 example", the msgid will be ".%(an_example_1)s." and msgstr will be "This: %(an_example_1)s example" + # If key is "an-example" and value is "This: $1 example $2 good", the msgstr will be ".%(an_example_1)s.%(an_example_2)s." and msgstr will be "This: %(an_example_1)s example %(an_example_2)s good" + value = re.sub(r'\$(\d+)', '%(' + key.replace("-", "_") + '_\\1)s', value) + key = '.' + '.'.join([f'%({key.replace("-", "_")}_{i+1})s' for i in range(count)]) + '.' + po.append(POEntry(msgid=key, msgstr=value)) + return po + + def save_po_file(self, po, po_path): + os.makedirs(os.path.dirname(po_path), exist_ok=True) + po.save(po_path) + print(f"Successfully converted to {po_path}") + + def flatten_dict(self, d, parent_key='', sep='.'): + """Flattens a nested dictionary.""" + items = [] + for k, v in d.items(): + new_key = f'{parent_key}{sep}{k}' if parent_key else k + if isinstance(v, dict): + items.extend(self.flatten_dict(v, new_key, sep=sep).items()) + else: + items.append((new_key, v)) + return dict(items) + + + + + + diff --git a/contests/management/commands/update.py b/contests/management/commands/update.py new file mode 100644 index 0000000..c765fad --- /dev/null +++ b/contests/management/commands/update.py @@ -0,0 +1,52 @@ +import time +import requests +from datetime import timedelta +from django.core.management import BaseCommand, call_command +from django.db import models +from django.utils import timezone +from contests.models import Contest + +class Command(BaseCommand): + help = 'Update contests' + + def handle(self, *args, **options): + steps = ["load_edits", "load_users", "load_reverts"] + + contests = Contest.objects.filter( + start_time__lt=timezone.now() + ).filter( + models.Q(started_update__isnull=True) | + models.Q(started_update__lt=timezone.now() - timedelta(minutes=10)) + ).filter( + models.Q(next_update__isnull=True) | + ( + models.Q(end_time__gt=timezone.now() - timedelta(days=2)) & + models.Q(started_update__lt=models.F('finished_update')) & + models.Q(next_update__lt=timezone.now()) + ) + ) + + if not contests.exists(): + self.stdout.write("Sem atualizações previstas.\n") + return + + # Loop de concursos + for contest in contests: + # Grava horário de início + contest.started_update = timezone.now() + contest.save(update_fields=['started_update']) + + # Loop de scripts + for command_name in steps: + try: + # Executa o comando correspondente + self.stdout.write(f"Executando {command_name} para {contest.name_id}...\n") + call_command(command_name, contest.name_id) + self.stdout.write(f"Executado {command_name} para {contest.name_id}.\n") + except Exception as e: + self.stderr.write(f"Erro ao executar {command_name} para {contest.name_id}: {str(e)}\n") + + # Grava horário de finalização e define o próximo update + contest.finished_update = timezone.now() + contest.next_update = timezone.now() + timedelta(days=1) + contest.save(update_fields=['finished_update', 'next_update']) \ No newline at end of file diff --git a/contests/migrations/0001_initial.py b/contests/migrations/0001_initial.py new file mode 100644 index 0000000..6d890a4 --- /dev/null +++ b/contests/migrations/0001_initial.py @@ -0,0 +1,126 @@ +# Generated by Django 4.2.15 on 2024-08-22 14:16 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Article', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('articleID', models.IntegerField()), + ('title', models.TextField()), + ('active', models.BooleanField(default=True)), + ], + ), + migrations.CreateModel( + name='Contest', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name_id', models.CharField(max_length=30, unique=True)), + ('start_time', models.DateTimeField()), + ('end_time', models.DateTimeField()), + ('name', models.TextField()), + ('revert_time', models.SmallIntegerField(default=24)), + ('official_list_pageid', models.IntegerField()), + ('category_pageid', models.IntegerField(blank=True, null=True)), + ('category_petscan', models.IntegerField(blank=True, null=True)), + ('endpoint', models.URLField()), + ('api_endpoint', models.URLField()), + ('outreach_name', models.TextField()), + ('campaign_event_id', models.IntegerField(blank=True, default=None, null=True)), + ('bytes_per_points', models.IntegerField(default=3000)), + ('max_bytes_per_article', models.IntegerField(default=90000)), + ('minimum_bytes', models.IntegerField(blank=True, null=True)), + ('pictures_per_points', models.SmallIntegerField(default=5)), + ('pictures_mode', models.SmallIntegerField(default=0)), + ('max_pic_per_article', models.SmallIntegerField(blank=True, null=True)), + ('theme', models.TextField()), + ('color', models.TextField(blank=True, default='')), + ('started_update', models.DateTimeField(blank=True, null=True)), + ('finished_update', models.DateTimeField(blank=True, null=True)), + ('next_update', models.DateTimeField(blank=True, null=True)), + ], + ), + migrations.CreateModel( + name='Edit', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('diff', models.IntegerField()), + ('timestamp', models.DateTimeField(blank=True)), + ('user_id', models.IntegerField()), + ('orig_bytes', models.IntegerField(blank=True, default=0)), + ('new_page', models.BooleanField(default=False)), + ], + ), + migrations.CreateModel( + name='Evaluation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('valid_edit', models.BooleanField(default=False)), + ('pictures', models.SmallIntegerField(default=0)), + ('real_bytes', models.IntegerField(blank=True, default=0)), + ('status', models.CharField(choices=[('0', 'Pending'), ('1', 'Done'), ('2', 'Hold'), ('3', 'Skipped')], default='0', max_length=1)), + ('when', models.DateTimeField(auto_now_add=True)), + ('obs', models.TextField(blank=True, null=True)), + ], + ), + migrations.CreateModel( + name='Evaluator', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('user_status', models.CharField(default='P', max_length=1)), + ('contest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contests.contest')), + ], + ), + migrations.CreateModel( + name='Group', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + ), + migrations.CreateModel( + name='Participant', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('user', models.CharField(max_length=100)), + ('timestamp', models.DateTimeField(blank=True)), + ('global_id', models.IntegerField()), + ('local_id', models.IntegerField(blank=True)), + ('attached', models.DateTimeField(blank=True)), + ('contest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contests.contest')), + ], + ), + migrations.CreateModel( + name='Qualification', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('0', 'Reverted'), ('1', 'Active')], default='1', max_length=1)), + ('when', models.DateTimeField(auto_now_add=True)), + ('contest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contests.contest')), + ('diff', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contests.edit')), + ('evaluator', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='contests.evaluator')), + ], + ), + migrations.CreateModel( + name='ParticipantEnrollment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('enrolled', models.BooleanField(default=True)), + ('when', models.DateTimeField(auto_now_add=True)), + ('contest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contests.contest')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contests.participant')), + ], + ), + ] diff --git a/contests/migrations/0002_initial.py b/contests/migrations/0002_initial.py new file mode 100644 index 0000000..ceb3b62 --- /dev/null +++ b/contests/migrations/0002_initial.py @@ -0,0 +1,75 @@ +# Generated by Django 4.2.15 on 2024-08-22 14:16 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('contests', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='evaluator', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='evaluation', + name='contest', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contests.contest'), + ), + migrations.AddField( + model_name='evaluation', + name='edit', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contests.edit'), + ), + migrations.AddField( + model_name='evaluation', + name='evaluator', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='contests.evaluator'), + ), + migrations.AddField( + model_name='edit', + name='article', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='contests.article'), + ), + migrations.AddField( + model_name='edit', + name='contest', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contests.contest'), + ), + migrations.AddField( + model_name='edit', + name='participant', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='contests.participant'), + ), + migrations.AddField( + model_name='contest', + name='group', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='contests.group'), + ), + migrations.AddField( + model_name='article', + name='contest', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contests.contest'), + ), + migrations.AlterUniqueTogether( + name='participant', + unique_together={('contest', 'user')}, + ), + migrations.AlterUniqueTogether( + name='evaluator', + unique_together={('user', 'contest')}, + ), + migrations.AlterUniqueTogether( + name='edit', + unique_together={('contest', 'diff')}, + ), + ] diff --git a/contests/migrations/__init__.py b/contests/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/contests/models.py b/contests/models.py new file mode 100644 index 0000000..b3439b5 --- /dev/null +++ b/contests/models.py @@ -0,0 +1,135 @@ +from django.db import models + +class Group(models.Model): + name = models.CharField(max_length=100, unique=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return self.name + +class Contest(models.Model): + name_id = models.CharField(max_length=30, unique=True) + start_time = models.DateTimeField() + end_time = models.DateTimeField() + name = models.TextField() + group = models.ForeignKey('Group', on_delete=models.SET_NULL, null=True) + revert_time = models.SmallIntegerField(default=24) + official_list_pageid = models.IntegerField() + category_pageid = models.IntegerField(blank=True, null=True) + category_petscan = models.IntegerField(blank=True, null=True) + endpoint = models.URLField() + api_endpoint = models.URLField() + outreach_name = models.TextField() + campaign_event_id = models.IntegerField(default=None, blank=True, null=True) + bytes_per_points = models.IntegerField(default=3000) + max_bytes_per_article = models.IntegerField(default=90000) + minimum_bytes = models.IntegerField(blank=True, null=True) + pictures_per_points = models.SmallIntegerField(default=5) + pictures_mode = models.SmallIntegerField(default=0) + max_pic_per_article = models.SmallIntegerField(blank=True, null=True) + theme = models.TextField() + color = models.TextField(blank=True, default='') + started_update = models.DateTimeField(blank=True, null=True) + finished_update = models.DateTimeField(blank=True, null=True) + next_update = models.DateTimeField(blank=True, null=True) + + def __str__(self): + return self.name_id + +class Evaluator(models.Model): + user = models.ForeignKey('credentials.CustomUser', on_delete=models.PROTECT) + contest = models.ForeignKey('Contest', on_delete=models.CASCADE) + user_status = models.CharField(max_length=1, default='P') + + def __str__(self): + return self.user.username + + class Meta: + unique_together = ['user', 'contest'] + +class Article(models.Model): + contest = models.ForeignKey('Contest', on_delete=models.CASCADE) + articleID = models.IntegerField() + title = models.TextField() + active = models.BooleanField(default=True) + + def __str__(self): + return (f"{self.contest.name_id} - {self.articleID} - {self.title}") + + +class Participant(models.Model): + contest = models.ForeignKey('Contest', on_delete=models.CASCADE) + user = models.CharField(max_length=100) + timestamp = models.DateTimeField(blank=True) + global_id = models.IntegerField() + local_id = models.IntegerField(blank=True) + attached = models.DateTimeField(blank=True) + + def __str__(self): + return self.user + + class Meta: + unique_together = ['contest', 'user'] + +class ParticipantEnrollment(models.Model): + contest = models.ForeignKey('Contest', on_delete=models.CASCADE) + user = models.ForeignKey('Participant', on_delete=models.CASCADE) + enrolled = models.BooleanField(default=True) + when = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return (f"{self.contest.name_id} - {self.user.user}") + + +class Edit(models.Model): + contest = models.ForeignKey('Contest', on_delete=models.CASCADE) + diff = models.IntegerField() + article = models.ForeignKey('Article', on_delete=models.SET_NULL, null=True) + timestamp = models.DateTimeField(blank=True) + user_id = models.IntegerField() + participant = models.ForeignKey('Participant', on_delete=models.SET_NULL, null=True) + orig_bytes = models.IntegerField(blank=True, default=0) + new_page = models.BooleanField(default=False) + + def __str__(self): + return (f"{self.contest.name_id} - {self.diff}") + + class Meta: + unique_together = ['contest', 'diff'] + + +class Qualification(models.Model): + STATUS_CHOICE = ( + ('0', 'Reverted'), + ('1', 'Active'), + ) + contest = models.ForeignKey('Contest', on_delete=models.CASCADE) + diff = models.ForeignKey('Edit', on_delete=models.CASCADE) + evaluator = models.ForeignKey('Evaluator', on_delete=models.SET_NULL, null=True) + status = models.CharField(max_length=1, choices=STATUS_CHOICE, default='1') + when = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return (f"{self.contest.name_id} - {self.diff.diff}") + + +class Evaluation(models.Model): + STATUS_CHOICE = ( + ('0', 'Pending'), + ('1', 'Done'), + ('2', 'Hold'), + ('3', 'Skipped'), + ) + contest = models.ForeignKey('Contest', on_delete=models.CASCADE) + evaluator = models.ForeignKey('Evaluator', on_delete=models.SET_NULL, null=True) + edit = models.ForeignKey('Edit', on_delete=models.CASCADE) + valid_edit = models.BooleanField(default=False) + pictures = models.SmallIntegerField(default=0) + real_bytes = models.IntegerField(blank=True, default=0) + status = models.CharField(max_length=1, choices=STATUS_CHOICE, default='0') + when = models.DateTimeField(auto_now_add=True) + obs = models.TextField(blank=True, null=True) + + def __str__(self): + return (f"{self.contest.name_id} - {self.edit.diff} - {self.evaluator.user.username} - {self.status} - {self.when}") \ No newline at end of file diff --git a/contests/templates/base.html b/contests/templates/base.html new file mode 100644 index 0000000..b2d59ca --- /dev/null +++ b/contests/templates/base.html @@ -0,0 +1,124 @@ +{% load static %} +{% load i18n %} +{% get_current_language_bidi as LANGUAGE_BIDI %} + + + + + {{ contest.name }} + + + + + + {% block head %}{% endblock %} + + + +
+ + {% block pagename %}{% trans 'triage' %}{% endblock %} + {{ contest.name }} +
+ + + {% block content %}{% endblock %} + + \ No newline at end of file diff --git a/contests/templates/contest.html b/contests/templates/contest.html new file mode 100644 index 0000000..ae6ce91 --- /dev/null +++ b/contests/templates/contest.html @@ -0,0 +1,159 @@ +{% load static %} +{% load i18n %} + + + + + {{ contest.name }} + + + + + + + +
+ logo + + + {{ contest.name }} +
+ + +
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+ + + + diff --git a/contests/templates/home.html b/contests/templates/home.html new file mode 100644 index 0000000..169cbb6 --- /dev/null +++ b/contests/templates/home.html @@ -0,0 +1,177 @@ +{% load static %} +{% load i18n %} + + + + + {% trans 'main-title' %} + + + + + + + + +
+
+
{% csrf_token %} + + +
+
+ logo +
+ + + {% if user.is_authenticated %} + + {% else %} + + {% endif %} +
+ +
+ drawing +
+ + +
+
+
+ +

{% trans 'contest-select' %}

+
+
+
+ {% for group in contests_groups %} + + {% endfor %} +
+ {% for group, contests in contests_chooser.items %} +
+ {% for contest_data in contests %} +

+ {{ contest_data.1 }} +

+ {% endfor %} +
+ {% endfor %} +
+
+
+ + + +
+
+
{% trans 'index-about-short' %}
+
+

{% trans 'index-about-intro' %}

+

{% trans 'index-about-main' %}

+
+
+
+ + +
+
+
+ {% trans 'index-enroll-short' %} +
+ folder +
+
+

{% trans 'index-enroll-intro' %}

+

{% trans 'index-enroll-main' %}

+
+
+
+ + + + + + \ No newline at end of file diff --git a/contests/templates/triage.html b/contests/templates/triage.html new file mode 100644 index 0000000..c6c5fec --- /dev/null +++ b/contests/templates/triage.html @@ -0,0 +1,477 @@ +{% extends "base.html" %} +{% load static %} +{% load i18n %} +{% get_current_language_bidi as LANGUAGE_BIDI %} + + +{% block pagename %}{% trans 'triage' %}{% endblock %} + +{% block head %} + + {% if LANGUAGE_BIDI %} + + {% endif %} + + + + {% if action != None %} + + {% endif %} +{% endblock %} + +{% block onload %}calculateAuthorship('{% if edit.diff %}{{ edit.diff }}{% else %}false{% endif %}','{{ contest.endpoint }}'){% endblock %} + +{% block content %} +
+
+ {% if action.edit %} +
+

{% trans 'triage-lasteval' %}

+

+ {% trans 'triage-diff' %}: + {{ action.edit.diff }} +

+

+ {% trans 'triage-validedit' %}: {% if action.valid_edit %} {% trans 'yes' %}{% else %} {% trans 'no' %}{% endif %} +

+

+ {% trans 'triage-withimage' %}: {% if action.picture %} {% trans 'yes' %}{% else %} {% trans 'no' %}{% endif %} +

+

+ +

+
+ {% endif %} +
+

{% trans 'triage-evaluation' %}

+
+ {% csrf_token %} + +
+

{% trans 'isvalid' %}

+ +
+ +

+
+
+ {% if contest.picture_mode == 2 %} +

{% trans 'withimage' %}

+ +
+ {% else %} +

{% trans 'withimage' %}

+ +
+ +

+ {% endif %} +
+

+ +
+ +

+ + +
+
+
+ +
+
+
+ {% csrf_token %} + + + +
+
+
+
+
+

{% trans 'edits' %}

+
+
+
{% trans 'triage-toeval' %}
+

{{ onqueue }}

+
+
+
{% trans 'triage-towait' %}
+

{{ onwait }}

+
+
+
+
+
{% trans 'triage-onhold' %}
+

{{ onhold }}

+
+
+
{% trans 'triage-onskip' %}
+

{{ onskip }}

+
+
+

+

+ {% csrf_token %} + + +
+

+ {% if evaluator_status == 'G' %} +

+

+ {% csrf_token %} + + +
+

+ {% endif %} +
+
+
+ {% if error == 'updating' %} +
+

+

{% trans 'triage-database' %}

+ {% trans 'triage-databaseabout' %} +

+
+ {% elif not compare %} +
+

+

{% trans 'triage-noedit' %}

+

+
+ {% else %} +
+

{% trans 'triage-details' %}

+
+ {% trans 'label-user' %} + {{ compare.touser }} +
+ {% trans 'label-page' %} + {{ compare.totitle }} +
+ {% trans 'triage-authorship' %} + {% trans 'triage-verify' %} + +
+ {% trans 'label-timestamp' %} + {{ edit.timestamp }} (UTC) +
+
+ {% trans 'label-diff' %} + {{ edit.orig_bytes }} bytes +
+ {% trans 'triage-diff' %}: + {{ edit.diff }} +
+ {% trans 'triage-copyvio' %}: + {% trans 'triage-verify' %} + +
+ {% trans 'label-summary' %} + {{ compare.tocomment }} +
+
+ +
+

{% trans 'triage-differential' %}

+ + + + + + + + {{ compare_html | safe }} + + + {{ compare_mobile_html | safe }} + +
+
+ {% endif %} +
+
+
+

{% trans 'triage-recenthistory' %}

+ {% for event in context %} +

+ {{ event.user }} +
+ {{ event.timestamp }} +
+ {{ event.bytes }} bytes +

+ {% endfor %} +
+
+

{% trans 'triage-generalinfo' %}

+

+ {% trans 'triage-contestname' %} +
+ {{ contest.name }} +

+

+ {% trans 'triage-loggedname' %} +
+ {{ request.user }} +

+

+ {% trans 'triage-conteststart' %} +
+ {{ contest.start_time }} +

+

+ {% trans 'triage-contestend' %} +
+ {{ contest.end_time }} +

+

+ {% trans 'triage-lastupdate' %} +
+ {{ contest.finished_update }} +

+

+ {% trans 'triage-delay' %} +
+ {% blocktrans with triage_hours_1=contest.revert_time %}.{{ triage_hours_1 }}.{% endblocktrans %} +

+

+ {% trans 'triage-bpp' %} +
+ {% blocktrans with triage_bytes_1=contest.bytes_per_points %}.{{ triage_bytes_1 }}.{% endblocktrans %} +

+

+ {% trans 'triage-maxbytes' %} +
+ {% blocktrans with triage_bytes_1=contest.max_bytes_per_article %}.{{ triage_bytes_1 }}.{% endblocktrans %} + / + {% blocktrans with triage_points_1=triage_points %}.{{triage_points_1}}.{% endblocktrans %} +

+

+ {% trans 'triage-minbytes' %} +
+ {% if not contest.minimum_bytes %} + {% trans 'triage-indef' %} + {% elif contest.minimum_bytes == -1 %} + {% trans 'triage-includingall' %} + {% else %} + {% blocktrans with triage_bytes_1=contest.minimum_bytes %}.{{ triage_bytes_1 }}.{% endblocktrans %} + {% endif %} +

+

+ {% trans 'triage-ipp' %} +
+ {% if contest.pictures_per_points == 0 %} + {% trans 'triage-noimages' %} + {% else %} + {% blocktrans with triage_images_1=contest.pictures_per_points %}.{{ triage_images_1 }}.{% endblocktrans %} + {% endif %} +

+

+ {% trans 'triage-imagemode' %} +
+ {% if contest.pictures_per_points == 2 %} + {% trans 'triage-byimage' %} + {% elif contest.pictures_per_points == 1 %} + {% trans 'triage-byedition' %} + {% else %} + {% trans 'triage-bypage' %} + {% endif %} +

+

+ {% trans 'triage-maximages' %} +
+ {% if contest.max_pic_per_article %} + {{ contest.max_pic_per_article }} + {% else %} + {% trans 'triage-indef' %} + {% endif %} +

+
+
+
+{% endblock %} \ No newline at end of file diff --git a/contests/tests.py b/contests/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/contests/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/contests/triage.py b/contests/triage.py new file mode 100644 index 0000000..110ada7 --- /dev/null +++ b/contests/triage.py @@ -0,0 +1,356 @@ +import requests +from datetime import datetime, timedelta +from django.shortcuts import render, redirect, get_object_or_404 +from .models import Contest, Edit, Evaluation, Participant, ParticipantEnrollment, Qualification, Evaluator +from django.utils import timezone +from django.db.models import Sum, Case, When, Value, IntegerField, Q, OuterRef, Subquery + + +class TriageHandler: + def __init__(self, contest, user, api_endpoint): + self.contest = contest + self.user = user + self.api_endpoint = api_endpoint + + def do_evaluate(self, request): + if request.POST.get('skip'): + diff = request.POST.get('diff') or None + if not diff: + raise ValueError('Edit not found') + else: + if self.skip_edit(diff): + return {'action': 'skip'} + + elif request.POST.get('release'): + if self.release_edit(): + return {'action': 'release'} + + elif request.POST.get('unhold'): + if Evaluator.objects.get(contest=contest, user=self.user).user_status != 'G': + raise PermissionError('User is not a group member') + else: + if self.unhold_edit(): + return {'action': 'unhold'} + + else: + return self.evaluate_edit(request) + + + def get_evaluate(self, request): + # Fetch the contest data + contest = self.contest + return_dict = {'contest': contest } + + # Check if the update start time is greater than the end time (indicating an update is in progress) + if contest.start_time > contest.end_time: + return_dict.update({'error': 'updating'}) + + # Fetch the next available edit for triage + next_edit = self.get_next_edit(contest) + if not next_edit: + return_dict.update({'error': 'noedit'}) + else: + # Mark the edit as being held by the current user + self.hold_edit(next_edit, request.user) + + # Fetch comparison data for the edit + compare_data, compare_data_mobile = self.fetch_compare_data(contest.api_endpoint, next_edit.diff) + if not compare_data: + self.mark_edit_reverted(next_edit) + return_dict.update({'error': 'reverted'}) + else: + # Fetch the revision history of the article related to the edit + revision_history = self.fetch_revision_history(contest.api_endpoint, next_edit.article.articleID, contest.start_time) + last_revision = revision_history[-1] if revision_history else None + + # Validate the parent ID of the last (oldest) revision + if last_revision and not self.is_valid_parentid(last_revision['parentid']): + raise ValueError('Parent ID is not a number') + + # Update the revision history with data from the previous revision + self.update_history_with_previous_revision(contest.api_endpoint, revision_history, last_revision) + + # Build the context for rendering the template + render_context = self.build_context(revision_history, next_edit.diff) + + return_dict.update({ + 'context': render_context, + 'edit': next_edit, + 'compare': compare_data, + 'compare_html': compare_data['*'], + 'compare_mobile': compare_data_mobile, + 'compare_mobile_html': compare_data_mobile['*'], + 'history': revision_history, + }) + + # Calculate edit statistics (e.g., on queue, on hold) + edit_stats = self.calculate_edit_stats(contest) + return_dict.update({ + 'onqueue': edit_stats['onqueue'], + 'onwait': edit_stats['onwait'], + 'onskip': edit_stats['onskip'], + 'onhold': edit_stats['onhold'], + 'onpending': edit_stats['onhold'] + edit_stats['onskip'], + }) + + # Return the data for rendering the template + return return_dict + + def skip_edit(self, diff): + return Evaluation.objects.create( + contest=self.contest, + evaluator=Evaluator.objects.get(contest=self.contest, user=self.user), + edit=Edit.objects.get(diff=diff), + status='3' + ) + + def release_edit(self): + evaluator = Evaluator.objects.get(contest=self.contest, user=self.user) + + subquery = Evaluation.objects.filter( + contest=self.contest, + evaluator=evaluator, + edit=OuterRef('edit') + ).order_by('-when').values('pk')[:1] + + skipped = Evaluation.objects.filter( + contest=self.contest, + pk__in=Subquery(subquery), + status='3' + ) + + return Evaluation.objects.bulk_create([ + Evaluation(contest=self.contest, evaluator=evaluator, edit=skip.edit) for skip in skipped + ]) + + def evaluate_edit(self, request): + + diff = request.POST.get('diff') or None + if not diff: + raise ValueError('Edit not found') + + if self.contest.pictures_mode == 2 and isnumeric(request.POST.get('picture')): + picture = request.POST.get('picture') + else: + picture = True if request.POST.get('picture') == 'sim' else False + + overwrite_value = request.POST.get('overwrite') + real_bytes = int(overwrite_value) if overwrite_value and overwrite_value.isnumeric() else 0 + + evaluation = Evaluation.objects.create( + contest=self.contest, + evaluator=Evaluator.objects.get(contest=self.contest, user=self.user), + edit=Edit.objects.get(diff=request.POST.get('diff')), + valid_edit=True if request.POST.get('valid') == 'sim' else False, + pictures=picture, + real_bytes=real_bytes, + status='1', + obs=request.POST.get('obs') or None + ) + return evaluation.__dict__ + + def get_next_edit(self, contest): + + subquery = Evaluation.objects.filter( + contest=contest, + edit=OuterRef('edit') + ).order_by('-when').values('pk')[:1] + + evaluated = Evaluation.objects.filter( + contest=contest, + pk__in=Subquery(subquery), + status__in=['1', '3'] + ).values_list('edit', flat=True) + + held = Evaluation.objects.filter( + contest=contest, + pk__in=Subquery(subquery), + status='2' + ).exclude( + evaluator=Evaluator.objects.get(contest=contest, user=self.user) + ).values_list('edit', flat=True) + + active = Qualification.objects.filter( + contest=contest, + status=1, + ).values_list('diff_id', flat=True) + + edit = Edit.objects.filter( + pk__in=active, + contest=contest, + orig_bytes__gte=contest.minimum_bytes or 1, + timestamp__lte=timezone.now() - timedelta(hours=contest.revert_time), + participant__isnull=False, + ).exclude(pk__in=evaluated).exclude(pk__in=held).order_by('timestamp').first() + + return edit + + def unhold_edit(self): + subquery = Evaluation.objects.filter( + contest=contest, + edit=OuterRef('edit') + ).order_by('-when').values('pk')[:1] + + lockeds = Evaluation.objects.filter( + contest=contest, + pk__in=Subquery(subquery), + status__in=['2', '3'] + ) + + return Evaluation.objects.bulk_create([ + Evaluation(contest=self.contest, edit=locked.edit) for locked in lockeds + ]) + + # Function to mark the edit as being held by the user + def hold_edit(self, edit, user): + subquery = Evaluation.objects.filter( + contest=self.contest, + edit=OuterRef('edit') + ).order_by('-when').values('pk')[:1] + + held = Evaluation.objects.filter( + contest=self.contest, + pk__in=Subquery(subquery), + status=2 + ) + + if not held: + Evaluation.objects.create( + contest=self.contest, + evaluator=Evaluator.objects.get(contest=self.contest, user=user), + edit=edit, + status='2' # Status '2' indicates the edit is on hold + ) + + # Function to fetch comparison data for the edit + def fetch_compare_data(self, api_endpoint, diff): + compare_params = { + 'action': 'compare', + 'prop': 'title|diff|comment|user|ids', + 'format': 'json', + 'fromrev': diff, + 'torelative': 'prev', + } + compare = requests.get(api_endpoint, params=compare_params).json().get('compare', {}) + + compare_params['difftype'] = 'inline' + compare_mobile = requests.get(api_endpoint, params=compare_params).json().get('compare', {}) + + return compare, compare_mobile + + # Function to mark the edit as reverted + def mark_edit_reverted(self, edit): + edit.reverted = True + edit.save() + + # Function to fetch the revision history of the article related to the edit + def fetch_revision_history(self, api_endpoint, article_id, start_time): + history_params = { + 'action': 'query', + 'format': 'json', + 'prop': 'revisions', + 'pageids': article_id, + 'rvprop': 'timestamp|user|size|ids', + 'rvlimit': 'max', + 'rvend': start_time.strftime('%Y-%m-%dT%H:%M:%S.000Z'), + } + return requests.get(api_endpoint, params=history_params).json().get('query', {}).get('pages', {}).get(str(article_id), {}).get('revisions', []) + + # Function to validate the parent ID of the last revision + def is_valid_parentid(self, parentid): + return isinstance(parentid, int) and parentid >= 0 + + # Function to update the revision history with data from the previous revision + def update_history_with_previous_revision(self, api_endpoint, revision_history, last_revision): + if last_revision and last_revision['parentid'] != 0: + previous_revision_params = { + 'action': 'compare', + 'format': 'json', + 'fromrev': last_revision['revid'], + 'torelative': 'prev', + 'prop': 'size', + } + previous_revision = requests.get(api_endpoint, params=previous_revision_params).json().get('compare', {}) + revision_history.append({ + 'size': previous_revision.get('fromsize', 0), + 'timestamp': '1970-01-01T00:00:00', + 'user': 'None', + 'revid': 0, + }) + + # Function to build the context for rendering the template + def build_context(self, revision_history, current_diff): + context = [] + for i, edit in enumerate(revision_history): + if i + 1 < len(revision_history): + delta = edit['size'] - revision_history[i + 1]['size'] + else: + delta = edit['size'] + delta_color = 'green' if delta > 0 else ('red' if delta < 0 else 'grey') + history_class = 'w3-small w3-leftbar w3-border-grey w3-padding-small' if edit['revid'] == current_diff else 'w3-small' + + if edit['revid'] != 0: + context.append({ + 'class': history_class, + 'user': edit['user'], + 'timestamp': edit['timestamp'], + 'color': delta_color, + 'bytes': delta, + 'revid': edit['revid'], + }) + return context + + # Function to calculate edit statistics (e.g., on queue, on hold) + def calculate_edit_stats(self, contest): + + subquery = Evaluation.objects.filter( + contest=contest, + edit=OuterRef('edit') + ).order_by('-when').values('pk')[:1] + + evaluated = Evaluation.objects.filter( + contest=contest, + pk__in=Subquery(subquery), + status='1' + ).values_list('edit', flat=True) + + held = Evaluation.objects.filter( + contest=contest, + pk__in=Subquery(subquery), + status='2' + ).values_list('edit', flat=True) + + skipped = Evaluation.objects.filter( + contest=contest, + pk__in=Subquery(subquery), + status='3' + ).values_list('edit', flat=True) + + active = Qualification.objects.filter( + contest=contest, + status=1, + ).values_list('diff_id', flat=True) + + wait = Edit.objects.filter( + pk__in=active, + contest=contest, + orig_bytes__gte=contest.minimum_bytes or 1, + timestamp__gte=timezone.now() - timedelta(hours=contest.revert_time), + participant__isnull=False, + ).exclude(pk__in=evaluated).exclude(pk__in=held).exclude(pk__in=skipped) + + queue = Edit.objects.filter( + pk__in=active, + contest=contest, + orig_bytes__gte=contest.minimum_bytes or 1, + timestamp__lte=timezone.now() - timedelta(hours=contest.revert_time), + participant__isnull=False, + ).exclude(pk__in=evaluated).exclude(pk__in=held).exclude(pk__in=skipped) + + + onwait = wait.count() + onskip = Edit.objects.filter(pk__in=skipped).count() + onhold = Edit.objects.filter(pk__in=held).count() + onqueue = queue.count() + + return {'onqueue': onqueue, 'onwait': onwait, 'onskip': onskip, 'onhold': onhold} diff --git a/contests/urls.py b/contests/urls.py new file mode 100644 index 0000000..31a6381 --- /dev/null +++ b/contests/urls.py @@ -0,0 +1,9 @@ +from django.urls import path +from .views import contest_view, home_view, color_view, triage_view + +urlpatterns = [ + path('', home_view, name='home_view'), + path('contests/', contest_view, name='contest_view'), + path('color/', color_view, name='color_view'), + path('triage/', triage_view, name='triage_view'), +] diff --git a/contests/views.py b/contests/views.py new file mode 100644 index 0000000..124f38e --- /dev/null +++ b/contests/views.py @@ -0,0 +1,197 @@ +from django.shortcuts import render, redirect, get_object_or_404 +from .models import Contest, Edit, Participant, Qualification, Evaluator +from .triage import TriageHandler +from django.db import connection +from datetime import datetime, timedelta +from django.db.models import Count, Sum, Case, When, Value, IntegerField, Q, F, OuterRef, Subquery +from django.db.models.functions import TruncDay +from django.utils import timezone +from django.core.exceptions import PermissionDenied +from django.http import HttpResponse +from django.utils.html import escape +from django.contrib.auth.decorators import login_required +from django.utils import translation + +def color_view(request): + color = request.GET.get('color') + + # Check if the color parameter is provided + if not color: + return HttpResponse("/* Color parameter is missing */", content_type="text/css") + + # Escape the color parameter to avoid XSS attacks + color = escape(color) + + # Define the CSS content + css_content = f""" + .w3-color, + .w3-hover-color:hover {{ + color: #fff !important; + background-color: #{color} !important; + }} + + .w3-text-color, + .w3-hover-text-color:hover {{ + color: #{color} !important; + }} + + .w3-border-color, + .w3-hover-border-color:hover {{ + border-color: #{color} !important; + }} + """ + + # Return the CSS content as a response with the correct content type + return HttpResponse(css_content, content_type="text/css") + +def home_view(request): + # Get contests from the database + contests = Contest.objects.all().order_by('-start_time') + + contests_chooser = {} + for contest in contests: + group = contest.group.name + if group not in contests_chooser: + contests_chooser[group] = [] + contests_chooser[group].append([contest.name_id, contest.name]) + contests_groups = list(contests_chooser.keys()) + + # Render the main template + return render(request, 'home.html', { + 'contests_groups': contests_groups, + 'contests_chooser': contests_chooser, + }) + +def contest_view(request): + + contest_name_id = request.GET.get('contest') + if not contest_name_id: + return redirect('/') + contest = get_object_or_404(Contest, name_id=contest_name_id) + + date_min = contest.start_time + date_max = contest.end_time + date_range = date_range = [date_min + timedelta(days=x) for x in range((date_max - date_min).days + 1)] + + subquery = Qualification.objects.filter( + contest=contest, + diff=OuterRef('diff') + ).order_by('-when').values('pk')[:1] + approved_edits = Qualification.objects.filter( + contest=contest, + pk__in=Subquery(subquery), + status=1 + ).values_list('diff__diff', flat=True) + + new_articles = { + entry['date']: entry['count'] + for entry in Edit.objects + .filter(contest=contest, new_page=True, timestamp__range=(date_min, date_max)) + .annotate(date=TruncDay('timestamp')) + .values('date') + .annotate(count=Count('id')) + .order_by('date') + } + new_participants = { + entry['date']: entry['count'] + for entry in Participant.objects + .filter(contest=contest, timestamp__range=(date_min, date_max)) + .annotate(date=TruncDay('timestamp')) + .values('date') + .annotate(count=Count('id')) + .order_by('date') + } + total_edits = { + entry['date']: entry['count'] + for entry in Edit.objects + .filter(contest=contest, timestamp__range=(date_min, date_max)) + .annotate(date=TruncDay('timestamp')) + .values('date') + .annotate(count=Count('id')) + .order_by('date') + } + total_bytes = { + entry['date']: entry['sum_bytes'] + for entry in Edit.objects + .filter(contest=contest, orig_bytes__gte=0, timestamp__range=(date_min, date_max)) + .annotate(date=TruncDay('timestamp')) + .values('date') + .annotate(sum_bytes=Sum('orig_bytes')) + .order_by('date') + } + valid_edits = { + entry['date']: entry['count'] + for entry in Edit.objects + .filter(contest=contest, pk__in=approved_edits, timestamp__range=(date_min, date_max)) + .annotate(date=TruncDay('timestamp')) + .values('date') + .annotate(count=Count('id')) + .order_by('date') + } + valid_bytes = { + entry['date']: entry['sum_bytes'] + for entry in Edit.objects + .filter(contest=contest, pk__in=approved_edits, timestamp__range=(date_min, date_max)) + .annotate(date=TruncDay('timestamp')) + .values('date') + .annotate(sum_bytes=Sum('orig_bytes')) + .order_by('date') + } + + # Prepare the result + dates = [] + new_articles_list = [] + new_participants_list = [] + total_edits_list = [] + total_bytes_list = [] + valid_edits_list = [] + valid_bytes_list = [] + + for date in date_range: + dates.append(str(abs(date - date_min).days)) + new_articles_list.append(str(new_articles.get(date, 0))) + new_participants_list.append(str(new_participants.get(date, 0))) + total_edits_list.append(str(total_edits.get(date, 0))) + total_bytes_list.append(str(total_bytes.get(date, 0))) + valid_edits_list.append(str(valid_edits.get(date, 0))) + valid_bytes_list.append(str(valid_bytes.get(date, 0))) + + result = { + 'date': ', '.join(dates), + 'new_articles': ', '.join(new_articles_list), + 'new_participants': ', '.join(new_participants_list), + 'total_edits': ', '.join(total_edits_list), + 'total_bytes': ', '.join(total_bytes_list), + 'valid_edits': ', '.join(valid_edits_list), + 'valid_bytes': ', '.join(valid_bytes_list), + } + + return render(request, 'contest.html', {'contest': contest, 'result': result}) + +@login_required() +def triage_view(request): + contest_name_id = request.GET.get('contest') + if not contest_name_id: + return redirect('/') + + contest = get_object_or_404(Contest, name_id=contest_name_id) + try: + Evaluator.objects.get(contest=contest, user=request.user) + except Evaluator.DoesNotExist: + raise PermissionDenied("You are not allowed to access this page.") + + handler = TriageHandler(contest=contest, user=request.user, api_endpoint=contest.api_endpoint) + if request.method == 'POST': + do_evaluate = handler.do_evaluate(request) + else: + do_evaluate = {'action': None} + get_evaluate = handler.get_evaluate(request) + + triage_dict = get_evaluate | do_evaluate + triage_dict.update({ + 'triage_points': int(contest.max_bytes_per_article / contest.bytes_per_points), + 'evaluator_status': Evaluator.objects.get(contest=contest, user=request.user).user_status, + 'right': 'left' if translation.get_language_bidi() else 'right', + 'left': 'right' if translation.get_language_bidi() else 'left', + }) + return render(request, "triage.html", triage_dict) \ No newline at end of file diff --git a/credentials/__init__.py b/credentials/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/credentials/admin.py b/credentials/admin.py new file mode 100644 index 0000000..d9a978f --- /dev/null +++ b/credentials/admin.py @@ -0,0 +1,10 @@ +from django.contrib import admin +from credentials.models import CustomUser + +# Register your models here. +class AccountUserAdmin(admin.ModelAdmin): + list_display = ('username', 'email', 'is_staff', 'is_active', 'date_joined') + search_fields = ('username', 'email') + readonly_fields = ('date_joined',) + +admin.site.register(CustomUser, AccountUserAdmin) \ No newline at end of file diff --git a/credentials/apps.py b/credentials/apps.py new file mode 100644 index 0000000..ae2f28a --- /dev/null +++ b/credentials/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CredentialsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'credentials' diff --git a/credentials/migrations/0001_initial.py b/credentials/migrations/0001_initial.py new file mode 100644 index 0000000..fe44f64 --- /dev/null +++ b/credentials/migrations/0001_initial.py @@ -0,0 +1,40 @@ +# Generated by Django 4.2.15 on 2024-08-22 14:16 + +import django.contrib.auth.models +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='CustomUser', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(max_length=100, unique=True, verbose_name='Username')), + ('email', models.EmailField(blank=True, max_length=255, null=True, verbose_name='Email address')), + ('is_staff', models.BooleanField(default=False, verbose_name='Staff status')), + ('is_active', models.BooleanField(default=True, verbose_name='Active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Date joined')), + ('user_groups', models.JSONField(null=True)), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ], + options={ + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/credentials/migrations/__init__.py b/credentials/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/credentials/models.py b/credentials/models.py new file mode 100644 index 0000000..e52f9c9 --- /dev/null +++ b/credentials/models.py @@ -0,0 +1,38 @@ +from django.db import models +from django.utils import timezone +from django.dispatch import receiver +from django.db.models.signals import post_save +from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, UserManager + +class CustomUser(AbstractBaseUser, PermissionsMixin): + username = models.CharField( + "Username", + max_length=100, + unique=True + ) + email = models.EmailField( + "Email address", + max_length=255, + null=True, + blank=True + ) + is_staff = models.BooleanField( + "Staff status", + default=False + ) + is_active = models.BooleanField( + "Active", + default=True + ) + date_joined = models.DateTimeField( + "Date joined", + default=timezone.now + ) + user_groups = models.JSONField( + null=True, + blank=False + ) + + objects = UserManager() + USERNAME_FIELD = 'username' + EMAIL_FIELD = 'email' diff --git a/credentials/pipeline.py b/credentials/pipeline.py new file mode 100644 index 0000000..d9467ab --- /dev/null +++ b/credentials/pipeline.py @@ -0,0 +1,20 @@ +def get_username(strategy, details, user=None, *args, **kwargs): + """ + This pipeline function customizes the behavior of python-social-auth to return the username + based on the project's custom user model. + + Parameters: + - strategy: The strategy used by python-social-auth. + - details: A dictionary containing user details retrieved from the authentication provider. + - user: An optional User object. If provided, the function returns the username of this user. + - *args: Additional positional arguments required by python-social-auth. + - **kwargs: Additional keyword arguments required by python-social-auth. + + Returns: + - dict: A dictionary containing the username. If a user is provided, it returns {'username': user.username}. + Otherwise, it returns {'username': details['username']}. + """ + if user: + return {"username": user.username} + else: + return {"username": details['username']} diff --git a/credentials/tests.py b/credentials/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/credentials/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/credentials/urls.py b/credentials/urls.py new file mode 100644 index 0000000..6dbde5f --- /dev/null +++ b/credentials/urls.py @@ -0,0 +1,8 @@ +from django.urls import path, include +from . import views + +urlpatterns = [ + path("login/", views.login_oauth, name="login"), + path("oauth/", include("social_django.urls", namespace="social_django")), + path("logout/", views.logout, name="logout"), +] diff --git a/credentials/views.py b/credentials/views.py new file mode 100644 index 0000000..15a4469 --- /dev/null +++ b/credentials/views.py @@ -0,0 +1,16 @@ +from django.shortcuts import render, reverse, redirect +from django.contrib.auth.decorators import login_required +from django.contrib.auth import logout as auth_logout + +# Create your views here. +@login_required() +def triage_view(request): + context = {} + return render(request, "triage.html", context) + +def login_oauth(request): + return redirect(reverse('social:begin', kwargs={"backend": "mediawiki"})) + +def logout(request): + auth_logout(request) + return redirect(reverse('home_view')) \ No newline at end of file diff --git a/font/GPL.txt b/font/GPL.txt deleted file mode 100644 index b890e55..0000000 --- a/font/GPL.txt +++ /dev/null @@ -1,343 +0,0 @@ - GNU GENERAL PUBLIC LICENSE (with font exception) - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc. - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Library General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - -As a special exception, if you create a document which uses this font, and embed this font or unaltered portions of this font into the document, this font does not by itself cause the resulting document to be covered by the GNU General Public License. This exception does not however invalidate any other reasons why the document might be covered by the GNU General Public License. If you modify this font, you may extend this exception to your version of the font, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA - - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Library General -Public License instead of this License. - diff --git a/font/LICENCE.txt b/font/LICENCE.txt deleted file mode 100644 index dd8b672..0000000 --- a/font/LICENCE.txt +++ /dev/null @@ -1,7 +0,0 @@ -- Lizenz / Licence - - -Unsere Schriften sind frei im Sinne der GPL, d.h. (stark vereinfacht) dass Veränderungen an der Schriftart erlaubt sind unter der Bedingung, dass diese wieder der Öffentlichkeit unter gleicher Lizenz freigegeben werden. Querdenker behaupten oft, dass bei der Verwendung einer GPL-Schrift eingebettet in beispielsweise eine PDF auch diese freigestellt werden müsse. Deshalb gibt es die sogenannte "Font-exception" der GPL (welche diesem Lizenztext hinzugefügt wurde). Weitere Informationen zur GPL (Lizenztext mit Font-Exzeption als GPL.txt in diesem Paket). -Zusätzlich stehen die Schriften unter der Open Font License (siehe OFL.txt). - -Our fonts are free in the sense of the GPL. In short: Changing the font is allowed as long as the derivative work is published under the same licence again. Pedantics keep claiming that the embedded use of GPL-fonts in i.e. PDFs requires the free publication of the PDF as well. This is why our GPL contains the so called "font exception". Further information about the GPL (licence text with font exception see GPL.txt in this package). -Additionally our fonts are licensed under the Open Fonts License (see OFL.txt). \ No newline at end of file diff --git a/font/LinLibertine_Re-4.7.3.otf b/font/LinLibertine_Re-4.7.3.otf deleted file mode 100644 index 139b9ef76f957cbd317b08723a2571e240d88a18..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439388 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¥8)m?V8m>FCjty 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

g|hvI9CXMO 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 SHr/dev/null 2>&1 ; pwd -P )" - -# Check if the username contains "test" -if [[ "$SCRIPTPATH" != *"test"* ]]; then - echo "Path does not contain 'test'. Performing git pull and exiting." - git pull -r - exit 0 -fi - -# Run git for-each-ref and store the output -git fetch -p -branches=$(git for-each-ref --format='%(committerdate:format:%s):%(refname)' refs/remotes/origin/) - -# Initialize variables to keep track of the most recent branch and its commit date -most_recent_branch="" -most_recent_date="0" - -# Loop through the branches -IFS=$'\n' # Set the input field separator to newline -for branch in $branches; do - # Extract the commit date and branch name - commit_date=$(echo "$branch" | cut -d: -f1) - branch_name=$(echo "$branch" | cut -d: -f2 | sed 's/^refs\/remotes\/origin\///') - - # Check if the branch name is "HEAD" and skip it - if [[ "$branch_name" == "HEAD" ]]; then - continue - fi - - # Check if the branch name contains "hotfix", "feature", or "develop" - if [[ $branch_name == *hotfix* ]]; then - priority=4 - elif [[ $branch_name == *release* ]]; then - priority=3 - elif [[ $branch_name == *feature* ]]; then - priority=2 - elif [[ $branch_name == *develop* ]]; then - priority=1 - else - priority=0 - fi - - # Compare commit dates and priorities - if [[ "$commit_date" > "$most_recent_date" || ( "$commit_date" == "$most_recent_date" && $priority > $most_recent_priority ) ]]; then - most_recent_date="$commit_date" - most_recent_branch="$branch_name" - most_recent_priority="$priority" - fi -done - -# Checkout the most recent branch -if [ -n "$most_recent_branch" ]; then - git checkout "$most_recent_branch" - git pull -r - echo "Checked out branch: $most_recent_branch" -else - echo "No remote branches found." -fi diff --git a/index.php b/index.php deleted file mode 100644 index 9e153b0..0000000 --- a/index.php +++ /dev/null @@ -1,275 +0,0 @@ - - - - - - <?=§('main-title')?> - - - - - - - - -
-
-
- -
-
- logo -
- - -
-
- drawing -
- - -
- - - -
-
-
-
-

-

-
-
-
- - -
-
-
- -
- folder -
-
-

-

-
-
-
- - - - - - diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..69405cd --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'wikiscore.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6e4178b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +django +markdown +python-dateutil +django-filter +social-auth-app-django +rest-social-auth +polib +python-dotenv \ No newline at end of file diff --git a/sonar-project.properties b/sonar-project.properties deleted file mode 100644 index aa3f98b..0000000 --- a/sonar-project.properties +++ /dev/null @@ -1,12 +0,0 @@ -sonar.projectKey=wikimovimentobrasil_wikiscore -sonar.organization=wikimovimentobrasil - -# This is the name and version displayed in the SonarCloud UI. -#sonar.projectName=wikimovimentobrasil_wikiscore -#sonar.projectVersion=1.0 - -# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. -#sonar.sources=. - -# Encoding of the source code. Default is default system encoding -#sonar.sourceEncoding=UTF-8 diff --git a/update.php b/update.php deleted file mode 100644 index 8bddf3d..0000000 --- a/update.php +++ /dev/null @@ -1,102 +0,0 @@ - NOW() AND - `started_update` < `finished_update` AND - `next_update` < NOW() - ) - ) -'; -$contests_query = mysqli_prepare($con, $contests_statement); -mysqli_stmt_execute($contests_query); -$contests_result = mysqli_stmt_get_result($contests_query); -while ($row = mysqli_fetch_assoc($contests_result)) { - $contests_array[] = $row['name_id']; -} -if (!isset($contests_array)) die("Sem atualizações previstas.\n"); - -//Define comandos a ser executados para cada concurso -$steps = ["load_edits", "load_users", "load_reverts"]; - -//Define queries -$start_query = mysqli_prepare( - $con, - "UPDATE - `manage__contests` - SET - `started_update` = NOW() - WHERE - `name_id` = ?" -); -$finish_query = mysqli_prepare( - $con, - "UPDATE - `manage__contests` - SET - `finished_update` = NOW(), - `next_update` = INTERVAL 1 DAY + NOW() - WHERE - `name_id` = ?" -); -mysqli_stmt_bind_param($start_query, 's', $contest); -mysqli_stmt_bind_param($finish_query, 's', $contest); - -//Loop de concursos -foreach ($contests_array as $contest) { - - //Grava horário de início - mysqli_stmt_execute($start_query); - - //Loop de scripts - foreach ($steps as $script) { - - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, "https://wikiscore.toolforge.org/index.php?contest={$contest}&page={$script}"); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true ); - curl_setopt($ch, CURLOPT_USERAGENT, 'WikiCronJob/1.0'); - - $result = curl_exec($ch); - if (curl_errno($ch)) $result = curl_error($ch); - curl_close($ch); - - print(time()."{$contest}\t{$script}\n"); - } - - //Grava horário de finalização - mysqli_stmt_execute($finish_query); -} -?> diff --git a/wikiscore/__init__.py b/wikiscore/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wikiscore/asgi.py b/wikiscore/asgi.py new file mode 100644 index 0000000..b991717 --- /dev/null +++ b/wikiscore/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for wikiscore project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'wikiscore.settings') + +application = get_asgi_application() diff --git a/wikiscore/settings.py b/wikiscore/settings.py new file mode 100644 index 0000000..298dfb7 --- /dev/null +++ b/wikiscore/settings.py @@ -0,0 +1,162 @@ +""" +Django settings for wikiscore project. + +Generated by 'django-admin startproject' using Django 4.2.9. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.2/ref/settings/ +""" +import os +from pathlib import Path +from datetime import timedelta +from dotenv import load_dotenv + +load_dotenv() + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +# Secrets +SECRET_KEY = os.environ.get('SECRET_KEY') +SOCIAL_AUTH_MEDIAWIKI_URL = 'https://meta.wikimedia.org/w/index.php' +SOCIAL_AUTH_MEDIAWIKI_KEY = os.environ.get("SOCIAL_AUTH_MEDIAWIKI_KEY") +SOCIAL_AUTH_MEDIAWIKI_SECRET = os.environ.get("SOCIAL_AUTH_MEDIAWIKI_SECRET") +SOCIAL_AUTH_MEDIAWIKI_CALLBACK = 'http://127.0.0.1:8000/oauth/complete/mediawiki/' +LOGIN_URL = 'login' +LOGIN_REDIRECT_URL = '/' +SOCIAL_AUTH_URL_NAMESPACE = 'social' +PROTECTED_USER_FIELDS = ['groups'] + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'contests', + 'credentials', + 'social_django', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + "django.middleware.locale.LocaleMiddleware", + "django.middleware.common.CommonMiddleware", + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'social_django.middleware.SocialAuthExceptionMiddleware', +] + +ROOT_URLCONF = 'wikiscore.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR, 'templates')], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + 'social_django.context_processors.backends', + 'social_django.context_processors.login_redirect', + ], + }, + }, +] + +AUTHENTICATION_BACKENDS = ( + 'social_core.backends.mediawiki.MediaWiki', + 'django.contrib.auth.backends.ModelBackend', +) + +AUTH_USER_MODEL = 'credentials.CustomUser' + +SOCIAL_AUTH_PIPELINE = ( + 'social_core.pipeline.social_auth.social_details', + 'social_core.pipeline.social_auth.social_uid', + 'social_core.pipeline.social_auth.auth_allowed', + 'social_core.pipeline.social_auth.social_user', + 'credentials.pipeline.get_username', + 'social_core.pipeline.user.create_user', + 'social_core.pipeline.social_auth.associate_user', + 'social_core.pipeline.social_auth.load_extra_data', + 'social_core.pipeline.user.user_details', +) + +WSGI_APPLICATION = 'wikiscore.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/4.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.2/topics/i18n/ + +LANGUAGE_CODE = 'en' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + +LOCALE_PATHS = [ + os.path.join(BASE_DIR, 'locale'), +] + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.2/howto/static-files/ + +STATIC_URL = 'static/' +STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')] + +# Default primary key field type +# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/wikiscore/urls.py b/wikiscore/urls.py new file mode 100644 index 0000000..94ddec0 --- /dev/null +++ b/wikiscore/urls.py @@ -0,0 +1,27 @@ +""" +URL configuration for wikiscore project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path +from django.conf.urls import include + +urlpatterns = [ + path('admin/', admin.site.urls), + path('', include('contests.urls')), + path('', include('credentials.urls')), + path('', include('social_django.urls')), + path("i18n/", include("django.conf.urls.i18n")), +] diff --git a/wikiscore/wsgi.py b/wikiscore/wsgi.py new file mode 100644 index 0000000..3711f4c --- /dev/null +++ b/wikiscore/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for wikiscore project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'wikiscore.settings') + +application = get_wsgi_application() From f50a423e9a42d75930adc2cf2f245f9ddbac91b9 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Thu, 22 Aug 2024 13:58:18 -0300 Subject: [PATCH 03/75] chore: Add language code conversion function --- .gitignore | 3 ++- contests/apps.py | 4 ++++ contests/locale.py | 29 +++++++++++++++++++++++ contests/management/commands/translate.py | 20 ++++++++++++---- contests/templates/contest.html | 3 --- wikiscore/settings.py | 2 ++ 6 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 contests/locale.py diff --git a/.gitignore b/.gitignore index 4373817..51b7cbf 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ media/ .vscode/ pytest.ini coverage.xml -static/ \ No newline at end of file +static/ +locale/ \ No newline at end of file diff --git a/contests/apps.py b/contests/apps.py index d8b7671..0720762 100644 --- a/contests/apps.py +++ b/contests/apps.py @@ -4,3 +4,7 @@ class ContestsConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'contests' + + def ready(self): + from .locale import add_custom_languages + add_custom_languages() \ No newline at end of file diff --git a/contests/locale.py b/contests/locale.py new file mode 100644 index 0000000..b41fe77 --- /dev/null +++ b/contests/locale.py @@ -0,0 +1,29 @@ +import os +from django.conf import settings +from django.conf.locale import LANG_INFO + +def get_available_languages(): + locale_dir = os.path.join(settings.BASE_DIR, 'locale') + languages = [] + + if os.path.exists(locale_dir): + for folder_name in os.listdir(locale_dir): + if folder_name == 'qqq': # Skip the message documentation folder + continue + folder_path = os.path.join(locale_dir, folder_name) + if os.path.isdir(folder_path) and os.path.exists(os.path.join(folder_path, 'LC_MESSAGES')): + folder_name = folder_name.replace('_', '-').lower() + languages.append((folder_name, folder_name)) # Append the language code and name + else: + languages = [('en', 'English')] + + return languages + +def add_custom_languages(): + if 'xal' not in LANG_INFO: + LANG_INFO['xal'] = { + 'name': 'Kalmyk', + 'name_local': 'Хальмг', # Native name + 'bidi': False, # Set to True if it's a right-to-left language + 'code': 'xal', + } \ No newline at end of file diff --git a/contests/management/commands/translate.py b/contests/management/commands/translate.py index 20a3557..c259139 100644 --- a/contests/management/commands/translate.py +++ b/contests/management/commands/translate.py @@ -14,15 +14,27 @@ def handle(self, *args, **options): if filename.endswith('.json'): filepath = os.path.join(json_dir, filename) translations = self.load_translations(filepath) - language = filename.split('.')[0] - po = self.convert_to_po(translations, language) - po_path = os.path.join(settings.BASE_DIR, 'locale', language, 'LC_MESSAGES', 'django.po') + language_code = filename.split('.')[0] + language_code = self.convert_language_code(language_code) + po = self.convert_to_po(translations, language_code) + po_path = os.path.join(settings.BASE_DIR, 'locale', language_code, 'LC_MESSAGES', 'django.po') self.save_po_file(po, po_path) def load_translations(self, filepath): with open(filepath, 'r', encoding='utf-8') as f: return json.load(f) + def convert_language_code(self, code): + parts = code.split('-') + language = parts[0] + if len(parts) == 1: + return language + region = parts[1] + if len(region) == 2: + return f'{language}_{region.upper()}' + else: + return f'{language}_{region[0].upper()}{region[1:]}' + def convert_to_po(self, translations, language): po = POFile(encoding='utf-8') po.metadata = { @@ -30,7 +42,7 @@ def convert_to_po(self, translations, language): 'Content-Transfer-Encoding': '8bit', 'Language': language, } - for key, value in translations.items(): + for key, value in self.flatten_dict(translations).items(): if isinstance(value, str): # Count the number of placeholders in the string count = len(re.findall(r'\$\d+', value)) diff --git a/contests/templates/contest.html b/contests/templates/contest.html index ae6ce91..3e6f84d 100644 --- a/contests/templates/contest.html +++ b/contests/templates/contest.html @@ -18,8 +18,6 @@ {{ contest.name }} - -
@@ -37,7 +35,6 @@
- +{% endif %} +{% endblock %} \ No newline at end of file diff --git a/contests/urls.py b/contests/urls.py index 31a6381..84c078b 100644 --- a/contests/urls.py +++ b/contests/urls.py @@ -1,9 +1,11 @@ from django.urls import path from .views import contest_view, home_view, color_view, triage_view +from .counter import counter_view urlpatterns = [ path('', home_view, name='home_view'), path('contests/', contest_view, name='contest_view'), path('color/', color_view, name='color_view'), path('triage/', triage_view, name='triage_view'), + path('counter/', counter_view, name='counter_view'), ] From 7d90f1128733968b086fe65927dd7b52f4dd4e13 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Mon, 26 Aug 2024 16:12:07 -0300 Subject: [PATCH 09/75] chore: Add backtrack view and template for contests --- contests/templates/backtrack.html | 64 +++++++++++++++++++++++++++++++ contests/templates/base.html | 2 +- contests/urls.py | 3 +- contests/views.py | 35 ++++++++++++++++- 4 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 contests/templates/backtrack.html diff --git a/contests/templates/backtrack.html b/contests/templates/backtrack.html new file mode 100644 index 0000000..cbeb7b2 --- /dev/null +++ b/contests/templates/backtrack.html @@ -0,0 +1,64 @@ +{% extends "base.html" %} +{% load static %} +{% load i18n %} +{% get_current_language_bidi as LANGUAGE_BIDI %} + + +{% block pagename %}{% trans 'backtrack' %}{% endblock %} + + +{% block content %} +
+
+
+

+ {% trans 'backtrack-about' %} +

+
+
+ {% for user, data in result %} +
+

{{ user }}

+
+
    + {% for diff in data.diffs %} +
  • +
    + + {{ diff.diff }} + +
    + + {% blocktrans with backtrack_stats_1=diff.timestamp backtrack_stats_2=diff.bytes %}.{{ backtrack_stats_1 }}.{{ backtrack_stats_2 }}.{% endblocktrans %} + +
    +
    + {% csrf_token %} + + +
    +
  • + {% endfor %} +
+
+
+
{% blocktrans with backtrack_enrollment_1=data.enrollment_timestamp %}.{{ backtrack_enrollment_1 }}.{% endblocktrans %}
+
+
+ {% endfor %} +
+ {% if diff %} + + {% endif %} +{% endblock %} \ No newline at end of file diff --git a/contests/templates/base.html b/contests/templates/base.html index 2cb919a..14c8cd2 100644 --- a/contests/templates/base.html +++ b/contests/templates/base.html @@ -81,7 +81,7 @@
{% trans 'triage-panel' %}
rel="noopener" class="w3-bar-item w3-button w3-padding ">   {% trans 'triage-evaluated' %} -   {% trans 'backtrack' %} diff --git a/contests/urls.py b/contests/urls.py index 84c078b..ab3ca75 100644 --- a/contests/urls.py +++ b/contests/urls.py @@ -1,5 +1,5 @@ from django.urls import path -from .views import contest_view, home_view, color_view, triage_view +from .views import contest_view, home_view, color_view, triage_view, backtrack_view from .counter import counter_view urlpatterns = [ @@ -8,4 +8,5 @@ path('color/', color_view, name='color_view'), path('triage/', triage_view, name='triage_view'), path('counter/', counter_view, name='counter_view'), + path('backtrack/', backtrack_view, name='backtrack_view'), ] diff --git a/contests/views.py b/contests/views.py index 124f38e..f002c89 100644 --- a/contests/views.py +++ b/contests/views.py @@ -194,4 +194,37 @@ def triage_view(request): 'right': 'left' if translation.get_language_bidi() else 'right', 'left': 'right' if translation.get_language_bidi() else 'left', }) - return render(request, "triage.html", triage_dict) \ No newline at end of file + return render(request, "triage.html", triage_dict) + +@login_required() +def backtrack_view(request): + contest = get_object_or_404(Contest, name_id=request.GET.get('contest')) + + qualified = False + diff = None + if request.POST.get('diff'): + diff = request.POST.get('diff') + edit = Edit.objects.get(diff=diff) + evaluator = Evaluator.objects.get(contest=contest, user=request.user) + new_qualification = Qualification.objects.create(contest=contest, diff=edit, evaluator=evaluator) + qualified = Edit.objects.filter(contest=contest, diff=diff, last_qualification__isnull=True).update(last_qualification=new_qualification) + + edits = Edit.objects.filter( + contest=contest, + last_qualification__isnull=True, + participant__isnull=False + ).order_by('participant__user', 'timestamp') + + result = defaultdict(lambda: {'enrollment_timestamp': None, 'diffs': []}) + for edit in edits: + user = edit.participant.user + result[user]['enrollment_timestamp'] = edit.participant.timestamp + result[user]['diffs'].append({'diff': edit.diff, 'bytes': edit.orig_bytes, 'timestamp': edit.timestamp}) + + return render(request, 'backtrack.html', { + 'contest': contest, + 'result': result.items(), + 'diff': diff if qualified else None, + 'right': 'left' if translation.get_language_bidi() else 'right', + }) + From afa90f1d83f5e2cafbf0447424c56fa3ca5f8576 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Mon, 26 Aug 2024 16:13:43 -0300 Subject: [PATCH 10/75] chore: Add contest_evaluator_required decorator to restricted views --- contests/views.py | 42 +++++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/contests/views.py b/contests/views.py index f002c89..595a903 100644 --- a/contests/views.py +++ b/contests/views.py @@ -5,12 +5,27 @@ from datetime import datetime, timedelta from django.db.models import Count, Sum, Case, When, Value, IntegerField, Q, F, OuterRef, Subquery from django.db.models.functions import TruncDay -from django.utils import timezone +from django.utils import timezone, translation +from django.utils.html import escape from django.core.exceptions import PermissionDenied from django.http import HttpResponse -from django.utils.html import escape from django.contrib.auth.decorators import login_required -from django.utils import translation +from collections import defaultdict +from functools import wraps + +def contest_evaluator_required(view_func): + @wraps(view_func) + def _wrapped_view(request, *args, **kwargs): + contest_name_id = request.GET.get('contest') + if not contest_name_id: + return redirect('/') + contest = get_object_or_404(Contest, name_id=contest_name_id) + try: + Evaluator.objects.get(contest=contest, user=request.user) + except Evaluator.DoesNotExist: + raise PermissionDenied("You are not allowed to access this page.") + return view_func(request, *args, **kwargs) + return _wrapped_view def color_view(request): color = request.GET.get('color') @@ -63,12 +78,18 @@ def home_view(request): }) def contest_view(request): - contest_name_id = request.GET.get('contest') if not contest_name_id: return redirect('/') contest = get_object_or_404(Contest, name_id=contest_name_id) + is_evaluator = False + try: + Evaluator.objects.get(contest=contest, user=request.user) + is_evaluator = True + except Evaluator.DoesNotExist: + pass + date_min = contest.start_time date_max = contest.end_time date_range = date_range = [date_min + timedelta(days=x) for x in range((date_max - date_min).days + 1)] @@ -164,21 +185,15 @@ def contest_view(request): 'total_bytes': ', '.join(total_bytes_list), 'valid_edits': ', '.join(valid_edits_list), 'valid_bytes': ', '.join(valid_bytes_list), + 'is_evaluator': is_evaluator, } return render(request, 'contest.html', {'contest': contest, 'result': result}) @login_required() +@contest_evaluator_required def triage_view(request): - contest_name_id = request.GET.get('contest') - if not contest_name_id: - return redirect('/') - - contest = get_object_or_404(Contest, name_id=contest_name_id) - try: - Evaluator.objects.get(contest=contest, user=request.user) - except Evaluator.DoesNotExist: - raise PermissionDenied("You are not allowed to access this page.") + contest = get_object_or_404(Contest, name_id=request.GET.get('contest')) handler = TriageHandler(contest=contest, user=request.user, api_endpoint=contest.api_endpoint) if request.method == 'POST': @@ -197,6 +212,7 @@ def triage_view(request): return render(request, "triage.html", triage_dict) @login_required() +@contest_evaluator_required def backtrack_view(request): contest = get_object_or_404(Contest, name_id=request.GET.get('contest')) From 1ddffc623de398b044f844afcfa3cbf3a7b9088d Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Mon, 26 Aug 2024 16:54:56 -0300 Subject: [PATCH 11/75] chore: Refactor counter view to use CounterHandler class --- contests/counter.py | 250 ++++++++++++++++---------------- contests/templates/counter.html | 2 + contests/urls.py | 3 +- contests/views.py | 11 ++ 4 files changed, 142 insertions(+), 124 deletions(-) diff --git a/contests/counter.py b/contests/counter.py index b92d14e..16524eb 100644 --- a/contests/counter.py +++ b/contests/counter.py @@ -2,139 +2,145 @@ from django.utils import timezone from contests.models import Contest, Edit from django.shortcuts import render, redirect, get_object_or_404 +from datetime import timezone as dt_timezone -def counter_view(request): - contest_name_id = request.GET.get('contest') - if not contest_name_id: - return redirect('/') - contest = get_object_or_404(Contest, name_id=contest_name_id) +class CounterHandler: + def __init__(self, contest): + self.contest = contest - if request.POST.get('time_round'): - request_time = request.POST.get('time_round') - else: - request_time = timezone.now() - - time_round = request_time.strftime('%Y-%m-%d %H:%M:%S') - - # Query for calculating points from edits - query = f""" - SELECT - `user_table`.`user_id` AS `id`, - `user_table`.`user`, - IFNULL(`points`.`sum`, 0) AS `sum`, - IFNULL(`points`.`total edits`, 0) AS `total_edits`, - IFNULL(`points`.`bytes points`, 0) AS `bytes_points`, - IFNULL(`points`.`total pictures`, 0) AS `total_pictures`, - IFNULL(`points`.`pictures points`, 0) AS `pictures_points`, - IFNULL(`points`.`total points`, 0) AS `total_points` - FROM ( - SELECT - DISTINCT `contests_edit`.`user_id`, - `contests_participant`.`user` - FROM - `contests_edit` - INNER JOIN `contests_participant` ON `contests_participant`.`local_id` = `contests_edit`.`user_id` - ) AS `user_table` - LEFT JOIN ( + def get_points(self, time_round): + query = f""" SELECT - t1.`user_id`, - t1.`sum`, - t1.`total edits`, - t1.`bytes points`, - t2.`total pictures`, - t2.`pictures points`, - (t1.`bytes points` + t2.`pictures points`) AS `total points` + `user_table`.`user_id` AS `id`, + `user_table`.`user`, + IFNULL(`points`.`sum`, 0) AS `sum`, + IFNULL(`points`.`total edits`, 0) AS `total_edits`, + IFNULL(`points`.`bytes points`, 0) AS `bytes_points`, + IFNULL(`points`.`total pictures`, 0) AS `total_pictures`, + IFNULL(`points`.`pictures points`, 0) AS `pictures_points`, + IFNULL(`points`.`total points`, 0) AS `total_points` FROM ( SELECT - edits_ruled.`user_id`, - SUM(edits_ruled.`bytes`) AS `sum`, - SUM(edits_ruled.`valid_edits`) AS `total edits`, - FLOOR(SUM(edits_ruled.`bytes`) / edits_ruled.`bytes_per_points`) AS `bytes points` - FROM ( - SELECT - `contests_edit`.`article_id`, - `contests_edit`.`user_id`, - CASE - WHEN SUM(`contests_evaluation`.`real_bytes`) > `contests_contest`.`max_bytes_per_article` - THEN `contests_contest`.`max_bytes_per_article` - ELSE SUM(`contests_evaluation`.`real_bytes`) - END AS `bytes`, - COUNT(`contests_evaluation`.`valid_edit`) AS `valid_edits`, - `contests_contest`.`bytes_per_points` AS `bytes_per_points` - FROM - `contests_edit` - LEFT JOIN `contests_evaluation` ON `contests_edit`.`last_evaluation_id` = `contests_evaluation`.`id` - LEFT JOIN `contests_contest` ON `contests_edit`.`contest_id` = `contests_contest`.`id` - WHERE - `contests_edit`.`contest_id` = '{contest.id}' - AND `contests_evaluation`.`valid_edit` = '1' - AND `contests_edit`.`timestamp` < '{time_round}' - GROUP BY - `contests_edit`.`user_id`, - `contests_edit`.`article_id` - ) AS edits_ruled - GROUP BY - edits_ruled.`user_id` - ) AS `t1` + DISTINCT `contests_edit`.`user_id`, + `contests_participant`.`user` + FROM + `contests_edit` + INNER JOIN `contests_participant` ON `contests_participant`.`local_id` = `contests_edit`.`user_id` + ) AS `user_table` LEFT JOIN ( SELECT - `distinct`.`user_id`, - `distinct`.`article_id`, - SUM(`distinct`.`pictures`) AS `total pictures`, - CASE - WHEN `distinct`.`pictures_per_points` = 0 - THEN 0 - ELSE FLOOR(SUM(`distinct`.`pictures`) / `distinct`.`pictures_per_points`) - END AS `pictures points` + t1.`user_id`, + t1.`sum`, + t1.`total edits`, + t1.`bytes points`, + t2.`total pictures`, + t2.`pictures points`, + (t1.`bytes points` + t2.`pictures points`) AS `total points` FROM ( SELECT - `contests_edit`.`user_id`, - `contests_edit`.`article_id`, - `contests_evaluation`.`pictures`, - `contests_edit`.`id`, - `contests_contest`.`pictures_per_points` - FROM - `contests_edit` - LEFT JOIN `contests_evaluation` ON `contests_edit`.`last_evaluation_id` = `contests_evaluation`.`id` - LEFT JOIN `contests_contest` ON `contests_edit`.`contest_id` = `contests_contest`.`id` - WHERE - `contests_edit`.`contest_id` = '{contest.id}' - AND `contests_evaluation`.`pictures` IS NOT NULL - AND `contests_edit`.`timestamp` < '{time_round}' + edits_ruled.`user_id`, + SUM(edits_ruled.`bytes`) AS `sum`, + SUM(edits_ruled.`valid_edits`) AS `total edits`, + FLOOR(SUM(edits_ruled.`bytes`) / edits_ruled.`bytes_per_points`) AS `bytes points` + FROM ( + SELECT + `contests_edit`.`article_id`, + `contests_edit`.`user_id`, + CASE + WHEN SUM(`contests_evaluation`.`real_bytes`) > `contests_contest`.`max_bytes_per_article` + THEN `contests_contest`.`max_bytes_per_article` + ELSE SUM(`contests_evaluation`.`real_bytes`) + END AS `bytes`, + COUNT(`contests_evaluation`.`valid_edit`) AS `valid_edits`, + `contests_contest`.`bytes_per_points` AS `bytes_per_points` + FROM + `contests_edit` + LEFT JOIN `contests_evaluation` ON `contests_edit`.`last_evaluation_id` = `contests_evaluation`.`id` + LEFT JOIN `contests_contest` ON `contests_edit`.`contest_id` = `contests_contest`.`id` + WHERE + `contests_edit`.`contest_id` = '{self.contest.id}' + AND `contests_evaluation`.`valid_edit` = '1' + AND `contests_edit`.`timestamp` < '{time_round}' + GROUP BY + `contests_edit`.`user_id`, + `contests_edit`.`article_id` + ) AS edits_ruled GROUP BY + edits_ruled.`user_id` + ) AS `t1` + LEFT JOIN ( + SELECT + `distinct`.`user_id`, + `distinct`.`article_id`, + SUM(`distinct`.`pictures`) AS `total pictures`, CASE - WHEN `contests_contest`.`pictures_mode` = 0 - THEN `contests_edit`.`user_id` - END, - CASE - WHEN `contests_contest`.`pictures_mode` = 0 - THEN `contests_edit`.`article_id` - END, - CASE - WHEN `contests_contest`.`pictures_mode` = 0 - THEN `contests_evaluation`.`pictures` - ELSE `contests_edit`.`id` END - ) AS `distinct` - GROUP BY - `distinct`.`user_id` - ) AS `t2` ON t1.`user_id` = t2.`user_id` - ) AS `points` ON `user_table`.`user_id` = `points`.`user_id` - ORDER BY - `points`.`total points` DESC, - `points`.`sum` DESC, - `user_table`.`user` ASC; - """ + WHEN `distinct`.`pictures_per_points` = 0 + THEN 0 + ELSE FLOOR(SUM(`distinct`.`pictures`) / `distinct`.`pictures_per_points`) + END AS `pictures points` + FROM ( + SELECT + `contests_edit`.`user_id`, + `contests_edit`.`article_id`, + `contests_evaluation`.`pictures`, + `contests_edit`.`id`, + `contests_contest`.`pictures_per_points` + FROM + `contests_edit` + LEFT JOIN `contests_evaluation` ON `contests_edit`.`last_evaluation_id` = `contests_evaluation`.`id` + LEFT JOIN `contests_contest` ON `contests_edit`.`contest_id` = `contests_contest`.`id` + WHERE + `contests_edit`.`contest_id` = '{self.contest.id}' + AND `contests_evaluation`.`pictures` IS NOT NULL + AND `contests_edit`.`timestamp` < '{time_round}' + GROUP BY + CASE + WHEN `contests_contest`.`pictures_mode` = 0 + THEN `contests_edit`.`user_id` + END, + CASE + WHEN `contests_contest`.`pictures_mode` = 0 + THEN `contests_edit`.`article_id` + END, + CASE + WHEN `contests_contest`.`pictures_mode` = 0 + THEN `contests_evaluation`.`pictures` + ELSE `contests_edit`.`id` END + ) AS `distinct` + GROUP BY + `distinct`.`user_id` + ) AS `t2` ON t1.`user_id` = t2.`user_id` + ) AS `points` ON `user_table`.`user_id` = `points`.`user_id` + ORDER BY + `points`.`total points` DESC, + `points`.`sum` DESC, + `user_table`.`user` ASC; + """ + + counter = Edit.objects.raw(query) + return counter + + def get_context(self, request): + if request.POST.get('time_round'): + time_round_str = request.POST.get('time_round') + try: + request_time = timezone.datetime.strptime(time_round_str, '%Y-%m-%dT%H:%M:%S') + except ValueError: + request_time = timezone.datetime.strptime(time_round_str, '%Y-%m-%dT%H:%M') + request_time = request_time.replace(tzinfo=dt_timezone.utc) + else: + request_time = timezone.now() + + time_round = request_time.strftime('%Y-%m-%d %H:%M:%S') - counter = Edit.objects.raw(query) - print(contest.start_time) - print(request_time) + counter = self.get_points(time_round) - return render(request, 'counter.html', { - 'contest': contest, - 'counter': counter, - 'date': request_time.strftime('%Y-%m-%d'), - 'time': request_time.strftime('%H:%M:%S'), - 'time_form': request_time.strftime('%Y-%m-%dT%H:%M:%S'), - 'contest_begun': contest.start_time < request_time, - }) + return { + 'contest': self.contest, + 'counter': counter, + 'date': request_time.strftime('%Y-%m-%d'), + 'time': request_time.strftime('%H:%M:%S'), + 'time_form': request_time.strftime('%Y-%m-%dT%H:%M:%S'), + 'contest_begun': self.contest.start_time < request_time, + } diff --git a/contests/templates/counter.html b/contests/templates/counter.html index 4d84555..74f2249 100644 --- a/contests/templates/counter.html +++ b/contests/templates/counter.html @@ -17,6 +17,7 @@
+ {% csrf_token %} {% trans 'counter-uptotime' %} @@ -63,6 +64,7 @@ onSubmit='return confirm( "{% trans 'counter-confirm' %}" )'> + {% csrf_token %} Date: Mon, 26 Aug 2024 16:57:57 -0300 Subject: [PATCH 12/75] chore: Update triage link in base.html template --- contests/templates/base.html | 4 ++-- contests/templates/contest.html | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/contests/templates/base.html b/contests/templates/base.html index 14c8cd2..77ff214 100644 --- a/contests/templates/base.html +++ b/contests/templates/base.html @@ -61,7 +61,7 @@
{% trans 'triage-panel' %}
-   {% trans 'triage' %} @@ -94,7 +94,7 @@
{% trans 'triage-panel' %}
  {% trans 'triage-list' %} -   {% trans 'triage-cat' %} diff --git a/contests/templates/contest.html b/contests/templates/contest.html index 3e6f84d..1b3fe2a 100644 --- a/contests/templates/contest.html +++ b/contests/templates/contest.html @@ -15,7 +15,9 @@
logo - + {% if result.is_evaluator %} + + {% endif %} {{ contest.name }}
From a8878429219fcedb4b9995423ac92a2aa3331f35 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Wed, 28 Aug 2024 07:26:04 -0300 Subject: [PATCH 13/75] chore: Add compare view and template for contest --- contests/compare.py | 212 +++++++++++++++++++++++++++ contests/templates/base.html | 2 +- contests/templates/compare.html | 249 ++++++++++++++++++++++++++++++++ contests/urls.py | 3 +- contests/views.py | 9 ++ 5 files changed, 473 insertions(+), 2 deletions(-) create mode 100644 contests/compare.py create mode 100644 contests/templates/compare.html diff --git a/contests/compare.py b/contests/compare.py new file mode 100644 index 0000000..fc2d66f --- /dev/null +++ b/contests/compare.py @@ -0,0 +1,212 @@ +from django.shortcuts import render +from django.utils import timezone +from datetime import timedelta +import requests +from .models import Contest, Edit, Qualification, Evaluator, Article + +class CompareHandler: + def __init__(self, contest): + self.contest = contest + self.update = False + + def execute(self, request): + """Handle the request.""" + contest = self.contest + return_dict = {'contest': contest } + update_request = False + early_update = self.check_recent_update(contest) + + if request.method == 'POST': + if request.POST.get('update') and not early_update: + self.call_update(contest) + update_request = True + if request.POST.get('diff'): + self.fix_inconsistent_edit(contest, request.POST.get('diff')) + + return_dict.update({ + 'inconsistent_edits': self.get_inconsistent_edits(contest), + 'reverted_edits': self.get_reverted_edits(contest), + 'update_countdown': self.get_update_countdown(contest), + 'early_update': early_update, + 'update_request': update_request, + }) + + if contest.official_list_pageid and contest.category_pageid: + return_dict.update({'articles': self.generate_list_cat_intersection(contest)}) + else: + return_dict.update({ + 'articles': { + 'list_wikidata': [], + 'list_official_not_category': [], + 'list_category_not_official': [] + }, + }) + + if contest.api_endpoint == 'https://pt.wikipedia.org/w/api.php': + return_dict.update({'articles': {'deletion': self.get_deletion_pages(contest)}}) + else: + return_dict.update({'articles': {'deletion': []}}) + + return return_dict + + def generate_list_cat_intersection(self, contest): + """Gera lista de artigos que estão tanto na lista oficial quanto na categoria.""" + list_official = [page.title for page in self.get_list_articles(contest) if 'missing' not in page] + + category = self.get_category_articles(contest) + list_category = [page['title'] for page in category] + list_wikidata = [page['title'] for page in category if 'pageprops' not in page or 'wikibase_item' not in page['pageprops']] + + list_official_not_category = list(set(list_official) - set(list_category)) + list_category_not_official = list(set(list_category) - set(list_official)) + + return { + 'list_wikidata': list_wikidata, + 'list_official_not_category': list_official_not_category, + 'list_category_not_official': list_category_not_official, + } + + + def get_list_articles(self, contest): + """Coleta lista de artigos na lista oficial.""" + + list_ = [] + list_api_params = { + 'action': 'query', + 'format': 'json', + 'generator': 'links', + 'pageids': contest.official_list_pageid, + 'gplnamespace': '0', + 'gpllimit': 'max', + } + response = requests.get(contest.api_endpoint, params=list_api_params).json() + list_.extend(response['query']['pages']) + + while 'continue' in response: + list_api_params['gplcontinue'] = response['continue']['gplcontinue'] + response = requests.get(contest.api_endpoint, params=list_api_params).json() + list_.extend(response['query']['pages']) + + return list_ + + def get_category_articles(self, contest): + """Coleta lista de artigos na categoria.""" + list_ = [] + categorymembers_api_params = { + "action": "query", + "format": "json", + "prop": "pageprops", + "generator": "categorymembers", + "ppprop": "wikibase_item", + "cmnamespace": "0", + "gcmpageid": contest.category_pageid, + "gcmprop": "title", + "gcmlimit": "max", + } + response = requests.get(contest.api_endpoint, params=categorymembers_api_params).json() + list_.extend(response['query']['pages']) + + while 'continue' in response: + categorymembers_api_params['cmcontinue'] = response['continue']['cmcontinue'] + response = requests.get(contest.api_endpoint, params=categorymembers_api_params).json() + list_.extend(response['query']['pages']) + + return list_ + + def get_deletion_pages(self, contest): + """Coleta páginas marcadas para eliminação.""" + list_ = [] + deletion_api_params = { + 'action': 'query', + 'format': 'json', + 'list': 'categorymembers', + 'cmpageid': 1001045, + 'cmnamespace': '4', + 'cmlimit': 'max', + 'cmprop': 'title', + } + response = requests.get(contest.api_endpoint, params=deletion_api_params).json() + list_.extend(response['query']['categorymembers']) + + while 'continue' in response: + deletion_api_params['cmcontinue'] = response['continue']['cmcontinue'] + response = requests.get(contest.api_endpoint, params=deletion_api_params).json() + list_.extend(response['query']['categorymembers']) + + articles = [page['title'][32:] for page in list_] + + cats_ = [] + cats = [3501865, 2419924] + for cat in cats: + cats_api_params = { + 'action': 'query', + 'format': 'json', + 'list': 'categorymembers', + 'cmpageid': cat, + 'cmnamespace': '0', + 'cmlimit': 'max', + 'cmprop': 'title', + } + response = requests.get(contest.api_endpoint, params=cats_api_params).json() + cats_.extend(response['query']['categorymembers']) + + while 'continue' in response: + deletion_api_params['cmcontinue'] = response['continue']['cmcontinue'] + response = requests.get(contest.api_endpoint, params=deletion_api_params).json() + cats_.extend(response['query']['categorymembers']) + + articles.extend([page['title'] for page in cats_]) + + articles = list(dict.fromkeys(articles)) + cat = [word.replace('_', ' ') for word in Article.objects.filter(contest=contest).values_list('title', flat=True)] + intersect = list(set(articles) & set(cat)) + return intersect + + def fix_inconsistent_edit(self, contest, diff): + """Corrige edições inconsistentes.""" + qualif = Qualification.objects.create( + contest=contest, + status='0', + diff=Edit.objects.get(diff=diff), + evaluator=Evaluator.objects.get(contest=contest, user=self.user), + ) + Edit.objects.filter(diff=diff).update(last_qualification=qualif) + + def get_inconsistent_edits(self, contest): + """Coleta edições inconsistentes.""" + query = Edit.objects.filter( + contest=contest, + article__active=False, + last_qualification__status='1', + ) + return query + + def get_reverted_edits(self, contest): + """Coleta edições revertidas e validadas.""" + query = Edit.objects.filter( + contest=contest, + last_qualification__status='0', + last_evaluation__valid_edit=True, + ) + return query + + def get_update_countdown(self, contest): + """Coleta contagem regressiva para atualização.""" + if contest.end_time + timedelta(days=2) < timezone.now(): + return False + elif contest.next_update > timezone.now() and not self.update: + return contest.next_update - timezone.now() + else: + return 0 + + def call_update(self, contest): + """Chama atualização.""" + Contest.objects.filter(name_id=contest.name_id).update(next_update=None) + self.update = True + + def check_recent_update(self, contest): + """Verifica se houve atualização recente.""" + if (timezone.now() - contest.finished_update) < timedelta(minutes=30) or contest.next_update==None: + return True + return False + \ No newline at end of file diff --git a/contests/templates/base.html b/contests/templates/base.html index 77ff214..8219fe6 100644 --- a/contests/templates/base.html +++ b/contests/templates/base.html @@ -73,7 +73,7 @@
{% trans 'triage-panel' %}
rel="noopener" class="w3-bar-item w3-button w3-padding ">   {% trans 'modify' %}
-   {% trans 'compare' %} diff --git a/contests/templates/compare.html b/contests/templates/compare.html new file mode 100644 index 0000000..7634c68 --- /dev/null +++ b/contests/templates/compare.html @@ -0,0 +1,249 @@ +{% extends "base.html" %} +{% load static %} +{% load i18n %} + + +{% block pagename %}{% trans 'compare' %}{% endblock %} + +{% block head %} + +{% if update_request %} + +{% endif %} +{% endblock %} + +{% block onload %}startCountdown({{ update_countdown }}){% endblock %} + +{% block content %} +
+ {% if update_countdown is False %} +
+ {% if early_update and update_request %} +

{% trans 'compare-next' %} {% trans 'compare-soon' %}

+ {% else %} +

{% trans 'compare-ended' %}

+ + {% csrf_token %} +

+ +

+ + {% endif %} +
+ {% else %} +
+

{% trans 'compare-next' %} ...

+
+ {% csrf_token %} +

+ + + {% trans 'compare-early' %} + +

+
+
+ {% endif %} +
+
+
+
+
+
+

{% trans 'compare-unlisted' %}

+
+
    +
  • + {% trans 'compare-unlisted-about' %} +
  • + {% for title in articles.list_official_not_category %} +
  • + + {{ title }} + +
  • + {% endfor %} +
+
+
+
+
+
+

{% trans 'compare-uncated' %}

+
+
    +
  • + {% trans 'compare-uncated-about' %} +
  • + {% for title in articles.list_category_not_official %} +
  • + + {{ title }} + +
  • + {% endfor %} +
+
+
+
+
+
+

{% trans 'compare-deletion' %}

+
+
    +
  • + {% trans 'compare-deletion-about' %} +
  • + {% for title in articles.deletion %} +
  • + + {{ title }} + +
  • + {% endfor %} +
+
+
+
+
+
+
+
+

{% trans 'compare-nowikidata' %}

+
+
    +
  • + {% trans 'compare-nowikidata-about' %} +
  • + {% for title in articles.list_wikidata %} +
  • + + {{ title }} + +
  • + {% endfor %} +
+
+
+
+
+
+

{% trans 'compare-inconsistency' %}

+
+
    +
  • + {% trans 'compare-inconsistency-about' %} +
  • + {% for edits in inconsistent_edits %} +
  • + +
    + + +
    +
  • + {% endfor %} +
+
+
+
+
+
+

{% trans 'compare-rollback' %}

+
+
    +
  • + {% trans 'compare-rollback-about' %} +
  • + {% for edits in reverted_edits %} +
  • + + +
  • + {% endfor %} +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/contests/urls.py b/contests/urls.py index 5b4f51a..db5193f 100644 --- a/contests/urls.py +++ b/contests/urls.py @@ -1,5 +1,5 @@ from django.urls import path -from .views import contest_view, home_view, color_view, triage_view, backtrack_view, counter_view +from .views import contest_view, home_view, color_view, triage_view, backtrack_view, counter_view, compare_view urlpatterns = [ path('', home_view, name='home_view'), @@ -8,4 +8,5 @@ path('triage/', triage_view, name='triage_view'), path('counter/', counter_view, name='counter_view'), path('backtrack/', backtrack_view, name='backtrack_view'), + path('compare/', compare_view, name='compare_view'), ] diff --git a/contests/views.py b/contests/views.py index 376399d..07420ef 100644 --- a/contests/views.py +++ b/contests/views.py @@ -2,6 +2,7 @@ from .models import Contest, Edit, Participant, Qualification, Evaluator from .triage import TriageHandler from .counter import CounterHandler +from .compare import CompareHandler from django.db import connection from datetime import datetime, timedelta from django.db.models import Count, Sum, Case, When, Value, IntegerField, Q, F, OuterRef, Subquery @@ -255,3 +256,11 @@ def backtrack_view(request): 'right': 'left' if translation.get_language_bidi() else 'right', }) +@login_required() +@contest_evaluator_required +def compare_view(request): + contest = get_object_or_404(Contest, name_id=request.GET.get('contest')) + + handler = CompareHandler(contest=contest) + + return render(request, 'compare.html', handler.execute(request)) \ No newline at end of file From 81f998c6d8b011cf2fe0f9ebe50637b856d6d541 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Wed, 28 Aug 2024 11:34:18 -0300 Subject: [PATCH 14/75] chore: Add edits view and template for contest --- contests/templates/base.html | 2 +- contests/templates/edits.csv | 4 + contests/templates/edits.html | 135 ++++++++++++++++++++++++++++ contests/templatetags/__init__.py | 0 contests/templatetags/titlescore.py | 9 ++ contests/urls.py | 3 +- contests/views.py | 27 +++++- 7 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 contests/templates/edits.csv create mode 100644 contests/templates/edits.html create mode 100644 contests/templatetags/__init__.py create mode 100644 contests/templatetags/titlescore.py diff --git a/contests/templates/base.html b/contests/templates/base.html index 8219fe6..dc482e9 100644 --- a/contests/templates/base.html +++ b/contests/templates/base.html @@ -77,7 +77,7 @@
{% trans 'triage-panel' %}
rel="noopener" class="w3-bar-item w3-button w3-padding ">   {% trans 'compare' %} -   {% trans 'triage-evaluated' %} diff --git a/contests/templates/edits.csv b/contests/templates/edits.csv new file mode 100644 index 0000000..860f06b --- /dev/null +++ b/contests/templates/edits.csv @@ -0,0 +1,4 @@ +{% load titlescore %}{% load i18n %}SEP=, +"{% trans 'edits-diff' %}","{% trans 'edits-curid' %}","{% trans 'edits-title' %}","{% trans 'edits-timestamp' %}","{% trans 'edits-user' %}","{% trans 'edits-attached' %}","{% trans 'edits-bytes' %}","{% trans 'edits-newpage' %}","{% trans 'edits-valid' %}","{% trans 'edits-enrolled' %}","{% trans 'edits-withimage' %}","{% trans 'edits-reverted' %}","{% trans 'edits-evaluator' %}","{% trans 'edits-evaltimestamp' %}","{% trans 'edits-comment' %}" +{% for query in data %}"{{ query.diff | addslashes }}",{{ query.article.articleID | addslashes }}","{{ query.article.title | titlescore | addslashes }}","{{ query.timestamp | addslashes }}","{{ query.participant.user | addslashes }}","{{ query.participant.attached | addslashes }}","{{ query.orig_bytes | addslashes }}","{{ query.new_page | addslashes}}","{{ query.last_evaluation.valid_edit | addslashes}}","{% if query.participant is None %}Never{% else %}{{ query.participant.last_enrollment.enrolled }}{% endif %}","{{ query.last_evaluation.pictures | addslashes}}","{% if query.last_qualification.status == "1" %}False{% else %}True{% endif %}","{{ query.last_evaluation.evaluator.user | addslashes}}","{{ query.last_evaluation.when | addslashes}}","{{ query.last_evaluation.obs | default_if_none:'' | addslashes }}" +{% endfor %} \ No newline at end of file diff --git a/contests/templates/edits.html b/contests/templates/edits.html new file mode 100644 index 0000000..0d6fb44 --- /dev/null +++ b/contests/templates/edits.html @@ -0,0 +1,135 @@ +{% extends "base.html" %} +{% load static %} +{% load i18n %} +{% load titlescore %} +{% get_current_language_bidi as LANGUAGE_BIDI %} + + +{% block pagename %}{% trans 'triage-evaluated' %}{% endblock %} + +{% block head %} + + + + + + + + + +{% endblock %} + +{% block content %} +
+
+
+
{% trans 'edits-about' %}
+
+ {% csrf_token %} + + +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + {% for query in edits %} + {% if query.participant is not None %} + + + + + + + + + + + + + + + + {% endif %} + {% endfor %} + +
{% trans 'edits-diff' %}{% trans 'edits-title' %}{% trans 'edits-timestamp' %}{% trans 'edits-user' %}{% trans 'edits-attached' %}{% trans 'edits-bytes' %}{% trans 'edits-newpage' %}{% trans 'edits-valid' %}{% trans 'edits-withimage' %}{% trans 'edits-reverted' %}{% trans 'edits-evaluator' %}{% trans 'edits-evaltimestamp' %}{% trans 'edits-comment' %}
{{ query.diff }}{{ query.article.title | titlescore }}{{ query.timestamp }}{{ query.participant.user }}{{ query.participant.attached }}{{ query.orig_bytes }} + {% if query.new_page == True %} + + {% else %} + + {% endif %} + + {% if query.last_evaluation.valid_edit == True %} + + {% else %} + + {% endif %} + {{ query.last_evaluation.pictures }} + {% if query.last_qualification.status == 1 %} + + {% else %} + + {% endif %} + {{ query.last_evaluation.evaluator.user }}{{ query.last_evaluation.when }}{{ query.last_evaluation.obs | default_if_none:"" }}
+ +
+
+
+{% endblock %} \ No newline at end of file diff --git a/contests/templatetags/__init__.py b/contests/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/contests/templatetags/titlescore.py b/contests/templatetags/titlescore.py new file mode 100644 index 0000000..77b0ea7 --- /dev/null +++ b/contests/templatetags/titlescore.py @@ -0,0 +1,9 @@ +from django import template +from django.template.defaultfilters import stringfilter + +register = template.Library() + +@register.filter +@stringfilter +def titlescore(value): + return value.replace('_', ' ') diff --git a/contests/urls.py b/contests/urls.py index db5193f..4290854 100644 --- a/contests/urls.py +++ b/contests/urls.py @@ -1,5 +1,5 @@ from django.urls import path -from .views import contest_view, home_view, color_view, triage_view, backtrack_view, counter_view, compare_view +from .views import contest_view, home_view, color_view, triage_view, backtrack_view, counter_view, compare_view, edits_view urlpatterns = [ path('', home_view, name='home_view'), @@ -9,4 +9,5 @@ path('counter/', counter_view, name='counter_view'), path('backtrack/', backtrack_view, name='backtrack_view'), path('compare/', compare_view, name='compare_view'), + path('edits/', edits_view, name='edits_view'), ] diff --git a/contests/views.py b/contests/views.py index 07420ef..e52553b 100644 --- a/contests/views.py +++ b/contests/views.py @@ -14,6 +14,8 @@ from django.contrib.auth.decorators import login_required from collections import defaultdict from functools import wraps +from django.http import HttpResponse +from django.template import loader def contest_evaluator_required(view_func): @wraps(view_func) @@ -263,4 +265,27 @@ def compare_view(request): handler = CompareHandler(contest=contest) - return render(request, 'compare.html', handler.execute(request)) \ No newline at end of file + return render(request, 'compare.html', handler.execute(request)) + +@login_required() +@contest_evaluator_required +def edits_view(request): + contest = get_object_or_404(Contest, name_id=request.GET.get('contest')) + + edits = Edit.objects.filter(contest=contest, participant__isnull=False) + + if request.POST.get('csv'): + response = HttpResponse( + content_type="text/csv; charset=windows-1252", + headers={"Content-Disposition": 'attachment; filename="edits.csv"'}, + ) + t = loader.get_template("edits.csv") + c = {'data': edits} + response.write(t.render(c)) + return response + + return render(request, 'edits.html', { + 'contest': contest, + 'edits': edits, + 'right': 'left' if translation.get_language_bidi() else 'right', + }) \ No newline at end of file From 88ff5ab47c7c1e69784147ca4655c27a9ddf1df5 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Wed, 28 Aug 2024 14:10:38 -0300 Subject: [PATCH 15/75] feat: Customize python-social-auth pipeline to save username if changed --- credentials/pipeline.py | 22 ++++++++++++++++++++++ wikiscore/settings.py | 1 + 2 files changed, 23 insertions(+) diff --git a/credentials/pipeline.py b/credentials/pipeline.py index d9467ab..d13516e 100644 --- a/credentials/pipeline.py +++ b/credentials/pipeline.py @@ -18,3 +18,25 @@ def get_username(strategy, details, user=None, *args, **kwargs): return {"username": user.username} else: return {"username": details['username']} + +def save_profile(backend, user, response, *args, **kwargs): + """ + This pipeline function customizes the behavior of python-social-auth to save the username + to the project's custom user model if the username has changed on the authentication provider. + + Parameters: + - backend: The backend used by python-social-auth. + - user: The User object to be saved. + - response: The response from the authentication provider. + - *args: Additional positional arguments required by python-social-auth. + - **kwargs: Additional keyword arguments required by python-social-auth. + + Returns: + - None + """ + if backend.name == 'mediawiki': + if user.username: + if kwargs.get('details', False).get('username', False): + if user.username != kwargs['details']['username']: + user.username = kwargs['details']['username'] + user.save() \ No newline at end of file diff --git a/wikiscore/settings.py b/wikiscore/settings.py index 64fa885..d243870 100644 --- a/wikiscore/settings.py +++ b/wikiscore/settings.py @@ -98,6 +98,7 @@ 'social_core.pipeline.social_auth.social_user', 'credentials.pipeline.get_username', 'social_core.pipeline.user.create_user', + 'credentials.pipeline.save_profile', 'social_core.pipeline.social_auth.associate_user', 'social_core.pipeline.social_auth.load_extra_data', 'social_core.pipeline.user.user_details', From 00559919f0a91e2f1c5296bbdabff104476e9fe5 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Thu, 29 Aug 2024 10:44:32 -0300 Subject: [PATCH 16/75] chore: Add Profile model for user profiles and update Evaluator model to use Profile instead of CustomUser --- contests/compare.py | 2 +- .../migrations/0005_alter_evaluator_user.py | 20 ++++++++++ ..._rename_user_evaluator_profile_and_more.py | 23 +++++++++++ contests/models.py | 8 ++-- contests/templates/edits.csv | 2 +- contests/triage.py | 16 ++++---- contests/views.py | 7 ++-- credentials/admin.py | 5 ++- credentials/migrations/0002_profile.py | 24 +++++++++++ credentials/models.py | 8 ++++ credentials/pipeline.py | 40 ++++++++++++++++--- 11 files changed, 131 insertions(+), 24 deletions(-) create mode 100644 contests/migrations/0005_alter_evaluator_user.py create mode 100644 contests/migrations/0006_rename_user_evaluator_profile_and_more.py create mode 100644 credentials/migrations/0002_profile.py diff --git a/contests/compare.py b/contests/compare.py index fc2d66f..b1a1937 100644 --- a/contests/compare.py +++ b/contests/compare.py @@ -168,7 +168,7 @@ def fix_inconsistent_edit(self, contest, diff): contest=contest, status='0', diff=Edit.objects.get(diff=diff), - evaluator=Evaluator.objects.get(contest=contest, user=self.user), + evaluator=Evaluator.objects.get(contest=contest, profile=self.user.profile), ) Edit.objects.filter(diff=diff).update(last_qualification=qualif) diff --git a/contests/migrations/0005_alter_evaluator_user.py b/contests/migrations/0005_alter_evaluator_user.py new file mode 100644 index 0000000..482ec25 --- /dev/null +++ b/contests/migrations/0005_alter_evaluator_user.py @@ -0,0 +1,20 @@ +# Generated by Django 5.1 on 2024-08-29 10:01 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contests', '0004_participant_last_enrollment'), + ('credentials', '0002_profile'), + ] + + operations = [ + migrations.AlterField( + model_name='evaluator', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='credentials.profile'), + ), + ] diff --git a/contests/migrations/0006_rename_user_evaluator_profile_and_more.py b/contests/migrations/0006_rename_user_evaluator_profile_and_more.py new file mode 100644 index 0000000..b3efcbd --- /dev/null +++ b/contests/migrations/0006_rename_user_evaluator_profile_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1 on 2024-08-29 12:06 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('contests', '0005_alter_evaluator_user'), + ('credentials', '0002_profile'), + ] + + operations = [ + migrations.RenameField( + model_name='evaluator', + old_name='user', + new_name='profile', + ), + migrations.AlterUniqueTogether( + name='evaluator', + unique_together={('profile', 'contest')}, + ), + ] diff --git a/contests/models.py b/contests/models.py index 8814dfa..d3f5553 100644 --- a/contests/models.py +++ b/contests/models.py @@ -38,15 +38,15 @@ def __str__(self): return self.name_id class Evaluator(models.Model): - user = models.ForeignKey('credentials.CustomUser', on_delete=models.PROTECT) + profile = models.ForeignKey('credentials.Profile', on_delete=models.PROTECT) contest = models.ForeignKey('Contest', on_delete=models.CASCADE) user_status = models.CharField(max_length=1, default='P') def __str__(self): - return self.user.username + return self.profile.username class Meta: - unique_together = ['user', 'contest'] + unique_together = ['profile', 'contest'] class Article(models.Model): contest = models.ForeignKey('Contest', on_delete=models.CASCADE) @@ -135,4 +135,4 @@ class Evaluation(models.Model): obs = models.TextField(blank=True, null=True) def __str__(self): - return (f"{self.contest.name_id} - {self.diff.diff} - {self.evaluator.user.username} - {self.status} - {self.when}") \ No newline at end of file + return (f"{self.contest.name_id} - {self.diff.diff} - {self.evaluator.profile.username} - {self.status} - {self.when}") \ No newline at end of file diff --git a/contests/templates/edits.csv b/contests/templates/edits.csv index 860f06b..bf12a51 100644 --- a/contests/templates/edits.csv +++ b/contests/templates/edits.csv @@ -1,4 +1,4 @@ {% load titlescore %}{% load i18n %}SEP=, "{% trans 'edits-diff' %}","{% trans 'edits-curid' %}","{% trans 'edits-title' %}","{% trans 'edits-timestamp' %}","{% trans 'edits-user' %}","{% trans 'edits-attached' %}","{% trans 'edits-bytes' %}","{% trans 'edits-newpage' %}","{% trans 'edits-valid' %}","{% trans 'edits-enrolled' %}","{% trans 'edits-withimage' %}","{% trans 'edits-reverted' %}","{% trans 'edits-evaluator' %}","{% trans 'edits-evaltimestamp' %}","{% trans 'edits-comment' %}" -{% for query in data %}"{{ query.diff | addslashes }}",{{ query.article.articleID | addslashes }}","{{ query.article.title | titlescore | addslashes }}","{{ query.timestamp | addslashes }}","{{ query.participant.user | addslashes }}","{{ query.participant.attached | addslashes }}","{{ query.orig_bytes | addslashes }}","{{ query.new_page | addslashes}}","{{ query.last_evaluation.valid_edit | addslashes}}","{% if query.participant is None %}Never{% else %}{{ query.participant.last_enrollment.enrolled }}{% endif %}","{{ query.last_evaluation.pictures | addslashes}}","{% if query.last_qualification.status == "1" %}False{% else %}True{% endif %}","{{ query.last_evaluation.evaluator.user | addslashes}}","{{ query.last_evaluation.when | addslashes}}","{{ query.last_evaluation.obs | default_if_none:'' | addslashes }}" +{% for query in data %}"{{ query.diff | addslashes }}",{{ query.article.articleID | addslashes }}","{{ query.article.title | titlescore | addslashes }}","{{ query.timestamp | addslashes }}","{{ query.participant.user | addslashes }}","{{ query.participant.attached | addslashes }}","{{ query.orig_bytes | addslashes }}","{{ query.new_page | addslashes}}","{{ query.last_evaluation.valid_edit | addslashes}}","{% if query.participant is None %}Never{% else %}{{ query.participant.last_enrollment.enrolled }}{% endif %}","{{ query.last_evaluation.pictures | addslashes}}","{% if query.last_qualification.status == "1" %}False{% else %}True{% endif %}","{{ query.last_evaluation.evaluator.profile | addslashes}}","{{ query.last_evaluation.when | addslashes}}","{{ query.last_evaluation.obs | default_if_none:'' | addslashes }}" {% endfor %} \ No newline at end of file diff --git a/contests/triage.py b/contests/triage.py index 4a54a84..0399edc 100644 --- a/contests/triage.py +++ b/contests/triage.py @@ -27,7 +27,7 @@ def do_evaluate(self, request): return {'action': 'release'} elif request.POST.get('unhold'): - if Evaluator.objects.get(contest=contest, user=self.user).user_status != 'G': + if Evaluator.objects.get(contest=contest, profile=self.user.profile).user_status != 'G': raise PermissionError('User is not a group member') else: if self.unhold_edit(): @@ -102,7 +102,7 @@ def get_evaluate(self, request): def skip_edit(self, diff): evaluation = Evaluation.objects.create( contest=self.contest, - evaluator=Evaluator.objects.get(contest=self.contest, user=self.user), + evaluator=Evaluator.objects.get(contest=self.contest, profile=self.user.profile), diff=Edit.objects.get(diff=diff), status='3' ) @@ -110,7 +110,7 @@ def skip_edit(self, diff): return evaluation def release_edit(self): - evaluator = Evaluator.objects.get(contest=self.contest, user=self.user) + evaluator = Evaluator.objects.get(contest=self.contest, profile=self.user.profile) skipped = Edit.objects.filter( contest=self.contest, @@ -145,7 +145,7 @@ def evaluate_edit(self, request): evaluation = Evaluation.objects.create( contest=self.contest, - evaluator=Evaluator.objects.get(contest=self.contest, user=self.user), + evaluator=Evaluator.objects.get(contest=self.contest, profile=self.user.profile), diff=Edit.objects.get(diff=request.POST.get('diff')), valid_edit=True if request.POST.get('valid') == 'sim' else False, pictures=picture, @@ -168,7 +168,7 @@ def get_next_edit(self, contest): ).filter( Q(last_evaluation=None) | Q(last_evaluation__status='0') | - (Q(last_evaluation__status='2') & Q(last_evaluation__evaluator=Evaluator.objects.get(contest=contest, user=self.user))) + (Q(last_evaluation__status='2') & Q(last_evaluation__evaluator=Evaluator.objects.get(contest=contest, profile=self.user.profile))) ).order_by('timestamp').first() return edit @@ -202,17 +202,17 @@ def hold_edit(self, edit, user): ).filter( Q(last_evaluation=None) | Q(last_evaluation__status='0') | - (Q(last_evaluation__status='2') & Q(last_evaluation__evaluator=Evaluator.objects.get(contest=self.contest, user=self.user))) + (Q(last_evaluation__status='2') & Q(last_evaluation__evaluator=Evaluator.objects.get(contest=self.contest, profile=self.user.profile))) ).first() if held_edit: # Check if the edit is already held by the same user (status='2' and evaluator is the current user) - if held_edit.last_evaluation and held_edit.last_evaluation.status == '2' and held_edit.last_evaluation.evaluator.user == self.user: + if held_edit.last_evaluation and held_edit.last_evaluation.status == '2' and held_edit.last_evaluation.evaluator.profile == self.user.profile: # Edit is already held by the current user, no need to create a new evaluation return True # Create a new evaluation if the edit is not already held by the current user - evaluator = Evaluator.objects.get(contest=self.contest, user=self.user) + evaluator = Evaluator.objects.get(contest=self.contest, profile=self.user.profile) evaluation = Evaluation.objects.create( contest=self.contest, evaluator=evaluator, diff --git a/contests/views.py b/contests/views.py index e52553b..5887c1e 100644 --- a/contests/views.py +++ b/contests/views.py @@ -3,6 +3,7 @@ from .triage import TriageHandler from .counter import CounterHandler from .compare import CompareHandler +from credentials.models import Profile from django.db import connection from datetime import datetime, timedelta from django.db.models import Count, Sum, Case, When, Value, IntegerField, Q, F, OuterRef, Subquery @@ -25,7 +26,7 @@ def _wrapped_view(request, *args, **kwargs): return redirect('/') contest = get_object_or_404(Contest, name_id=contest_name_id) try: - Evaluator.objects.get(contest=contest, user=request.user) + Evaluator.objects.get(contest=contest, profile=request.user.profile, user_status__in=['A', 'G']) except Evaluator.DoesNotExist: raise PermissionDenied("You are not allowed to access this page.") return view_func(request, *args, **kwargs) @@ -89,7 +90,7 @@ def contest_view(request): is_evaluator = False try: - Evaluator.objects.get(contest=contest, user=request.user) + Evaluator.objects.get(contest=contest, profile=request.user.profile) is_evaluator = True except Evaluator.DoesNotExist: pass @@ -209,7 +210,7 @@ def triage_view(request): triage_dict = get_evaluate | do_evaluate triage_dict.update({ 'triage_points': int(contest.max_bytes_per_article / contest.bytes_per_points), - 'evaluator_status': Evaluator.objects.get(contest=contest, user=request.user).user_status, + 'evaluator_status': Evaluator.objects.get(contest=contest, profile=request.user.profile).user_status, 'right': 'left' if translation.get_language_bidi() else 'right', 'left': 'right' if translation.get_language_bidi() else 'left', }) diff --git a/credentials/admin.py b/credentials/admin.py index d9a978f..d562594 100644 --- a/credentials/admin.py +++ b/credentials/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from credentials.models import CustomUser +from credentials.models import CustomUser, Profile # Register your models here. class AccountUserAdmin(admin.ModelAdmin): @@ -7,4 +7,5 @@ class AccountUserAdmin(admin.ModelAdmin): search_fields = ('username', 'email') readonly_fields = ('date_joined',) -admin.site.register(CustomUser, AccountUserAdmin) \ No newline at end of file +admin.site.register(CustomUser, AccountUserAdmin) +admin.site.register(Profile) \ No newline at end of file diff --git a/credentials/migrations/0002_profile.py b/credentials/migrations/0002_profile.py new file mode 100644 index 0000000..ea9671c --- /dev/null +++ b/credentials/migrations/0002_profile.py @@ -0,0 +1,24 @@ +# Generated by Django 5.1 on 2024-08-29 09:59 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('credentials', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Profile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('global_id', models.CharField(max_length=50, unique=True)), + ('username', models.CharField(max_length=100)), + ('account', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/credentials/models.py b/credentials/models.py index e52f9c9..95ca06b 100644 --- a/credentials/models.py +++ b/credentials/models.py @@ -36,3 +36,11 @@ class CustomUser(AbstractBaseUser, PermissionsMixin): objects = UserManager() USERNAME_FIELD = 'username' EMAIL_FIELD = 'email' + +class Profile(models.Model): + global_id = models.CharField(max_length=50, unique=True) + username = models.CharField(max_length=100) + account = models.OneToOneField(CustomUser, on_delete=models.SET_NULL, null=True, blank=True) + + def __str__(self): + return self.username if self.username else "No username" \ No newline at end of file diff --git a/credentials/pipeline.py b/credentials/pipeline.py index d13516e..4513a42 100644 --- a/credentials/pipeline.py +++ b/credentials/pipeline.py @@ -1,3 +1,9 @@ +from django.db import transaction +from .models import Profile +import logging + +logger = logging.getLogger(__name__) + def get_username(strategy, details, user=None, *args, **kwargs): """ This pipeline function customizes the behavior of python-social-auth to return the username @@ -35,8 +41,32 @@ def save_profile(backend, user, response, *args, **kwargs): - None """ if backend.name == 'mediawiki': - if user.username: - if kwargs.get('details', False).get('username', False): - if user.username != kwargs['details']['username']: - user.username = kwargs['details']['username'] - user.save() \ No newline at end of file + details = kwargs.get('details', {}) + + try: + new_username = details.get('username') + global_id = details.get('userID') + + if not global_id: + logger.error("No global_id provided in the MediaWiki response.") + return + + if not new_username: + logger.warning(f"Username is missing for global_id {global_id}.") + return + + with transaction.atomic(): + if user.username != new_username: + user.username = new_username + user.save() + + profile, created = Profile.objects.get_or_create(global_id=global_id) + if profile.account != user: + profile.account = user + profile.save() + if profile.username != new_username: + profile.username = new_username + profile.save() + + except Exception as e: + logger.error(f"Error while saving profile for user {user.id}: {str(e)}") From 97ed0fded35bb227afcf8cbd1f841602bf824983 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Thu, 29 Aug 2024 11:24:11 -0300 Subject: [PATCH 17/75] chore: Add evaluators view and template for contest --- contests/evaluators.py | 87 ++++++++++++++++ contests/templates/base.html | 2 +- contests/templates/edits.html | 2 +- contests/templates/evaluators.html | 156 +++++++++++++++++++++++++++++ contests/urls.py | 3 +- contests/views.py | 1 + 6 files changed, 248 insertions(+), 3 deletions(-) create mode 100644 contests/evaluators.py create mode 100644 contests/templates/evaluators.html diff --git a/contests/evaluators.py b/contests/evaluators.py new file mode 100644 index 0000000..1d57582 --- /dev/null +++ b/contests/evaluators.py @@ -0,0 +1,87 @@ +import requests +from credentials.models import Profile +from django.db.models import Count, Q +from django.utils import translation +from contests.models import Evaluator, Edit + +class EvaluatorsHandler: + def __init__(self, contest): + self.contest = contest + + def execute(self, request): + contest = self.contest + if request.method == 'POST' and Evaluator.objects.get(contest=contest, profile=request.user.profile).user_status == 'G': + if request.POST.get('new'): + username = request.POST.get('new') + try: + profile = Profile.objects.get(username=username) + except Profile.DoesNotExist: + api_params = { + 'action': 'query', + 'meta': 'globaluserinfo', + 'guiuser': username, + 'format': 'json', + } + response = requests.get(contest.api_endpoint, params=api_params) + data = response.json() + + if 'query' in data and 'globaluserinfo' in data['query'] and 'id' in data['query']['globaluserinfo']: + global_id = data['query']['globaluserinfo']['id'] + profile = Profile.objects.create(global_id=global_id, username=username) + Evaluator.objects.create(contest=contest, profile=profile, user_status='A') + + else: + Evaluator.objects.create(contest=contest, profile=profile, user_status='A') + + if request.POST.get('user'): + if request.POST.get('off'): + Evaluator.objects.filter( + contest=contest, + profile__username=request.POST.get('user'), + ).update(user_status='P') + + if request.POST.get('on'): + Evaluator.objects.filter( + contest=contest, + profile__username=request.POST.get('user'), + ).update(user_status='A') + + if request.POST.get('reset'): + Edit.objects.filter( + contest=contest, + last_evaluation__evaluator__profile__username=request.POST.get('user'), + ).update(last_evaluation=None) + + evaluators = Profile.objects.filter( + evaluator__contest=contest, + evaluator__user_status='A', + ).annotate( + evaluation_count=Count('evaluator__evaluation'), + filter=Q(evaluator__evaluation__status='1') + ).values('global_id', 'username', 'evaluation_count') + managers = Profile.objects.filter( + evaluator__contest=contest, + evaluator__user_status='G', + ).annotate( + evaluation_count=Count('evaluator__evaluation'), + filter=Q(evaluator__evaluation__status='1') + ).values('global_id', 'username', 'evaluation_count') + disabled = Profile.objects.filter( + Q(evaluator__contest=contest, evaluator__user_status='P') | + ~Q(evaluator__contest=contest) + ).annotate( + evaluation_count=Count('evaluator__evaluation'), + filter=Q(evaluator__evaluation__status='1') + ).values('global_id', 'username', 'evaluation_count') + + return_dict = { + 'contest': contest, + 'evaluators': evaluators, + 'managers': managers, + 'disabled': disabled, + 'status': Evaluator.objects.get(contest=contest, profile=request.user.profile).user_status, + 'right': 'left' if translation.get_language_bidi() else 'right', + 'left': 'right' if translation.get_language_bidi() else 'left', + } + + return return_dict \ No newline at end of file diff --git a/contests/templates/base.html b/contests/templates/base.html index dc482e9..7912c03 100644 --- a/contests/templates/base.html +++ b/contests/templates/base.html @@ -85,7 +85,7 @@
{% trans 'triage-panel' %}
rel="noopener" class="w3-bar-item w3-button w3-padding ">   {% trans 'backtrack' %} -   {% trans 'evaluators' %} diff --git a/contests/templates/edits.html b/contests/templates/edits.html index 0d6fb44..4229834 100644 --- a/contests/templates/edits.html +++ b/contests/templates/edits.html @@ -106,7 +106,7 @@ {% endif %} - {{ query.last_evaluation.evaluator.user }} + {{ query.last_evaluation.evaluator.profile }} {{ query.last_evaluation.when }} {{ query.last_evaluation.obs | default_if_none:"" }} diff --git a/contests/templates/evaluators.html b/contests/templates/evaluators.html new file mode 100644 index 0000000..43a6ee2 --- /dev/null +++ b/contests/templates/evaluators.html @@ -0,0 +1,156 @@ +{% extends "base.html" %} +{% load static %} +{% load i18n %} +{% load titlescore %} +{% get_current_language_bidi as LANGUAGE_BIDI %} + + +{% block pagename %}{% trans 'evaluators' %}{% endblock %} + +{% block content %} +
+
+
+

{% trans 'evaluators-about' %}

+
+
+ {% if status == "G" %} +
+
+

{% trans 'evaluators-neweval' %}

+
+
+
    +
  • + +
    + {% csrf_token %} + + +
    +
  • +
+
+
+ {% endif %} +
+
+

{% trans 'evaluators-manager' %}

+
+
+
    + {% for evaluator in managers %} +
  • + +
    + {{ evaluator.username }}
    + {{ evaluator.global_id }}
    + {% blocktrans with evaluators_stats_1=evaluator.evaluation_count %}.{{evaluators_stats_1}}.{% endblocktrans %} +
    + {% if status == "G" %} +
    + {% csrf_token %} + + + +
    + {% endif %} +
  • + {% endfor %} +
+
+
+
+
+

{% trans 'evaluators' %}

+
+
+
    + {% for evaluator in evaluators %} +
  • + +
    + {{ evaluator.username }}
    + {{ evaluator.global_id }}
    + {% blocktrans with evaluators_stats_1=evaluator.evaluation_count %}.{{evaluators_stats_1}}.{% endblocktrans %} +
    + {% if status == "G" %} +
    + {% csrf_token %} + + + +
    +
    + {% csrf_token %} + + + +
    + {% endif %} +
  • + {% endfor %} +
+
+
+
+
+

{% trans 'evaluators-disabled' %}

+
+
+
    + {% for evaluator in disabled %} +
  • + +
    + {{ evaluator.username }}
    + {{ evaluator.global_id }}
    + {% blocktrans with evaluators_stats_1=evaluator.evaluation_count %}.{{evaluators_stats_1}}.{% endblocktrans %} +
    + {% if status == "G" %} +
    + {% csrf_token %} + + + +
    +
    + {% csrf_token %} + + + +
    + {% endif %} +
  • + {% endfor %} +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/contests/urls.py b/contests/urls.py index 4290854..7422998 100644 --- a/contests/urls.py +++ b/contests/urls.py @@ -1,5 +1,5 @@ from django.urls import path -from .views import contest_view, home_view, color_view, triage_view, backtrack_view, counter_view, compare_view, edits_view +from .views import contest_view, home_view, color_view, triage_view, backtrack_view, counter_view, compare_view, edits_view, evaluators_view urlpatterns = [ path('', home_view, name='home_view'), @@ -10,4 +10,5 @@ path('backtrack/', backtrack_view, name='backtrack_view'), path('compare/', compare_view, name='compare_view'), path('edits/', edits_view, name='edits_view'), + path('evaluators/', evaluators_view, name='evaluators_view'), ] diff --git a/contests/views.py b/contests/views.py index 5887c1e..3c61b93 100644 --- a/contests/views.py +++ b/contests/views.py @@ -4,6 +4,7 @@ from .counter import CounterHandler from .compare import CompareHandler from credentials.models import Profile +from .evaluators import EvaluatorsHandler from django.db import connection from datetime import datetime, timedelta from django.db.models import Count, Sum, Case, When, Value, IntegerField, Q, F, OuterRef, Subquery From d2f7e3ee5397ed7392d0c62d76a6d3d1711c324d Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Thu, 29 Aug 2024 13:08:01 -0300 Subject: [PATCH 18/75] chore: Refactor evaluators handler to improve code structure and readability --- contests/evaluators.py | 146 ++++++++++++++++++++++------------------- 1 file changed, 78 insertions(+), 68 deletions(-) diff --git a/contests/evaluators.py b/contests/evaluators.py index 1d57582..0310646 100644 --- a/contests/evaluators.py +++ b/contests/evaluators.py @@ -9,79 +9,89 @@ def __init__(self, contest): self.contest = contest def execute(self, request): - contest = self.contest - if request.method == 'POST' and Evaluator.objects.get(contest=contest, profile=request.user.profile).user_status == 'G': - if request.POST.get('new'): - username = request.POST.get('new') - try: - profile = Profile.objects.get(username=username) - except Profile.DoesNotExist: - api_params = { - 'action': 'query', - 'meta': 'globaluserinfo', - 'guiuser': username, - 'format': 'json', - } - response = requests.get(contest.api_endpoint, params=api_params) - data = response.json() + evaluator = self.get_current_evaluator(request) + + if request.method == 'POST': + if evaluator.user_status == 'G': + if request.POST.get('new'): + self.add_new_evaluator(request.POST.get('new')) - if 'query' in data and 'globaluserinfo' in data['query'] and 'id' in data['query']['globaluserinfo']: - global_id = data['query']['globaluserinfo']['id'] - profile = Profile.objects.create(global_id=global_id, username=username) - Evaluator.objects.create(contest=contest, profile=profile, user_status='A') + if request.POST.get('user'): + self.update_evaluator_status(request) - else: - Evaluator.objects.create(contest=contest, profile=profile, user_status='A') - - if request.POST.get('user'): - if request.POST.get('off'): - Evaluator.objects.filter( - contest=contest, - profile__username=request.POST.get('user'), - ).update(user_status='P') + return { + 'contest': self.contest, + 'evaluators': self.get_evaluators_by_status('A'), + 'managers': self.get_evaluators_by_status('G'), + 'disabled': self.get_disabled_evaluators(), + 'status': evaluator.user_status, + 'right': 'left' if translation.get_language_bidi() else 'right', + 'left': 'right' if translation.get_language_bidi() else 'left', + } - if request.POST.get('on'): - Evaluator.objects.filter( - contest=contest, - profile__username=request.POST.get('user'), - ).update(user_status='A') + def get_current_evaluator(self, request): + return Evaluator.objects.get(contest=self.contest, profile=request.user.profile) - if request.POST.get('reset'): - Edit.objects.filter( - contest=contest, - last_evaluation__evaluator__profile__username=request.POST.get('user'), - ).update(last_evaluation=None) + def add_new_evaluator(self, username): + profile = self.get_or_create_profile(username) + if profile: + Evaluator.objects.create(contest=self.contest, profile=profile, user_status='A') - evaluators = Profile.objects.filter( - evaluator__contest=contest, - evaluator__user_status='A', - ).annotate( - evaluation_count=Count('evaluator__evaluation'), - filter=Q(evaluator__evaluation__status='1') - ).values('global_id', 'username', 'evaluation_count') - managers = Profile.objects.filter( - evaluator__contest=contest, - evaluator__user_status='G', - ).annotate( - evaluation_count=Count('evaluator__evaluation'), - filter=Q(evaluator__evaluation__status='1') - ).values('global_id', 'username', 'evaluation_count') - disabled = Profile.objects.filter( - Q(evaluator__contest=contest, evaluator__user_status='P') | - ~Q(evaluator__contest=contest) - ).annotate( - evaluation_count=Count('evaluator__evaluation'), - filter=Q(evaluator__evaluation__status='1') - ).values('global_id', 'username', 'evaluation_count') + def get_or_create_profile(self, username): + try: + return Profile.objects.get(username=username) + except Profile.DoesNotExist: + return self.fetch_profile_from_api(username) - return_dict = { - 'contest': contest, - 'evaluators': evaluators, - 'managers': managers, - 'disabled': disabled, - 'status': Evaluator.objects.get(contest=contest, profile=request.user.profile).user_status, - 'right': 'left' if translation.get_language_bidi() else 'right', - 'left': 'right' if translation.get_language_bidi() else 'left', + def fetch_profile_from_api(self, username): + api_params = { + 'action': 'query', + 'meta': 'globaluserinfo', + 'guiuser': username, + 'format': 'json', } + response = requests.get(self.contest.api_endpoint, params=api_params) + data = response.json() + + if 'query' in data and 'globaluserinfo' in data['query'] and 'id' in data['query']['globaluserinfo']: + global_id = data['query']['globaluserinfo']['id'] + return Profile.objects.create(global_id=global_id, username=username) + + def update_evaluator_status(self, request): + username = request.POST.get('user') + if request.POST.get('off'): + self.update_status(username, 'P') + elif request.POST.get('on'): + self.update_status(username, 'A') + elif request.POST.get('reset'): + self.reset_evaluations(username) + + def update_status(self, username, status): + Evaluator.objects.filter( + contest=self.contest, + profile__username=username, + ).update(user_status=status) + + def reset_evaluations(self, username): + Edit.objects.filter( + contest=self.contest, + last_evaluation__evaluator__profile__username=username, + ).update(last_evaluation=None) + + def get_evaluators_by_status(self, status): + return Profile.objects.filter( + evaluator__contest=self.contest, + evaluator__user_status=status, + ).annotate( + evaluation_count=Count('evaluator__evaluation'), + filter=Q(evaluator__evaluation__status='1') + ).values('global_id', 'username', 'evaluation_count') - return return_dict \ No newline at end of file + def get_disabled_evaluators(self): + return Profile.objects.filter( + Q(evaluator__contest=self.contest, evaluator__user_status='P') | + ~Q(evaluator__contest=self.contest) + ).annotate( + evaluation_count=Count('evaluator__evaluation'), + filter=Q(evaluator__evaluation__status='1') + ).values('global_id', 'username', 'evaluation_count') \ No newline at end of file From 40ca5a99cc94297d66f68bf6a0458d2f5d6fbc66 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Thu, 29 Aug 2024 13:28:32 -0300 Subject: [PATCH 19/75] chore: Update imports in contests/views.py to use handlers as module --- contests/handlers/__init__.py | 0 contests/{ => handlers}/compare.py | 2 +- contests/{ => handlers}/counter.py | 0 contests/{ => handlers}/evaluators.py | 0 contests/{ => handlers}/triage.py | 2 +- contests/views.py | 8 ++++---- 6 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 contests/handlers/__init__.py rename contests/{ => handlers}/compare.py (99%) rename contests/{ => handlers}/counter.py (100%) rename contests/{ => handlers}/evaluators.py (100%) rename contests/{ => handlers}/triage.py (99%) diff --git a/contests/handlers/__init__.py b/contests/handlers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/contests/compare.py b/contests/handlers/compare.py similarity index 99% rename from contests/compare.py rename to contests/handlers/compare.py index b1a1937..f2a39ca 100644 --- a/contests/compare.py +++ b/contests/handlers/compare.py @@ -2,7 +2,7 @@ from django.utils import timezone from datetime import timedelta import requests -from .models import Contest, Edit, Qualification, Evaluator, Article +from contests.models import Contest, Edit, Qualification, Evaluator, Article class CompareHandler: def __init__(self, contest): diff --git a/contests/counter.py b/contests/handlers/counter.py similarity index 100% rename from contests/counter.py rename to contests/handlers/counter.py diff --git a/contests/evaluators.py b/contests/handlers/evaluators.py similarity index 100% rename from contests/evaluators.py rename to contests/handlers/evaluators.py diff --git a/contests/triage.py b/contests/handlers/triage.py similarity index 99% rename from contests/triage.py rename to contests/handlers/triage.py index 0399edc..5fd770f 100644 --- a/contests/triage.py +++ b/contests/handlers/triage.py @@ -1,7 +1,7 @@ import requests from datetime import datetime, timedelta from django.shortcuts import render, redirect, get_object_or_404 -from .models import Contest, Edit, Evaluation, Participant, ParticipantEnrollment, Qualification, Evaluator +from contests.models import Contest, Edit, Evaluation, Participant, ParticipantEnrollment, Qualification, Evaluator from django.utils import timezone from django.db import transaction from django.db.models import Sum, Case, When, Value, IntegerField, Q, OuterRef, Subquery, Count diff --git a/contests/views.py b/contests/views.py index 3c61b93..4e6e2d3 100644 --- a/contests/views.py +++ b/contests/views.py @@ -1,10 +1,6 @@ from django.shortcuts import render, redirect, get_object_or_404 from .models import Contest, Edit, Participant, Qualification, Evaluator -from .triage import TriageHandler -from .counter import CounterHandler -from .compare import CompareHandler from credentials.models import Profile -from .evaluators import EvaluatorsHandler from django.db import connection from datetime import datetime, timedelta from django.db.models import Count, Sum, Case, When, Value, IntegerField, Q, F, OuterRef, Subquery @@ -18,6 +14,10 @@ from functools import wraps from django.http import HttpResponse from django.template import loader +from handlers.triage import TriageHandler +from handlers.counter import CounterHandler +from handlers.compare import CompareHandler +from handlers.evaluators import EvaluatorsHandler def contest_evaluator_required(view_func): @wraps(view_func) From aaa52302d538a079c5a2409f819ec88644be8c50 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Thu, 29 Aug 2024 14:31:16 -0300 Subject: [PATCH 20/75] chore: Remove color_view and update base.html and contest.html templates to dynamically set color styles --- contests/templates/base.html | 9 ++++++++- contests/templates/contest.html | 9 ++++++++- contests/urls.py | 3 +-- contests/views.py | 31 ------------------------------- 4 files changed, 17 insertions(+), 35 deletions(-) diff --git a/contests/templates/base.html b/contests/templates/base.html index 7912c03..ae750a0 100644 --- a/contests/templates/base.html +++ b/contests/templates/base.html @@ -9,7 +9,14 @@ - + {% if contest.color %} + + {% endif %} {% block head %}{% endblock %} diff --git a/contests/templates/contest.html b/contests/templates/contest.html index 1b3fe2a..1f7c975 100644 --- a/contests/templates/contest.html +++ b/contests/templates/contest.html @@ -7,7 +7,14 @@ {{ contest.name }} - + {% if contest.color %} + + {% endif %} diff --git a/contests/urls.py b/contests/urls.py index 7422998..ed0ae30 100644 --- a/contests/urls.py +++ b/contests/urls.py @@ -1,10 +1,9 @@ from django.urls import path -from .views import contest_view, home_view, color_view, triage_view, backtrack_view, counter_view, compare_view, edits_view, evaluators_view +from .views import contest_view, home_view, triage_view, backtrack_view, counter_view, compare_view, edits_view, evaluators_view urlpatterns = [ path('', home_view, name='home_view'), path('contests/', contest_view, name='contest_view'), - path('color/', color_view, name='color_view'), path('triage/', triage_view, name='triage_view'), path('counter/', counter_view, name='counter_view'), path('backtrack/', backtrack_view, name='backtrack_view'), diff --git a/contests/views.py b/contests/views.py index 4e6e2d3..f2f487c 100644 --- a/contests/views.py +++ b/contests/views.py @@ -33,37 +33,6 @@ def _wrapped_view(request, *args, **kwargs): return view_func(request, *args, **kwargs) return _wrapped_view -def color_view(request): - color = request.GET.get('color') - - # Check if the color parameter is provided - if not color: - return HttpResponse("/* Color parameter is missing */", content_type="text/css") - - # Escape the color parameter to avoid XSS attacks - color = escape(color) - - # Define the CSS content - css_content = f""" - .w3-color, - .w3-hover-color:hover {{ - color: #fff !important; - background-color: #{color} !important; - }} - - .w3-text-color, - .w3-hover-text-color:hover {{ - color: #{color} !important; - }} - - .w3-border-color, - .w3-hover-border-color:hover {{ - border-color: #{color} !important; - }} - """ - - # Return the CSS content as a response with the correct content type - return HttpResponse(css_content, content_type="text/css") def home_view(request): # Get contests from the database From 204e6be00958dab9bd7fb3a9e0cc5a48e154a784 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Fri, 30 Aug 2024 12:21:29 -0300 Subject: [PATCH 21/75] chore: Add modify view and template for contest --- contests/handlers/modify.py | 73 ++++++++++++ contests/templates/base.html | 2 +- contests/templates/modify.html | 199 +++++++++++++++++++++++++++++++++ contests/urls.py | 3 +- contests/views.py | 9 +- 5 files changed, 283 insertions(+), 3 deletions(-) create mode 100644 contests/handlers/modify.py create mode 100644 contests/templates/modify.html diff --git a/contests/handlers/modify.py b/contests/handlers/modify.py new file mode 100644 index 0000000..b770e9e --- /dev/null +++ b/contests/handlers/modify.py @@ -0,0 +1,73 @@ +import requests +from django.core.exceptions import PermissionDenied +from django.shortcuts import render +from contests.models import Evaluator, Edit, Evaluation + + + +class ModifyHandler(): + def __init__(self, contest): + self.contest = contest + + def execute(self, request): + contest = self.contest + edit = None + diff = None + content = None + author = None + comment = None + evaluation = None + allowed = False + + if Evaluator.objects.get( + contest=contest, + profile=request.user.profile + ).user_status == 'G' or edit.last_evaluation.evaluator.profile == request.user.profile: + allowed = True + + if request.method == 'POST' and request.POST.get('diff'): + diff = request.POST.get('diff') + edit = Edit.objects.get(diff=diff) + + compare_params = { + 'action': 'compare', + 'prop': 'title|diff|comment|user|ids', + 'format': 'json', + 'fromrev': diff, + 'torelative': 'prev', + } + compare = requests.get(contest.api_endpoint, params=compare_params).json().get('compare', {}) + + content = compare.get('*', '') + author = compare.get('touser', '') + comment = compare.get('tocomment', '') + + if request.POST.get('obs'): + if allowed: + evaluation = Evaluation.objects.create( + contest=contest, + evaluator=Evaluator.objects.get(contest=contest, profile=request.user.profile), + diff=edit, + valid_edit=True if request.POST.get('valid') == '1' else False, + pictures=request.POST.get('pic') if request.POST.get('pic').isnumeric() else 0, + real_bytes=request.POST.get('overwrite') or Edit.objects.get(contest=contest, diff=diff).orig_bytes, + status=1, + obs=request.POST.get('obs'), + ) + edit.last_evaluation = evaluation + edit.save() + else: + raise PermissionDenied("You are not allowed to perform this action.") + + + return_dict = { + 'contest': contest, + 'edit': edit, + 'diff': diff, + 'evaluation': evaluation, + 'allowed': allowed, + 'content': content, + 'author': author, + 'comment': comment, + } + return return_dict \ No newline at end of file diff --git a/contests/templates/base.html b/contests/templates/base.html index ae750a0..5c79a49 100644 --- a/contests/templates/base.html +++ b/contests/templates/base.html @@ -76,7 +76,7 @@
{% trans 'triage-panel' %}
rel="noopener" class="w3-bar-item w3-button w3-padding ">   {% trans 'counter' %} -   {% trans 'modify' %} diff --git a/contests/templates/modify.html b/contests/templates/modify.html new file mode 100644 index 0000000..89c136d --- /dev/null +++ b/contests/templates/modify.html @@ -0,0 +1,199 @@ +{% extends "base.html" %} +{% load static %} +{% load i18n %} +{% get_current_language_bidi as LANGUAGE_BIDI %} + + +{% block pagename %}{% trans 'modify' %}{% endblock %} + +{% block head %} + + +{% endblock %} + +{% block content %} +
+
+
+

{% trans 'modify-consult' %}

+ {% csrf_token %} +

+ + +

+

+ +

+
+
+

{% trans 'modify-reavaluate' %}

+ {% csrf_token %} + +
+

{% trans 'isvalid' %}

+ +
+ +

+
+
+

{% trans 'withimage' %}

+ {% if contest.pictures_mode == 2 %} + +

+ {% else %} + +
+ +

+ {% endif %} +
+

+ +
+ + +

+
+
+ {% if content %} +

{% trans 'modify-diffstats' %}

+
    +
  • {% trans 'modify-label-edition' %}
    + + {{ diff }} + +
  • +
  • {% trans 'modify-label-curid' %}
    + + {{ edit.article.articleID }} + +
  • +
  • {% trans 'label-timestamp' %}
    {{ edit.timestamp }}
  • +
  • {% trans 'label-user' %}
    {{ author }}
  • +
  • {% trans 'modify-label-bytes' %}
    {{ edit.orig_bytes }} -> {{ edit.last_evaluation.real_bytes }}
  • +
  • {% trans 'label-summary' %}
    {{ comment }}
  • +
  • {% trans 'modify-label-newpage' %}
    {% if edit.new_page %}{% trans 'yes' %}{% else %}{% trans 'no' %}{% endif %}
  • +
  • {% trans 'modify-label-valid' %}
    {% if edit.last_evaluation.valid_edit %}{% trans 'yes' %}{% else %}{% trans 'no' %}{% endif %}
  • +
  • {% trans 'modify-label-enrolled' %}
    {% if edit.participant.last_enrollment.enrolled %}{% trans 'yes' %}{% else %}{% trans 'no' %}{% endif %}
  • +
  • {% trans 'modify-label-withimage' %}
    {{ edit.last_evaluation.pictures }}
  • +
  • {% trans 'modify-label-reverted' %}
    {% if edit.last_qualification.status == 0 %}{% trans 'yes' %}{% else %}{% trans 'no' %}{% endif %}
  • +
  • {% trans 'modify-label-evaluator' %}
    {{ edit.last_evaluation.evaluator.profile.username }}
  • +
  • {% trans 'modify-label-evaltimestamp' %}
    {{ edit.last_evaluation.when }}
  • +
  • {% trans 'modify-label-comment' %}
    {{ edit.last_evaluation.obs|default_if_none:"" }} 
  • +
+ {% endif %} +
+
+
+ {% if content %} +
+

{% trans 'modify-showdiff' %}

+ + {{ content | safe }} + +
+
+ {% elif diff and not content %} + + {% endif %} + {% if evalution %} + + {% endif %} +
+
+{% endblock %} \ No newline at end of file diff --git a/contests/urls.py b/contests/urls.py index ed0ae30..798189d 100644 --- a/contests/urls.py +++ b/contests/urls.py @@ -1,5 +1,5 @@ from django.urls import path -from .views import contest_view, home_view, triage_view, backtrack_view, counter_view, compare_view, edits_view, evaluators_view +from .views import contest_view, home_view, triage_view, backtrack_view, counter_view, compare_view, edits_view, evaluators_view, modify_view urlpatterns = [ path('', home_view, name='home_view'), @@ -10,4 +10,5 @@ path('compare/', compare_view, name='compare_view'), path('edits/', edits_view, name='edits_view'), path('evaluators/', evaluators_view, name='evaluators_view'), + path('modify/', modify_view, name='modify_view'), ] diff --git a/contests/views.py b/contests/views.py index f2f487c..f5a8fcb 100644 --- a/contests/views.py +++ b/contests/views.py @@ -18,6 +18,7 @@ from handlers.counter import CounterHandler from handlers.compare import CompareHandler from handlers.evaluators import EvaluatorsHandler +from .handlers.modify import ModifyHandler def contest_evaluator_required(view_func): @wraps(view_func) @@ -259,4 +260,10 @@ def edits_view(request): 'contest': contest, 'edits': edits, 'right': 'left' if translation.get_language_bidi() else 'right', - }) \ No newline at end of file + }) + +@login_required() +@contest_evaluator_required +def modify_view(request, contest): + handler = ModifyHandler(contest=contest) + return render_with_bidi(request, 'modify.html', handler.execute(request)) From 25d13e560428c60eefe2a65dae00b2737469a100 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Fri, 30 Aug 2024 12:26:15 -0300 Subject: [PATCH 22/75] chore: Refactor contest views to use common functions for retrieving contest and checking evaluator permission --- contests/views.py | 119 ++++++++++++++++++++++------------------------ 1 file changed, 56 insertions(+), 63 deletions(-) diff --git a/contests/views.py b/contests/views.py index f5a8fcb..10ca899 100644 --- a/contests/views.py +++ b/contests/views.py @@ -1,42 +1,51 @@ from django.shortcuts import render, redirect, get_object_or_404 -from .models import Contest, Edit, Participant, Qualification, Evaluator -from credentials.models import Profile -from django.db import connection -from datetime import datetime, timedelta -from django.db.models import Count, Sum, Case, When, Value, IntegerField, Q, F, OuterRef, Subquery -from django.db.models.functions import TruncDay -from django.utils import timezone, translation -from django.utils.html import escape -from django.core.exceptions import PermissionDenied -from django.http import HttpResponse from django.contrib.auth.decorators import login_required -from collections import defaultdict -from functools import wraps +from django.core.exceptions import PermissionDenied from django.http import HttpResponse from django.template import loader -from handlers.triage import TriageHandler -from handlers.counter import CounterHandler -from handlers.compare import CompareHandler -from handlers.evaluators import EvaluatorsHandler +from django.utils import translation +from django.utils.html import escape +from django.db.models.functions import TruncDay +from datetime import timedelta +from functools import wraps +from collections import defaultdict +from .models import Contest, Edit, Participant, Qualification, Evaluator +from .handlers.triage import TriageHandler +from .handlers.counter import CounterHandler +from .handlers.compare import CompareHandler +from .handlers.evaluators import EvaluatorsHandler from .handlers.modify import ModifyHandler +from credentials.models import Profile + +def get_contest_from_request(request): + contest_name_id = request.GET.get('contest') + if not contest_name_id: + return redirect('/') + return get_object_or_404(Contest, name_id=contest_name_id) + +def check_evaluator_permission(request, contest): + try: + Evaluator.objects.get(contest=contest, profile=request.user.profile, user_status__in=['A', 'G']) + except Evaluator.DoesNotExist: + raise PermissionDenied("You are not allowed to access this page.") def contest_evaluator_required(view_func): @wraps(view_func) def _wrapped_view(request, *args, **kwargs): - contest_name_id = request.GET.get('contest') - if not contest_name_id: - return redirect('/') - contest = get_object_or_404(Contest, name_id=contest_name_id) - try: - Evaluator.objects.get(contest=contest, profile=request.user.profile, user_status__in=['A', 'G']) - except Evaluator.DoesNotExist: - raise PermissionDenied("You are not allowed to access this page.") - return view_func(request, *args, **kwargs) + contest = get_contest_from_request(request) + check_evaluator_permission(request, contest) + return view_func(request, contest, *args, **kwargs) return _wrapped_view +def render_with_bidi(request, template_name, context): + bidi_context = { + 'right': 'left' if translation.get_language_bidi() else 'right', + 'left': 'right' if translation.get_language_bidi() else 'left', + } + context.update(bidi_context) + return render(request, template_name, context) def home_view(request): - # Get contests from the database contests = Contest.objects.all().order_by('-start_time') contests_chooser = {} @@ -46,8 +55,7 @@ def home_view(request): contests_chooser[group] = [] contests_chooser[group].append([contest.name_id, contest.name]) contests_groups = list(contests_chooser.keys()) - - # Render the main template + return render(request, 'home.html', { 'contests_groups': contests_groups, 'contests_chooser': contests_chooser, @@ -168,9 +176,7 @@ def contest_view(request): @login_required() @contest_evaluator_required -def triage_view(request): - contest = get_object_or_404(Contest, name_id=request.GET.get('contest')) - +def triage_view(request, contest): handler = TriageHandler(contest=contest, user=request.user, api_endpoint=contest.api_endpoint) if request.method == 'POST': do_evaluate = handler.do_evaluate(request) @@ -189,26 +195,21 @@ def triage_view(request): @login_required() @contest_evaluator_required -def counter_view(request): - contest = get_object_or_404(Contest, name_id=request.GET.get('contest')) - +def counter_view(request, contest): handler = CounterHandler(contest=contest) - context = handler.get_context(request) - - return render(request, "counter.html", context) + return render_with_bidi(request, "counter.html", handler.get_context(request)) @login_required() @contest_evaluator_required -def backtrack_view(request): +def backtrack_view(request, contest): contest = get_object_or_404(Contest, name_id=request.GET.get('contest')) qualified = False diff = None if request.POST.get('diff'): diff = request.POST.get('diff') - edit = Edit.objects.get(diff=diff) - evaluator = Evaluator.objects.get(contest=contest, user=request.user) - new_qualification = Qualification.objects.create(contest=contest, diff=edit, evaluator=evaluator) + evaluator = Evaluator.objects.get(contest=contest, profile=request.user.profile) + new_qualification = Qualification.objects.create(contest=contest, diff=Edit.objects.get(diff=diff), evaluator=evaluator) qualified = Edit.objects.filter(contest=contest, diff=diff, last_qualification__isnull=True).update(last_qualification=new_qualification) edits = Edit.objects.filter( @@ -232,35 +233,27 @@ def backtrack_view(request): @login_required() @contest_evaluator_required -def compare_view(request): - contest = get_object_or_404(Contest, name_id=request.GET.get('contest')) - +def compare_view(request, contest): handler = CompareHandler(contest=contest) - - return render(request, 'compare.html', handler.execute(request)) + return render_with_bidi(request, 'compare.html', handler.execute(request)) @login_required() @contest_evaluator_required -def edits_view(request): - contest = get_object_or_404(Contest, name_id=request.GET.get('contest')) - - edits = Edit.objects.filter(contest=contest, participant__isnull=False) - +def edits_view(request, contest): + edits = Edit.objects.filter(contest=contest) if request.POST.get('csv'): - response = HttpResponse( - content_type="text/csv; charset=windows-1252", - headers={"Content-Disposition": 'attachment; filename="edits.csv"'}, - ) - t = loader.get_template("edits.csv") - c = {'data': edits} - response.write(t.render(c)) + response = HttpResponse(content_type="text/csv; charset=windows-1252", + headers={"Content-Disposition": 'attachment; filename="edits.csv"'}) + response.write(loader.get_template("edits.csv").render({'data': edits})) return response - return render(request, 'edits.html', { - 'contest': contest, - 'edits': edits, - 'right': 'left' if translation.get_language_bidi() else 'right', - }) + return render_with_bidi(request, 'edits.html', {'contest': contest, 'edits': edits}) + +@login_required() +@contest_evaluator_required +def evaluators_view(request, contest): + handler = EvaluatorsHandler(contest=contest) + return render_with_bidi(request, 'evaluators.html', handler.execute(request)) @login_required() @contest_evaluator_required From e5446e5fb8e653717079e856d89bedf7b6ef1d47 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Fri, 30 Aug 2024 12:28:27 -0300 Subject: [PATCH 23/75] chore: Add ContestHandler class for executing contest-related actions --- contests/handlers/contest.py | 92 +++++++++++++++++++++++++ contests/templates/contest.html | 22 +++--- contests/views.py | 115 ++------------------------------ 3 files changed, 108 insertions(+), 121 deletions(-) create mode 100644 contests/handlers/contest.py diff --git a/contests/handlers/contest.py b/contests/handlers/contest.py new file mode 100644 index 0000000..27c26c8 --- /dev/null +++ b/contests/handlers/contest.py @@ -0,0 +1,92 @@ +from datetime import timedelta +from django.db.models import Count, Sum, Subquery, OuterRef +from django.db.models.functions import TruncDay +from contests.models import Evaluator, Edit, Participant, Qualification + + +class ContestHandler(): + def __init__(self, contest): + self.contest = contest + + def execute(self, request): + contest = self.contest + is_evaluator = self.check_evaluator(request.user) + + date_range = self.get_date_range(contest.start_time, contest.end_time) + + approved_edits = self.get_approved_edits(contest) + + stats = { + 'new_articles': self.get_stat_by_date(Edit, contest, 'new_page', True), + 'new_participants': self.get_stat_by_date(Participant, contest), + 'total_edits': self.get_stat_by_date(Edit, contest), + 'total_bytes': self.get_stat_by_date(Edit, contest, 'orig_bytes__gte', 0, 'orig_bytes', sum_field=True), + 'valid_edits': self.get_stat_by_date(Edit, contest, 'pk__in', approved_edits), + 'valid_bytes': self.get_stat_by_date(Edit, contest, 'pk__in', approved_edits, 'orig_bytes', sum_field=True) + } + + return self.build_response_dict(stats, date_range, contest, is_evaluator) + + def check_evaluator(self, user): + try: + Evaluator.objects.get(contest=self.contest, profile=user.profile) + return True + except Evaluator.DoesNotExist: + return False + + def get_date_range(self, start_date, end_date): + return [start_date + timedelta(days=x) for x in range((end_date - start_date).days + 1)] + + def get_approved_edits(self, contest): + subquery = Qualification.objects.filter( + contest=contest, + diff=OuterRef('diff') + ).order_by('-when').values('pk')[:1] + + return Qualification.objects.filter( + contest=contest, + pk__in=Subquery(subquery), + status=1 + ).values_list('diff__diff', flat=True) + + def get_stat_by_date(self, model, contest, filter_field=None, filter_value=None, value_field='id', sum_field=False): + queryset = model.objects.filter(contest=contest, timestamp__range=(contest.start_time, contest.end_time)) + + if filter_field and filter_value is not None: + queryset = queryset.filter(**{filter_field: filter_value}) + + annotated_queryset = queryset.annotate(date=TruncDay('timestamp')).values('date') + + if sum_field: + return {entry['date']: entry['sum_value'] for entry in annotated_queryset.annotate(sum_value=Sum(value_field)).order_by('date')} + + return {entry['date']: entry['count'] for entry in annotated_queryset.annotate(count=Count(value_field)).order_by('date')} + + def build_response_dict(self, stats, date_range, contest, is_evaluator): + response_data = { + 'contest': contest, + 'date': [], + 'new_articles': [], + 'new_participants': [], + 'total_edits': [], + 'total_bytes': [], + 'valid_edits': [], + 'valid_bytes': [], + 'is_evaluator': is_evaluator, + } + + for date in date_range: + day_diff = str(abs(date - contest.start_time).days) + response_data['date'].append(day_diff) + response_data['new_articles'].append(str(stats['new_articles'].get(date, 0))) + response_data['new_participants'].append(str(stats['new_participants'].get(date, 0))) + response_data['total_edits'].append(str(stats['total_edits'].get(date, 0))) + response_data['total_bytes'].append(str(stats['total_bytes'].get(date, 0))) + response_data['valid_edits'].append(str(stats['valid_edits'].get(date, 0))) + response_data['valid_bytes'].append(str(stats['valid_bytes'].get(date, 0))) + + for key in response_data: + if isinstance(response_data[key], list): + response_data[key] = ', '.join(response_data[key]) + + return response_data diff --git a/contests/templates/contest.html b/contests/templates/contest.html index 1f7c975..ce73715 100644 --- a/contests/templates/contest.html +++ b/contests/templates/contest.html @@ -20,10 +20,12 @@
- logo + + logo + - {% if result.is_evaluator %} - + {% if is_evaluator %} + {% endif %} {{ contest.name }}
@@ -45,7 +47,7 @@
+ + +
+

{% trans 'manage-title' %}

+
+
+
+
+
+

{% trans 'manage-about' %}

+
+
+ {% for contest in contests %} +
+
+

{% if contest.pk %}{{ contest.name }}{% else %}{% trans 'manage-newcontest' %}{% endif %}

+
+
+
+ {% csrf_token %} +
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + + + + + + + + + + +
+
+ + + + + + + +
+
+ + + + + + + +
+
+ + + + +
+
+ + + + + + + + + + +
+
+ + + + + + + + + + +
+
+ +
+
+ + +
+
+ + +
+
+ + {% if contest.pk %} +
+
+ + +
+
+ +
+
+
+
+ +
+
+ +
+
+ {% else %} + + + + + {% endif %} +
+
+
+ {% csrf_token %} +
+

{% trans 'manage-confirmmanager' %}

+ + + +
+
+ +
+
+ +
+
+
+
+
+
+ {% endfor %} +
+ + \ No newline at end of file diff --git a/contests/urls.py b/contests/urls.py index 798189d..6fde235 100644 --- a/contests/urls.py +++ b/contests/urls.py @@ -1,5 +1,5 @@ from django.urls import path -from .views import contest_view, home_view, triage_view, backtrack_view, counter_view, compare_view, edits_view, evaluators_view, modify_view +from .views import contest_view, home_view, triage_view, backtrack_view, counter_view, compare_view, edits_view, evaluators_view, modify_view, manage_view urlpatterns = [ path('', home_view, name='home_view'), @@ -11,4 +11,5 @@ path('edits/', edits_view, name='edits_view'), path('evaluators/', evaluators_view, name='evaluators_view'), path('modify/', modify_view, name='modify_view'), + path('manage/', manage_view, name='manage_view'), ] diff --git a/contests/views.py b/contests/views.py index 1f12e62..8442c0b 100644 --- a/contests/views.py +++ b/contests/views.py @@ -16,6 +16,7 @@ from .handlers.compare import CompareHandler from .handlers.evaluators import EvaluatorsHandler from .handlers.modify import ModifyHandler +from .handlers.manage import ManageHandler from credentials.models import Profile def get_contest_from_request(request): @@ -151,3 +152,9 @@ def evaluators_view(request, contest): def modify_view(request, contest): handler = ModifyHandler(contest=contest) return render_with_bidi(request, 'modify.html', handler.execute(request)) + +@login_required() +def manage_view(request): + if request.user.profile.group_set.exists(): + handler = ManageHandler() + return render_with_bidi(request, 'manage.html', handler.execute(request)) \ No newline at end of file From 29ef9b9fbb6e5c7937037f85f0dd941ea35f4ef1 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Sun, 1 Sep 2024 12:12:09 -0300 Subject: [PATCH 26/75] chore: Add contest statistics to ContestHandler class --- contests/handlers/contest.py | 57 ++++++++++++++++++++- contests/templates/contest.html | 91 ++++++++++++++++++++++++++++++++- 2 files changed, 145 insertions(+), 3 deletions(-) diff --git a/contests/handlers/contest.py b/contests/handlers/contest.py index 27c26c8..9bcb58c 100644 --- a/contests/handlers/contest.py +++ b/contests/handlers/contest.py @@ -1,7 +1,7 @@ from datetime import timedelta -from django.db.models import Count, Sum, Subquery, OuterRef +from django.db.models import Count, Sum, Subquery, OuterRef, Case, When from django.db.models.functions import TruncDay -from contests.models import Evaluator, Edit, Participant, Qualification +from contests.models import Evaluator, Edit, Participant, Qualification, Article class ContestHandler(): @@ -62,6 +62,53 @@ def get_stat_by_date(self, model, contest, filter_field=None, filter_value=None, return {entry['date']: entry['count'] for entry in annotated_queryset.annotate(count=Count(value_field)).order_by('date')} + def most_edited_article(self, contest): + most_edited = ( + Edit.objects.filter(contest=contest) + .values('article', 'article__title') + .annotate(total=Count('diff'), bytes_sum=Sum('last_evaluation__real_bytes')) + .order_by('-total') + .first() + ) + return most_edited + + def biggest_delta(self, contest): + biggest_delta = ( + Edit.objects.filter(contest=contest) + .values('article', 'article__title') + .annotate(total=Sum('last_evaluation__real_bytes')) + .order_by('-total') + .first() + ) + return biggest_delta + + def biggest_edit(self, contest): + biggest_edit = ( + Edit.objects.filter(contest=contest, last_evaluation__valid_edit=True) + .values('article', 'article__title', 'diff', 'last_evaluation__real_bytes') + .order_by('-last_evaluation__real_bytes') + .first() + ) + return biggest_edit + + def count_participants(self, contest): + return Participant.objects.filter(contest=contest, timestamp__isnull=False, last_enrollment__enrolled=True).count() + + def count_articles(self, contest): + return Article.objects.filter(contest=contest, active=True).count() + + def edits_summary(self, contest): + edits_summary = ( + Edit.objects.filter(contest=contest) + .aggregate( + new_pages=Sum(Case(When(new_page=True, then=1), default=0)), + edited_articles=Count('article', distinct=True), + valid_edits=Sum(Case(When(last_evaluation__valid_edit=True, then=1), default=0)), + all_bytes=Sum(Case(When(last_evaluation__real_bytes__gt=0, then='last_evaluation__real_bytes'), default=0)), + ) + ) + return edits_summary + def build_response_dict(self, stats, date_range, contest, is_evaluator): response_data = { 'contest': contest, @@ -73,6 +120,12 @@ def build_response_dict(self, stats, date_range, contest, is_evaluator): 'valid_edits': [], 'valid_bytes': [], 'is_evaluator': is_evaluator, + 'most_edited': self.most_edited_article(contest), + 'biggest_delta': self.biggest_delta(contest), + 'biggest_edit': self.biggest_edit(contest), + 'edits_summary': self.edits_summary(contest), + 'participants': self.count_participants(contest), + 'articles': self.count_articles(contest), } for date in date_range: diff --git a/contests/templates/contest.html b/contests/templates/contest.html index ce73715..fee4903 100644 --- a/contests/templates/contest.html +++ b/contests/templates/contest.html @@ -1,5 +1,7 @@ {% load static %} {% load i18n %} +{% load titlescore %} +{% trans 'edits-title' as title %} @@ -23,12 +25,99 @@ logo - + {% if is_evaluator %} {% endif %} {{ contest.name }} +
+
+

{% trans 'main-title' %}

+
+
+
+
+
{% trans 'counter-allpages' %}
+

{{ articles |floatformat:"g" }}

+
+
+
{% trans 'counter-alledited' %}
+

{{ edits_summary.edited_articles |floatformat:"g" }}

+
+
+
{% trans 'counter-allcreated' %}
+

{{ edits_summary.new_pages |floatformat:"g" }}

+
+
+
+
+
{% trans 'counter-allenrolled' %}
+

{{ participants |floatformat:"g" }}

+
+
+
{% trans 'counter-allvalidated' %}
+

{{ edits_summary.valid_edits |floatformat:"g" }}

+
+
+
{% trans 'counter-allbytes' %}
+

{{ edits_summary.all_bytes |floatformat:"g" }}

+
+
+
+
+
+
+
+
+
+
{% trans 'counter-most-edited' %}
+
+
+

+ + {{ most_edited.article__title| titlescore |default_if_none:title }} + +
+ {% blocktrans with counter_editions_1=most_edited.total %}.{{ counter_editions_1 }}.{% endblocktrans %} +

+
+
+
+
+
+
+
{% trans 'counter-biggest-delta' %}
+
+
+

+ + {{ biggest_delta.article__title| titlescore |default_if_none:title }} + +
+ {% blocktrans with triage_bytes_1=biggest_delta.total %}.{{ triage_bytes_1 }}.{% endblocktrans %} +

+
+
+
+
+
+
+
{% trans 'counter-biggest-edition' %}
+
+
+

+ + {{ biggest_edit.article__title| titlescore |default_if_none:title }} + +
+ {% blocktrans with triage_bytes_1=biggest_edit.last_evaluation__real_bytes %}.{{ triage_bytes_1 }}.{% endblocktrans %} +

+
+
+
+
+
From b36fffbde00c3693f868539afab7cbc81af35870 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Sun, 1 Sep 2024 15:47:26 -0300 Subject: [PATCH 27/75] chore: Refactor load_users.py and models.py to handle users without global IDs --- contests/management/commands/load_users.py | 76 +++++++++++-------- .../0009_alter_participant_global_id.py | 18 +++++ ...010_alter_participant_attached_and_more.py | 33 ++++++++ contests/models.py | 8 +- 4 files changed, 100 insertions(+), 35 deletions(-) create mode 100644 contests/migrations/0009_alter_participant_global_id.py create mode 100644 contests/migrations/0010_alter_participant_attached_and_more.py diff --git a/contests/management/commands/load_users.py b/contests/management/commands/load_users.py index 3a326c0..f967956 100644 --- a/contests/management/commands/load_users.py +++ b/contests/management/commands/load_users.py @@ -131,22 +131,29 @@ def process_enrollments(self, enrollments, contest, wiki_id): timestamp = parser.parse(enrollment['enrollment_timestamp']) self.stdout.write(f"Coletando informações do usuário {username} ({global_id})...") - if not global_id: - self.stdout.write("Usuário sem ID global. Ignorando...") - continue - self.insert_or_update_user(global_id, username, contest, wiki_id, timestamp) def insert_or_update_user(self, global_id, username, contest, wiki_id, timestamp): """Inserts or updates the user in the Participant table.""" - try: - participant = Participant.objects.get(global_id=global_id, contest=contest) - local_id = participant.local_id - self.stdout.write(f"Usuário {username} já está na tabela. Ignorando...") - except Participant.DoesNotExist: - participant = None - local_id = self.add_user_contest(global_id, contest, wiki_id, timestamp) - self.stdout.write(f"Usuário {username} inserido com sucesso!") + if global_id: + try: + participant = Participant.objects.get(global_id=global_id, contest=contest) + local_id = participant.local_id + self.stdout.write(f"Usuário {username} já está na tabela. Ignorando...") + except Participant.DoesNotExist: + participant = None + local_id = self.add_user_contest(global_id, contest, wiki_id, timestamp, username) + self.stdout.write(f"Usuário {username} inserido com sucesso!") + else: + self.stdout.write(f"Usuário {username} sem ID global.") + local_id = None + try: + participant = Participant.objects.get(user=username, contest=contest) + except Participant.DoesNotExist: + self.stdout.write(f"Usuário {username} não encontrado. Inserindo...") + self.add_user_contest(global_id, contest, wiki_id, timestamp, username) + participant = None + if participant and participant.user != username: self.stdout.write(f"Usuário {username} mudou de nome. Atualizando...") @@ -158,31 +165,38 @@ def insert_or_update_user(self, global_id, username, contest, wiki_id, timestamp else: self.update_user_edits(local_id, contest, timestamp) - def add_user_contest(self, global_id, contest, wiki_id, timestamp): + def add_user_contest(self, global_id, contest, wiki_id, timestamp, username): """Adds a user to the contest.""" - centralauth_response = self.fetch_user_data(global_id, contest) - centralauth_merged = centralauth_response['query']['globaluserinfo']['merged'] - local_id = next((merged['id'] for merged in centralauth_merged if merged['wiki'] == wiki_id), None) - user = centralauth_response['query']['globaluserinfo']['name'] - attached = centralauth_response['query']['globaluserinfo']['registration'] - - if not local_id: - return None + if global_id: + centralauth_response = self.fetch_user_data(global_id, contest) + centralauth_merged = centralauth_response['query']['globaluserinfo']['merged'] + local_id = next((merged['id'] for merged in centralauth_merged if merged['wiki'] == wiki_id), None) + user = centralauth_response['query']['globaluserinfo']['name'] + attached = centralauth_response['query']['globaluserinfo']['registration'] else: - new_participant = Participant.objects.create( - contest=contest, - user=user, - timestamp=timestamp, - global_id=global_id, - local_id=local_id, - attached=attached, - ) + local_id = None + user = None + attached = None + global_id = None + user=username + + new_participant = Participant.objects.create( + contest=contest, + user=user, + timestamp=timestamp, + global_id=global_id, + local_id=local_id, + attached=attached, + ) + + if global_id and local_id: new_enrollment = ParticipantEnrollment.objects.create( contest=contest, user=new_participant ) Participant.objects.filter(global_id=global_id, contest=contest).update(last_enrollment=new_enrollment) - return local_id + + return local_id def fetch_user_data(self, global_id, contest): """Fetches user data from the contest API.""" @@ -227,5 +241,5 @@ def unlock_edits(self, contest): ) Evaluation.objects.bulk_create([ - Evaluation(contest=self.contest, diff=locked.diff) for locked in lockeds + Evaluation(contest=contest, diff=locked.diff) for locked in lockeds ]) diff --git a/contests/migrations/0009_alter_participant_global_id.py b/contests/migrations/0009_alter_participant_global_id.py new file mode 100644 index 0000000..6f23ab1 --- /dev/null +++ b/contests/migrations/0009_alter_participant_global_id.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1 on 2024-09-01 18:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contests', '0008_alter_contest_outreach_name'), + ] + + operations = [ + migrations.AlterField( + model_name='participant', + name='global_id', + field=models.IntegerField(blank=True), + ), + ] diff --git a/contests/migrations/0010_alter_participant_attached_and_more.py b/contests/migrations/0010_alter_participant_attached_and_more.py new file mode 100644 index 0000000..a003e6e --- /dev/null +++ b/contests/migrations/0010_alter_participant_attached_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.1 on 2024-09-01 18:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contests', '0009_alter_participant_global_id'), + ] + + operations = [ + migrations.AlterField( + model_name='participant', + name='attached', + field=models.DateTimeField(null=True), + ), + migrations.AlterField( + model_name='participant', + name='global_id', + field=models.IntegerField(null=True), + ), + migrations.AlterField( + model_name='participant', + name='local_id', + field=models.IntegerField(null=True), + ), + migrations.AlterField( + model_name='participant', + name='timestamp', + field=models.DateTimeField(), + ), + ] diff --git a/contests/models.py b/contests/models.py index 1c6bd7a..94966c0 100644 --- a/contests/models.py +++ b/contests/models.py @@ -62,10 +62,10 @@ def __str__(self): class Participant(models.Model): contest = models.ForeignKey('Contest', on_delete=models.CASCADE) user = models.CharField(max_length=100) - timestamp = models.DateTimeField(blank=True) - global_id = models.IntegerField() - local_id = models.IntegerField(blank=True) - attached = models.DateTimeField(blank=True) + timestamp = models.DateTimeField() + global_id = models.IntegerField(null=True) + local_id = models.IntegerField(null=True) + attached = models.DateTimeField(null=True) last_enrollment = models.ForeignKey('ParticipantEnrollment', on_delete=models.SET_NULL, null=True) def __str__(self): From d1011ab614a00505d6faa4fd8c489002b6c45ab3 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Sun, 1 Sep 2024 15:48:12 -0300 Subject: [PATCH 28/75] chore: Refactor count_articles method in ContestHandler to use API for retrieving article count --- contests/handlers/contest.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/contests/handlers/contest.py b/contests/handlers/contest.py index 9bcb58c..0157af0 100644 --- a/contests/handlers/contest.py +++ b/contests/handlers/contest.py @@ -1,5 +1,6 @@ +import requests from datetime import timedelta -from django.db.models import Count, Sum, Subquery, OuterRef, Case, When +from django.db.models import Count, Sum, Subquery, OuterRef, Case, When, Q from django.db.models.functions import TruncDay from contests.models import Evaluator, Edit, Participant, Qualification, Article @@ -92,10 +93,23 @@ def biggest_edit(self, contest): return biggest_edit def count_participants(self, contest): - return Participant.objects.filter(contest=contest, timestamp__isnull=False, last_enrollment__enrolled=True).count() + return Participant.objects.filter( + contest=contest, timestamp__isnull=False + ).filter( + Q(last_enrollment__enrolled=True) | Q(last_enrollment__isnull=True) + ).count() def count_articles(self, contest): - return Article.objects.filter(contest=contest, active=True).count() + api_params = { + 'action': 'query', + 'generator': 'links', + 'pageids': contest.official_list_pageid, + 'gplnamespace': 0, + 'gpllimit': 'max', + 'format': 'json' + } + response = requests.get(contest.api_endpoint, params=api_params).json() + return len(response['query']['pages']) def edits_summary(self, contest): edits_summary = ( @@ -104,7 +118,10 @@ def edits_summary(self, contest): new_pages=Sum(Case(When(new_page=True, then=1), default=0)), edited_articles=Count('article', distinct=True), valid_edits=Sum(Case(When(last_evaluation__valid_edit=True, then=1), default=0)), - all_bytes=Sum(Case(When(last_evaluation__real_bytes__gt=0, then='last_evaluation__real_bytes'), default=0)), + all_bytes=Sum(Case( + When(orig_bytes__gt=0, then='orig_bytes'), + default=0 + )), ) ) return edits_summary From 64f29e62d4ae4e2e07af72dce3afe242aadd5d31 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Sun, 1 Sep 2024 15:49:14 -0300 Subject: [PATCH 29/75] chore: Allow reset participant's evaluation in CounterHandler --- contests/handlers/counter.py | 22 +++++++++++++++++++++- contests/templates/counter.html | 5 ++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/contests/handlers/counter.py b/contests/handlers/counter.py index 16524eb..bdc1196 100644 --- a/contests/handlers/counter.py +++ b/contests/handlers/counter.py @@ -1,6 +1,6 @@ from django.shortcuts import render from django.utils import timezone -from contests.models import Contest, Edit +from contests.models import Contest, Edit, Evaluator, Participant from django.shortcuts import render, redirect, get_object_or_404 from datetime import timezone as dt_timezone @@ -135,6 +135,16 @@ def get_context(self, request): counter = self.get_points(time_round) + manager = True if Evaluator.objects.get( + contest=self.contest, + profile=request.user.profile + ).user_status == 'G' else False + + if request.method == 'POST': + success = self.reset_participant(request) + else: + success = False + return { 'contest': self.contest, 'counter': counter, @@ -142,5 +152,15 @@ def get_context(self, request): 'time': request_time.strftime('%H:%M:%S'), 'time_form': request_time.strftime('%Y-%m-%dT%H:%M:%S'), 'contest_begun': self.contest.start_time < request_time, + 'manager': manager, + 'success': success, } + + def reset_participant(self, request): + participant_id = request.POST.get('user_id') + participant = Edit.objects.filter( + contest=self.contest, + participant=Participant.objects.get(local_id=participant_id) + ).update(last_evaluation=None) + return True if participant.count() > 0 else False diff --git a/contests/templates/counter.html b/contests/templates/counter.html index 74f2249..3795ccd 100644 --- a/contests/templates/counter.html +++ b/contests/templates/counter.html @@ -33,7 +33,6 @@
{% if contest_begun %} - # TODO: stats.php
@@ -44,7 +43,7 @@ - {% if user.user_status == 'G' %} + {% if manager %} {% endif %} @@ -57,7 +56,7 @@ - {% if user.user_status == 'G' %} + {% if manager %}
{% trans 'counter-images' %} {% trans 'counter-ppi' %} {% trans 'counter-points' %}{% trans 'counter-redefine' %}
{{ row.total_pictures }} {{ row.pictures_points }} {{ row.total_points }}
Date: Sun, 1 Sep 2024 18:20:18 -0300 Subject: [PATCH 30/75] chore: Refactor settings.py to use separate settings_env.py file for environment-specific configurations --- wikiscore/settings.py | 31 +++--------------- wikiscore/settings_env.py | 66 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 27 deletions(-) create mode 100644 wikiscore/settings_env.py diff --git a/wikiscore/settings.py b/wikiscore/settings.py index d243870..9025576 100644 --- a/wikiscore/settings.py +++ b/wikiscore/settings.py @@ -12,31 +12,19 @@ import os from pathlib import Path from datetime import timedelta -from dotenv import load_dotenv from contests.locale import get_available_languages - -load_dotenv() - -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent +from wikiscore.settings_env import ( + BASE_DIR, HOME, SECRET_KEY, SOCIAL_AUTH_MEDIAWIKI_URL, SOCIAL_AUTH_MEDIAWIKI_KEY, + SOCIAL_AUTH_MEDIAWIKI_SECRET, DEBUG, ALLOWED_HOSTS, SOCIAL_AUTH_MEDIAWIKI_CALLBACK, DATABASES +) # Secrets -SECRET_KEY = os.environ.get('SECRET_KEY') -SOCIAL_AUTH_MEDIAWIKI_URL = 'https://meta.wikimedia.org/w/index.php' -SOCIAL_AUTH_MEDIAWIKI_KEY = os.environ.get("SOCIAL_AUTH_MEDIAWIKI_KEY") -SOCIAL_AUTH_MEDIAWIKI_SECRET = os.environ.get("SOCIAL_AUTH_MEDIAWIKI_SECRET") -SOCIAL_AUTH_MEDIAWIKI_CALLBACK = 'http://127.0.0.1:8000/oauth/complete/mediawiki/' LOGIN_URL = 'login' LOGIN_REDIRECT_URL = '/' SOCIAL_AUTH_URL_NAMESPACE = 'social' PROTECTED_USER_FIELDS = ['groups'] LANGUAGES = get_available_languages() -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS = [] - # Application definition @@ -107,17 +95,6 @@ WSGI_APPLICATION = 'wikiscore.wsgi.application' -# Database -# https://docs.djangoproject.com/en/4.2/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', - } -} - - # Password validation # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators diff --git a/wikiscore/settings_env.py b/wikiscore/settings_env.py new file mode 100644 index 0000000..575b9fa --- /dev/null +++ b/wikiscore/settings_env.py @@ -0,0 +1,66 @@ +import os +from pathlib import Path +from dotenv import load_dotenv + +load_dotenv() + +BASE_DIR = Path(__file__).resolve().parent.parent +HOME = os.environ.get('HOME') or "" +SECRET_KEY = os.environ.get("SECRET_KEY") +SOCIAL_AUTH_MEDIAWIKI_URL = 'https://meta.wikimedia.org/w/index.php' +SOCIAL_AUTH_MEDIAWIKI_KEY = os.environ.get("SOCIAL_AUTH_MEDIAWIKI_KEY") +SOCIAL_AUTH_MEDIAWIKI_SECRET = os.environ.get("SOCIAL_AUTH_MEDIAWIKI_SECRET") + +def configure_settings(): + if os.path.exists(HOME + '/replica.my.cnf'): + debug = False + hosts = ['capx-backend.toolforge.org','toolforge.org'] + callback = 'https://capx.toolforge.org/oauth' + message = 'You are running in production mode' + + databases = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': os.environ.get("TOOL_TOOLSDB_USER") + '__wikiscore', # type: ignore + 'USER': os.environ.get("TOOL_TOOLSDB_USER"), + 'PASSWORD': os.environ.get("TOOL_TOOLSDB_PASSWORD"), + 'HOST': 'tools.db.svc.wikimedia.cloud', + 'PORT': '', + }, + 'old_db': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': os.environ.get("TOOL_TOOLSDB_USER") + '__wikiconcursos', # type: ignore + 'USER': os.environ.get("TOOL_TOOLSDB_USER"), + 'PASSWORD': os.environ.get("TOOL_TOOLSDB_PASSWORD"), + 'HOST': 'tools.db.svc.wikimedia.cloud', + 'PORT': '', + } + } + + else: + debug = True + hosts = ['127.0.0.1'] + callback = 'http://127.0.0.1:8000/oauth/complete/mediawiki/' + message = 'You are running in local mode, please make sure to set up the replica.my.cnf file to run in production mode' + + databases = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } + } + + return { + 'DEBUG': debug, + 'ALLOWED_HOSTS': hosts, + 'SOCIAL_AUTH_MEDIAWIKI_CALLBACK': callback, + 'DATABASES': databases, + 'MESSAGE': message, + } + +settings = configure_settings() +DEBUG = settings['DEBUG'] +ALLOWED_HOSTS = settings['ALLOWED_HOSTS'] +SOCIAL_AUTH_MEDIAWIKI_CALLBACK = settings['SOCIAL_AUTH_MEDIAWIKI_CALLBACK'] +DATABASES = settings['DATABASES'] +print(settings['MESSAGE']) \ No newline at end of file From 7f17ad4102d15baee0954c1cf3167d84ca0ff13a Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Sun, 1 Sep 2024 18:24:45 -0300 Subject: [PATCH 31/75] chore: Squash migration files --- contests/migrations/0001_initial.py | 90 ++++++++++--------- contests/migrations/0002_initial.py | 75 ++++++++++------ ...tion_diff_edit_last_evaluation_and_more.py | 29 ------ .../0004_participant_last_enrollment.py | 19 ---- .../migrations/0005_alter_evaluator_user.py | 20 ----- ..._rename_user_evaluator_profile_and_more.py | 23 ----- contests/migrations/0007_group_manager.py | 19 ---- .../0008_alter_contest_outreach_name.py | 18 ---- .../0009_alter_participant_global_id.py | 18 ---- ...010_alter_participant_attached_and_more.py | 33 ------- credentials/migrations/0001_initial.py | 15 +++- credentials/migrations/0002_profile.py | 24 ----- 12 files changed, 107 insertions(+), 276 deletions(-) delete mode 100644 contests/migrations/0003_rename_edit_evaluation_diff_edit_last_evaluation_and_more.py delete mode 100644 contests/migrations/0004_participant_last_enrollment.py delete mode 100644 contests/migrations/0005_alter_evaluator_user.py delete mode 100644 contests/migrations/0006_rename_user_evaluator_profile_and_more.py delete mode 100644 contests/migrations/0007_group_manager.py delete mode 100644 contests/migrations/0008_alter_contest_outreach_name.py delete mode 100644 contests/migrations/0009_alter_participant_global_id.py delete mode 100644 contests/migrations/0010_alter_participant_attached_and_more.py delete mode 100644 credentials/migrations/0002_profile.py diff --git a/contests/migrations/0001_initial.py b/contests/migrations/0001_initial.py index 6d890a4..dc42f11 100644 --- a/contests/migrations/0001_initial.py +++ b/contests/migrations/0001_initial.py @@ -1,7 +1,7 @@ -# Generated by Django 4.2.15 on 2024-08-22 14:16 +# Generated by Django 5.1 on 2024-09-01 21:23 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): @@ -12,15 +12,6 @@ class Migration(migrations.Migration): ] operations = [ - migrations.CreateModel( - name='Article', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('articleID', models.IntegerField()), - ('title', models.TextField()), - ('active', models.BooleanField(default=True)), - ], - ), migrations.CreateModel( name='Contest', fields=[ @@ -35,7 +26,7 @@ class Migration(migrations.Migration): ('category_petscan', models.IntegerField(blank=True, null=True)), ('endpoint', models.URLField()), ('api_endpoint', models.URLField()), - ('outreach_name', models.TextField()), + ('outreach_name', models.TextField(null=True)), ('campaign_event_id', models.IntegerField(blank=True, default=None, null=True)), ('bytes_per_points', models.IntegerField(default=3000)), ('max_bytes_per_article', models.IntegerField(default=90000)), @@ -51,76 +42,89 @@ class Migration(migrations.Migration): ], ), migrations.CreateModel( - name='Edit', + name='Group', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('diff', models.IntegerField()), - ('timestamp', models.DateTimeField(blank=True)), - ('user_id', models.IntegerField()), - ('orig_bytes', models.IntegerField(blank=True, default=0)), - ('new_page', models.BooleanField(default=False)), + ('name', models.CharField(max_length=100, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), ], ), migrations.CreateModel( - name='Evaluation', + name='Participant', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('valid_edit', models.BooleanField(default=False)), - ('pictures', models.SmallIntegerField(default=0)), - ('real_bytes', models.IntegerField(blank=True, default=0)), - ('status', models.CharField(choices=[('0', 'Pending'), ('1', 'Done'), ('2', 'Hold'), ('3', 'Skipped')], default='0', max_length=1)), + ('user', models.CharField(max_length=100)), + ('timestamp', models.DateTimeField()), + ('global_id', models.IntegerField(null=True)), + ('local_id', models.IntegerField(null=True)), + ('attached', models.DateTimeField(null=True)), + ], + ), + migrations.CreateModel( + name='ParticipantEnrollment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('enrolled', models.BooleanField(default=True)), ('when', models.DateTimeField(auto_now_add=True)), - ('obs', models.TextField(blank=True, null=True)), ], ), migrations.CreateModel( - name='Evaluator', + name='Qualification', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('user_status', models.CharField(default='P', max_length=1)), - ('contest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contests.contest')), + ('status', models.CharField(choices=[('0', 'Reverted'), ('1', 'Active')], default='1', max_length=1)), + ('when', models.DateTimeField(auto_now_add=True)), ], ), migrations.CreateModel( - name='Group', + name='Article', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100, unique=True)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), + ('articleID', models.IntegerField()), + ('title', models.TextField()), + ('active', models.BooleanField(default=True)), + ('contest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contests.contest')), ], ), migrations.CreateModel( - name='Participant', + name='Edit', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('user', models.CharField(max_length=100)), + ('diff', models.IntegerField()), ('timestamp', models.DateTimeField(blank=True)), - ('global_id', models.IntegerField()), - ('local_id', models.IntegerField(blank=True)), - ('attached', models.DateTimeField(blank=True)), + ('user_id', models.IntegerField()), + ('orig_bytes', models.IntegerField(blank=True, default=0)), + ('new_page', models.BooleanField(default=False)), + ('article', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='contests.article')), ('contest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contests.contest')), ], ), migrations.CreateModel( - name='Qualification', + name='Evaluation', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('status', models.CharField(choices=[('0', 'Reverted'), ('1', 'Active')], default='1', max_length=1)), + ('valid_edit', models.BooleanField(default=False)), + ('pictures', models.SmallIntegerField(default=0)), + ('real_bytes', models.IntegerField(blank=True, default=0)), + ('status', models.CharField(choices=[('0', 'Pending'), ('1', 'Done'), ('2', 'Hold'), ('3', 'Skipped')], default='0', max_length=1)), ('when', models.DateTimeField(auto_now_add=True)), + ('obs', models.TextField(blank=True, null=True)), ('contest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contests.contest')), ('diff', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contests.edit')), - ('evaluator', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='contests.evaluator')), ], ), + migrations.AddField( + model_name='edit', + name='last_evaluation', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='contests.evaluation'), + ), migrations.CreateModel( - name='ParticipantEnrollment', + name='Evaluator', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('enrolled', models.BooleanField(default=True)), - ('when', models.DateTimeField(auto_now_add=True)), + ('user_status', models.CharField(default='P', max_length=1)), ('contest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contests.contest')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contests.participant')), ], ), ] diff --git a/contests/migrations/0002_initial.py b/contests/migrations/0002_initial.py index ceb3b62..eb2b836 100644 --- a/contests/migrations/0002_initial.py +++ b/contests/migrations/0002_initial.py @@ -1,8 +1,7 @@ -# Generated by Django 4.2.15 on 2024-08-22 14:16 +# Generated by Django 5.1 on 2024-09-01 21:23 -from django.conf import settings -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): @@ -10,25 +9,15 @@ class Migration(migrations.Migration): initial = True dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('contests', '0001_initial'), + ('credentials', '0001_initial'), ] operations = [ migrations.AddField( model_name='evaluator', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='evaluation', - name='contest', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contests.contest'), - ), - migrations.AddField( - model_name='evaluation', - name='edit', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contests.edit'), + name='profile', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='credentials.profile'), ), migrations.AddField( model_name='evaluation', @@ -36,12 +25,17 @@ class Migration(migrations.Migration): field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='contests.evaluator'), ), migrations.AddField( - model_name='edit', - name='article', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='contests.article'), + model_name='group', + name='manager', + field=models.ManyToManyField(to='credentials.profile'), ), migrations.AddField( - model_name='edit', + model_name='contest', + name='group', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='contests.group'), + ), + migrations.AddField( + model_name='participant', name='contest', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contests.contest'), ), @@ -51,22 +45,47 @@ class Migration(migrations.Migration): field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='contests.participant'), ), migrations.AddField( - model_name='contest', - name='group', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='contests.group'), + model_name='participantenrollment', + name='contest', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contests.contest'), + ), + migrations.AddField( + model_name='participantenrollment', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contests.participant'), + ), + migrations.AddField( + model_name='participant', + name='last_enrollment', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='contests.participantenrollment'), ), migrations.AddField( - model_name='article', + model_name='qualification', name='contest', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contests.contest'), ), - migrations.AlterUniqueTogether( - name='participant', - unique_together={('contest', 'user')}, + migrations.AddField( + model_name='qualification', + name='diff', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contests.edit'), + ), + migrations.AddField( + model_name='qualification', + name='evaluator', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='contests.evaluator'), + ), + migrations.AddField( + model_name='edit', + name='last_qualification', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='contests.qualification'), ), migrations.AlterUniqueTogether( name='evaluator', - unique_together={('user', 'contest')}, + unique_together={('profile', 'contest')}, + ), + migrations.AlterUniqueTogether( + name='participant', + unique_together={('contest', 'user')}, ), migrations.AlterUniqueTogether( name='edit', diff --git a/contests/migrations/0003_rename_edit_evaluation_diff_edit_last_evaluation_and_more.py b/contests/migrations/0003_rename_edit_evaluation_diff_edit_last_evaluation_and_more.py deleted file mode 100644 index c619e96..0000000 --- a/contests/migrations/0003_rename_edit_evaluation_diff_edit_last_evaluation_and_more.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 5.1 on 2024-08-23 11:17 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('contests', '0002_initial'), - ] - - operations = [ - migrations.RenameField( - model_name='evaluation', - old_name='edit', - new_name='diff', - ), - migrations.AddField( - model_name='edit', - name='last_evaluation', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='contests.evaluation'), - ), - migrations.AddField( - model_name='edit', - name='last_qualification', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='contests.qualification'), - ), - ] diff --git a/contests/migrations/0004_participant_last_enrollment.py b/contests/migrations/0004_participant_last_enrollment.py deleted file mode 100644 index 9303a08..0000000 --- a/contests/migrations/0004_participant_last_enrollment.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.1 on 2024-08-23 12:03 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('contests', '0003_rename_edit_evaluation_diff_edit_last_evaluation_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='participant', - name='last_enrollment', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='contests.participantenrollment'), - ), - ] diff --git a/contests/migrations/0005_alter_evaluator_user.py b/contests/migrations/0005_alter_evaluator_user.py deleted file mode 100644 index 482ec25..0000000 --- a/contests/migrations/0005_alter_evaluator_user.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 5.1 on 2024-08-29 10:01 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('contests', '0004_participant_last_enrollment'), - ('credentials', '0002_profile'), - ] - - operations = [ - migrations.AlterField( - model_name='evaluator', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='credentials.profile'), - ), - ] diff --git a/contests/migrations/0006_rename_user_evaluator_profile_and_more.py b/contests/migrations/0006_rename_user_evaluator_profile_and_more.py deleted file mode 100644 index b3efcbd..0000000 --- a/contests/migrations/0006_rename_user_evaluator_profile_and_more.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.1 on 2024-08-29 12:06 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('contests', '0005_alter_evaluator_user'), - ('credentials', '0002_profile'), - ] - - operations = [ - migrations.RenameField( - model_name='evaluator', - old_name='user', - new_name='profile', - ), - migrations.AlterUniqueTogether( - name='evaluator', - unique_together={('profile', 'contest')}, - ), - ] diff --git a/contests/migrations/0007_group_manager.py b/contests/migrations/0007_group_manager.py deleted file mode 100644 index 324335f..0000000 --- a/contests/migrations/0007_group_manager.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.1 on 2024-08-30 16:43 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('contests', '0006_rename_user_evaluator_profile_and_more'), - ('credentials', '0002_profile'), - ] - - operations = [ - migrations.AddField( - model_name='group', - name='manager', - field=models.ManyToManyField(to='credentials.profile'), - ), - ] diff --git a/contests/migrations/0008_alter_contest_outreach_name.py b/contests/migrations/0008_alter_contest_outreach_name.py deleted file mode 100644 index d8e02ca..0000000 --- a/contests/migrations/0008_alter_contest_outreach_name.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.1 on 2024-08-30 19:06 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('contests', '0007_group_manager'), - ] - - operations = [ - migrations.AlterField( - model_name='contest', - name='outreach_name', - field=models.TextField(null=True), - ), - ] diff --git a/contests/migrations/0009_alter_participant_global_id.py b/contests/migrations/0009_alter_participant_global_id.py deleted file mode 100644 index 6f23ab1..0000000 --- a/contests/migrations/0009_alter_participant_global_id.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.1 on 2024-09-01 18:01 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('contests', '0008_alter_contest_outreach_name'), - ] - - operations = [ - migrations.AlterField( - model_name='participant', - name='global_id', - field=models.IntegerField(blank=True), - ), - ] diff --git a/contests/migrations/0010_alter_participant_attached_and_more.py b/contests/migrations/0010_alter_participant_attached_and_more.py deleted file mode 100644 index a003e6e..0000000 --- a/contests/migrations/0010_alter_participant_attached_and_more.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 5.1 on 2024-09-01 18:11 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('contests', '0009_alter_participant_global_id'), - ] - - operations = [ - migrations.AlterField( - model_name='participant', - name='attached', - field=models.DateTimeField(null=True), - ), - migrations.AlterField( - model_name='participant', - name='global_id', - field=models.IntegerField(null=True), - ), - migrations.AlterField( - model_name='participant', - name='local_id', - field=models.IntegerField(null=True), - ), - migrations.AlterField( - model_name='participant', - name='timestamp', - field=models.DateTimeField(), - ), - ] diff --git a/credentials/migrations/0001_initial.py b/credentials/migrations/0001_initial.py index fe44f64..07b69bd 100644 --- a/credentials/migrations/0001_initial.py +++ b/credentials/migrations/0001_initial.py @@ -1,8 +1,10 @@ -# Generated by Django 4.2.15 on 2024-08-22 14:16 +# Generated by Django 5.1 on 2024-09-01 21:23 import django.contrib.auth.models -from django.db import migrations, models +import django.db.models.deletion import django.utils.timezone +from django.conf import settings +from django.db import migrations, models class Migration(migrations.Migration): @@ -37,4 +39,13 @@ class Migration(migrations.Migration): ('objects', django.contrib.auth.models.UserManager()), ], ), + migrations.CreateModel( + name='Profile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('global_id', models.CharField(max_length=50, unique=True)), + ('username', models.CharField(max_length=100)), + ('account', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ], + ), ] diff --git a/credentials/migrations/0002_profile.py b/credentials/migrations/0002_profile.py deleted file mode 100644 index ea9671c..0000000 --- a/credentials/migrations/0002_profile.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 5.1 on 2024-08-29 09:59 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('credentials', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Profile', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('global_id', models.CharField(max_length=50, unique=True)), - ('username', models.CharField(max_length=100)), - ('account', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), - ], - ), - ] From 60f449e0bb806b68cfb151159773543a57325ba4 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Sun, 1 Sep 2024 19:22:39 -0300 Subject: [PATCH 32/75] chore: Toolforge adjustments --- app.py | 7 +++++++ contests/__init__.py | 3 +++ requirements.txt | 3 ++- wikiscore/settings.py | 2 +- wikiscore/settings_env.py | 4 ++-- 5 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 app.py diff --git a/app.py b/app.py new file mode 100644 index 0000000..57a3bd9 --- /dev/null +++ b/app.py @@ -0,0 +1,7 @@ +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "wikiscore.settings") + +app = get_wsgi_application() \ No newline at end of file diff --git a/contests/__init__.py b/contests/__init__.py index e69de29..aa60bed 100644 --- a/contests/__init__.py +++ b/contests/__init__.py @@ -0,0 +1,3 @@ +import pymysql + +pymysql.install_as_MySQLdb() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 6e4178b..dc6a1b8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,5 @@ django-filter social-auth-app-django rest-social-auth polib -python-dotenv \ No newline at end of file +python-dotenv +pymysql \ No newline at end of file diff --git a/wikiscore/settings.py b/wikiscore/settings.py index 9025576..471ac50 100644 --- a/wikiscore/settings.py +++ b/wikiscore/settings.py @@ -133,7 +133,7 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.2/howto/static-files/ -STATIC_URL = 'static/' +STATIC_URL = '/static/' STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')] # Default primary key field type diff --git a/wikiscore/settings_env.py b/wikiscore/settings_env.py index 575b9fa..57d4cc7 100644 --- a/wikiscore/settings_env.py +++ b/wikiscore/settings_env.py @@ -14,8 +14,8 @@ def configure_settings(): if os.path.exists(HOME + '/replica.my.cnf'): debug = False - hosts = ['capx-backend.toolforge.org','toolforge.org'] - callback = 'https://capx.toolforge.org/oauth' + hosts = [ os.environ.get("TOOLNAME") + '.toolforge.org', 'toolforge.org' ] + callback = 'https://' + os.environ.get("TOOLNAME") + '.toolforge.org/oauth' message = 'You are running in production mode' databases = { From da8e51b91b1290eebbd1f14df013810c4d2c5207 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Sun, 1 Sep 2024 21:00:48 -0300 Subject: [PATCH 33/75] chore: Update OAuth callback URL in settings_env.py --- wikiscore/settings_env.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wikiscore/settings_env.py b/wikiscore/settings_env.py index 57d4cc7..22f3752 100644 --- a/wikiscore/settings_env.py +++ b/wikiscore/settings_env.py @@ -15,7 +15,7 @@ def configure_settings(): if os.path.exists(HOME + '/replica.my.cnf'): debug = False hosts = [ os.environ.get("TOOLNAME") + '.toolforge.org', 'toolforge.org' ] - callback = 'https://' + os.environ.get("TOOLNAME") + '.toolforge.org/oauth' + callback = 'https://' + os.environ.get("TOOLNAME") + '.toolforge.org/oauth/complete/mediawiki/' message = 'You are running in production mode' databases = { From 989e060f0c8e4d19b0a57f82ccb224907b0f1e18 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Sun, 1 Sep 2024 21:43:31 -0300 Subject: [PATCH 34/75] chore: Update STATIC_ROOT in settings.py for static file serving --- wikiscore/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wikiscore/settings.py b/wikiscore/settings.py index 471ac50..e3a91c0 100644 --- a/wikiscore/settings.py +++ b/wikiscore/settings.py @@ -134,7 +134,7 @@ # https://docs.djangoproject.com/en/4.2/howto/static-files/ STATIC_URL = '/static/' -STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')] +STATIC_ROOT = os.path.join(HOME, 'static') # Default primary key field type # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field From 7cfdec57a92bd193d927702695b6d6374e205592 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Tue, 3 Sep 2024 11:12:19 -0300 Subject: [PATCH 35/75] chore: Refactor count_articles method in ContestHandler to use API for retrieving article count --- contests/handlers/contest.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/contests/handlers/contest.py b/contests/handlers/contest.py index 0157af0..c1d8937 100644 --- a/contests/handlers/contest.py +++ b/contests/handlers/contest.py @@ -100,6 +100,7 @@ def count_participants(self, contest): ).count() def count_articles(self, contest): + list_ = [] api_params = { 'action': 'query', 'generator': 'links', @@ -109,7 +110,14 @@ def count_articles(self, contest): 'format': 'json' } response = requests.get(contest.api_endpoint, params=api_params).json() - return len(response['query']['pages']) + list_.extend(response['query']['pages']) + + while 'continue' in response: + api_params['gplcontinue'] = response['continue']['gplcontinue'] + response = requests.get(contest.api_endpoint, params=api_params).json() + list_.extend(response['query']['pages']) + + return len(list_) def edits_summary(self, contest): edits_summary = ( From ca438aeeb82d3794a0636eefc47545f5b84971df Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Tue, 3 Sep 2024 11:19:15 -0300 Subject: [PATCH 36/75] fix: Temporary comment out due to "This version of MariaDB doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'" --- contests/handlers/contest.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contests/handlers/contest.py b/contests/handlers/contest.py index c1d8937..316cabc 100644 --- a/contests/handlers/contest.py +++ b/contests/handlers/contest.py @@ -22,8 +22,8 @@ def execute(self, request): 'new_participants': self.get_stat_by_date(Participant, contest), 'total_edits': self.get_stat_by_date(Edit, contest), 'total_bytes': self.get_stat_by_date(Edit, contest, 'orig_bytes__gte', 0, 'orig_bytes', sum_field=True), - 'valid_edits': self.get_stat_by_date(Edit, contest, 'pk__in', approved_edits), - 'valid_bytes': self.get_stat_by_date(Edit, contest, 'pk__in', approved_edits, 'orig_bytes', sum_field=True) + #'valid_edits': self.get_stat_by_date(Edit, contest, 'pk__in', approved_edits), + #'valid_bytes': self.get_stat_by_date(Edit, contest, 'pk__in', approved_edits, 'orig_bytes', sum_field=True) } return self.build_response_dict(stats, date_range, contest, is_evaluator) @@ -160,8 +160,8 @@ def build_response_dict(self, stats, date_range, contest, is_evaluator): response_data['new_participants'].append(str(stats['new_participants'].get(date, 0))) response_data['total_edits'].append(str(stats['total_edits'].get(date, 0))) response_data['total_bytes'].append(str(stats['total_bytes'].get(date, 0))) - response_data['valid_edits'].append(str(stats['valid_edits'].get(date, 0))) - response_data['valid_bytes'].append(str(stats['valid_bytes'].get(date, 0))) + #response_data['valid_edits'].append(str(stats['valid_edits'].get(date, 0))) + #response_data['valid_bytes'].append(str(stats['valid_bytes'].get(date, 0))) for key in response_data: if isinstance(response_data[key], list): From af8ec2d33e8798e265ba3c5aea4d6c75b618b074 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Tue, 3 Sep 2024 14:08:39 -0300 Subject: [PATCH 37/75] chore: Update static file paths in .gitignore and settings.py --- .gitignore | 2 +- {static => contests/static}/Desenho_01.png | Bin {static => contests/static}/Logo_Branco.svg | 0 {static => contests/static}/Logo_Preto_Tagline.svg | 0 {static => contests/static}/authorship.js | 0 {static => contests/static}/copyvios.js | 0 {static => contests/static}/diff.css | 0 {static => contests/static}/folder.svg | 0 {static => contests/static}/rtl.css | 0 {static => contests/static}/w3.css | 0 wikiscore/settings.py | 4 ++-- wikiscore/urls.py | 4 +++- 12 files changed, 6 insertions(+), 4 deletions(-) rename {static => contests/static}/Desenho_01.png (100%) rename {static => contests/static}/Logo_Branco.svg (100%) rename {static => contests/static}/Logo_Preto_Tagline.svg (100%) rename {static => contests/static}/authorship.js (100%) rename {static => contests/static}/copyvios.js (100%) rename {static => contests/static}/diff.css (100%) rename {static => contests/static}/folder.svg (100%) rename {static => contests/static}/rtl.css (100%) rename {static => contests/static}/w3.css (100%) diff --git a/.gitignore b/.gitignore index 51b7cbf..503d8fb 100644 --- a/.gitignore +++ b/.gitignore @@ -15,5 +15,5 @@ media/ .vscode/ pytest.ini coverage.xml -static/ +static_files/ locale/ \ No newline at end of file diff --git a/static/Desenho_01.png b/contests/static/Desenho_01.png similarity index 100% rename from static/Desenho_01.png rename to contests/static/Desenho_01.png diff --git a/static/Logo_Branco.svg b/contests/static/Logo_Branco.svg similarity index 100% rename from static/Logo_Branco.svg rename to contests/static/Logo_Branco.svg diff --git a/static/Logo_Preto_Tagline.svg b/contests/static/Logo_Preto_Tagline.svg similarity index 100% rename from static/Logo_Preto_Tagline.svg rename to contests/static/Logo_Preto_Tagline.svg diff --git a/static/authorship.js b/contests/static/authorship.js similarity index 100% rename from static/authorship.js rename to contests/static/authorship.js diff --git a/static/copyvios.js b/contests/static/copyvios.js similarity index 100% rename from static/copyvios.js rename to contests/static/copyvios.js diff --git a/static/diff.css b/contests/static/diff.css similarity index 100% rename from static/diff.css rename to contests/static/diff.css diff --git a/static/folder.svg b/contests/static/folder.svg similarity index 100% rename from static/folder.svg rename to contests/static/folder.svg diff --git a/static/rtl.css b/contests/static/rtl.css similarity index 100% rename from static/rtl.css rename to contests/static/rtl.css diff --git a/static/w3.css b/contests/static/w3.css similarity index 100% rename from static/w3.css rename to contests/static/w3.css diff --git a/wikiscore/settings.py b/wikiscore/settings.py index e3a91c0..813bfee 100644 --- a/wikiscore/settings.py +++ b/wikiscore/settings.py @@ -133,8 +133,8 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.2/howto/static-files/ -STATIC_URL = '/static/' -STATIC_ROOT = os.path.join(HOME, 'static') +STATIC_URL = 'static/' +STATIC_ROOT = os.path.join(BASE_DIR, 'static_files/') # Default primary key field type # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field diff --git a/wikiscore/urls.py b/wikiscore/urls.py index 94ddec0..b7fb23f 100644 --- a/wikiscore/urls.py +++ b/wikiscore/urls.py @@ -16,7 +16,9 @@ """ from django.contrib import admin from django.urls import path +from django.conf import settings from django.conf.urls import include +from django.conf.urls.static import static urlpatterns = [ path('admin/', admin.site.urls), @@ -24,4 +26,4 @@ path('', include('credentials.urls')), path('', include('social_django.urls')), path("i18n/", include("django.conf.urls.i18n")), -] +] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) From fb32d3fa09da8843ab65976b6eba8c91fef2b94b Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Tue, 3 Sep 2024 14:11:51 -0300 Subject: [PATCH 38/75] chore: Add MIME types for CSS and JavaScript files in settings.py --- wikiscore/settings.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/wikiscore/settings.py b/wikiscore/settings.py index 813bfee..3e9e1e8 100644 --- a/wikiscore/settings.py +++ b/wikiscore/settings.py @@ -10,6 +10,7 @@ https://docs.djangoproject.com/en/4.2/ref/settings/ """ import os +import mimetypes from pathlib import Path from datetime import timedelta from contests.locale import get_available_languages @@ -135,6 +136,8 @@ STATIC_URL = 'static/' STATIC_ROOT = os.path.join(BASE_DIR, 'static_files/') +mimetypes.add_type("text/css", ".css", True) +mimetypes.add_type("text/javascript", ".js", True) # Default primary key field type # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field From f0c81a6f15b938e95cd79318a896414700a391ee Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Tue, 3 Sep 2024 14:54:59 -0300 Subject: [PATCH 39/75] refactor: Add check for anonymous user in ContestHandler's check_evaluator method --- contests/handlers/contest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contests/handlers/contest.py b/contests/handlers/contest.py index 316cabc..8eb9525 100644 --- a/contests/handlers/contest.py +++ b/contests/handlers/contest.py @@ -29,6 +29,8 @@ def execute(self, request): return self.build_response_dict(stats, date_range, contest, is_evaluator) def check_evaluator(self, user): + if user.is_anonymous: + return False try: Evaluator.objects.get(contest=self.contest, profile=user.profile) return True From f3b6084e840a51ff154c9a1376d64f745c8297d7 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Tue, 3 Sep 2024 15:49:49 -0300 Subject: [PATCH 40/75] chore: Update SQL query in CounterHandler to filter user_table by contest ID --- contests/handlers/counter.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contests/handlers/counter.py b/contests/handlers/counter.py index bdc1196..93f8da4 100644 --- a/contests/handlers/counter.py +++ b/contests/handlers/counter.py @@ -26,6 +26,9 @@ def get_points(self, time_round): FROM `contests_edit` INNER JOIN `contests_participant` ON `contests_participant`.`local_id` = `contests_edit`.`user_id` + WHERE + `contests_edit`.`contest_id` = '{self.contest.id}' + AND `contests_participant`.`contest_id` = '{self.contest.id}' ) AS `user_table` LEFT JOIN ( SELECT From 97e28c014e24ab359ea4ad383254aead40f87da6 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Tue, 3 Sep 2024 15:50:50 -0300 Subject: [PATCH 41/75] chore: Update ContestHandler to avoid MariaDB error --- contests/handlers/contest.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/contests/handlers/contest.py b/contests/handlers/contest.py index 8eb9525..45af644 100644 --- a/contests/handlers/contest.py +++ b/contests/handlers/contest.py @@ -22,8 +22,8 @@ def execute(self, request): 'new_participants': self.get_stat_by_date(Participant, contest), 'total_edits': self.get_stat_by_date(Edit, contest), 'total_bytes': self.get_stat_by_date(Edit, contest, 'orig_bytes__gte', 0, 'orig_bytes', sum_field=True), - #'valid_edits': self.get_stat_by_date(Edit, contest, 'pk__in', approved_edits), - #'valid_bytes': self.get_stat_by_date(Edit, contest, 'pk__in', approved_edits, 'orig_bytes', sum_field=True) + 'valid_edits': self.get_stat_by_date(Edit, contest, 'pk__in', approved_edits), + 'valid_bytes': self.get_stat_by_date(Edit, contest, 'pk__in', approved_edits, 'orig_bytes', sum_field=True) } return self.build_response_dict(stats, date_range, contest, is_evaluator) @@ -41,16 +41,10 @@ def get_date_range(self, start_date, end_date): return [start_date + timedelta(days=x) for x in range((end_date - start_date).days + 1)] def get_approved_edits(self, contest): - subquery = Qualification.objects.filter( + return Edit.objects.filter( contest=contest, - diff=OuterRef('diff') - ).order_by('-when').values('pk')[:1] - - return Qualification.objects.filter( - contest=contest, - pk__in=Subquery(subquery), - status=1 - ).values_list('diff__diff', flat=True) + last_evaluation__valid_edit=True + ).values('diff') def get_stat_by_date(self, model, contest, filter_field=None, filter_value=None, value_field='id', sum_field=False): queryset = model.objects.filter(contest=contest, timestamp__range=(contest.start_time, contest.end_time)) @@ -162,8 +156,8 @@ def build_response_dict(self, stats, date_range, contest, is_evaluator): response_data['new_participants'].append(str(stats['new_participants'].get(date, 0))) response_data['total_edits'].append(str(stats['total_edits'].get(date, 0))) response_data['total_bytes'].append(str(stats['total_bytes'].get(date, 0))) - #response_data['valid_edits'].append(str(stats['valid_edits'].get(date, 0))) - #response_data['valid_bytes'].append(str(stats['valid_bytes'].get(date, 0))) + response_data['valid_edits'].append(str(stats['valid_edits'].get(date, 0))) + response_data['valid_bytes'].append(str(stats['valid_bytes'].get(date, 0))) for key in response_data: if isinstance(response_data[key], list): From 1875ff528bb591091bd0237cbb9181b63d2a1db9 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Tue, 3 Sep 2024 16:14:36 -0300 Subject: [PATCH 42/75] chore: Refactor ContestHandler to optimize database queries and improve code readability --- contests/handlers/contest.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/contests/handlers/contest.py b/contests/handlers/contest.py index 45af644..9bc7898 100644 --- a/contests/handlers/contest.py +++ b/contests/handlers/contest.py @@ -44,7 +44,7 @@ def get_approved_edits(self, contest): return Edit.objects.filter( contest=contest, last_evaluation__valid_edit=True - ).values('diff') + ) def get_stat_by_date(self, model, contest, filter_field=None, filter_value=None, value_field='id', sum_field=False): queryset = model.objects.filter(contest=contest, timestamp__range=(contest.start_time, contest.end_time)) @@ -63,7 +63,7 @@ def most_edited_article(self, contest): most_edited = ( Edit.objects.filter(contest=contest) .values('article', 'article__title') - .annotate(total=Count('diff'), bytes_sum=Sum('last_evaluation__real_bytes')) + .annotate(total=Count('diff')) .order_by('-total') .first() ) @@ -73,7 +73,9 @@ def biggest_delta(self, contest): biggest_delta = ( Edit.objects.filter(contest=contest) .values('article', 'article__title') - .annotate(total=Sum('last_evaluation__real_bytes')) + .annotate(total=Sum(Case( + When(last_evaluation__valid_edit=True, then='last_evaluation__real_bytes'), + default='orig_bytes'))) .order_by('-total') .first() ) @@ -123,6 +125,7 @@ def edits_summary(self, contest): edited_articles=Count('article', distinct=True), valid_edits=Sum(Case(When(last_evaluation__valid_edit=True, then=1), default=0)), all_bytes=Sum(Case( + When(last_evaluation__valid_edit=True, then='last_evaluation__real_bytes'), When(orig_bytes__gt=0, then='orig_bytes'), default=0 )), From 1d2694555e68cd89c3ae7d1d19cc86a9066ac88d Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Tue, 3 Sep 2024 16:31:31 -0300 Subject: [PATCH 43/75] feat: Import data from old database in Django management command --- contests/management/commands/import.py | 191 +++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 contests/management/commands/import.py diff --git a/contests/management/commands/import.py b/contests/management/commands/import.py new file mode 100644 index 0000000..d090f5f --- /dev/null +++ b/contests/management/commands/import.py @@ -0,0 +1,191 @@ +from django.core.management.base import BaseCommand +from django.db import connections +from contests.models import Contest, Group, Evaluator, Article, Participant, ParticipantEnrollment, Qualification, Edit, Evaluation +from credentials.models import Profile +import random +from django.utils.timezone import make_aware + +class Command(BaseCommand): + help = 'Import data from the old database' + + def handle(self, *args, **options): + self.stdout.write("Iniciando a importação de dados...") + + with connections['old_db'].cursor() as cursor: + cursor.execute('SELECT * FROM manage__contests') + columns = cursor.description + contests = [{columns[index][0]:column for index, column in enumerate(value)} for value in cursor.fetchall()] + + for contest in contests: + self.stdout.write(f"Importando o concurso: {contest.get('name_id')}") + + group, _ = Group.objects.get_or_create(name=contest.get('group', None)) + try: + Contest.objects.get(name_id=contest.get('name_id', None)).delete() + self.stdout.write(f"Concurso existente deletado: {contest.get('name_id')}") + except Contest.DoesNotExist: + pass + + contest_instance = Contest.objects.create( + name_id=contest.get('name_id', None), + start_time=make_aware(contest.get('start_time', None)), + end_time=make_aware(contest.get('end_time', None)), + name=contest.get('name', None), + group=group, + revert_time=contest.get('revert_time', None), + official_list_pageid=contest.get('official_list_pageid', None), + category_pageid=contest.get('category_pageid', None), + category_petscan=contest.get('category_petscan', None), + endpoint=contest.get('endpoint', None), + api_endpoint=contest.get('api_endpoint', None), + outreach_name=contest.get('outreach_name', None), + campaign_event_id=None, + bytes_per_points=contest.get('bytes_per_points', None), + max_bytes_per_article=contest.get('max_bytes_per_article', None), + minimum_bytes=contest.get('minimum_bytes', None), + pictures_per_points=contest.get('pictures_per_points', None), + pictures_mode=contest.get('pictures_mode', None), + max_pic_per_article=contest.get('max_pic_per_article', None), + theme=contest.get('theme', None), + color=contest.get('color', '') if contest.get('color', None) else '', + ) + self.stdout.write(f"Concurso importado: {contest_instance.name_id}") + + # Importando os artigos + self.stdout.write(f"Importando artigos do concurso: {contest_instance.name_id}") + cursor.execute('SELECT * FROM ' + contest_instance.name_id + '__articles') + columns = cursor.description + articles = [{columns[index][0]:column for index, column in enumerate(value)} for value in cursor.fetchall()] + for article in articles: + Article.objects.create( + contest=contest_instance, + articleID=article.get('articleID', None), + title=article.get('title', None), + ) + self.stdout.write(f"Artigos importados para o concurso: {contest_instance.name_id}") + + # Importando os usuários + self.stdout.write(f"Importando participantes do concurso: {contest_instance.name_id}") + cursor.execute('SELECT * FROM ' + contest_instance.name_id + '__users') + columns = cursor.description + users = [{columns[index][0]:column for index, column in enumerate(value)} for value in cursor.fetchall()] + for user in users: + participant = Participant.objects.create( + contest=contest_instance, + user=user.get('user', None), + timestamp=make_aware(user.get('timestamp', None)), + global_id=user.get('global_id', None), + local_id=user.get('local_id', None), + attached=make_aware(user.get('attached')) if user.get('attached', None) else None, + last_enrollment=None, + ) + enrollment = ParticipantEnrollment.objects.create( + contest=contest_instance, + user=participant, + enrolled=True, + ) + participant.last_enrollment = enrollment + participant.save() + self.stdout.write(f"Participantes importados para o concurso: {contest_instance.name_id}") + + # Importando os avaliadores + self.stdout.write(f"Importando avaliadores do concurso: {contest_instance.name_id}") + cursor.execute('SELECT * FROM ' + contest_instance.name_id + '__credentials') + columns = cursor.description + credentials = [{columns[index][0]:column for index, column in enumerate(value)} for value in cursor.fetchall()] + for credential in credentials: + try: + profile = Profile.objects.get(username=credential.get('user_name', None)) + except Profile.DoesNotExist: + while True: + mock_global_id = f'A{random.randint(100000000, 999999999)}' + if not Profile.objects.filter(global_id=mock_global_id).exists(): + break + + profile = Profile.objects.create( + global_id=mock_global_id, + username=credential.get('user_name', None), + account=None, + ) + + Evaluator.objects.create( + profile=profile, + contest=contest_instance, + user_status=credential.get('user_status', None), + ) + self.stdout.write(f"Avaliadores importados para o concurso: {contest_instance.name_id}") + + # Importando os edits + self.stdout.write(f"Importando edições do concurso: {contest_instance.name_id}") + cursor.execute('SELECT * FROM ' + contest_instance.name_id + '__edits') + columns = cursor.description + edits = [{columns[index][0]:column for index, column in enumerate(value)} for value in cursor.fetchall()] + + done = 0 + + for edit in edits: + try: + article = Article.objects.get(contest=contest_instance, articleID=edit.get('article', None)) + except Article.DoesNotExist: + article = Article.objects.create( + contest=contest_instance, + articleID=edit.get('article', None), + title='Title not found', + ) + + try: + user = Participant.objects.get(contest=contest_instance, local_id=edit.get('user_id', None)) + except Participant.DoesNotExist: + user = None + + if edit.get('timestamp', None) is None: + continue + + diff = Edit.objects.create( + contest=contest_instance, + diff=edit.get('diff', None), + article=article, + timestamp=make_aware(edit.get('timestamp', None)), + user_id=edit.get('user_id', None), + participant=user, + orig_bytes=edit.get('orig_bytes') if edit.get('orig_bytes', None) else edit.get('bytes', None), + new_page=True if edit.get('new_page', None) == 1 else False, + last_qualification=None, + last_evaluation=None, + ) + if user is not None: + qual = Qualification.objects.create( + contest=contest_instance, + diff=diff, + evaluator=None, + status=1 if edit.get('reverted', None) is None else 0, + ) + diff.last_qualification = qual + + by_value = edit.get('by', None) + if by_value is not None and not by_value.startswith(('hold-', 'skip-')): + evaluator = Evaluator.objects.get( + profile__username=by_value, + contest=contest_instance + ) + evaluation = Evaluation.objects.create( + contest=contest_instance, + evaluator=evaluator, + diff=diff, + valid_edit=True if edit.get('valid_edit', None) == 1 else False, + pictures=edit.get('pictures', None), + real_bytes=edit.get('bytes', None), + status=1, + obs=edit.get('obs', None), + ) + diff.last_evaluation = evaluation + + diff.save() + + done += 1 + if done % 100 == 0: + self.stdout.write(f"{done} edições processadas...") + + self.stdout.write(f"Edições importadas para o concurso: {contest_instance.name_id}") + + self.stdout.write("Importação de dados concluída.") From 1cb6c22684ecdaa86ac1cd5ceebc9ad898a7390e Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Tue, 3 Sep 2024 21:50:56 -0300 Subject: [PATCH 44/75] fix: Replacing update check vars by the right ones --- contests/handlers/triage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contests/handlers/triage.py b/contests/handlers/triage.py index 5fd770f..0b86323 100644 --- a/contests/handlers/triage.py +++ b/contests/handlers/triage.py @@ -43,7 +43,7 @@ def get_evaluate(self, request): return_dict = {'contest': contest } # Check if the update start time is greater than the end time (indicating an update is in progress) - if contest.start_time > contest.end_time: + if (contest.started_update is not None and contest.finished_update is None) or (contest.started_update > contest.finished_update): return_dict.update({'error': 'updating'}) # Fetch the next available edit for triage From ed4fe549f97d16b9a7e9fedd9068273401c9114a Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Tue, 3 Sep 2024 21:51:31 -0300 Subject: [PATCH 45/75] fix: Fix enrollment check in load_users.py to avoid users without global_id --- contests/management/commands/load_users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contests/management/commands/load_users.py b/contests/management/commands/load_users.py index f967956..7b02fa7 100644 --- a/contests/management/commands/load_users.py +++ b/contests/management/commands/load_users.py @@ -116,7 +116,7 @@ def process_enrollments(self, enrollments, contest, wiki_id): enrollments_ids = set(enrollment['global_id'] for enrollment in enrollments) for enrollment in already_enrolled: - if str(enrollment) not in enrollments_ids: + if enrollment != 0 and str(enrollment) not in enrollments_ids: self.stdout.write(f"Usuário {enrollment} não está mais inscrito. Desinscrevendo...") unenroll = ParticipantEnrollment.objects.create( contest=contest, From bea61e5b8374c4c10ecffa7cd8d6ed2d754fff1b Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Tue, 3 Sep 2024 21:52:48 -0300 Subject: [PATCH 46/75] fix: Handle error response in CompareHandler's get_category_list method --- contests/handlers/compare.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contests/handlers/compare.py b/contests/handlers/compare.py index f2a39ca..f9de432 100644 --- a/contests/handlers/compare.py +++ b/contests/handlers/compare.py @@ -104,6 +104,9 @@ def get_category_articles(self, contest): "gcmlimit": "max", } response = requests.get(contest.api_endpoint, params=categorymembers_api_params).json() + if 'error' in response: + return list_ + list_.extend(response['query']['pages']) while 'continue' in response: From 7787a52cb62428d0444ac4ea950d349fb55d7aa7 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Tue, 3 Sep 2024 21:53:27 -0300 Subject: [PATCH 47/75] fix: Fix logic to handle contest updates and check for recent updates --- contests/handlers/compare.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/contests/handlers/compare.py b/contests/handlers/compare.py index f9de432..a61f4a1 100644 --- a/contests/handlers/compare.py +++ b/contests/handlers/compare.py @@ -197,8 +197,10 @@ def get_update_countdown(self, contest): """Coleta contagem regressiva para atualização.""" if contest.end_time + timedelta(days=2) < timezone.now(): return False + elif contest.next_update is None: + return 0 elif contest.next_update > timezone.now() and not self.update: - return contest.next_update - timezone.now() + return (contest.next_update - timezone.now()).total_seconds() else: return 0 @@ -209,7 +211,18 @@ def call_update(self, contest): def check_recent_update(self, contest): """Verifica se houve atualização recente.""" - if (timezone.now() - contest.finished_update) < timedelta(minutes=30) or contest.next_update==None: + # Caso todas as variáveis estejam nulas, retorna False + if contest.started_update is None and contest.finished_update is None and contest.next_update is None: + return False + + # Caso a atualização tenha sido iniciada e terminada, mas não há próxima atualização definida, retorna False + if contest.started_update is not None and contest.finished_update is not None and contest.next_update is None: + return False + + # Se a atualização foi iniciada, mas ainda não foi terminada ou terminou há menos de 30 minutos + if contest.finished_update is None or contest.finished_update < contest.started_update or (timezone.now() - contest.finished_update) < timedelta(minutes=30): return True + + # Caso contrário, não há atualização em andamento ou recente return False \ No newline at end of file From 3ea0bf190518bc9e934a6298629b0058e78e9826 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Tue, 3 Sep 2024 21:59:26 -0300 Subject: [PATCH 48/75] fix: Improve logic to handle contest updates and check for recent updates --- contests/handlers/triage.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/contests/handlers/triage.py b/contests/handlers/triage.py index 0b86323..a5fab96 100644 --- a/contests/handlers/triage.py +++ b/contests/handlers/triage.py @@ -43,7 +43,11 @@ def get_evaluate(self, request): return_dict = {'contest': contest } # Check if the update start time is greater than the end time (indicating an update is in progress) - if (contest.started_update is not None and contest.finished_update is None) or (contest.started_update > contest.finished_update): + if ( + contest.started_update is not None and contest.finished_update is None + ) or ( + contest.started_update is not None and contest.finished_update is not None and contest.started_update > contest.finished_update + ): return_dict.update({'error': 'updating'}) # Fetch the next available edit for triage From 8a38a427b2fcfd9a15fce053ec04a92a6fb7bb10 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Tue, 3 Sep 2024 22:22:39 -0300 Subject: [PATCH 49/75] chore: Optimize database queries and improve code readability in edits_view --- contests/views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/contests/views.py b/contests/views.py index 8442c0b..c6966d8 100644 --- a/contests/views.py +++ b/contests/views.py @@ -132,7 +132,10 @@ def compare_view(request, contest): @login_required() @contest_evaluator_required def edits_view(request, contest): - edits = Edit.objects.filter(contest=contest) + edits = Edit.objects.filter(contest=contest).select_related( + 'article', 'participant', 'last_evaluation__evaluator__profile', 'last_qualification' + ) + if request.POST.get('csv'): response = HttpResponse(content_type="text/csv; charset=windows-1252", headers={"Content-Disposition": 'attachment; filename="edits.csv"'}) From 92c95ae2242fa07315d979c0bf072a0f092c58de Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Wed, 4 Sep 2024 08:45:20 -0300 Subject: [PATCH 50/75] chore: Optimize database queries and improve code readability in home_view --- contests/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contests/views.py b/contests/views.py index c6966d8..b05d012 100644 --- a/contests/views.py +++ b/contests/views.py @@ -48,7 +48,7 @@ def render_with_bidi(request, template_name, context): return render(request, template_name, context) def home_view(request): - contests = Contest.objects.all().order_by('-start_time') + contests = Contest.objects.select_related('group').order_by('-start_time') contests_chooser = {} for contest in contests: From a91999f6c2b7ed2b39b7ae15ef851dcdd1390c5d Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Wed, 4 Sep 2024 09:37:29 -0300 Subject: [PATCH 51/75] fix: Handle error response in ContestHandler's get_category_list method --- contests/handlers/contest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contests/handlers/contest.py b/contests/handlers/contest.py index 9bc7898..7a15c15 100644 --- a/contests/handlers/contest.py +++ b/contests/handlers/contest.py @@ -108,8 +108,10 @@ def count_articles(self, contest): 'format': 'json' } response = requests.get(contest.api_endpoint, params=api_params).json() - list_.extend(response['query']['pages']) + if not 'query' in response: + return list_ + list_.extend(response['query']['pages']) while 'continue' in response: api_params['gplcontinue'] = response['continue']['gplcontinue'] response = requests.get(contest.api_endpoint, params=api_params).json() From 426ebbefb3f19189c5e163af34dc902707625d30 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Wed, 4 Sep 2024 09:42:12 -0300 Subject: [PATCH 52/75] fix: Handle error response when querying API in CompareHandler and load_edits.py --- contests/handlers/compare.py | 8 +++++++- contests/management/commands/load_edits.py | 3 +++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/contests/handlers/compare.py b/contests/handlers/compare.py index a61f4a1..c4a1ee2 100644 --- a/contests/handlers/compare.py +++ b/contests/handlers/compare.py @@ -80,6 +80,9 @@ def get_list_articles(self, contest): 'gpllimit': 'max', } response = requests.get(contest.api_endpoint, params=list_api_params).json() + if not 'query' in response: + return list_ + list_.extend(response['query']['pages']) while 'continue' in response: @@ -104,7 +107,7 @@ def get_category_articles(self, contest): "gcmlimit": "max", } response = requests.get(contest.api_endpoint, params=categorymembers_api_params).json() - if 'error' in response: + if not 'query' in response: return list_ list_.extend(response['query']['pages']) @@ -129,6 +132,9 @@ def get_deletion_pages(self, contest): 'cmprop': 'title', } response = requests.get(contest.api_endpoint, params=deletion_api_params).json() + if not 'query' in response: + return list_ + list_.extend(response['query']['categorymembers']) while 'continue' in response: diff --git a/contests/management/commands/load_edits.py b/contests/management/commands/load_edits.py index 3426f5f..f0ea749 100644 --- a/contests/management/commands/load_edits.py +++ b/contests/management/commands/load_edits.py @@ -94,6 +94,9 @@ def get_category_articles(self, contest): "cmlimit": "max" } response = requests.get(contest.api_endpoint, params=categorymembers_api_params).json() + if not 'query' in response: + return list_ + list_.extend(response['query']['categorymembers']) # Coleta segunda página da lista, caso exista From 5e54e0fe96537c762db61b911d1191c704bef0b7 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Wed, 4 Sep 2024 11:03:03 -0300 Subject: [PATCH 53/75] feat: Add history_qualifications and history_evaluations to modify' page --- contests/handlers/modify.py | 19 +++++++++++++++++-- contests/templates/modify.html | 34 ++++++++++++++++++++++++++++++++++ translations/en.json | 3 +++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/contests/handlers/modify.py b/contests/handlers/modify.py index cea6889..ab39437 100644 --- a/contests/handlers/modify.py +++ b/contests/handlers/modify.py @@ -1,7 +1,8 @@ import requests from django.core.exceptions import PermissionDenied from django.shortcuts import render -from contests.models import Evaluator, Edit, Evaluation +from contests.models import Evaluator, Edit, Evaluation, Qualification +from contests.models import Contest class ModifyHandler(): @@ -17,6 +18,8 @@ def execute(self, request): comment = None evaluation = None allowed = False + history_qualifications = None + history_evaluations = None if Evaluator.objects.get( contest=contest, @@ -26,7 +29,7 @@ def execute(self, request): if request.method == 'POST' and request.POST.get('diff'): diff = request.POST.get('diff') - edit = Edit.objects.get(diff=diff) + edit = Edit.objects.get(contest=contest, diff=diff) compare_params = { 'action': 'compare', @@ -40,6 +43,16 @@ def execute(self, request): content = compare.get('*', '') author = compare.get('touser', '') comment = compare.get('tocomment', '') + + history_qualifications = Qualification.objects.filter( + contest=contest, + diff=edit + ).select_related('evaluator__profile').order_by('-when') + + history_evaluations = Evaluation.objects.filter( + contest=contest, + diff=edit + ).select_related('evaluator__profile').order_by('-when') if request.POST.get('obs'): if allowed: @@ -67,5 +80,7 @@ def execute(self, request): 'content': content, 'author': author, 'comment': comment, + 'history_qualifications': history_qualifications, + 'history_evaluations': history_evaluations, } return return_dict \ No newline at end of file diff --git a/contests/templates/modify.html b/contests/templates/modify.html index 89c136d..6e911aa 100644 --- a/contests/templates/modify.html +++ b/contests/templates/modify.html @@ -187,6 +187,40 @@

{% trans 'modify-showdiff' %}

{{ content | safe }}

+ + + + + + + {% for qualification in history_qualifications %} + + + + + + {% endfor %} +
{% trans 'modify-qualified' %}{% trans 'edits-evaluator' %}{% trans 'modify-timestamp' %}
{% if qualification.status == 1 %}{% trans 'yes' %}{% else %}{% trans 'no' %}{% endif %}{% if qualification.evaluator is None %}{% trans 'modify-automatic' %}{% else %}{{ qualification.evaluator.profile.username }}{% endif %}{{ qualification.when }}
+ + + + + + + + + + {% for eval in history_evaluations %} + + + + + + + + + {% endfor %} +
{% trans 'isvalid' %}{% trans 'withimage' %}{% trans 'edits-bytes' %}{% trans 'triage-observation' %}{% trans 'edits-evaluator' %}{% trans 'modify-timestamp' %}
{% if eval.valid_edit %}{% trans 'yes' %}{% else %}{% trans 'no' %}{% endif %}{{ eval.pictures }}{{ eval.real_bytes }}{{ eval.obs|default_if_none:"" }}{{ eval.evaluator.profile.username }}{{ eval.when }}
{% elif diff and not content %} diff --git a/translations/en.json b/translations/en.json index 53ac252..4d8a9cd 100644 --- a/translations/en.json +++ b/translations/en.json @@ -23,6 +23,9 @@ "modify-diff": "Diff", "modify-load": "Load edit", "modify-reavaluate": "Reevaluate", + "modify-timestamp": "Timestamp", + "modify-qualified": "Qualified?", + "modify-automatic": "Automatic", "isvalid": "Valid edit?", "yes": "Yes", "no": "No", From 756f6aeefd625d77d3e11050ee9a2397742ff98d Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Wed, 4 Sep 2024 11:03:19 -0300 Subject: [PATCH 54/75] chore: Update compare.html to use form submission for reevaluation --- contests/templates/compare.html | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/contests/templates/compare.html b/contests/templates/compare.html index 7634c68..13f08a0 100644 --- a/contests/templates/compare.html +++ b/contests/templates/compare.html @@ -233,13 +233,14 @@

{% trans 'compare-rollback' %}

"{{ contest.endpoint }}?diff={{ edits.diff|urlencode }}", "_blank" )'>{% trans 'compare-seediff' %}{{ edits.diff }} - + + {% csrf_token %} + + + {% endfor %} From 848a20aae94f4574d54bb2833d3a6c124bb583b5 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Mon, 9 Sep 2024 13:42:32 -0300 Subject: [PATCH 55/75] feat: Add redirect_view to handle redirects from PHP links with language and contest parameters --- contests/urls.py | 7 ++++++- contests/views.py | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/contests/urls.py b/contests/urls.py index 6fde235..98e3380 100644 --- a/contests/urls.py +++ b/contests/urls.py @@ -1,5 +1,9 @@ from django.urls import path -from .views import contest_view, home_view, triage_view, backtrack_view, counter_view, compare_view, edits_view, evaluators_view, modify_view, manage_view +from .views import ( + contest_view, home_view, triage_view, backtrack_view, + counter_view, compare_view, edits_view, evaluators_view, + modify_view, manage_view, redirect_view +) urlpatterns = [ path('', home_view, name='home_view'), @@ -12,4 +16,5 @@ path('evaluators/', evaluators_view, name='evaluators_view'), path('modify/', modify_view, name='modify_view'), path('manage/', manage_view, name='manage_view'), + path('index.php', redirect_view, name='redirect_view'), ] diff --git a/contests/views.py b/contests/views.py index b05d012..b53541b 100644 --- a/contests/views.py +++ b/contests/views.py @@ -3,6 +3,7 @@ from django.core.exceptions import PermissionDenied from django.http import HttpResponse from django.template import loader +from django.conf import settings from django.utils import translation from django.utils.html import escape from django.db.models.functions import TruncDay @@ -47,6 +48,24 @@ def render_with_bidi(request, template_name, context): context.update(bidi_context) return render(request, template_name, context) +def redirect_view(request): + lang = request.GET.get('lang') + contest = request.GET.get('contest') + page = request.GET.get('page') + + if contest and not page: + response = redirect(f"/contests/?contest={contest}") + elif contest and page: + response = redirect(f"/{page}/?contest={contest}") + else: + response = redirect('/') + + if lang: + translation.activate(lang) + response.set_cookie(settings.LANGUAGE_COOKIE_NAME, lang) + + return response + def home_view(request): contests = Contest.objects.select_related('group').order_by('-start_time') From 968ab6cb53f494e949f6d1f426d6d9bbea92f63a Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Mon, 9 Sep 2024 13:43:51 -0300 Subject: [PATCH 56/75] chore: Optimize and reorganize imports in contests/views.py --- contests/views.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/contests/views.py b/contests/views.py index b53541b..fbd8ad6 100644 --- a/contests/views.py +++ b/contests/views.py @@ -1,16 +1,13 @@ -from django.shortcuts import render, redirect, get_object_or_404 -from django.contrib.auth.decorators import login_required -from django.core.exceptions import PermissionDenied from django.http import HttpResponse -from django.template import loader from django.conf import settings from django.utils import translation -from django.utils.html import escape -from django.db.models.functions import TruncDay -from datetime import timedelta +from django.template import loader +from django.core.exceptions import PermissionDenied +from django.contrib.auth.decorators import login_required +from django.shortcuts import render, redirect, get_object_or_404 from functools import wraps from collections import defaultdict -from .models import Contest, Edit, Participant, Qualification, Evaluator +from .models import Contest, Edit, Qualification, Evaluator from .handlers.triage import TriageHandler from .handlers.contest import ContestHandler from .handlers.counter import CounterHandler @@ -18,7 +15,7 @@ from .handlers.evaluators import EvaluatorsHandler from .handlers.modify import ModifyHandler from .handlers.manage import ManageHandler -from credentials.models import Profile + def get_contest_from_request(request): contest_name_id = request.GET.get('contest') @@ -77,7 +74,7 @@ def home_view(request): contests_chooser[group].append([contest.name_id, contest.name]) contests_groups = list(contests_chooser.keys()) - return render(request, 'home.html', { + return render_with_bidi(request, 'home.html', { 'contests_groups': contests_groups, 'contests_chooser': contests_chooser, }) @@ -102,7 +99,6 @@ def triage_view(request, contest): 'triage_points': int(contest.max_bytes_per_article / contest.bytes_per_points), 'evaluator_status': Evaluator.objects.get(contest=contest, profile=request.user.profile).user_status, }) - return render(request, "triage.html", triage_dict) return render_with_bidi(request, "triage.html", triage_dict) @login_required() From 3072863575d5846446a896fc7598ea532bafc422 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Mon, 9 Sep 2024 13:48:45 -0300 Subject: [PATCH 57/75] fix: Refactor Evaluation model's __str__ method to include evaluator's username or 'None' --- contests/models.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/contests/models.py b/contests/models.py index 94966c0..2fcde0f 100644 --- a/contests/models.py +++ b/contests/models.py @@ -136,4 +136,7 @@ class Evaluation(models.Model): obs = models.TextField(blank=True, null=True) def __str__(self): - return (f"{self.contest.name_id} - {self.diff.diff} - {self.evaluator.profile.username} - {self.status} - {self.when}") \ No newline at end of file + return (f"{self.contest.name_id} - {self.diff.diff} - {self.evaluator.profile.username} - {self.status} - {self.when}") + + name = self.evaluator.profile.username if self.evaluator else 'None' + return (f"{self.contest.name_id} - {self.diff.diff} - {name} - {self.status} - {self.when}") \ No newline at end of file From 7e01bd8ad7bbc55cbda7994a5a1b03c2d0aee9fc Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Sat, 21 Sep 2024 21:33:00 -0300 Subject: [PATCH 58/75] feat: Add git branch and commit information to home view --- contests/views.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/contests/views.py b/contests/views.py index fbd8ad6..3a4c1da 100644 --- a/contests/views.py +++ b/contests/views.py @@ -7,6 +7,7 @@ from django.shortcuts import render, redirect, get_object_or_404 from functools import wraps from collections import defaultdict +from pathlib import Path from .models import Contest, Edit, Qualification, Evaluator from .handlers.triage import TriageHandler from .handlers.contest import ContestHandler @@ -45,6 +46,24 @@ def render_with_bidi(request, template_name, context): context.update(bidi_context) return render(request, template_name, context) +def get_active_branch_name(): + head_dir = Path(".") / ".git" / "HEAD" + with head_dir.open("r") as f: content = f.read().splitlines() + + for line in content: + if line[0:4] == "ref:": + return line.partition("refs/heads/")[2] + +def get_active_commit_message(): + head_dir = Path(".") / ".git" / "COMMIT_EDITMSG" + with head_dir.open("r") as f: content = f.readlines() + return content[0] + +def get_active_commit_hash(): + head_dir = Path(".") / ".git" / "ORIG_HEAD" + with head_dir.open("r") as f: content = f.read().splitlines() + return content[0][:7] + def redirect_view(request): lang = request.GET.get('lang') contest = request.GET.get('contest') @@ -77,6 +96,8 @@ def home_view(request): return render_with_bidi(request, 'home.html', { 'contests_groups': contests_groups, 'contests_chooser': contests_chooser, + 'git_branch': 'Branch: ' + get_active_branch_name(), + 'git_commit': 'Commit: ' + get_active_commit_hash() + ' - ' + get_active_commit_message(), }) def contest_view(request): From 4e144e1ac36d13dc550a9bf56f522f77ccfe4c62 Mon Sep 17 00:00:00 2001 From: albertoleoncio Date: Sun, 22 Sep 2024 18:38:41 -0300 Subject: [PATCH 59/75] feat: Add context in translation strings --- contests/templates/backtrack.html | 14 +-- contests/templates/base.html | 28 ++--- contests/templates/compare.html | 54 +++++----- contests/templates/contest.html | 48 ++++----- contests/templates/counter.html | 34 +++--- contests/templates/edits.html | 32 +++--- contests/templates/evaluators.html | 42 ++++---- contests/templates/home.html | 28 ++--- contests/templates/manage.html | 104 +++++++++---------- contests/templates/modify.html | 88 ++++++++-------- contests/templates/triage.html | 160 ++++++++++++++--------------- 11 files changed, 316 insertions(+), 316 deletions(-) diff --git a/contests/templates/backtrack.html b/contests/templates/backtrack.html index cbeb7b2..e55c724 100644 --- a/contests/templates/backtrack.html +++ b/contests/templates/backtrack.html @@ -4,7 +4,7 @@ {% get_current_language_bidi as LANGUAGE_BIDI %} -{% block pagename %}{% trans 'backtrack' %}{% endblock %} +{% block pagename %}{% trans 'Backtrack' context 'backtrack' %}{% endblock %} {% block content %} @@ -12,7 +12,7 @@

- {% trans 'backtrack-about' %} + {% trans 'This page lists the edits participats did in the context of the wikicontest but were made before their registration in the Outreach Dashboard. If necessary, click on the button to accept the edit. After acceptance, the edit will be available in the evaluation queue.' context 'backtrack-about' %}

@@ -33,7 +33,7 @@
- {% blocktrans with backtrack_stats_1=diff.timestamp backtrack_stats_2=diff.bytes %}.{{ backtrack_stats_1 }}.{{ backtrack_stats_2 }}.{% endblocktrans %} + {% blocktrans with 1=diff.timestamp 2=diff.bytes context 'backtrack-stats' %}Edit on {{1}} with {{2}} bytes{% endblocktrans %}
@@ -41,23 +41,23 @@ + >{% trans 'Accept edit' context 'backtrack-accept' %}
{% endfor %}
-
{% blocktrans with backtrack_enrollment_1=data.enrollment_timestamp %}.{{ backtrack_enrollment_1 }}.{% endblocktrans %}
+
{% blocktrans with 1=data.enrollment_timestamp context 'backtrack-enrollment' %}Participant enrolled on {{1}}{% endblocktrans %}
{% endfor %} {% if diff %} {% endif %} diff --git a/contests/templates/base.html b/contests/templates/base.html index 8f34284..0d30f98 100644 --- a/contests/templates/base.html +++ b/contests/templates/base.html @@ -53,7 +53,7 @@   logo - {% block pagename %}{% trans 'triage' %}{% endblock %} + {% block pagename %}{% trans 'Triage' context 'triage' %}{% endblock %} {{ contest.name }}