def _build_row(self, row, exited, data_dict):
        try:
            data_dict[row.contract_id]["stages"].append(
                {
                    "name": row.stage_name,
                    "id": row.stage_id,
                    "entered": localize_datetime(row.entered).isoformat(),
                    "exited": localize_datetime(exited).isoformat(),
                    "seconds": max([(exited - row.entered).total_seconds(), 0]),
                }
            )
        except KeyError:
            data_dict[row.contract_id] = {
                "description": row.description,
                "email": row.email,
                "department": row.department,
                "contract_id": row.contract_id,
                "stages": [
                    {
                        "name": row.stage_name,
                        "id": row.stage_id,
                        "entered": localize_datetime(row.entered).isoformat(),
                        "exited": localize_datetime(exited).isoformat(),
                        "seconds": max([(exited - row.entered).total_seconds(), 0]),
                    }
                ],
            }

        return data_dict
    def reshape_metrics_granular(self, enter_and_exit=False):
        '''Transform long data from database into wide data for consumption

        Take in a result set (list of tuples), return a dictionary of results.
        The key for the dictionary is the contract id, and the values are a list
        of (fieldname, value). Metadata (common to all rows) is listed first, and
        timing information from each stage is listed afterwords. Sorting is assumed
        to be done on the database layer

        Arguments:
            enter_and_exit: A boolean option of whether to add both the
                enter and exit times to the results list

        Returns:
            * Results - a dictionary of lists which can be used to generate
              a .csv or .tsv file to be downloaded by the client
            * Headers - A list of strings which can be used to create the
              headers for the downloadable file
        '''
        raw_data = self.get_metrics_csv_data()
        results = defaultdict(list)
        headers = []

        for ix, row in enumerate(raw_data):
            if ix == 0:
                headers.extend([
                    'item_number', 'description', 'assigned_to', 'department'
                ])

            # if this is a new contract row, append metadata
            if len(results[row.contract_id]) == 0:
                results[row.contract_id].extend([
                    row.contract_id,
                    row.description,
                    row.email,
                    row.department,
                ])

            # append the stage date data
            if enter_and_exit and row.exited:
                results[row.contract_id].extend([
                    localize_datetime(row.exited),
                    localize_datetime(row.entered)
                ])
                if row.stage_name + '_exit' not in headers:
                    headers.append(row.stage_name.replace(' ', '_') + '_exit')
                    headers.append(row.stage_name.replace(' ', '_') + '_enter')
            else:
                results[row.contract_id].extend(
                    [localize_datetime(row.exited)])

                if row.stage_name not in headers:
                    headers.append(row.stage_name)

        return results, headers
    def reshape_metrics_granular(self, enter_and_exit=False):
        '''Transform long data from database into wide data for consumption

        Take in a result set (list of tuples), return a dictionary of results.
        The key for the dictionary is the contract id, and the values are a list
        of (fieldname, value). Metadata (common to all rows) is listed first, and
        timing information from each stage is listed afterwords. Sorting is assumed
        to be done on the database layer

        Arguments:
            enter_and_exit: A boolean option of whether to add both the
                enter and exit times to the results list

        Returns:
            * Results - a dictionary of lists which can be used to generate
              a .csv or .tsv file to be downloaded by the client
            * Headers - A list of strings which can be used to create the
              headers for the downloadable file
        '''
        raw_data = self.get_metrics_csv_data()
        results = defaultdict(list)
        headers = []

        for ix, row in enumerate(raw_data):
            if ix == 0:
                headers.extend(['item_number', 'description', 'assigned_to', 'department'])

            # if this is a new contract row, append metadata
            if len(results[row.contract_id]) == 0:
                results[row.contract_id].extend([
                    row.contract_id,
                    row.description,
                    row.email,
                    row.department,
                ])

            # append the stage date data
            if enter_and_exit and row.exited:
                results[row.contract_id].extend([
                    localize_datetime(row.exited),
                    localize_datetime(row.entered)
                ])
                if row.stage_name + '_exit' not in headers:
                    headers.append(row.stage_name.replace(' ', '_') + '_exit')
                    headers.append(row.stage_name.replace(' ', '_') + '_enter')
            else:
                results[row.contract_id].extend([
                    localize_datetime(row.exited)
                ])

                if row.stage_name not in headers:
                    headers.append(row.stage_name)

        return results, headers
    def reshape_metrics_granular(self, enter_and_exit=False, flat=True):
        '''Transform long data from database into wide data for consumption

        Take in a result set (list of tuples), return a dictionary of results.
        The key for the dictionary is the contract id, and the values are a list
        of (fieldname, value). Metadata (common to all rows) is listed first, and
        timing information from each stage is listed afterwords. Sorting is assumed
        to be done on the database layer
        '''
        raw_data = self.get_metrics_csv_data()
        results = defaultdict(list)
        headers = []

        for ix, row in enumerate(raw_data):
            if ix == 0:
                headers.extend(['item_number', 'description', 'assigned_to', 'department'])

            # if this is a new contract row, append metadata
            if len(results[row.contract_id]) == 0:
                results[row.contract_id].extend([
                    row.contract_id,
                    row.description,
                    row.email,
                    row.department,
                ])

            # append the stage date data
            if enter_and_exit and row.exited:
                results[row.contract_id].extend([
                    localize_datetime(row.exited),
                    localize_datetime(row.entered)
                ])
                if row.stage_name + '_exit' not in headers:
                    headers.append(row.stage_name.replace(' ', '_') + '_exit')
                    headers.append(row.stage_name.replace(' ', '_') + '_enter')
            else:
                results[row.contract_id].extend([
                    localize_datetime(row.exited)
                ])

                if row.stage_name not in headers:
                    headers.append(row.stage_name)

        return results, headers
    def _build_row(self, row, exited, data_dict):
        try:
            data_dict[row.contract_id]['stages'].append({
                'name':
                row.stage_name,
                'id':
                row.stage_id,
                'entered':
                localize_datetime(row.entered).isoformat(),
                'exited':
                localize_datetime(exited).isoformat(),
                'seconds':
                max([(exited - row.entered).total_seconds(), 0]),
            })
        except KeyError:
            data_dict[row.contract_id] = {
                'description':
                row.description,
                'email':
                row.email,
                'department':
                row.department,
                'contract_id':
                row.contract_id,
                'stages': [{
                    'name':
                    row.stage_name,
                    'id':
                    row.stage_id,
                    'entered':
                    localize_datetime(row.entered).isoformat(),
                    'exited':
                    localize_datetime(exited).isoformat(),
                    'seconds':
                    max([(exited - row.entered).total_seconds(), 0]),
                }]
            }

        return data_dict
    def _build_row(self, row, exited, data_dict):
        try:
            data_dict[row.contract_id]['stages'].append({
                'name': row.stage_name, 'id': row.stage_id,
                'entered': localize_datetime(row.entered).isoformat(),
                'exited': localize_datetime(exited).isoformat(),
                'seconds': max([(exited - row.entered).total_seconds(), 0]),
            })
        except KeyError:
            data_dict[row.contract_id] = {
                'description': row.description,
                'email': row.email,
                'department': row.department,
                'contract_id': row.contract_id,
                'stages': [{
                    'name': row.stage_name, 'id': row.stage_id,
                    'entered': localize_datetime(row.entered).isoformat(),
                    'exited': localize_datetime(exited).isoformat(),
                    'seconds': max([(exited - row.entered).total_seconds(), 0]),
                }]
            }

        return data_dict
def start_work(contract_id=-1):
    contract = ContractBase.query.get(contract_id)

    if contract:
        first_stage = contract.get_first_stage()

        if first_stage and first_stage.stage_id != contract.current_stage_id:
            return redirect(url_for('conductor.detail', contract_id=contract.id))
        elif first_stage:
            contract.start = localize_datetime(contract.get_first_stage().entered)
    else:
        contract = ContractBase()
    form = NewContractForm(obj=contract)

    if form.validate_on_submit():
        if contract_id == -1:
            contract, _ = get_or_create(
                db.session, ContractBase, description=form.data.get('description'),
                department=form.data.get('department'), is_visible=False
            )
        else:
            contract = ContractBase.clone(contract)
            contract.description = form.data.get('description')
            contract.department = form.data.get('department')
            db.session.add(contract)
            db.session.commit()

        assigned = assign_a_contract(
            contract, form.data.get('flow'), form.data.get('assigned'),
            start_time=form.data.get('start').astimezone(pytz.UTC),
            clone=False
        )
        db.session.commit()

        if assigned:
            flash('Successfully assigned {} to {}!'.format(assigned.description, assigned.assigned.email), 'alert-success')
            return redirect(url_for('conductor.detail', contract_id=assigned.id))
        else:
            flash("That flow doesn't exist!", 'alert-danger')
    return render_template('conductor/new.html', form=form, contract_id=contract_id, contract=contract)
Example #8
0
def start_work(contract_id=-1):
    '''Start work on a contract

    When new work is started on a contract, a new contract is created
    (either as a clone of an existing contract, or as brand new work),
    assigned to the :py:class:`~purchasing.data.flows.Flow` and
    :py:class:`~purchasing.users.models.User` indicated in the
    :py:class:`~purchasing.conductor.forms.NewContractForm`

    :param contract_id: Primary key ID for a
        :py:class:`~purchasing.data.contracts.ContractBase`

    .. seealso::
        :py:meth:`~purchasing.data.contracts.ContractBase.clone` for
        more information on how new contracts get started, and
        :py:class:`~purchasing.conductor.forms.NewContractForm` for more
        on the form to start new work.

    :status 200: Render the new contract view
    :status 302: Create or start new work on a cloned contract
    '''
    contract = ContractBase.query.get(contract_id)

    if contract:
        first_stage = contract.get_first_stage()

        if first_stage and first_stage.stage_id != contract.current_stage_id:
            return redirect(
                url_for('conductor.detail', contract_id=contract.id))
        elif first_stage:
            contract.start = localize_datetime(
                contract.get_first_stage().entered)
    else:
        contract = ContractBase()
    form = NewContractForm(obj=contract)

    if form.validate_on_submit():
        if contract_id == -1:
            contract, _ = get_or_create(
                db.session,
                ContractBase,
                description=form.data.get('description'),
                department=form.data.get('department'),
                is_visible=False)
        elif not first_stage:
            contract = ContractBase.clone(contract)
            contract.description = form.data.get('description')
            contract.department = form.data.get('department')
            db.session.add(contract)
            db.session.commit()

        assigned = assign_a_contract(
            contract,
            form.data.get('flow'),
            form.data.get('assigned'),
            start_time=form.data.get('start').astimezone(
                pytz.UTC).replace(tzinfo=None),
            clone=False)
        db.session.commit()

        if assigned:
            flash(
                'Successfully assigned {} to {}!'.format(
                    assigned.description, assigned.assigned.email),
                'alert-success')
            return redirect(
                url_for('conductor.detail', contract_id=assigned.id))
        else:
            flash("That flow doesn't exist!", 'alert-danger')
    return render_template('conductor/new.html',
                           form=form,
                           contract_id=contract_id,
                           contract=contract)
Example #9
0
def detail(contract_id, stage_id=-1):
    '''View to control an individual stage update process

    This is the primary view for conductor. All actions that users can
    take with a :py:class:`~purchasing.data.contracts.ContractBase` flow
    through here.

    .. seealso::
        There are a number of forms that are used on this page to allow
        the users to do actions directly from the management step:

        * :py:class:`~purchasing.conductor.forms.NoteForm` to allow
          users to take notes
        * :py:class:`~purchasing.conductor.forms.SendUpdateForm` to allow
          users to send email updates
        * :py:class:`~purchasing.conductor.forms.PostOpportunityForm` to allow
          users to post opportunities directly to :doc:`/beacon`
        * :py:class:`~purchasing.conductor.forms.ContractMetadataForm` to allow
          users to update a contract's metadata
        * :py:class:`~purchasing.conductor.forms.CompleteForm` to allow
          users to transition to the next :py:class:`~purchasing.data.stages.Stage`
          in the contract's :py:class:`~purchasing.data.flows.Flow`

    :param contract_id: Primary key ID for a
        :py:class:`~purchasing.data.contracts.ContractBase`
    :param stage_id: Primary key ID for a
        :py:class:`~purchasing.data.contract_stages.ContractStage`

    :status 200: Render the detail
    :status 302: Post a specific form. This form will either transition
        to the next contract stage if it is a
        :py:class:`~purchasing.conductor.forms.CompleteForm`, or perform
        whatever action is in that form's ``post_validate_action`` method
    :status 404: Contract not found
    '''
    contract = ContractBase.query.get(contract_id)
    if not contract:
        abort(404)

    if contract.completed_last_stage():
        return redirect(url_for('conductor.edit', contract_id=contract.id))

    if stage_id == -1:
        # redirect to the current stage page
        contract_stage = contract.get_current_stage()

        if contract_stage:
            return redirect(
                url_for('conductor.detail',
                        contract_id=contract_id,
                        stage_id=contract_stage.id))
        current_app.logger.warning(
            'Could not find stages for this contract, aborting!')
        abort(500)

    stages = contract.get_contract_stages()

    try:
        active_stage = [i for i in stages if i.id == stage_id][0]
        current_stage = [i for i in stages if i.entered and not i.exited][0]
        if active_stage.entered is None:
            abort(404)
    except IndexError:
        abort(404)

    note_form = NoteForm()
    update_form = SendUpdateForm(obj=UpdateFormObj(current_stage))
    opportunity_form = PostOpportunityForm(
        obj=contract.opportunity if contract.
        opportunity else ConductorToBeaconObj(contract))
    metadata_form = ContractMetadataForm(obj=ContractMetadataObj(contract))
    complete_form = CompleteForm()

    if session.get('invalid_date-{}'.format(contract_id), False):
        complete_form.errors['complete'] = session.pop(
            'invalid_date-{}'.format(contract_id))

    forms = {
        'activity': note_form,
        'update': update_form,
        'post': opportunity_form,
        'update-metadata': metadata_form
    }

    active_tab = '#activity'

    submitted_form_name = request.args.get('form', None)

    if submitted_form_name:
        submitted_form = forms[submitted_form_name]

        if submitted_form.validate_on_submit():
            action = ContractStageActionItem(
                contract_stage_id=stage_id,
                action_type=submitted_form_name,
                taken_by=current_user.id,
                taken_at=datetime.datetime.utcnow())
            action = submitted_form.post_validate_action(
                action, contract, current_stage)

            db.session.add(action)
            db.session.commit()

            return redirect(
                url_for('conductor.detail',
                        contract_id=contract_id,
                        stage_id=stage_id))
        else:
            active_tab = '#' + submitted_form_name

    actions = contract.filter_action_log()
    # actions = contract.build_complete_action_log()
    subscribers, total_subscribers = contract.build_subscribers()
    flows = Flow.query.filter(Flow.id != contract.flow_id).all()

    current_app.logger.info(
        'CONDUCTOR DETAIL VIEW - Detail view for contract {} (ID: {}), stage {}'
        .format(contract.description, contract.id, current_stage.name))

    opportunity_form.display_cleanup()
    if len(stages) > 0:
        return render_template(
            'conductor/detail.html',
            stages=stages,
            actions=actions,
            active_tab=active_tab,
            note_form=note_form,
            update_form=update_form,
            opportunity_form=opportunity_form,
            metadata_form=metadata_form,
            complete_form=complete_form,
            contract=contract,
            current_user=current_user,
            active_stage=active_stage,
            current_stage=current_stage,
            flows=flows,
            subscribers=subscribers,
            total_subscribers=total_subscribers,
            current_stage_enter=localize_datetime(current_stage.entered),
            categories=opportunity_form.get_categories(),
            subcategories=opportunity_form.get_subcategories())
    abort(404)
def start_work(contract_id=-1):
    '''Start work on a contract

    When new work is started on a contract, a new contract is created
    (either as a clone of an existing contract, or as brand new work),
    assigned to the :py:class:`~purchasing.data.flows.Flow` and
    :py:class:`~purchasing.users.models.User` indicated in the
    :py:class:`~purchasing.conductor.forms.NewContractForm`

    :param contract_id: Primary key ID for a
        :py:class:`~purchasing.data.contracts.ContractBase`

    .. seealso::
        :py:meth:`~purchasing.data.contracts.ContractBase.clone` for
        more information on how new contracts get started, and
        :py:class:`~purchasing.conductor.forms.NewContractForm` for more
        on the form to start new work.

    :status 200: Render the new contract view
    :status 302: Create or start new work on a cloned contract
    '''
    contract = ContractBase.query.get(contract_id)

    if contract:
        first_stage = contract.get_first_stage()

        if first_stage and contract.completed_last_stage():
            pass
        elif first_stage and first_stage.stage_id != contract.current_stage_id:
            return redirect(url_for('conductor.detail', contract_id=contract.id))
        elif first_stage:
            contract.start = localize_datetime(contract.get_first_stage().entered)
    else:
        contract = ContractBase()
    form = NewContractForm(obj=contract)

    if form.validate_on_submit():
        if contract_id == -1:
            contract, _ = get_or_create(
                db.session, ContractBase, description=form.data.get('description'),
                department=form.data.get('department'), is_visible=False
            )
        elif not first_stage or contract.completed_last_stage():
            contract = ContractBase.clone(contract)
            contract.description = form.data.get('description')
            contract.department = form.data.get('department')
            db.session.add(contract)
            db.session.commit()

        assigned = assign_a_contract(
            contract, form.data.get('flow'), form.data.get('assigned'),
            start_time=form.data.get('start').astimezone(pytz.UTC).replace(tzinfo=None),
            clone=False
        )
        db.session.commit()

        if assigned:
            flash('Successfully assigned {} to {}!'.format(assigned.description, assigned.assigned.email), 'alert-success')
            return redirect(url_for('conductor.detail', contract_id=assigned.id))
        else:
            flash("That flow doesn't exist!", 'alert-danger')
    return render_template('conductor/new.html', form=form, contract_id=contract_id, contract=contract)
def detail(contract_id, stage_id=-1):
    '''View to control an individual stage update process

    This is the primary view for conductor. All actions that users can
    take with a :py:class:`~purchasing.data.contracts.ContractBase` flow
    through here.

    .. seealso::
        There are a number of forms that are used on this page to allow
        the users to do actions directly from the management step:

        * :py:class:`~purchasing.conductor.forms.NoteForm` to allow
          users to take notes
        * :py:class:`~purchasing.conductor.forms.SendUpdateForm` to allow
          users to send email updates
        * :py:class:`~purchasing.conductor.forms.PostOpportunityForm` to allow
          users to post opportunities directly to :doc:`/beacon`
        * :py:class:`~purchasing.conductor.forms.ContractMetadataForm` to allow
          users to update a contract's metadata
        * :py:class:`~purchasing.conductor.forms.CompleteForm` to allow
          users to transition to the next :py:class:`~purchasing.data.stages.Stage`
          in the contract's :py:class:`~purchasing.data.flows.Flow`

    :param contract_id: Primary key ID for a
        :py:class:`~purchasing.data.contracts.ContractBase`
    :param stage_id: Primary key ID for a
        :py:class:`~purchasing.data.contract_stages.ContractStage`

    :status 200: Render the detail
    :status 302: Post a specific form. This form will either transition
        to the next contract stage if it is a
        :py:class:`~purchasing.conductor.forms.CompleteForm`, or perform
        whatever action is in that form's ``post_validate_action`` method
    :status 404: Contract not found
    '''
    contract = ContractBase.query.get(contract_id)
    if not contract:
        abort(404)

    if contract.completed_last_stage():
        return redirect(url_for('conductor.edit', contract_id=contract.id))

    if stage_id == -1:
        # redirect to the current stage page
        contract_stage = contract.get_current_stage()

        if contract_stage:
            return redirect(url_for(
                'conductor.detail', contract_id=contract_id, stage_id=contract_stage.id
            ))
        current_app.logger.warning('Could not find stages for this contract, aborting!')
        abort(500)

    stages = contract.get_contract_stages()

    try:
        active_stage = [i for i in stages if i.id == stage_id][0]
        current_stage = [i for i in stages if i.entered and not i.exited][0]
        if active_stage.entered is None:
            abort(404)
    except IndexError:
        abort(404)

    note_form = NoteForm()
    update_form = SendUpdateForm(obj=UpdateFormObj(current_stage))
    opportunity_form = PostOpportunityForm(
        obj=contract.opportunity if contract.opportunity else ConductorToBeaconObj(contract)
    )
    metadata_form = ContractMetadataForm(obj=ContractMetadataObj(contract))
    complete_form = CompleteForm()

    if session.get('invalid_date-{}'.format(contract_id), False):
        complete_form.errors['complete'] = session.pop('invalid_date-{}'.format(contract_id))

    forms = {
        'activity': note_form, 'update': update_form,
        'post': opportunity_form, 'update-metadata': metadata_form
    }

    active_tab = '#activity'

    submitted_form_name = request.args.get('form', None)

    if submitted_form_name:
        submitted_form = forms[submitted_form_name]

        if submitted_form.validate_on_submit():
            action = ContractStageActionItem(
                contract_stage_id=stage_id, action_type=submitted_form_name,
                taken_by=current_user.id, taken_at=datetime.datetime.utcnow()
            )
            action = submitted_form.post_validate_action(action, contract, current_stage)

            db.session.add(action)
            db.session.commit()

            return redirect(url_for(
                'conductor.detail', contract_id=contract_id, stage_id=stage_id
            ))
        else:
            active_tab = '#' + submitted_form_name

    actions = contract.filter_action_log()
    # actions = contract.build_complete_action_log()
    subscribers, total_subscribers = contract.build_subscribers()
    flows = Flow.query.filter(Flow.id != contract.flow_id).all()

    current_app.logger.info(
        'CONDUCTOR DETAIL VIEW - Detail view for contract {} (ID: {}), stage {}'.format(
            contract.description, contract.id, current_stage.name
        )
    )

    opportunity_form.display_cleanup()
    if len(stages) > 0:
        return render_template(
            'conductor/detail.html',
            stages=stages, actions=actions, active_tab=active_tab,
            note_form=note_form, update_form=update_form,
            opportunity_form=opportunity_form, metadata_form=metadata_form,
            complete_form=complete_form, contract=contract,
            current_user=current_user, active_stage=active_stage,
            current_stage=current_stage, flows=flows, subscribers=subscribers,
            total_subscribers=total_subscribers,
            current_stage_enter=localize_datetime(current_stage.entered),
            categories=opportunity_form.get_categories(),
            subcategories=opportunity_form.get_subcategories()
        )
    abort(404)