Ejemplo n.º 1
0
    def test_get_end_date(self, mock_config):
        mock_config.get_config_value = mock.MagicMock(
            return_value='2017-01-22')

        # App init, necessary to get to the logging service
        app = self.get_app()

        time = Time(app.log, mock_config)

        end_date_answer = datetime.strptime('2017-01-22', "%Y-%m-%d").date()
        end_date_response = time.get_end_date()

        self.assertEqual(end_date_answer, end_date_response)
Ejemplo n.º 2
0
class Load(object):
    """
        Load updated Jira data into cache
    """
    def __init__(self, log, app_config):
        self.log = log
        self.config = Config(self.log)
        self.time = Time(self.log, self.config)
        self.log_config = LogConfig(self.log, app_config,
                                    self.config.config_path + 'load.log')

    def refresh_jira_cache(self):
        self.log.info('Load updated data from Jira into Cache')

        date_start = self.time.get_current_date()
        date_end = self.time.get_end_date()
        self.log.info('Load.main(): Start Date: ' +
                      date_start.strftime('%Y-%m-%d'))
        self.log.info('Load.main(): End Date: ' +
                      date_end.strftime('%Y-%m-%d'))

        loader = ImportData(self.log, self.config)
        # Import existing data (if any) into a Python object
        previous_data = Files(self.log).jsonl_load(
            self.config.filepath_data_completion)
        # Refresh the cache by checking if additional days can be added
        daily_data = loader.refresh_dailydata_cache(previous_data, date_start,
                                                    date_end)
        # Write back the data cache to file after clearing any existing one
        loader.write_dailydata_cache(daily_data)

        # Call Jira to get Remaining work
        remaining_work = loader.get_remaining_work()

        return daily_data, remaining_work

    def load_jira_cache(self):
        self.log.info('Load daily data and remaining work from cache')
        # Import existing data (if any) into a Python object
        daily_data = Files(self.log).jsonl_load(
            self.config.filepath_data_completion)
        remaining_work = Files(self.log).json_load(
            self.config.filepath_data_remaining)
        if remaining_work is None or daily_data is None:
            self.log.error(
                'Unable to load cached data, please run \'jav load\' first')
            exit()

        return daily_data, remaining_work
Ejemplo n.º 3
0
 def __init__(self, log, config):
     self.log = log
     self.config = config
     self.time = Time(self.log, self.config)
Ejemplo n.º 4
0
class BuildChart(object):
    def __init__(self, log, config):
        self.log = log
        self.config = config
        self.time = Time(self.log, self.config)

    def build_url_completed(self, current_date):
        return \
            self.config.get_config_value('jira_host') + '/issues/?jql=' \
            + quote(
                self.config.get_config_value('jira_jql_velocity')
                + ' ON(\"'
                + current_date.strftime("%Y-%m-%d")
                + '\")'
            )

    def build_url_remaining(self, jira_field, current_type):
        return \
            self.config.get_config_value('jira_host') + '/issues/?jql=' \
            + quote(
                self.config.get_config_value('jira_jql_remaining')
                + ' AND '
                + jira_field
                + ' = \"'
                + current_type
                + '\"'
            )

    def build_velocity_days(self, stats_data):
        self.log.info('Generating graph about daily effort')
        # Velocity Days
        x_dates = []
        y_values = []
        y_avg = []
        x_dates_display = []
        jira_url = []
        for scan_day in stats_data:
            x_dates.append(stats_data[scan_day]['datetime'])
            x_dates_display.append(stats_data[scan_day]['datetime'].strftime('%a %b %d, %Y'))
            y_values.append(stats_data[scan_day][self.config.get_config_value('stats_metric')])
            jira_url.append(self.build_url_completed(stats_data[scan_day]['datetime']))
            week_idx = '4'
            if week_idx not in stats_data[scan_day]['anyday']:
                week_idx = 4
            if 'avg' in stats_data[scan_day]['anyday'][week_idx]:
                y_avg.append(stats_data[scan_day]['anyday'][week_idx]['avg'])
            else:
                y_avg.append(0)
        # prepare some data
        data_dates = np.array(x_dates, dtype=np.datetime64)
        source = ColumnDataSource(
            data=dict(
                x_data_dates=data_dates,
                y_values=y_values,
                y_avg_values=y_avg,
                x_dates_display=x_dates_display,
                jira_url=jira_url
            )
        )
        # Declare tools
        hover = HoverTool(
            tooltips=[
                ('Date', '@x_dates_display'),
                (self.config.get_config_value('stats_metric').capitalize(), '@y_values'),
                ('Average', '@y_avg_values'),
            ]
        )
        # create a new plot with a a datetime axis type
        p = figure(width=1000, height=350, x_axis_type='datetime',
                   tools=['save', 'pan', 'box_zoom', 'reset', hover, 'tap'])
        if self.config.get_config_value('stats_metric') == 'tickets':
            metric_legend = 'Tickets'
        else:
            metric_legend = 'Story Points'
        # add renderers
        p.circle('x_data_dates', 'y_values', size=4, color='red', alpha=0.4, legend=metric_legend, source=source)
        p.line('x_data_dates', 'y_avg_values', color='blue', legend='4 Weeks Average', source=source)
        taptool = p.select(type=TapTool)
        taptool.callback = OpenURL(url='@jira_url')
        # NEW: customize by setting attributes
        p.title.text = 'Daily Velocity'
        p.legend.location = 'top_left'
        p.grid.grid_line_alpha = 0
        p.xaxis.axis_label = 'Days (Monday-Friday)'
        p.yaxis.axis_label = metric_legend
        p.ygrid.band_fill_color = 'olive'
        p.ygrid.band_fill_alpha = 0.1

        return p

    def build_velocity_weeks(self, stats_data):
        self.log.info('Generating graph about weekly effort')
        # Velocity Days
        x_dates = []
        y_values = []
        y_avg = []
        y_weeks = []
        daily_avg = []
        for scan_day in stats_data:
            x_dates.append(stats_data[scan_day]['datetime'])
            y_values.append(stats_data[scan_day][self.config.get_config_value('stats_metric')])
            y_weeks.append(stats_data[scan_day]['weektxt'])
            daily_avg.append(round(float(stats_data[scan_day][self.config.get_config_value('stats_metric')]) / 5, 1))
            if len(stats_data[scan_day]['stats']) > 0:
                week_idx = '4'
                if week_idx not in stats_data[scan_day]['stats']:
                    week_idx = 4
                if 'avg' in stats_data[scan_day]['stats'][week_idx]:
                    y_avg.append(stats_data[scan_day]['stats'][week_idx]['avg'])
                else:
                    y_avg.append(0)
        # prepare some data
        data_dates = np.array(x_dates, dtype=np.datetime64)
        source = ColumnDataSource(
            data=dict(
                x_data_dates=data_dates,
                y_values=y_values,
                y_avg_values=y_avg,
                y_weeks=y_weeks,
                daily_avg=daily_avg,
            )
        )
        # Declare tools
        hover = HoverTool(
            tooltips=[
                ('Week', '@y_weeks'),
                (self.config.get_config_value('stats_metric').capitalize(), '@y_values'),
                ('Avg ' + self.config.get_config_value('stats_metric') + ' per week', '@y_avg_values'),
                ('Avg ' + self.config.get_config_value('stats_metric') + ' per day', '@daily_avg'),
            ]
        )
        # create a new plot with a a datetime axis type
        p = figure(width=1000, height=350, x_axis_type='datetime', tools=['save', 'pan', 'box_zoom', 'reset', hover])
        if self.config.get_config_value('stats_metric') == 'tickets':
            metric_legend = 'Tickets'
        else:
            metric_legend = 'Story Points'
        # add renderers
        p.circle('x_data_dates', 'y_values', size=4, color='red', alpha=0.4, legend=metric_legend, source=source)
        p.line('x_data_dates', 'y_avg_values', color='blue', legend='4 Weeks Average', source=source)
        # NEW: customize by setting attributes
        p.title.text = 'Weekly Velocity'
        p.legend.location = 'top_left'
        p.grid.grid_line_alpha = 0
        p.xaxis.axis_label = 'Weeks'
        p.yaxis.axis_label = metric_legend
        p.ygrid.band_fill_color = 'olive'
        p.ygrid.band_fill_alpha = 0.1

        return p

    def get_points_per_type(self, stats_data):
        plot_values = []
        for scan_day in stats_data:
            total_metric = stats_data[scan_day][self.config.get_config_value('stats_metric')]
            for assignee in stats_data[scan_day]['types']:
                plot_values.append([
                    stats_data[scan_day]['types'][assignee]['type']
                    , stats_data[scan_day]['types'][assignee][self.config.get_config_value('stats_metric')]
                    , self.build_url_remaining('type', stats_data[scan_day]['types'][assignee]['type'])
                ])
            break
        return plot_values, total_metric

    @staticmethod
    def get_remaining_source(dat):
        source = ColumnDataSource(dict(
            x_values=dat.entity
            , y_values=dat.value
            , jira_url=dat.jira_url
        ))
        return source

    def chart_remaining_types(self, stats_data):
        self.log.info('Generating graph about remaining effort per ticket type')
        plot_values, total_metric = self.get_points_per_type(stats_data)
        dat = pd.DataFrame(plot_values, columns=['entity', 'value', 'jira_url'])
        source = self.get_remaining_source(dat)
        # Declare tools
        hover = HoverTool(
            tooltips=[
                ('Type:', '@x_values'),
                ('Points:', '@y_values')
            ]
        )
        plot = figure(
            plot_width=600
            , plot_height=300
            , x_axis_label='Ticket Type'
            , y_axis_label=self.config.get_config_value('stats_metric')
            , title='Remaining ' + self.config.get_config_value('stats_metric') + ' per Ticket Type (Total: ' + str(
                total_metric) + ')'
            , x_range=FactorRange(factors=list(dat.entity))
            , tools=['save', 'pan', 'box_zoom', 'reset', hover, 'tap']
        )
        taptool = plot.select(type=TapTool)
        taptool.callback = OpenURL(url='@jira_url')
        plot.vbar(source=source, x='x_values', top='y_values', bottom=0, width=0.3, color='green')
        return plot

    def get_points_per_assignee(self, stats_data):
        plot_values = []
        for scan_day in stats_data:
            total_metric = stats_data[scan_day][self.config.get_config_value('stats_metric')]
            for assignee in stats_data[scan_day]['assignees']:
                plot_values.append([
                    stats_data[scan_day]['assignees'][assignee]['displayName']
                    , stats_data[scan_day]['assignees'][assignee][self.config.get_config_value('stats_metric')]
                    , self.build_url_remaining('assignee', stats_data[scan_day]['assignees'][assignee]['displayName'])
                ])
            break
        return plot_values, total_metric

    def chart_remaining_assignees(self, stats_data):
        self.log.info('Generating graph about remaining effort per assignee')
        plot_values, total_metric = self.get_points_per_assignee(stats_data)

        dat = pd.DataFrame(plot_values, columns=['entity', 'value', 'jira_url'])
        source = self.get_remaining_source(dat)
        # Declare tools
        hover = HoverTool(
            tooltips=[
                ('Assignee:', '@x_values'),
                ('Points:', '@y_values')
            ]
        )
        plot = figure(
            plot_width=600
            , plot_height=300
            , x_axis_label='Assignee'
            , y_axis_label=self.config.get_config_value('stats_metric')
            , title='Remaining ' + self.config.get_config_value('stats_metric') + ' per Jira Assignee (Total: ' + str(
                total_metric) + ')'
            , x_range=FactorRange(factors=list(dat.entity))
            , tools=['save', 'pan', 'box_zoom', 'reset', hover, 'tap']
        )
        taptool = plot.select(type=TapTool)
        taptool.callback = OpenURL(url='@jira_url')

        plot.vbar(source=source, x='x_values', top='y_values', bottom=0, width=0.3, color='blue')
        return plot

    def chart_remaining_days(self, stats_data):
        self.log.info('Generating graph about estimated remaining days')
        plot_values = []
        for scan_day in stats_data:
            for rol_avg in stats_data[scan_day]['days_to_completion']:
                if rol_avg == 'current':
                    col_txt = 'This Week'
                elif rol_avg == 'all':
                    col_txt = 'All Time'
                else:
                    col_txt = str(rol_avg) + ' Weeks'
                plot_values.append([
                    col_txt
                    , stats_data[scan_day]['days_to_completion'][rol_avg]
                ])
            break
        dat = pd.DataFrame(plot_values, columns=['entity', 'value'])
        source = ColumnDataSource(dict(
            x_values=dat.entity
            , y_values=dat.value
        ))
        # Declare tools
        hover = HoverTool(
            tooltips=[
                ('Estimated based on:', '@x_values'),
                ('Days to completion:', '@y_values')
            ]
        )
        plot = figure(
            plot_width=600
            , plot_height=300
            , x_axis_label='Estimations calculated based on'
            , y_axis_label='Remaining days'
            , title='Estimated days to completion'
            , x_range=FactorRange(factors=list(dat.entity))
            , tools=['save', 'pan', 'box_zoom', 'reset', hover]
        )
        plot.vbar(source=source, x='x_values', top='y_values', bottom=0, width=0.3, color='red')
        return plot

    def main(self, stats_days, stats_weeks, stats_remaining):
        days_chart = self.build_velocity_days(stats_days)
        weeks_chart = self.build_velocity_weeks(stats_weeks)

        remaining_types = self.chart_remaining_types(stats_remaining)
        remaining_assignees = self.chart_remaining_assignees(stats_remaining)
        remaining_days = self.chart_remaining_days(stats_remaining)

        bokeh_layout = layout([
            [days_chart]
            , [weeks_chart]
            , [remaining_types, remaining_assignees, remaining_days]
        ], sizing_mode='stretch_both')

        # output to static HTML file
        output_file(self.config.filepath_charts + 'index.html',
                    title='[' + self.time.get_current_date().strftime(
                        "%Y-%m-%d") + '] - Jira Metrics, built on: ' + self.time.get_current_date().strftime(
                        "%Y-%m-%d"))
        save(bokeh_layout)
        show(bokeh_layout)
Ejemplo n.º 5
0
class PublishGithubPage(object):
    """
    This class publish the chart to github pages
    """
    def __init__(self, log, config):
        self.log = log
        self.config = config

        self.github = Github(self.log, self.config)
        self.time = Time(self.log, self.config)

    def main(self):
        """This hacky function publishes chart to github pages"""
        self.log.info('Publishing Website')
        self.log.info('Git local path: ' +
                      self.config.get_config_value('git_localpath'))
        self.config.set_config_value(
            'git_localpath',
            Files.prep_path(self.config.get_config_value('git_localpath')))

        if not self.github.is_folder_git_repo():
            self.log.info('Cloning Git Repository: ' +
                          self.config.get_config_value('git_repo'))
            self.github.git_clone()
        else:
            self.log.info(
                'Repository already available on local filesystem: ' +
                self.config.get_config_value('git_repo'))
            if self.github.git_pull() is not True:
                self.log.info('Git Stash')
                self.github.git_stash()

        # Test if requested branch exists, if not, create
        # Then switch to that branch
        remote_branches = self.github.get_branches()
        if not any(
                self.config.get_config_value('git_branch') in s
                for s in remote_branches):
            # Init branch
            self.log.warning(
                'The remote repository does not contain branch: ' +
                self.config.get_config_value('git_branch'))
            self.github.git_checkout_create(
                self.config.get_config_value('git_branch'))
            self.github.git_pull_branch(
                self.config.get_config_value('git_branch'), True)
            self.github.git_push_branch(
                self.config.get_config_value('git_branch'))
        elif any('* ' + self.config.get_config_value('git_branch') in s
                 for s in remote_branches):
            self.log.info('Local repository already selected branch: ' +
                          self.config.get_config_value('git_branch'))
        else:
            self.log.info('Switching to branch: ' +
                          self.config.get_config_value('git_branch'))
            self.github.git_checkout_f(
                self.config.get_config_value('git_branch'))

        # Next do one last pull and copy content
        self.github.git_stash()

        # Then copy chart to requested directory
        if not os.path.isfile(self.config.filepath_charts + 'index.html'):
            self.log.error('Unable to locate source chart file at: ' +
                           self.config.filepath_charts + 'index.html')
            exit()

        dst_path = Files.prep_path(
            self.config.get_config_value('git_localpath') +
            self.config.get_config_value('git_pathdirectory'))
        self.log.info('Preparing to copy chart file to: ' + dst_path)
        shutil.copyfile(self.config.filepath_charts + 'index.html',
                        dst_path + 'index.html')
        self.log.info('Chart file copied')
        self.github.git_add('--all')
        self.github.git_commit('Copied chart file - ' +
                               self.time.get_current_date().isoformat())
        self.github.git_push()
        self.log.info('Chart pushed to: ' +
                      self.config.get_config_value('git_pageurl'))
Ejemplo n.º 6
0
    def __init__(self, log, config):
        self.log = log
        self.config = config

        self.github = Github(self.log, self.config)
        self.time = Time(self.log, self.config)
Ejemplo n.º 7
0
 def __init__(self, log, app_config):
     self.log = log
     self.config = Config(self.log)
     self.time = Time(self.log, self.config)
     self.log_config = LogConfig(self.log, app_config,
                                 self.config.config_path + 'load.log')
Ejemplo n.º 8
0
 def test_get_current_date(self):
     answer_date = datetime.now(
         pytz.timezone('America/Toronto')).strftime('%Y-%m-%d')
     response_date = Time.get_current_date().strftime('%Y-%m-%d')
     self.assertEqual(answer_date, response_date)
Ejemplo n.º 9
0
 def __init__(self, log, app_config, single_config='config.yml'):
     self.log = log
     self.config = Config(self.log, config_filename=single_config)
     self.time = Time(self.log, self.config)
     self.log_config = LogConfig(self.log, app_config,
                                 self.config.config_path + 'load.log')