浏览代码

Added Perl scripts for Asana export to WeKan ®.

Thanks to GeekRuthie !
Lauri Ojansivu 3 年之前
父节点
当前提交
376bcbb373
共有 3 个文件被更改,包括 321 次插入0 次删除
  1. 21 0
      asana/LICENSE
  2. 178 0
      asana/export_boards.pl
  3. 122 0
      asana/load_tasks.pl

+ 21 - 0
asana/LICENSE

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2022 WeKan ® Team and GeekRuthie
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 178 - 0
asana/export_boards.pl

@@ -0,0 +1,178 @@
+#!/usr/local/perl-cbt/bin/perl
+use Modern::Perl;
+use Carp;
+use Data::Dumper;
+use HTTP::Request;
+use JSON;
+use LWP::UserAgent;
+use MIME::Base64 qw/encode_base64/;
+
+my $BASE_URL      = 'https://app.asana.com/api/1.0';
+my $ASANA_API_KEY = 'ASANA_PERSONAL_TOKEN';
+my $ua            = LWP::UserAgent->new();
+
+open my $input_wekan, '<', 'template.json';
+my $wekan_json = readline($input_wekan);
+close $input_wekan;
+my $wekan_board = decode_json($wekan_json);
+my %users;
+my %users_by_gid;
+# get user IDs from template board
+foreach my $user ( @{ $wekan_board->{users} } ) {
+   $users{ $user->{profile}->{fullname} } = $user->{_id};
+}
+# get list IDs from template (we ended up not using these)
+my %lists;
+foreach my $list ( @{ $wekan_board->{lists} } ) {
+   $lists{ $list->{title} } = $list->{_id};
+}
+
+my @headers;
+push @headers, ( 'Accept',        'application/json' );
+push @headers, ( 'Authorization', "Bearer $ASANA_API_KEY" );
+my $projects_req = HTTP::Request->new( "GET", "$BASE_URL/projects", \@headers, );
+my $projects_res = $ua->request($projects_req);
+my $projects     = decode_json( $projects_res->content )->{data};
+foreach my $project (@$projects) {
+   say "Project: ".$project->{name};
+   my $tasks_url =
+         '/tasks?project='
+       . $project->{gid}
+       . '&opt_fields=completed,name,notes,assignee,created_by,memberships.project.name, memberships.section.name,due_on,created_at,custom_fields';
+   my $tasks_req = HTTP::Request->new( 'GET', "$BASE_URL$tasks_url", \@headers );
+   my $tasks_res = $ua->request($tasks_req);
+   my @output_tasks;
+   my $tasks = decode_json( $tasks_res->content )->{data};
+   foreach my $task (@$tasks) {
+      next if $task->{completed};
+      say '   - '.$task->{name};
+      my $git_branch;
+      my $prio;
+      foreach my $custom ( @{ $task->{custom_fields} } ) {
+         if ( $custom->{name} eq 'git branch' ) {
+            $git_branch = $custom->{text_value};
+            next;
+         }
+         # We ended up not importing these.
+         if ( $custom->{name} eq 'Priority' && defined $custom->{display_value} ) {
+            $prio =
+                  $custom->{display_value} eq 'High' ? 'fwccC9'   
+                : $custom->{display_value} eq 'Med'  ? 'yPnaFa'
+                : $custom->{display_value} eq 'Low'  ? 'W4vMvm'
+                :                                      'ML5drH';
+            next;
+         }
+      }
+      if ( !defined $users_by_gid{ $task->{created_by}->{gid} } ) {
+         my $user_req =
+             HTTP::Request->new( 'GET', "$BASE_URL/users/" . $task->{created_by}->{gid},
+            \@headers );
+         my $user_res = $ua->request($user_req);
+         my $user     = decode_json( $user_res->content )->{data};
+         if ( defined $users{ $user->{name} } ) {
+            $users_by_gid{ $task->{created_by}->{gid} } = $users{ $user->{name} };
+         }
+      }
+      my $creator = $users_by_gid{ $task->{created_by}->{gid} } // undef;
+      if ( defined $task->{assignee} && !defined $users_by_gid{ $task->{assignee}->{gid} } ) {
+         my $user_req =
+             HTTP::Request->new( 'GET', "$BASE_URL/users/" . $task->{assignee}->{gid},
+            \@headers );
+         my $user_res = $ua->request($user_req);
+         my $user     = decode_json( $user_res->content )->{data};
+         if ( defined $users{ $user->{name} } ) {
+            $users_by_gid{ $task->{assignee}->{gid} } = $users{ $user->{name} };
+         }
+      }
+      my $assignee = defined $task->{assignee} ? $users_by_gid{ $task->{assignee}->{gid} } : undef;
+      my $list;
+      foreach my $membership ( @{ $task->{memberships} } ) {
+         next if $membership->{project}->{name} ne $project->{name};
+         $list = $membership->{section}->{name};
+      }
+
+      # I was trying to create JSON that I could use on the import screen in Wekan,
+      # but for bigger boards, it was just *too* hefty, so I took that JSON and used 
+      # APIs to import.
+      my %output_task = (
+         swimlaneId   => 'As4SNerx4Y4mMnJ8n',   # 'Bugs'  
+         sort         => 0,
+         type         => 'cardType-card',
+         archived     => JSON::false,
+         title        => $task->{name},
+         description  => $task->{notes},
+         createdAt    => $task->{created_at},
+         dueAt        => defined $task->{due_on} ? $task->{due_on} . 'T22:00:00.000Z' : undef,
+         customFields => [
+            {
+               _id   => 'rL8BpFHp5xxSFbDdr',
+               value => $git_branch,
+            },
+         ],
+         labelIds  => [$prio],
+         listId    => $list,
+         userId    => $creator,
+         assignees => [$assignee],
+      );
+      my @final_comments;
+      my $comments_req =
+          HTTP::Request->new( 'GET', "$BASE_URL/tasks/" . $task->{gid} . '/stories',
+         \@headers );
+      my $comments_res = $ua->request($comments_req);
+      my $comments     = decode_json( $comments_res->content )->{data};
+      foreach my $comment (@$comments) {
+         next if $comment->{type} ne 'comment';
+         if ( !defined $users_by_gid{ $comment->{created_by}->{gid} } ) {
+            my $user_req =
+                HTTP::Request->new( 'GET', "$BASE_URL/users/" . $comment->{created_by}->{gid},
+               \@headers );
+            my $user_res = $ua->request($user_req);
+            my $user     = decode_json( $user_res->content )->{data};
+            if ( defined $users{ $user->{name} } ) {
+               $users_by_gid{ $comment->{created_bye}->{gid} } = $users{ $user->{name} };
+            }
+         }
+         my $commentor    = $users_by_gid{ $comment->{created_by}->{gid} };
+         my %this_comment = (
+            text      => $comment->{text},
+            createdAt => $comment->{created_at},
+            userId    => $commentor,
+         );
+         push @final_comments, \%this_comment;
+      }
+      $output_task{comments} = \@final_comments;
+
+
+      my @final_attachments;
+      my $attachments_req =
+          HTTP::Request->new( 'GET', "$BASE_URL/tasks/" . $task->{gid} . '/attachments',
+         \@headers );
+      my $attachments_res = $ua->request($attachments_req);
+      my $attachments     = decode_json( $attachments_res->content )->{data};
+      foreach my $attachment (@$attachments) {
+         my $att_req =
+             HTTP::Request->new( 'GET', "$BASE_URL/attachments/" . $attachment->{gid},
+            \@headers );
+         my $att_res = $ua->request($att_req);
+         my $att     = decode_json( $att_res->content )->{data};
+         my $file_req=HTTP::Request->new('GET',$att->{download_url});
+         my $file_res=$ua->request($file_req);
+         my $file=encode_base64($file_res->content);
+
+           my %this_attachment = (
+              file => $file,
+              name => $att->{name},
+              createdAt => $att->{created_at},
+           );
+           push @final_attachments, \%this_attachment;
+
+      }
+      $output_task{attachments} = \@final_attachments;
+      push @output_tasks, \%output_task;
+   }
+   my $file_name = $project->{name};
+   $file_name =~ s/\//_/g;
+   open my $output_file, '>',$file_name.'_exported.json';
+   print $output_file encode_json(\@output_tasks);
+   close $output_file;
+}

+ 122 - 0
asana/load_tasks.pl

@@ -0,0 +1,122 @@
+#!/usr/local/perl-cbt/bin/perl
+use Modern::Perl;
+use Carp;
+use Data::Dumper;
+use HTTP::Request;
+use JSON;
+use LWP::UserAgent;
+use MIME::Base64 qw/decode_base64/;
+use Try::Tiny;
+
+my $BASE_URL = 'https://taskboard.example.com/api';
+my $TOKEN    = 'MY_TOKEN';
+my $me       = 'MY_USER_ID';
+my $ua       = LWP::UserAgent->new();
+
+my @headers;
+push @headers, ( 'Accept',        'application/json' );
+push @headers, ( 'Authorization', "Bearer $TOKEN" );
+
+my @form_headers;
+push @form_headers, ( 'Content-Type',  'application/json' );
+push @form_headers, ( 'Accept',        'application/json' );
+push @form_headers, ( 'Authorization', "Bearer $TOKEN" );
+
+# Prior to running this, I built all the boards, with labels and lists and swimlanes.
+# Grabbed the IDs for the boards for each project, and put them here to match up with
+# filenames from the Asana export script.
+
+my %board_to_use = (
+   Project_1        => 'F5ZiCXnf4d7qNBRjp',
+   Project_2        => 'Shw3tyfC2JWCutBLj',
+);
+
+opendir my $files_dir, '.'
+    or croak "Cannot open input directory: $!";
+my @files = readdir $files_dir;
+closedir $files_dir;
+
+foreach my $tasks_file (@files) {
+   next if $tasks_file !~ /_exported.json/;
+   my $project = $tasks_file;
+   $project =~ s/_exported.json//;
+   say "Project - $project";
+   my $board;
+   if ( $board_to_use{$project} ) {
+      $board = $board_to_use{$project};
+   }
+   say '   No board!' if !$board;
+   next               if !$board;
+   my $labels_req = HTTP::Request->new( 'GET', "$BASE_URL/boards/$board", \@headers );
+   my $labels_res = $ua->request($labels_req);
+   my $board_data = decode_json( $labels_res->content);
+   my $labels = $board_data->{labels};
+   my $label_to_use;
+   # We're merging several Asana boards onto one Wekan board, with labels per project.
+   foreach my $label (@$labels) {
+       $label_to_use = $label->{_id} if $label->{name} eq $project;
+   }
+
+   my $lanes_req = HTTP::Request->new( 'GET', "$BASE_URL/boards/$board/swimlanes", \@headers );
+   my $lanes_res = $ua->request($lanes_req);
+   my $lanes     = decode_json( $lanes_res->content );
+   my $lane_to_use;
+   foreach my $lane (@$lanes) {
+      # Our Asana didn't use swimlanes; all of our Wekan boards have a "Bugs" lane, so use that.
+      $lane_to_use = $lane->{_id} if $lane->{title} eq 'Bugs';
+   }
+
+   my $lists_req = HTTP::Request->new( 'GET', "$BASE_URL/boards/$board/lists", \@headers );
+   my $lists_res = $ua->request($lists_req);
+   my $lists     = decode_json( $lists_res->content );
+   my %list_to_use;
+   foreach my $list (@$lists) {
+      $list_to_use{ $list->{title} } = $list->{_id};
+   }
+
+   open my $task_export_file, '<', $tasks_file;
+   my $tasks_json = readline($task_export_file);
+   close $task_export_file;
+   my $tasks = decode_json($tasks_json);
+   foreach my $task (@$tasks) {
+      say '   - ' . $task->{title};
+      my %body_info = (
+         swimlaneId  => $lane_to_use,
+         authorId    => $task->{userId},
+         assignees   => $task->{assignees},
+         title       => $task->{title},
+         description => $task->{description},
+      );
+      my $body     = encode_json( \%body_info );
+      my $list     = $list_to_use{ $task->{listId} } // $list_to_use{'Backlog'};
+      my $task_req = HTTP::Request->new( 'POST', "$BASE_URL/boards/$board/lists/$list/cards",
+         \@form_headers, $body );
+      my $task_res = $ua->request($task_req);
+      my $res;
+      try {
+       $res      = decode_json( $task_res->content );
+      } catch {
+         # Did these manually afterward.
+          say "--->UNABLE TO LOAD TASK";
+          next;
+      };
+      my $card     = $res->{_id};
+
+      if ($label_to_use) {
+          my $card_edit_body = encode_json( { labelIds => [ $label_to_use ]});
+          my $card_edit_req = HTTP::Request->new( 'PUT', "$BASE_URL/boards/$board/lists/$list/cards/$card",
+         \@form_headers, $card_edit_body );
+         my $card_edit_res = $ua->request($card_edit_req);
+      }
+
+      foreach my $comment ( @{ $task->{comments} } ) {
+         my $comment_body =
+             encode_json( { authorId => $comment->{userId}, comment => $comment->{text} } );
+         my $comment_req =
+             HTTP::Request->new( 'POST', "$BASE_URL/boards/$board/cards/$card/comments",
+            \@form_headers, $comment_body );
+         my $comment_res = $ua->request($comment_req);
+      }
+   }
+}
+