class Members: def __init__(self, api_key, token): self.members = {} self.client = TrelloClient(api_key=api_key, token=token) def get_avatar_url(self, member_id): if member_id is None: return '' if member_id not in self.members: self.members[member_id] = self.__fetch_member(member_id) return self.members[member_id].get('avatar_url') def __fetch_member(self, member_id): json_obj = self.client.fetch_json( '/members/' + member_id, query_params={'badges': False}) id = json_obj.get('id', '') username = json_obj.get('username', '') full_name = json_obj.get('fullName', '') avatar_url = json_obj.get('avatarUrl', '') if avatar_url is None: avatar_url = '' return { 'id': id, 'username': username, 'full_name': full_name, 'avatar_url': self.__avatar_url(username, avatar_url + '/30.png') } def __avatar_url(self, username, url): return '![{username}]({url})'.format(username=username, url=url)
def sync_trello_cards(self): if not self.trello_id: logger.exception("Trello board id for %s not set", self.name) raise NotSetTrelloBoardID client = TrelloClient(**TRELLO_KEYS) tasks = client.fetch_json("boards/{}/cards".format(self.trello_id)) for task_dict in tasks: last_activity = task_dict.get('dateLastActivity') task, created = Task.objects.get_or_create( trello_id=task_dict['id'], project=self ) if created or last_activity != task.trello_last_activity: task.name = task_dict['name'] task.description = task_dict['desc'] task.trello_url = task_dict['shortUrl'] task.trello_last_activity = last_activity task.save() logger.info( "Trello card with id %s and name %s has ben %s", task_dict['id'], task_dict['name'], 'created' if created else 'updated' )
class TrelloColdBrew(): def __init__(self, api_key, token): self.client = TrelloClient(api_key=api_key, token=token) def add_workers_to_organization(self, organization, member_ids=None): member_ids = MEMBER_IDS if not member_ids else member_ids for member_id in member_ids: yield self.try_add_member(member_id, organization) def remove_workers_from_organization(self, organization, member_ids=None): member_ids = member_ids if member_ids else MEMBER_IDS for member_id in member_ids: yield self.try_remove_member(member_id, organization) def try_add_member(self, member_id, organization): try: return self._add_member(member_id, organization.id) except exceptions.ResourceUnavailable as e: if str(e).startswith('member not found at'): print('Trello member {} not found'.format(member_id)) else: raise e def try_remove_member(self, member_id, organization): try: return self._remove_member(member_id, organization.id) except exceptions.Unauthorized: print('cannot remove member {}'.format(member_id)) def _remove_member(self, member_id, organization_id): return self.client.fetch_json( '/organizations/{0}/members/{1}'.format(organization_id, member_id), http_method='DELETE', post_args={'idMember': member_id}, ) def _add_member(self, member_id, organization_id): return self.client.fetch_json( '/organizations/{0}/members/{1}'.format(organization_id, member_id), http_method='PUT', post_args={ 'idMember': member_id, "type": "normal" }, )
class TrelloHelper(): def __init__(self, api_key, token): self.client = TrelloClient(api_key=api_key, token=token) def create_organization(self, organization_name): post_args = {'displayName': organization_name} obj = self.client.fetch_json( '/organizations', http_method='POST', post_args=post_args ) return self.client.get_organization(obj['id']) def delete_organization(self, organization_id): self.client.fetch_json( '/organizations/{}'.format(organization_id), http_method='DELETE', ) def list_organizations(self): return self.client.list_organizations()
class EClaire(object): def __init__(self, credentials, boards=None): self.trello_client = TrelloClient( api_key=credentials['public_key'], token=credentials['member_token'], ) self.boards = boards def process_boards(self, dry_run=False, notify_fn=None, notify_config=None): """ Process each board in self.boards """ for name, board_config in self.boards.iteritems(): log.info('Polling %s', name) processed = self.process_board(board_config, dry_run) if board_config.get('notify', True) and notify_fn is not None: for card in processed: notify_fn(card, **notify_config) def process_board(self, board_config, dry_run=False): """ Process each card in a given board """ processed = [] for card in self.fetch_cards(board_id=board_config['id']): log.info('Printing card "%s"', card.name) pdf = generate_pdf(card) if not dry_run: print_card(pdf, printer_name=board_config['printer']) self.update_card(card, board_config) processed.append(card) return processed def fetch_cards(self, board_id): """ Fetch all candidate cards on a board for processing """ data = [] board = self.trello_client.get_board(board_id) for card in board.all_cards(): if FILTER_LABEL in [l.name for l in card.labels]: card.fetch_actions() data.append(card) return data def discover_labels(self): """ Store object references for special labels """ for name, config in self.boards.iteritems(): board = self.trello_client.get_board(config['id']) labels = {} for label in board.get_labels(limit=1000): if label.name in SPECIAL_LABELS: labels[label.name] = label missing = set(SPECIAL_LABELS) - set(labels.keys()) if missing: log.fatal('Board "%s" is missing the labels %s', board.name, ' and '.join(missing)) log.fatal('Exiting') sys.exit(1) config['labels'] = labels def remove_label(self, card, label): """ Remove a lable from a card. At the time of writing there is no way to remove a label with py-trello """ self.trello_client.fetch_json('/cards/' + card.id + '/idLabels/' + label.id, http_method="DELETE") def update_card(self, card, board_config): """ Replace PRINTME label with PRINTED """ printme_label = board_config['labels']['PRINTME'] printed_label = board_config['labels']['PRINTED'] self.remove_label(card, printme_label) if printed_label not in card.labels: card.add_label(printed_label) def list_boards(self): """ Fetch all board IDs from trello & print them out """ for board in self.trello_client.list_boards(): print 'Board:', board.name print ' ID:', board.id print
class TrelloManager: """Manage Trello connection and data.""" def __init__(self, api_key, api_secret, token): """Create a new TrelloManager using provided keys.""" self._cl = TrelloClient( api_key=api_key, api_secret=api_secret, token=token, ) # Start whitelisting no organization self._wl_org = set() # Start whitelisting no board self._wl_brd = set() def whitelist_org(self, oid): """Add an organization to whitelist, by ID.""" self._wl_org.add(oid) def blacklist_org(self, oid): """Remove an organization from whitelist, by ID.""" self._wl_org.discard(oid) def whitelist_brd(self, bid): """Whitelist a board by id.""" self._wl_brd.add(bid) def blacklist_brd(self, bid): """Blacklist a board by id.""" self._wl_brd.discard(bid) def org_names(self): """Fetch and return organization names.""" return {o.name for o in self.fetch_orgs()} def fetch_orgs(self): """Generate organizations and their blacklistedness.""" for o in self._cl.fetch_json('/members/me/organizations/'): yield Organization(o['id'], o['name'], o['id'] not in self._wl_org, o['url']) def fetch_boards(self, org=None): """Generate boards (in given org) and their blacklistedness.""" if org is None: for b in self._cl.fetch_json('/members/me/boards/'): # If board has not an organization, it is blacklisted iff # it's not in the whitelist bbl = b['id'] not in self._wl_brd # If board has an organization, it is blacklisted iff # both board or organization are not in whitelist # if b['idOrganization'] is not None: # bbl = bbl or b['idOrganization'] not in self._wl_org yield Board(b['id'], b['name'], bbl, b['url']) else: orgs = list(self.fetch_orgs()) id2na = {o.id: o.name for o in orgs} na2id = {o.name: o.id for o in orgs} # Convert names to id if org in na2id: org = na2id[org] # Cannot find ID if org not in id2na: return for b in self._cl.fetch_json(f'/organizations/{org}/boards/'): bl = b['id'] not in self._wl_brd yield Board(b['id'], b['name'], bl, b['url']) def fetch_lists(self, board): """Generate lists in given board.""" raise NotImplemented # for l in board.list_lists(): # yield l def fetch_cards(self, lid=None, bid=None): """Generate cards from list, board or everything.""" if bid is not None: seq = self._cl.fetch_json(f'/boards/{bid}/cards') elif lid is not None: seq = self._cl.fetch_json(f'/lists/{lid}/cards') else: seq = self._cl.fetch_json(f'/members/me/cards') for c in seq: if c['due'] is not None: c['due'] = parse_date(c['due']) yield Card(c['id'], c['name'], c['url'], c['due'], c['dueComplete']) def deprecated_fetch_data(self): """Fetch all the data from the server, updating cache.""" logging.info('TrelloManager: fetching_data list_organizations') self._orgs = [] self._boards = [] self._lists = [] self._cards = [] for o in self.fetch_orgs(): # Skip blacklisted boards if o.blacklisted: continue self._orgs.append(o) for b, bbl in self.fetch_boards(o): # Skip blacklisted boards if bbl: continue self._boards.append(b) for l in self.fetch_lists(b): self._lists.append(l) for c in self.fetch_cards(l): self._cards.append(c) yield (o, b, l, c)
class TrelloPlugin(DIBasePlugin, DIPluginDashboardMixin): platform = xapi_settings.PLATFORM_TRELLO platform_url = "https://trello.com" #created for "create" actions #added for "add" actions #updated for "update" actions #commented for card "comment" actions # xapi_verbs = ['created', 'added', 'updated', 'commented', 'closed', 'opened'] xapi_verbs = [ xapi_settings.VERB_CREATED, xapi_settings.VERB_ADDED, xapi_settings.VERB_UPDATED, xapi_settings.VERB_COMMENTED, xapi_settings.VERB_CLOSED, xapi_settings.VERB_OPENED ] #Note for "commented" actions #Task for "created", "added", "updated", and "commented" actions #Collection for any "created", "added", "updated" List actions (tentative use) # xapi_objects = ['Note', 'Task', 'Collection', 'Person', 'File', 'checklist-item', 'checklist'] xapi_objects = [ xapi_settings.OBJECT_NOTE, xapi_settings.OBJECT_TASK, xapi_settings.OBJECT_COLLECTION, xapi_settings.OBJECT_PERSON, xapi_settings.OBJECT_FILE, xapi_settings.OBJECT_CHECKLIST_ITEM, xapi_settings.OBJECT_CHECKLIST ] user_api_association_name = 'Trello UID' # eg the username for a signed up user that will appear in data extracted via a social API unit_api_association_name = 'Board ID' # eg hashtags or a group name config_json_keys = ['consumer_key', 'consumer_secret'] #from DIPluginDashboardMixin xapi_objects_to_includein_platformactivitywidget = [ xapi_settings.OBJECT_NOTE ] xapi_verbs_to_includein_verbactivitywidget = [ xapi_settings.VERB_CREATED, xapi_settings.VERB_SHARED, xapi_settings.VERB_LIKED, xapi_settings.VERB_COMMENTED ] #for OAuth1 authentication token_request_url = '' # Trello action type # Note: MoveCard, CloseCard, OpenCard are created for the toolkit to identify what users really did # (The original action type of moving/closing/opening card are all same: updateCard) ACTION_TYPE_COMMENT_CARD = 'commentCard' ACTION_TYPE_CREATE_CARD = 'createCard' ACTION_TYPE_UPDATE_CHECKITEM_STATE_ON_CARD = 'updateCheckItemStateOnCard' ACTION_TYPE_UPDATE_CARD = 'updateCard' ACTION_TYPE_ADD_ATTACHMENT_TO_CARD = 'addAttachmentToCard' ACTION_TYPE_ADD_CHECKLIST_TO_CARD = 'addChecklistToCard' ACTION_TYPE_ADD_MEMBER_TO_CARD = 'addMemberToCard' ACTION_TYPE_MOVE_CARD = 'moveCard' ACTION_TYPE_CLOSE_CARD = 'closeCard' ACTION_TYPE_OPEN_CARD = 'openCard' VERB_OBJECT_MAPPER = { xapi_settings.VERB_CREATED: [ACTION_TYPE_CREATE_CARD], xapi_settings.VERB_ADDED: [ ACTION_TYPE_ADD_ATTACHMENT_TO_CARD, ACTION_TYPE_ADD_CHECKLIST_TO_CARD, ACTION_TYPE_ADD_MEMBER_TO_CARD ], xapi_settings.VERB_UPDATED: [ACTION_TYPE_MOVE_CARD, ACTION_TYPE_UPDATE_CHECKITEM_STATE_ON_CARD], xapi_settings.VERB_COMMENTED: [ACTION_TYPE_COMMENT_CARD], xapi_settings.VERB_CLOSED: [ACTION_TYPE_CLOSE_CARD], xapi_settings.VERB_OPENED: [ACTION_TYPE_OPEN_CARD] } SEPARATOR_COLON = ": " SEPARATOR_HTML_TAG_BR = "<br>" MESSAGE_CARD_POSITION_CHANGED = 'Card position was changed.' def __init__(self): pass #retreival param is the user_id def perform_import(self, retreival_param, unit, token=None): #from clatoolkit.models import ApiCredentials #Set up trello auth and API self.TrelloCient = TrelloClient( api_key=os.environ.get("TRELLO_API_KEY"), token=token) #Get user-registered board in trello trello_board = self.TrelloCient.get_board(retreival_param) #Get boards activity/action feed trello_board.fetch_actions( 'all' ) #fetch_actions() collects actions feed and stores to trello_board.actions self.import_TrelloActivity(trello_board.actions, unit) def import_TrelloActivity(self, feed, unit): #User needs to sign up username and board (board can be left out but is needed) print 'Beginning Trello import!' for action in list(feed): #We need to connect this with our user profile somewhere when they initially auth u_id = action['idMemberCreator'] author = action['memberCreator']['username'] type = action['type'] #commentCard, updateCard, createList,etc data = action['data'] date = action['date'] board_name = data['board']['name'] # print 'got action type: %s' % (type) #Get all 'commented' verb actions if (type == self.ACTION_TYPE_COMMENT_CARD): target_obj_id = self.create_card_url(data) comment_from_uid = u_id comment_from_name = author comment_message = data['text'] comment_id = target_obj_id + '/' + action['id'] card_name = data['card']['name'] if username_exists(comment_from_uid, unit, self.platform): # Create "other" contextActivity object to store original activity in xAPI card_details = self.TrelloCient.fetch_json( '/cards/' + data['card']['id']) context = get_other_contextActivity( card_details['shortUrl'], 'Verb', type, xapi_settings.get_verb_iri( xapi_settings.VERB_COMMENTED)) context2 = get_other_contextActivity( card_details['shortUrl'], 'Object', data['card']['name'], xapi_settings.get_verb_iri( xapi_settings.VERB_COMMENTED)) other_context_list = [context, context2] user = get_user_from_screen_name(comment_from_uid, self.platform) insert_comment(user, target_obj_id, comment_id, comment_message, date, unit, self.platform, self.platform_url, parent_user_external=card_name, other_contexts=other_context_list) if (type == self.ACTION_TYPE_CREATE_CARD): object_id = self.create_card_url(data) + '/' + action['id'] card_name = data['card']['name'] parent_id = self.create_list_url(data) if username_exists(u_id, unit, self.platform): # Create "other" contextActivity object to store original activity in xAPI card_details = self.TrelloCient.fetch_json( '/cards/' + data['card']['id']) context = get_other_contextActivity( card_details['shortUrl'], 'Verb', type, xapi_settings.get_verb_iri(xapi_settings.VERB_CREATED)) context2 = get_other_contextActivity( card_details['shortUrl'], 'Object', data['list']['name'], xapi_settings.get_verb_iri(xapi_settings.VERB_CREATED)) other_context_list = [context, context2] usr_dict = ClaUserUtil.get_user_details_by_smid( u_id, self.platform) user = get_user_from_screen_name(u_id, self.platform) insert_task(user, object_id, card_name, date, unit, self.platform, self.platform_url, parent_id=parent_id, other_contexts=other_context_list) #Get all 'add' verbs (you tecnically aren't *creating* an attachment on a card so....) if (type in [ self.ACTION_TYPE_ADD_ATTACHMENT_TO_CARD, self.ACTION_TYPE_ADD_CHECKLIST_TO_CARD, self.ACTION_TYPE_ADD_MEMBER_TO_CARD ]): user = get_user_from_screen_name(u_id, self.platform) if user is None: continue card_url = self.create_card_url(data) # Create "other" contextActivity object to store original activity in xAPI other_context_list = [] card_details = self.TrelloCient.fetch_json('/cards/' + data['card']['id']) context = get_other_contextActivity( card_details['shortUrl'], 'Verb', type, xapi_settings.get_verb_iri(xapi_settings.VERB_ADDED)) other_context_list.append(context) context2 = get_other_contextActivity( card_details['shortUrl'], 'Object', data['card']['name'], xapi_settings.get_verb_iri(xapi_settings.VERB_ADDED)) other_context_list = [context, context2] if type == self.ACTION_TYPE_ADD_ATTACHMENT_TO_CARD: attachment = data['attachment'] attachment_id = card_url + '/' + data['attachment']['id'] attachment_data = attachment['name'] object_type = xapi_settings.OBJECT_FILE parent_user_external = '%sc/%s' % (self.platform_url, card_url) other_context_list.append( get_other_contextActivity( card_details['shortUrl'], 'Object', attachment['url'], xapi_settings.get_verb_iri( xapi_settings.VERB_ADDED))) insert_added_object( user, card_url, attachment_id, attachment_data, date, unit, self.platform, self.platform_url, object_type, parent_user_external=parent_user_external, other_contexts=other_context_list) if type == self.ACTION_TYPE_ADD_MEMBER_TO_CARD: #or 'addMemberToBoard': object_id = card_url + '/' + action['member']['id'] object_data = action['member']['username'] object_type = xapi_settings.OBJECT_PERSON parent_user_external = '%sc/%s' % (self.platform_url, card_url) insert_added_object( user, card_url, object_id, object_data, date, unit, self.platform, self.platform_url, object_type, parent_user_external=parent_user_external, other_contexts=other_context_list) if type == self.ACTION_TYPE_ADD_CHECKLIST_TO_CARD: object_id = card_url + '/' + data['checklist']['id'] object_data = None checklist_items = None object_type = xapi_settings.OBJECT_COLLECTION parent_user_external = '%sc/%s' % (self.platform_url, card_url) #get checklist contents try: checklist = self.TrelloCient.fetch_json( '/checklists/' + data['checklist']['id'], ) checklist_items = checklist['checkItems'] except Exception: print 'Could not retrieve checklist..' continue object_data = data['checklist']['name'] for item in checklist_items: # items are stored individually in other contextActivities other_context_list.append( get_other_contextActivity( card_details['shortUrl'], 'Object', item['name'], xapi_settings.get_verb_iri( xapi_settings.VERB_ADDED))) insert_added_object( user, card_url, object_id, object_data, date, unit, self.platform, self.platform_url, object_type, parent_user_external=parent_user_external, other_contexts=other_context_list) if (type in [ self.ACTION_TYPE_UPDATE_CHECKITEM_STATE_ON_CARD, self.ACTION_TYPE_UPDATE_CARD ]): user = get_user_from_screen_name(u_id, self.platform) if user is None: continue card_details = self.TrelloCient.fetch_json('/cards/' + data['card']['id']) #many checklist items will be bugged - we require webhooks! if type == self.ACTION_TYPE_UPDATE_CHECKITEM_STATE_ON_CARD: # Import check item ID & its state # Use action ID as an object ID to avoid ID conflict. object_id = self.create_card_url(data) + '/' + action['id'] obj_val = data['checkItem']['state'] # Create "other" contextActivity object to store original activity in xAPI other_context_list = [] other_context_list.append( get_other_contextActivity( self.create_checklist_url(data), 'Verb', type, xapi_settings.get_verb_iri( xapi_settings.VERB_UPDATED))) other_context_list.append( get_other_contextActivity( card_details['shortUrl'], 'Object', data['checkItem']['name'], xapi_settings.get_verb_iri( xapi_settings.OBJECT_CHECKLIST_ITEM))) other_context_list.append( get_other_contextActivity( card_details['shortUrl'], 'Object', data['card']['name'], xapi_settings.get_verb_iri( xapi_settings.VERB_UPDATED))) insert_updated_object( user, object_id, obj_val, date, unit, self.platform, self.platform_url, xapi_settings.OBJECT_CHECKLIST_ITEM, parent_id=self.create_checklist_url(data), obj_parent_type=xapi_settings.OBJECT_CHECKLIST, other_contexts=other_context_list) #up to us to figure out what's being updated if type == self.ACTION_TYPE_UPDATE_CARD: #Get and store the values that were changed, usually it's only one #TODO: Handle support for multiple changes, if that's possible try: change = [ changed_value for changed_value in data['old'] ] except Exception: print 'Error occurred getting changes...' # Use action ID as an object ID to avoid ID conflict. object_id = self.create_card_url(data) + '/' + action['id'] if change[0] == 'pos': # When user moves card in the same list (change order) object_text = data['card']['name'] # object_text = 'Change order of card: %s to %s' % (data['old']['pos'], data['card']['pos']) # object_text = card_name + object_text other_context_list = [] other_context_list.append( get_other_contextActivity( card_details['shortUrl'], 'Verb', self.ACTION_TYPE_MOVE_CARD, xapi_settings.get_verb_iri( xapi_settings.VERB_UPDATED))) other_context_list.append( get_other_contextActivity( card_details['shortUrl'], 'Object', self.MESSAGE_CARD_POSITION_CHANGED, xapi_settings.get_verb_iri( xapi_settings.VERB_UPDATED))) other_context_list.append( get_other_contextActivity( card_details['shortUrl'], 'Object', str(data['old']['pos']), xapi_settings.get_verb_iri( xapi_settings.VERB_UPDATED))) other_context_list.append( get_other_contextActivity( card_details['shortUrl'], 'Object', str(data['card']['pos']), xapi_settings.get_verb_iri( xapi_settings.VERB_UPDATED))) insert_updated_object( user, object_id, object_text, date, unit, self.platform, self.platform_url, xapi_settings.OBJECT_TASK, parent_id=self.create_list_url(data), obj_parent_type=xapi_settings.OBJECT_COLLECTION, other_contexts=other_context_list) else: # When user moves card from a list to another if data.has_key('listBefore'): object_text = data['card']['name'] # object_text = 'from %s to %s' % (data['listBefore']['name'], data['listAfter']['name']) # object_text = card_name + object_text other_context_list = [] other_context_list.append( get_other_contextActivity( card_details['shortUrl'], 'Verb', self.ACTION_TYPE_MOVE_CARD, xapi_settings.get_verb_iri( xapi_settings.VERB_UPDATED))) other_context_list.append( get_other_contextActivity( card_details['shortUrl'], 'Object', data['listBefore']['name'], xapi_settings.get_verb_iri( xapi_settings.VERB_UPDATED))) other_context_list.append( get_other_contextActivity( card_details['shortUrl'], 'Object', data['listAfter']['name'], xapi_settings.get_verb_iri( xapi_settings.VERB_UPDATED))) insert_updated_object( user, object_id, object_text, date, unit, self.platform, self.platform_url, xapi_settings.OBJECT_TASK, parent_id=self.create_card_url(data), obj_parent_type=xapi_settings. OBJECT_COLLECTION, other_contexts=other_context_list) # When user closes (archives)/opens card elif data['old'][change[0]] is False or data['old'][ change[0]] is True: verb = xapi_settings.VERB_CLOSED verb_iri = xapi_settings.get_verb_iri(verb) action_type = self.ACTION_TYPE_CLOSE_CARD object_text = data['card']['name'] # When card is opened if data['old'][change[0]] is True: verb = xapi_settings.VERB_OPENED verb_iri = xapi_settings.get_verb_iri(verb) action_type = self.ACTION_TYPE_OPEN_CARD other_context_list = [] other_context_list.append( get_other_contextActivity( card_details['shortUrl'], 'Verb', action_type, verb_iri)) other_context_list.append( get_other_contextActivity( card_details['shortUrl'], 'Object', data['list']['name'], verb_iri)) insert_closedopen_object( user, object_id, object_text, date, unit, self.platform, self.platform_url, xapi_settings.OBJECT_TASK, verb, parent_id=self.create_list_url(data), obj_parent_type=xapi_settings. OBJECT_COLLECTION, other_contexts=other_context_list) def create_card_url(self, data): return self.platform_url + '/c/' + str(data['card']['id']) def create_list_url(self, data): return self.platform_url + '/b/' + str( data['board']['id']) + '/' + str(data['list']['id']) def create_checklist_url(self, data): return self.platform_url + '/b/' + str( data['board']['id']) + '/' + str(data['checklist']['id']) def get_verbs(self): return self.xapi_verbs def get_objects(self): return self.xapi_objects def get_other_contextActivity_types(self, verbs=[]): ret = [] if verbs is None or len(verbs) == 0: ret = [ self.ACTION_TYPE_COMMENT_CARD, self.ACTION_TYPE_CREATE_CARD, self.ACTION_TYPE_UPDATE_CHECKITEM_STATE_ON_CARD, self.ACTION_TYPE_UPDATE_CARD, self.ACTION_TYPE_ADD_ATTACHMENT_TO_CARD, self.ACTION_TYPE_ADD_CHECKLIST_TO_CARD, self.ACTION_TYPE_ADD_MEMBER_TO_CARD, self.ACTION_TYPE_MOVE_CARD, self.ACTION_TYPE_CLOSE_CARD, self.ACTION_TYPE_OPEN_CARD ] else: for verb in verbs: action_types = self.VERB_OBJECT_MAPPER[verb] for type in action_types: ret.append(type) return ret def get_display_names(self, mapper): if mapper is None: return mapper ret = {} for key, val in mapper.iteritems(): for action in mapper[key]: ret[action] = self.get_action_type_display_name(action) return ret def get_action_type_display_name(self, action): if action == self.ACTION_TYPE_CREATE_CARD: return 'Created card' elif action == self.ACTION_TYPE_ADD_ATTACHMENT_TO_CARD: return 'Added attachment to card' elif action == self.ACTION_TYPE_ADD_CHECKLIST_TO_CARD: return 'Added checklist to card' elif action == self.ACTION_TYPE_ADD_MEMBER_TO_CARD: return 'Added member to card' elif action == self.ACTION_TYPE_MOVE_CARD: return 'Moved card' elif action == self.ACTION_TYPE_UPDATE_CHECKITEM_STATE_ON_CARD: return 'Updated checklist item state' elif action == self.ACTION_TYPE_COMMENT_CARD: return 'Commented on card' elif action == self.ACTION_TYPE_CLOSE_CARD: return 'Closed card' elif action == self.ACTION_TYPE_OPEN_CARD: return 'Opened card' else: return 'Unknown action type' def get_detail_values_by_fetch_results(self, xapi_statements): all_rows = [] # return all_rows for stmt in xapi_statements: single_row = [] # user name name = '' if 'name' in stmt['authority']['member'][0]: name = stmt['authority']['member'][0]['name'] else: name = stmt['authority']['member'][1]['name'] single_row.append(name) # verb or original action type other_context_activities = stmt['context']['contextActivities'][ 'other'] single_row.append( self.get_action_type_from_context(other_context_activities)) # Date dt = Utility.convert_to_datetime_object(stmt['timestamp']) date_str = str(dt.year) + ',' + str(dt.month) + ',' + str(dt.day) # date_str += ' ' + str(dt.hour) + ':' + str(dt.minute) + ':' + str(dt.second) single_row.append(date_str) # Value of an object single_row.append(self.get_object_diaplay_value(stmt)) all_rows.append(single_row) return all_rows def get_action_type_from_context(self, json): return json[0]['definition']['name']['en-US'] def get_object_diaplay_value(self, stmt): other_context_activities = stmt['context']['contextActivities'][ 'other'] action = self.get_action_type_from_context(other_context_activities) object_val = stmt['object']['definition']['name']['en-US'] if len(other_context_activities) <= 1: return object_val object_val = object_val contexts = other_context_activities value = '' index = 1 if action == self.ACTION_TYPE_CREATE_CARD: value = "created %s in %s" % ( self.italicize(object_val), self.italicize(contexts[index]['definition']['name']['en-US'])) elif action == self.ACTION_TYPE_ADD_ATTACHMENT_TO_CARD: value = "attached %s to %s" % ( self.italicize(object_val), self.italicize(contexts[index]['definition']['name']['en-US'])) value = value + self.SEPARATOR_HTML_TAG_BR index = index + 1 value = value + contexts[index]['definition']['name']['en-US'] elif action == self.ACTION_TYPE_ADD_CHECKLIST_TO_CARD: value = "added %s to %s" % ( self.italicize(object_val), self.italicize(contexts[index]['definition']['name']['en-US'])) value = value + self.SEPARATOR_HTML_TAG_BR index = index + 1 for key in range(index, len(contexts)): value = value + ' ' + contexts[key]['definition']['name'][ 'en-US'] + self.SEPARATOR_HTML_TAG_BR elif action == self.ACTION_TYPE_ADD_MEMBER_TO_CARD: value = "added %s to %s" % ( self.italicize(object_val), self.italicize(contexts[index]['definition']['name']['en-US'])) value = value + self.SEPARATOR_HTML_TAG_BR elif action == self.ACTION_TYPE_MOVE_CARD: if contexts[index]['definition']['name'][ 'en-US'] == self.MESSAGE_CARD_POSITION_CHANGED: value = contexts[index]['definition']['name']['en-US'] value = value + self.SEPARATOR_HTML_TAG_BR value = value + self.italicize(object_val) else: value = "moved %s" % (self.italicize(object_val)) value = value + " from %s" % (self.italicize( contexts[index]['definition']['name']['en-US'])) index = index + 1 value = value + " to %s" % (self.italicize( contexts[index]['definition']['name']['en-US'])) elif action == self.ACTION_TYPE_UPDATE_CHECKITEM_STATE_ON_CARD: value = "%s: %s" % (self.italicize( contexts[index]['definition']['name']['en-US']), self.italicize(object_val)) elif action == self.ACTION_TYPE_COMMENT_CARD: value = 'commented in %s' % self.italicize( contexts[index]['definition']['name']['en-US']) value = value + self.SEPARATOR_HTML_TAG_BR value = value + self.italicize( self.replace_linechange_with_br_tag(object_val)) elif action == self.ACTION_TYPE_CLOSE_CARD: value = "closed %s in %s" % ( self.italicize(object_val), self.italicize(contexts[index]['definition']['name']['en-US'])) elif action == self.ACTION_TYPE_OPEN_CARD: value = "opened %s in %s" % ( self.italicize(object_val), self.italicize(contexts[index]['definition']['name']['en-US'])) else: value = self.italicize(object_val) return value def italicize(self, value): return '<i>%s</i>' % (value) def replace_linechange_with_br_tag(self, target): return target.replace('\n', '<br>')
class TrelloList(object): """ Sugar class to work with Trello Lists. """ def __init__(self, board_id, list_id, api_key, token=None, **kwargs): """ Validate inputs and connect to Trello API. Exception is thrown if input details are not correct. :param board_id: Trello board ID where the List is located :type board_id: ``str`` :param list_id: Trello List ID itself :type list_id: ``str`` :param api_key: Trello API key :type api_key: ``str`` :param token: Trello API token :type token: ``str`` """ self.board_id = board_id self.list_id = list_id self.api_key = api_key # assume empty string '' as None self.token = token or None self.validate() self._client = TrelloClient(api_key=self.api_key, token=self.token) self._list = self._client.get_board(self.board_id).get_list( self.list_id) def validate(self): """ Ensure that Trello list details are correct. Raise an exception if validation failed. """ if not self.api_key: raise ValueError( '[TrelloListSensor] "api_key" config value is required!') assert isinstance(self.api_key, six.string_types) if self.token: assert isinstance(self.token, six.string_types) if not self.board_id: raise ValueError( '[TrelloListSensor]: "board_id" config value is required!') assert isinstance(self.board_id, six.string_types) if not self.list_id: raise ValueError( '[TrelloListSensor]: "list_id" config value is required!') assert isinstance(self.list_id, six.string_types) @property def key_name(self): """ Generate unique key name for built-in storage based on config values. :rtype: ``str`` """ return '{}.{}.date'.format(self.board_id, self.list_id) def fetch_actions(self, filter=None, since=None): """ Fetch actions for Trello List with possibility to specify filters. Example API request: https://api.trello.com/1/lists/{list_id}/actions?filter=createCard&since=2015-09-14T21:45:56.850Z&key={key_id}&token={token_id} :param filter: Action types to filter, separated by comma or as a sequence. :type filter: ``str`` or ``list`` :param since: Filter actions since specified date. :type since: ``str`` :return: Events occurred in Trello list. :rtype: ``list`` of ``dict`` """ return self._client.fetch_json('/lists/' + self._list.id + '/actions', query_params={ 'filter': filter, 'since': since, })
class TrelloList(object): """ Sugar class to work with Trello Lists. """ def __init__(self, board_id, list_id, api_key, token=None, **kwargs): """ Validate inputs and connect to Trello API. Exception is thrown if input details are not correct. :param board_id: Trello board ID where the List is located :type board_id: ``str`` :param list_id: Trello List ID itself :type list_id: ``str`` :param api_key: Trello API key :type api_key: ``str`` :param token: Trello API token :type token: ``str`` """ self.board_id = board_id self.list_id = list_id self.api_key = api_key # assume empty string '' as None self.token = token or None self.validate() self._client = TrelloClient(api_key=self.api_key, token=self.token) self._list = self._client.get_board(self.board_id).get_list(self.list_id) def validate(self): """ Ensure that Trello list details are correct. Raise an exception if validation failed. """ if not self.api_key: raise ValueError('[TrelloListSensor] "api_key" config value is required!') assert isinstance(self.api_key, basestring) if self.token: assert isinstance(self.token, basestring) if not self.board_id: raise ValueError('[TrelloListSensor]: "board_id" config value is required!') assert isinstance(self.board_id, basestring) if not self.list_id: raise ValueError('[TrelloListSensor]: "list_id" config value is required!') assert isinstance(self.list_id, basestring) @property def key_name(self): """ Generate unique key name for built-in storage based on config values. :rtype: ``str`` """ return '{}.{}.date'.format(self.board_id, self.list_id) def fetch_actions(self, filter=None, since=None): """ Fetch actions for Trello List with possibility to specify filters. Example API request: https://api.trello.com/1/lists/{list_id}/actions?filter=createCard&since=2015-09-14T21:45:56.850Z&key={key_id}&token={token_id} :param filter: Action types to filter, separated by comma or as a sequence. :type filter: ``str`` or ``list`` :param since: Filter actions since specified date. :type since: ``str`` :return: Events occurred in Trello list. :rtype: ``list`` of ``dict`` """ return self._client.fetch_json( '/lists/' + self._list.id + '/actions', query_params={ 'filter': filter, 'since': since, })
class Handler(BaseHandler): name = 'Trello' prefix = 'trello' patterns = [ ([ '(?P<command>desligar) (?P<users>.*)', '(?P<command>terminate) (?P<users>.*)', '{prefix} (?P<command>terminate) (?P<users>.*)' ], 'Desliga um funcionário'), ] def __init__(self, bot, slack, api_key=None, api_secret=None, oauth_token=None, oauth_secret=None): super().__init__(bot, slack) self.directed = True if not api_key: api_key = os.environ.get('TRELLO_API_KEY') if not api_secret: api_secret = os.environ.get('TRELLO_API_SECRET') if not oauth_token: oauth_token = os.environ.get('TRELLO_OAUTH_TOKEN') if not oauth_secret: oauth_secret = os.environ.get('TRELLO_OAUTH_SECRET') self.client = TrelloClient(api_key=api_key, api_secret=api_secret, token=oauth_token, token_secret=oauth_secret) self.org_name = os.environ.get('TRELLO_ORGANIZATION', 'pagarmeoficial') self.org_id = self.get_org_id(self.org_name) self.org = self.client.get_organization(self.org_id) self.set_job_status('Initialized') def get_org_id(self, name): orgs = self.client.list_organizations() for org in orgs: if org.name == name: return org.id def process(self, channel, user, ts, message, at_bot, command, **kwargs): if at_bot: if command in ['desligar', 'terminate']: user_handle = self.get_user_handle(user) self.log('@{}: {}'.format(user_handle, message)) self.set_job_status('Processing') if not self.authorized(user_handle, 'Terminator'): self.set_job_status('Unauthorized') self.post_message( channel=channel, text='@{} Unauthorized'.format(user_handle)) return False to_remove = [ x for x in kwargs['users'].split() if '@' not in x ] for r in to_remove: try: c = self.bot.get_config('alias_reverse', r) if c: if c not in to_remove: to_remove.append(c) except: continue for r in to_remove: try: c = self.bot.get_config('alias', r).split() for x in c: if x not in to_remove: to_remove.append(x) except: continue print(to_remove) if len(to_remove) == 0: self.log('No valid usernames') self.set_job_status('Finished') return self.post_message(channel=channel, text='@{} Removendo usuários: {}'.format( user_handle, ', '.join(to_remove))) all_boards = self.org.get_boards({'fields': 'id'}) members = { board.id: board.all_members() for board in all_boards } members_username = { board.id: [x.username for x in members[board.id]] for board in all_boards } for username in to_remove: response = '@{} User {} not found at any boards'.format( user_handle, username) removed = False removed_boards = [] m = None for mm in members: if isinstance(members[mm], list): for member in members[mm]: if member.username == username: m = member break else: if members[mm].username == username: m = mm break if m == None: self.log('User {} doesn\'t exists'.format(username)) continue try: self.client.fetch_json( '/organizations/{}/members/{}'.format( self.org_id, m.id), http_method='DELETE') except: traceback.print_exc() for board in all_boards: if username in members_username[board.id]: try: self.log('Found {} at board {}'.format( username, board.name)) removed = True removed_boards.append('"{}"'.format( board.name)) board.remove_member(m) self.log( 'User {} removed from board {}'.format( username, board.name)) except: self.post_message( channel, 'Failed to remove {} from board {}'.format( username, board.name)) self.log(traceback.format_exc()) if removed: response = '@{} User {} removed from boards {}'.format( user_handle, username, ', '.join(removed_boards)) if response: self.post_message(channel, response) else: self.log( 'User {} not found in any boards'.format(username)) self.post_message( channel, '@{} User {} not found in any boards'.format( user_handle, username)) self.set_job_status('Finished') self.set_job_end(datetime.now())
class EClaire(object): def __init__(self, credentials, boards=None): self.trello_client = TrelloClient( api_key=credentials['public_key'], token=credentials['member_token'], ) self.boards = boards def process_boards(self, dry_run=False, notify_fn=None, notify_config=None): """ Process each board in self.boards """ for name, board_config in self.boards.iteritems(): log.info('Polling %s', name) processed = self.process_board(board_config, dry_run) if board_config.get('notify', True) and notify_fn is not None: for card in processed: notify_fn(card, **notify_config) def process_board(self, board_config, dry_run=False): """ Process each card in a given board """ processed = [] for card in self.fetch_cards(board_id=board_config['id']): log.info('Printing card "%s"', card.name) pdf = generate_pdf(card) if not dry_run: print_card(pdf, printer_name=board_config['printer']) self.update_card(card, board_config) processed.append(card) return processed def fetch_cards(self, board_id): """ Fetch all candidate cards on a board for processing """ data = [] board = self.trello_client.get_board(board_id) for card in board.all_cards(): if FILTER_LABEL in [l.name for l in card.labels]: card.fetch_actions() data.append(card) return data def discover_labels(self): """ Store object references for special labels """ for name, config in self.boards.iteritems(): board = self.trello_client.get_board(config['id']) labels = {} for label in board.get_labels(limit=100): if label.name in SPECIAL_LABELS: labels[label.name] = label missing = set(SPECIAL_LABELS) - set(labels.keys()) if missing: log.fatal( 'Board "%s" is missing the labels %s', board.name, ' and '.join(missing) ) log.fatal('Exiting') sys.exit(1) config['labels'] = labels def remove_label(self, card, label): """ Remove a lable from a card. At the time of writing there is no way to remove a label with py-trello """ self.trello_client.fetch_json( '/cards/' + card.id + '/idLabels/' + label.id, http_method="DELETE" ) def update_card(self, card, board_config): """ Replace PRINTME label with PRINTED """ printme_label = board_config['labels']['PRINTME'] printed_label = board_config['labels']['PRINTED'] self.remove_label(card, printme_label) if printed_label not in card.labels: card.add_label(printed_label) def list_boards(self): """ Fetch all board IDs from trello & print them out """ for board in self.trello_client.list_boards(): print 'Board:', board.name print ' ID:', board.id print
class Command(BaseCommand): def __init__(self): super().__init__() self.trello_client = TrelloClient(settings.TRELLO_API_KEY, settings.TRELLO_API_TOKEN) self.github_client = Github( settings.GITHUB_TOKEN).get_organization('bluevine-dev') repos = self.github_client.get_repos() unmerged_pull_requests = [] for repo in repos: unmerged_pull_requests += [ pr for pr in repo.get_pulls() if not pr.merged_at ] self.unmerged_pull_requests = { pr.html_url: pr for pr in unmerged_pull_requests } def add_arguments(self, parser): # pylint: disable=no-self-use """ Add extra arguments """ super().add_arguments(parser) parser.add_argument('--csv', action='store_true', default=False, help='attach a csv to the generated email') parser.add_argument('--user', action='store', help='run command for specified user') def handle(self, *args, **options): """ Run command """ username = options.get('user') if username: users = User.objects.filter(username=username) else: users = User.objects.filter(role='TL') # generate unmerged pull request email to all desired users for user in users: member_id = self.trello_client.get_member(user.email).id data = self._get_unmerged_pull_requests(member_id) attachment = None if data and options['csv']: attachment = self.create_pull_requests_csv(data, member_id) logger.info(f'attachment {attachment} was created') self.send_unmerged_pull_requests_data( data=data, recipients=['*****@*****.**'], attachment=attachment) def _get_unmerged_pull_requests(self, member_id): """ Returns all unmerged pull requests related to cards """ boards = self.trello_client.list_boards(board_filter='open') pull_requests_per_board = [] for board in boards: cards = board.get_cards(card_filter='open') # get only member cards cards = [card for card in cards if member_id in card.member_id] pull_requests_per_card = [] for card in cards: attachments = card.get_attachments() pr_attachments = [ attachment for attachment in attachments if all(s in attachment.url for s in ['github.com', 'pull']) ] # check for unmerged pull request in card unmerged_pull_requests = [] for pr_attachment in pr_attachments: if pr_attachment.url in self.unmerged_pull_requests: pull_request_data = { 'name': pr_attachment.name, "url": pr_attachment.url, } unmerged_pull_requests.append(pull_request_data) if unmerged_pull_requests: card_data = { 'name': card.name, 'url': card.url, 'pull_requests': unmerged_pull_requests } pull_requests_per_card.append(card_data) if pull_requests_per_card: board_data = { 'name': board.name, 'cards': pull_requests_per_card } pull_requests_per_board.append(board_data) return pull_requests_per_board @staticmethod def send_unmerged_pull_requests_data(data, recipients, attachment=None): """ Sends an email according to given data """ html_content = Command.create_email_template(data) email_message = EmailMessage( subject='Trello Manager - Unmerged Pull Requests', body=html_content, from_email=settings.EMAIL_HOST_USER, to=recipients) email_message.content_subtype = 'html' if attachment: email_message.attach_file(attachment) email_message.send() logger.info(f'Email was sent to {recipients}') @staticmethod def create_pull_requests_csv(data, member_id): """ create a temporary csv file """ import os, tempfile, csv from datetime import datetime now = datetime.now().strftime("%m_%d_%Y__%H%M%S") file_path = os.path.join( tempfile.gettempdir(), f'unmerged_pull_request_{member_id}_{now}.csv') with open(file_path, 'w') as csv_file: headers = [ 'board name', 'card name', 'card url', 'pull request name', 'pull request url' ] writer = csv.writer(csv_file) writer.writerow(headers) for board in data: cards = board['cards'] for card in cards: pull_requests = card['pull_requests'] for pull_request in pull_requests: writer.writerow([ board['name'], card['name'], card['url'], pull_request['name'], pull_request['url'] ]) return file_path @staticmethod def fetch_data_from_pull_request_url(pull_request_url): """ Parses and returns pull request data from a pull request url """ split_url = pull_request_url.split("/") owner = split_url[3] repo = split_url[4] number = int(split_url[-1]) return owner, repo, number def _fetch_cards_by_member(self, member_id): """ Fetches all the cards for this member """ cards = self.trello_client.fetch_json('/members/' + member_id + '/cards', query_params={ 'filter': 'visible', 'fields': 'name,idBoard,url', 'attachments': 'true' }) return sorted(cards, key=lambda card: card['idBoard']) def _get_board_name_by_id(self, board_id): """ Returns the name of the board """ if board_id not in self.boards_names: self.boards_names[board_id] = self.trello_client.get_board( board_id).name return self.boards_names[board_id] def _get_pull_request_by_url(self, pull_request_url): owner, repo, number = self.fetch_data_from_pull_request_url( pull_request_url) return self.github_client.get_repo(repo).get_pull(int(number)) @staticmethod def create_email_template(data): email_template = Template(EMAIL_TEMPLATE) return email_template.render(Context({'boards': data}))
def handle(self, *args, **options): trello = TrelloClient(settings.TRELLO_API_KEY, settings.TRELLO_TOKEN) master = Board.objects.get(name=settings.TRELLO_MASTER) updatedcards_ids = [] for board in Board.objects.all().order_by('pk'): logger.info('Importing board {0}'.format(board.name)) # search for the remote board b = board.remote_object(trello) # check if the board was updated since the last update if board.last_activity is not None and b.date_last_activity <= board.last_activity: logger.info(' - No activity detected') # If it is the first update then import all the lists from the board. if not board.last_activity: logger.info("*** FIRST IMPORT ***") for lst in board.boardlist_set.all(): l = lst.remote_object(trello) lst.name = l.name lst.closed = l.closed lst.position = l.pos lst.save() lst.import_cards(l) else: # if is not the first board update, update only the latest modifications # the board was already imported once query = { 'since': board.last_activity.isoformat(timespec='microseconds') } data = trello.fetch_json('/boards/' + board.remoteid + '/actions', query_params=query) ids = [] for update in data: action_type = update.get('type', None) card_info = update['data'].get('card', None) date_last_activity = dateparser.parse(update.get('date')) if card_info: card_id = card_info['id'] if action_type == 'deleteCard': try: card = Card.objects.get(remoteid=card_id) card.last_activity = date_last_activity card.delete_remotely = True card.remoteid = None card.save() logger.info( "The card [{0}] in the board [{1}] was marked to be removed" .format(card.name, card.boardlist.board.name)) except Card.DoesNotExist: # ignore pass else: # append to the list all the modified cards ids updatedcards_ids.append(card_id) board.last_activity = b.date_last_activity board.save() cards = [] # search in all the cards from the master board, # which cards were updated b = master.remote_object(trello) for c in b.open_cards(): try: card = Card.objects.get(remoteid=c.id) if card.last_activity < c.date_last_activity: cards.append(c) except Card.DoesNotExist: pass # get all the remote cards. updatedcards_ids = list(set(updatedcards_ids)) for idx, i in enumerate(updatedcards_ids): logger.info("Get remote card ({0}/{1})".format( idx + 1, len(updatedcards_ids))) cards.append(trello.get_card(i)) # sort the cards by activity, so the latest card is the one updated cards = sorted(cards, key=lambda x: x.date_last_activity) lists = {} # cache all the boards lists for c in cards: # check if the boad list is already in cache, otherwise add it if c.list_id not in lists: lists[c.list_id] = BoardList.objects.get(remoteid=c.list_id) try: card = Card.objects.get(remoteid=c.id) # the card list is diferent, mark it to update if card.name != c.name: card.update_name = True card.name = c.name if card.desc != c.description: card.update_desc = True card.desc = c.description if card.closed != c.closed: card.update_closed = True card.closed = c.closed if card.boardlist != lists[c.list_id]: card.update_list = True card.boardlist = lists[c.list_id] except Card.DoesNotExist: # the card does not exists, create it card = Card(remoteid=c.id) card.name = c.name card.desc = c.description card.closed = c.closed card.boardlist = lists[c.list_id] card.position = c.pos card.last_activity = c.date_last_activity card.save() card.update_members_by_id(trello, c.member_id) card.update_labels_by_id(trello, c.idLabels) logger.info( "Updated card [{1} > {2} > {0}]: name:{3}, description:{4}, closed:{5}, list:{6}, members:{7}, labels:{8}" .format(card.name, card.boardlist.board.name, card.boardlist.name, card.update_name, card.update_desc, card.update_closed, card.update_list, card.update_members, card.update_labels))
class TrelloManager: """Manage Trello connection and data.""" def __init__(self, api_key, api_secret, token): """Create a new TrelloManager using provided keys.""" self._cl = TrelloClient( api_key=api_key, api_secret=api_secret, token=token, ) # Start whitelisting no organization self._wl_org = set() # Start whitelisting no board self._wl_brd = set() def whitelist_org(self, oid): """Add an organization to whitelist, by ID.""" self._wl_org.add(oid) def blacklist_org(self, oid): """Remove an organization from whitelist, by ID.""" self._wl_org.discard(oid) def whitelist_brd(self, bid): """Whitelist a board by id.""" self._wl_brd.add(bid) def blacklist_brd(self, bid): """Blacklist a board by id.""" self._wl_brd.discard(bid) def org_names(self): """Fetch and return organization names.""" return {o.name for o in self.fetch_orgs()} def fetch_orgs(self): """Generate organizations and their blacklistedness.""" for o in self._cl.fetch_json('/members/me/organizations/'): yield Organization(o['id'], o['name'], o['id'] not in self._wl_org, o['url']) def fetch_boards(self, org=None): """Generate boards (in given org) and their blacklistedness.""" if org is None: for b in self._cl.fetch_json('/members/me/boards/'): # If board has not an organization, it is blacklisted iff # it's not in the whitelist bbl = b['id'] not in self._wl_brd # If board has an organization, it is blacklisted iff # both board or organization are not in whitelist # if b['idOrganization'] is not None: # bbl = bbl or b['idOrganization'] not in self._wl_org yield Board(b['id'], b['name'], bbl, b['url']) else: orgs = list(self.fetch_orgs()) id2na = {o.id: o.name for o in orgs} na2id = {o.name: o.id for o in orgs} # Convert names to id if org in na2id: org = na2id[org] # Cannot find ID if org not in id2na: return for b in self._cl.fetch_json(f'/organizations/{org}/boards/'): bl = b['id'] not in self._wl_brd yield Board(b['id'], b['name'], bl, b['url']) def fetch_lists(self, bid=None): """Generate lists in given board.""" # Use all boards if none was specified bids = list(b.id for b in self.fetch_boards()) if bid is None else [bid] # Yield all the lists for b in bids: for l in self._cl.fetch_json(f'/boards/{b}/lists'): logging.info('Got list ' + l['id']) yield List(l['id'], l['name'], l['idBoard'], l['subscribed']) def get_card(self, cid): """Get a card by ID.""" c = self._cl.fetch_json(f'/cards/{cid}') if c['due'] is not None: c['due'] = parse_date(c['due']) return Card(c['id'], c['name'], c['url'], c['due'], c['dueComplete']) def fetch_cards(self, lid=None, bid=None): """Generate cards from list, board or everything.""" if bid is not None: seq = self._cl.fetch_json(f'/boards/{bid}/cards') elif lid is not None: seq = self._cl.fetch_json(f'/lists/{lid}/cards') else: seq = self._cl.fetch_json(f'/members/me/cards') for c in seq: if c['due'] is not None: c['due'] = parse_date(c['due']) yield Card(c['id'], c['name'], c['url'], c['due'], c['dueComplete']) def create_card(self, lid, name, due=None): """Create a card inside the specified list.""" if isinstance(due, datetime): due = due.isoformat() # strftime('%Y-%m-%dT%H:%M:%SZ') c = self._cl.fetch_json(f'/cards', http_method='POST', post_args={ 'idList': lid, 'name': name, 'due': due, }) return Card(c['id'], c['name'], c['url'], c['due'], c['dueComplete'])
today = datetime.now() es = Elasticsearch("localhost:9200") client = TrelloClient(API_KEY, token=API_TOKEN) board = client.get_board(BOARD_ID) members = board.all_members() for m in members: member = client.get_member(m.id) d = datetime(2017, 4, 1, 0, 0) while True: since = d before = since + relativedelta(months=1) actions = client.fetch_json( '/members/' + member.id + '/actions', query_params={'limit': 1000, "since": since.strftime("%Y-%m-%d"), "before": before.strftime("%Y-%m-%d")}) for a in actions: utc_date = parse(a["date"]) jst_date = utc_date.astimezone(timezone('Asia/Tokyo')) a["date"] = jst_date.strftime("%Y-%m-%dT%H:%M:%S%z") a["hour"] = jst_date.hour a["weekday"] = jst_date.weekday() if "text" in a["data"]: a["text_length"] = len(a["data"]["text"]) es.index(index="trello", doc_type=m.full_name, body=a) if today < before: break d = before
class TrelloPlugin(DIBasePlugin, DIPluginDashboardMixin): platform = "trello" platform_url = "http://www.trello.com/" #created for "create" actions #added for "add" actions #updated for "update" actions #commented for card "comment" actions xapi_verbs = ['created', 'added', 'updated', 'commented', 'closed', 'opened'] #Note for "commented" actions #Task for "created", "added", "updated", and "commented" actions #Collection for any "created", "added", "updated" List actions (tentative use) xapi_objects = ['Note', 'Task', 'Collection', 'Person', 'File', 'checklist-item', 'checklist'] user_api_association_name = 'Trello UID' # eg the username for a signed up user that will appear in data extracted via a social API unit_api_association_name = 'Board ID' # eg hashtags or a group name config_json_keys = ['consumer_key', 'consumer_secret'] #from DIPluginDashboardMixin xapi_objects_to_includein_platformactivitywidget = ['Note'] xapi_verbs_to_includein_verbactivitywidget = ['created', 'shared', 'liked', 'commented'] #for OAuth1 authentication token_request_url = '' def __init__(self): pass #retreival param is the user_id def perform_import(self, retreival_param, course_code, token=None): #from clatoolkit.models import ApiCredentials #Set up trello auth and API #self.usercontext_storage_dict = json.load(ApiCredentials.objects.get(platform=retreival_param).credentials_json) self.TrelloCient = TrelloClient( api_key=os.environ.get("TRELLO_API_KEY"), #api_secret=self.api_config_dict['api_secret'], token=token ) #Get user-registered board in trello trello_board = self.TrelloCient.get_board(retreival_param) #Get boards activity/action feed trello_board.fetch_actions('all') #fetch_actions() collects actions feed and stores to trello_board.actions #self.key = trello_board.api_key #self.token = trello_board.resource_owner_key self.import_TrelloActivity(trello_board.actions, course_code) def import_TrelloActivity(self, feed, course_code): #User needs to sign up username and board (board can be left out but is needed) #TODO: RP print 'Beginning trello import!' for action in list(feed): #print 'action: %s' % (action) #action = json.load(action) #We need to connect this with our user profile somewhere when they initially auth u_id = action['idMemberCreator'] author = action['memberCreator']['username'] type = action['type'] #commentCard, updateCard, createList,etc data = action['data'] date = action['date'] board_name = data['board']['name'] print 'got action type: %s' % (type) #print 'is action comment? %s' % (type == 'commentCard') #Get all 'commented' verb actions if (type == 'commentCard'): #do stuff target_obj_id = data['card']['id'] #date comment_from_uid = u_id comment_from_name = author comment_message = data['text'] comment_id = action['id'] if username_exists(comment_from_uid, course_code, self.platform): usr_dict = get_userdetails(comment_from_uid, self.platform) insert_comment(usr_dict, target_obj_id, comment_id, comment_message, comment_from_uid, comment_from_name, date, course_code, self.platform, self.platform_url) print 'Inserted comment!' #print 'is action card creation? %s' % (type == 'createCard') #Get all 'create' verb actions if (type == 'createCard'): #, 'createList']): #date #list_id = data['list']['id'] task_id = data['card']['id'] task_name = data['card']['name'] if username_exists(u_id, course_code, self.platform): usr_dict = get_userdetails(u_id, self.platform) insert_task(usr_dict, task_id, task_name, u_id, author, date, course_code, self.platform, self.platform_url) #, list_id=list_id) #TODO: RP print 'Inserted created card!' #Get all 'add' verbs (you tecnically aren't *creating* an attachment on #a card so....) #print 'is action an add event? %s' % (type in # ['addAttachmentToCard', 'addMemberToBoard', # 'emailCard', 'addChecklistToCard' # , 'addMemberToCard']) if (type in ['addAttachmentToCard', 'addMemberToBoard', 'emailCard', 'addChecklistToCard' , 'addMemberToCard']): usr_dict = None if username_exists(u_id, course_code, self.platform): usr_dict = get_userdetails(u_id, self.platform) if type is 'addAttachmentToCard' and usr_dict is not None: target_id = data['card']['id'] attachment = data['attachment'] attachment_id = attachment['id'] attachment_data = '%s - %s' % (attachment['name'], attachment['url']) object_type = 'File' shared_displayname = '%sc/%s' % (self.platform_url, target_id) insert_added_object(usr_dict, target_id, attachment_id, attachment_data, u_id, author, date, course_code, self.platform, self.platform_url, object_type, shared_displayname=shared_displayname) #TODO: RP print 'Added attachment!' if type is 'addMemberToCard' and usr_dict is not None: #or 'addMemberToBoard': target_id = data['card']['id'] object_id = data['idMember'] object_data = action['memeber']['username'] object_type = 'Person' shared_displayname = '%sc/%s' % (self.platform_url, target_id) insert_added_object(usr_dict, target_id, object_id, object_data, u_id, author, date, course_code, self.platform, self.platform_url, object_type, shared_displayname=shared_displayname) #TODO: RP print 'Added add member to card!' if type is 'addChecklistToCard' and usr_dict is not None: target_id = data['card']['id'] object_id = data['idMember'] object_data = None checklist_items = None object_type = 'Collection' shared_displayname = '%sc/%s' % (self.platform_url, target_id) #get checklist contents try: checklist = self.TrelloCient.fetch_json('/checklists/' + data['checklist']['id'],) checklist_items = checklist['checkItems'] except Exception: print 'Could not retrieve checklist..' #data will be a comma separated list of checklist-item ids (e.g.: 'id1,id2,id3...') object_data = ','.join([item['id'] for item in checklist_items]) insert_added_object(usr_dict, target_id, object_id, object_data, u_id, author, date, course_code, self.platform, self.platform_url, object_type, shared_displayname=shared_displayname) #TODO: RP print 'added add checklist to card!' #print 'is action type an update? %s' % (type in # ['updateCheckItemStateOnCard', 'updateBoard', # 'updateCard', 'updateCheckList', # 'updateList', 'updateMember']) #Get all 'updated' verbs if (type in ['updateCheckItemStateOnCard', 'updateBoard', 'updateCard', 'updateCheckList', 'updateList', 'updateMember']): usr_dict = None if username_exists(u_id, course_code, self.platform): usr_dict = get_userdetails(u_id, self.platform) #many checklist items will be bugged - we require webhooks! if type == 'updateCheckItemStateOnCard' and usr_dict is not None: insert_updated_object(usr_dict, data['checkItem']['id'], data['checkItem']['state'], u_id, author, date, course_code, self.platform, self.platform_url, 'checklist-item', obj_parent=data['checklist']['id'], obj_parent_type='checklist') #TODO: RP print 'add update checklist!' #type will only show 'updateCard' #up to us to figure out what's being updated if type == 'updateCard': #TODO: Remove Print print 'data: %s' % (data) #Get and store the values that were changed, usually it's only one #TODO: Handle support for multiple changes, if that's possible try: change = [changed_value for changed_value in data['old']] except Exception: print 'Error occurred getting changes...' #assert len(change) is 1 #TODO: Remove Print print 'got changes: %s' % (change) #Insert all updates that aren't closed if change[0] == 'pos': if 'listBefore' in data: insert_updated_object(usr_dict, data['card']['id'], 'Move card from %s to %s' % (data['listBefore']['name'], data['listAfter']['name']), u_id, author, date, course_code, self.platform, self.platform_url, 'Task', obj_parent=data['list']['name'], obj_parent_type='Collection') else: insert_updated_object(usr_dict, data['card']['id'], 'Move card from %s to %s' % (data['old']['pos'], data['card']['pos']), u_id, author, date, course_code, self.platform, self.platform_url, 'Task', obj_parent=data['list']['name'], obj_parent_type='Collection') #TODO: RP print 'added closed card!' #add in close/open verbs else: if data['old'][change[0]] is False: insert_closedopen_object(usr_dict, data['card']['id'], '%s:%s' % ('Closed', data['card']['name']), u_id, author, date, course_code, self.platform, self.platform_url, 'Task', 'closed', obj_parent=data['list']['name'], obj_parent_type='Collection') #TODO: RP print 'added closed/opened card!' elif data['old'][change[0]] is True: insert_closedopen_object(usr_dict, data['card']['id'], '%s:%s' % ('Opened', data['card']['name']), u_id, author, date, course_code, self.platform, self.platform_url, 'Task', 'opened', obj_parent=data['list']['name'], obj_parent_type='Collection') #TODO: RP print 'added closed/opened card!' def get_verbs(self): return self.xapi_verbs def get_objects(self): return self.xapi_objects
class EClaire(object): def __init__(self, credentials, boards=None, qrcode_enabled=True): self.trello_client = TrelloClient(api_key=credentials["public_key"], token=credentials["member_token"]) self.boards = boards self.qrcode_enabled = qrcode_enabled def process_boards(self, dry_run=False, notify_fn=None, notify_config=None): """ Process each board in self.boards """ for name, board_config in self.boards.items(): log.info("Polling %s", name) processed = self.process_board(board_config, dry_run) if board_config.get("notify", False) and notify_fn is not None: for card in processed: notify_fn(card, **notify_config) def process_board(self, board_config, dry_run=False): """ Process each card in a given board """ processed = [] for card in self.fetch_cards(board_id=board_config["id"]): log.info('Printing card "%s"', card.name) pdf = generate_pdf(card=card, qrcode_enabled=self.qrcode_enabled) if not dry_run: print_card(pdf, printer_name=board_config["printer"]) self.update_card(card, board_config) processed.append(card) return processed def fetch_cards(self, board_id): """ Fetch all candidate cards on a board for processing """ data = [] board = self.trello_client.get_board(board_id) for card in board.open_cards(): if card.labels and FILTER_LABEL in (l.name for l in card.labels): card.fetch_actions() data.append(card) return data def discover_labels(self): """ Store object references for special labels """ for name, config in self.boards.items(): board = self.trello_client.get_board(config["id"]) labels = {} for label in board.get_labels(limit=1000): if label.name in SPECIAL_LABELS: labels[label.name] = label missing = set(SPECIAL_LABELS) - set(labels.keys()) if missing: log.fatal('Board "%s" is missing the labels %s', board.name, " and ".join(missing)) log.fatal("Exiting") sys.exit(1) config["labels"] = labels def remove_label(self, card, label): """ Remove a lable from a card. At the time of writing there is no way to remove a label with py-trello """ self.trello_client.fetch_json("/cards/{}/idLabels/{}".format( card.id, label.id), http_method="DELETE") def update_card(self, card, board_config): """ Replace PRINTME label with PRINTED """ printme_label = board_config["labels"]["PRINTME"] printed_label = board_config["labels"]["PRINTED"] self.remove_label(card, printme_label) if printed_label not in card.labels: card.add_label(printed_label) def list_boards(self): """ Fetch all board IDs from trello & print them out """ for board in self.trello_client.list_boards(): print("Board:", board.name) print(" ID:", board.id) print()