コード例 #1
0
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)
コード例 #2
0
    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'
                )
コード例 #3
0
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"
            },
        )
コード例 #4
0
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()
コード例 #5
0
ファイル: base.py プロジェクト: helresa/eClaire
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
コード例 #6
0
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)
コード例 #7
0
ファイル: cladi_plugin.py プロジェクト: ikybs/CLAtoolkit
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>')
コード例 #8
0
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,
                                       })
コード例 #9
0
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,
            })
コード例 #10
0
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())
コード例 #11
0
ファイル: base.py プロジェクト: kogan/eClaire
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
コード例 #12
0
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}))
コード例 #13
0
    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))
コード例 #14
0
ファイル: trello.py プロジェクト: ristle/trellobot
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'])
コード例 #15
0
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
コード例 #16
0
ファイル: cladi_plugin.py プロジェクト: zwaters/CLAtoolkit
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
コード例 #17
0
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()