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 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)
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)
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)