diff --git a/Pipfile.lock b/Pipfile.lock index b9b8cab6..7f8a00cb 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "2b149e0d8c23814a2c701b53f5c75b36714a2ccd4e2a2769924ef6e2a3f09e97" + "sha256": "5fc8273838354406366b401529a6f512a73ac6a8ecea6699afa4ab7b4996bf13" }, "pipfile-spec": 6, "requires": { @@ -271,6 +271,7 @@ "sha256:1d936da41ee06216d89fdc7ead1ee9a5da2811a8787515a976b646e110c3f622", "sha256:e4ef42e82b0b493c5849eed98b5ab49d6767caf982127e9a33167f1153b36cc5" ], + "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.3.*'", "version": "==2018.5" }, "redis": { @@ -501,6 +502,7 @@ "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" ], + "markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.2.*'", "version": "==4.3.4" }, "itsdangerous": { @@ -618,6 +620,7 @@ "sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1", "sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1" ], + "markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.2.*'", "version": "==0.7.1" }, "prompt-toolkit": { @@ -640,6 +643,7 @@ "sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7", "sha256:e31fb2767eb657cbde86c454f02e99cb846d3cd9d61b318525140214fdc0e98e" ], + "markers": "python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.2.*'", "version": "==1.5.4" }, "pygments": { @@ -689,15 +693,11 @@ }, "pyyaml": { "hashes": [ - "sha256:1cbc199009e78f92d9edf554be4fe40fb7b0bef71ba688602a00e97a51909110", "sha256:254bf6fda2b7c651837acb2c718e213df29d531eebf00edb54743d10bcb694eb", "sha256:3108529b78577327d15eec243f0ff348a0640b0c3478d67ad7f5648f93bac3e2", "sha256:3c17fb92c8ba2f525e4b5f7941d850e7a48c3a59b32d331e2502a3cdc6648e76", - "sha256:6f89b5c95e93945b597776163403d47af72d243f366bf4622ff08bdfd1c950b7", "sha256:8d6d96001aa7f0a6a4a95e8143225b5d06e41b1131044913fecb8f85a125714b", - "sha256:be622cc81696e24d0836ba71f6272a2b5767669b0d79fdcf0295d51ac2e156c8", - "sha256:c8a88edd93ee29ede719080b2be6cb2333dfee1dccba213b422a9c8e97f2967b", - "sha256:f39411e380e2182ad33be039e8ee5770a5d9efe01a2bfb7ae58d9ba31c4a2a9d" + "sha256:c8a88edd93ee29ede719080b2be6cb2333dfee1dccba213b422a9c8e97f2967b" ], "version": "==4.2b4" }, diff --git a/alembic/versions/05d6272bdb43_rename_request_creator_.py b/alembic/versions/05d6272bdb43_rename_request_creator_.py new file mode 100644 index 00000000..c8a3966e --- /dev/null +++ b/alembic/versions/05d6272bdb43_rename_request_creator_.py @@ -0,0 +1,43 @@ +"""rename request creator + +Revision ID: 05d6272bdb43 +Revises: 77b065750596 +Create Date: 2018-08-07 20:21:22.559283 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '05d6272bdb43' +down_revision = '77b065750596' +branch_labels = None +depends_on = None + + +def upgrade(): + db = op.get_bind() + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('requests', sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=True)) + op.create_foreign_key('requests_user_id_fk', 'requests', 'users', ['user_id'], ['id']) + # ### end Alembic commands ### + + db.execute("UPDATE requests SET user_id = creator") + + op.alter_column('requests', 'user_id', nullable=False) + op.drop_column('requests', 'creator') + + + +def downgrade(): + db = op.get_bind() + + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('requests', sa.Column('creator', postgresql.UUID(), autoincrement=False, nullable=True)) + op.drop_constraint('requests_user_id_fk', 'requests', type_='foreignkey') + # ### end Alembic commands ### + + db.execute("UPDATE requests SET creator = user_id") + op.drop_column('requests', 'user_id') diff --git a/alembic/versions/4ea5917e7781_add_default_atat_role.py b/alembic/versions/4ea5917e7781_add_default_atat_role.py index 78b6ef55..21b03166 100644 --- a/alembic/versions/4ea5917e7781_add_default_atat_role.py +++ b/alembic/versions/4ea5917e7781_add_default_atat_role.py @@ -34,6 +34,4 @@ def upgrade(): def downgrade(): - db = op.get_bind() - db.execute("DELETE FROM roles WHERE name = 'default'") - + pass diff --git a/alembic/versions/96a9f3537996_add_roles_and_permissions.py b/alembic/versions/96a9f3537996_add_roles_and_permissions.py index 4380208a..0729127e 100644 --- a/alembic/versions/96a9f3537996_add_roles_and_permissions.py +++ b/alembic/versions/96a9f3537996_add_roles_and_permissions.py @@ -169,15 +169,4 @@ def upgrade(): def downgrade(): - db = op.get_bind() - db.execute(""" - DELETE FROM roles - WHERE name IN ( - 'ccpo', - 'owner', - 'admin', - 'developer', - 'billing_auditor', - 'security_auditor' - ); - """) + pass diff --git a/atst/domain/requests.py b/atst/domain/requests.py index 891266ed..85fb688a 100644 --- a/atst/domain/requests.py +++ b/atst/domain/requests.py @@ -31,8 +31,8 @@ class Requests(object): AUTO_APPROVE_THRESHOLD = 1000000 @classmethod - def create(cls, creator_id, body): - request = Request(creator=creator_id, body=body) + def create(cls, creator, body): + request = Request(creator=creator, body=body) request = Requests.set_status(request, RequestStatus.STARTED) db.session.add(request) @@ -41,11 +41,11 @@ class Requests(object): return request @classmethod - def exists(cls, request_id, creator_id): + def exists(cls, request_id, creator): try: return db.session.query( exists().where( - and_(Request.id == request_id, Request.creator == creator_id) + and_(Request.id == request_id, Request.creator == creator) ) ).scalar() except exc.DataError: @@ -61,10 +61,10 @@ class Requests(object): return request @classmethod - def get_many(cls, creator_id=None): + def get_many(cls, creator=None): filters = [] - if creator_id: - filters.append(Request.creator == creator_id) + if creator: + filters.append(Request.creator == creator) requests = ( db.session.query(Request) diff --git a/atst/domain/users.py b/atst/domain/users.py index 54cf4ae0..bc3d972f 100644 --- a/atst/domain/users.py +++ b/atst/domain/users.py @@ -37,6 +37,7 @@ class Users(object): db.session.add(user) db.session.commit() except IntegrityError: + db.session.rollback() raise AlreadyExistsError("user") return user diff --git a/atst/models/request.py b/atst/models/request.py index 66e5bf1e..bd14b5db 100644 --- a/atst/models/request.py +++ b/atst/models/request.py @@ -1,6 +1,6 @@ -from sqlalchemy import Column, func +from sqlalchemy import Column, func, ForeignKey from sqlalchemy.types import DateTime -from sqlalchemy.dialects.postgresql import JSONB, UUID +from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import relationship from atst.models import Base @@ -11,13 +11,19 @@ class Request(Base): __tablename__ = "requests" id = Id() - creator = Column(UUID(as_uuid=True)) time_created = Column(DateTime(timezone=True), server_default=func.now()) body = Column(JSONB) status_events = relationship( "RequestStatusEvent", backref="request", order_by="RequestStatusEvent.sequence" ) + user_id = Column(ForeignKey("users.id"), nullable=False) + creator = relationship("User") + @property def status(self): return self.status_events[-1].new_status + + @property + def status_displayname(self): + return self.status_events[-1].displayname diff --git a/atst/models/request_status_event.py b/atst/models/request_status_event.py index 2e492ac4..81505f2a 100644 --- a/atst/models/request_status_event.py +++ b/atst/models/request_status_event.py @@ -9,12 +9,12 @@ from atst.models.types import Id class RequestStatus(Enum): - STARTED = "started" - PENDING_FINANCIAL_VERIFICATION = "pending_financial_verification" - PENDING_CCPO_APPROVAL = "pending_ccpo_approval" - APPROVED = "approved" - EXPIRED = "expired" - DELETED = "deleted" + STARTED = "Started" + PENDING_FINANCIAL_VERIFICATION = "Pending Financial Verification" + PENDING_CCPO_APPROVAL = "Pending CCPO Approval" + APPROVED = "Approved" + EXPIRED = "Expired" + DELETED = "Deleted" class RequestStatusEvent(Base): @@ -29,3 +29,7 @@ class RequestStatusEvent(Base): sequence = Column( BigInteger, Sequence("request_status_events_sequence_seq"), nullable=False ) + + @property + def displayname(self): + return self.new_status.value diff --git a/atst/routes/dev.py b/atst/routes/dev.py index cbd02cea..f66e3f08 100644 --- a/atst/routes/dev.py +++ b/atst/routes/dev.py @@ -9,50 +9,56 @@ _DEV_USERS = { "dod_id": "1234567890", "first_name": "Sam", "last_name": "Seeceepio", - "atat_role": "ccpo", + "atat_role_name": "ccpo", + "email": "sam@test.com" }, "amanda": { "dod_id": "2345678901", "first_name": "Amanda", "last_name": "Adamson", - "atat_role": "default", + "atat_role_name": "default", + "email": "amanda@test.com" }, "brandon": { "dod_id": "3456789012", "first_name": "Brandon", "last_name": "Buchannan", - "atat_role": "default", + "atat_role_name": "default", + "email": "brandon@test.com" }, "christina": { "dod_id": "4567890123", "first_name": "Christina", "last_name": "Collins", - "atat_role": "default", + "atat_role_name": "default", + "email": "christina@test.com" }, "dominick": { "dod_id": "5678901234", "first_name": "Dominick", "last_name": "Domingo", - "atat_role": "default", + "atat_role_name": "default", + "email": "dominick@test.com" }, "erica": { "dod_id": "6789012345", "first_name": "Erica", "last_name": "Eichner", - "atat_role": "default", + "atat_role_name": "default", + "email": "erica@test.com" }, } - @bp.route("/login-dev") def login_dev(): role = request.args.get("username", "amanda") user_data = _DEV_USERS[role] - basic_data = {k:v for k,v in user_data.items() if k not in ["dod_id", "atat_role"]} - user = _set_user_permissions(user_data["dod_id"], user_data["atat_role"], basic_data) + user = Users.get_or_create_by_dod_id( + user_data["dod_id"], + atat_role_name=user_data["atat_role_name"], + first_name=user_data["first_name"], + last_name=user_data["last_name"], + email=user_data["email"] + ) session["user_id"] = user.id return redirect(url_for("atst.home")) - - -def _set_user_permissions(dod_id, role, user_data): - return Users.get_or_create_by_dod_id(dod_id, atat_role_name=role, **user_data) diff --git a/atst/routes/requests/index.py b/atst/routes/requests/index.py index 7e098b47..ec9b566c 100644 --- a/atst/routes/requests/index.py +++ b/atst/routes/requests/index.py @@ -5,31 +5,29 @@ from . import requests_bp from atst.domain.requests import Requests -def map_request(user, request): +def map_request(request): time_created = pendulum.instance(request.time_created) is_new = time_created.add(days=1) > pendulum.now() + app_count = request.body.get("details_of_use", {}).get("num_software_systems", 0) return { "order_id": request.id, "is_new": is_new, - "status": request.status, - "app_count": 1, + "status": request.status_displayname, + "app_count": app_count, "date": time_created.format("M/DD/YYYY"), - "full_name": user.full_name + "full_name": request.creator.full_name, } @requests_bp.route("/requests", methods=["GET"]) def requests_index(): requests = [] - if ( - "review_and_approve_jedi_workspace_request" - in g.current_user.atat_permissions - ): + if "review_and_approve_jedi_workspace_request" in g.current_user.atat_permissions: requests = Requests.get_many() else: - requests = Requests.get_many(creator_id=g.current_user.id) + requests = Requests.get_many(creator=g.current_user) - mapped_requests = [map_request(g.current_user, r) for r in requests] + mapped_requests = [map_request(r) for r in requests] return render_template("requests.html", requests=mapped_requests) diff --git a/atst/routes/requests/jedi_request_flow.py b/atst/routes/requests/jedi_request_flow.py index e750a9e0..af08251b 100644 --- a/atst/routes/requests/jedi_request_flow.py +++ b/atst/routes/requests/jedi_request_flow.py @@ -124,5 +124,5 @@ class JEDIRequestFlow(object): if self.request_id: Requests.update(self.request_id, request_data) else: - request = Requests.create(self.current_user.id, request_data) + request = Requests.create(self.current_user, request_data) self.request_id = request.id diff --git a/atst/routes/requests/requests_form.py b/atst/routes/requests/requests_form.py index ad415775..d384abdf 100644 --- a/atst/routes/requests/requests_form.py +++ b/atst/routes/requests/requests_form.py @@ -111,7 +111,7 @@ def requests_submit(request_id=None): def _check_can_view_request(request_id): if Permissions.REVIEW_AND_APPROVE_JEDI_WORKSPACE_REQUEST in g.current_user.atat_permissions: pass - elif Requests.exists(request_id, g.current_user.id): + elif Requests.exists(request_id, g.current_user): pass else: raise UnauthorizedError(g.current_user, "view request {}".format(request_id)) diff --git a/script/seed.py b/script/seed.py new file mode 100644 index 00000000..d865b9d7 --- /dev/null +++ b/script/seed.py @@ -0,0 +1,37 @@ +# Add root project dir to the python path +import os +import sys + +parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +sys.path.append(parent_dir) + +from atst.app import make_config, make_app +from atst.domain.users import Users +from atst.domain.requests import Requests +from atst.domain.exceptions import AlreadyExistsError +from tests.factories import RequestFactory +from atst.routes.dev import _DEV_USERS as DEV_USERS + + +def seed_db(): + users = [] + for dev_user in DEV_USERS.values(): + try: + user = Users.create(**dev_user) + users.append(user) + except AlreadyExistsError: + pass + + for user in users: + for dollar_value in [1, 200, 3000, 40000, 500000, 1000000]: + request = Requests.create( + user, RequestFactory.build_request_body(user, dollar_value) + ) + Requests.submit(request) + + +if __name__ == "__main__": + config = make_config() + app = make_app(config) + with app.app_context(): + seed_db() diff --git a/tests/conftest.py b/tests/conftest.py index c3add296..ab912679 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,6 @@ import alembic.command from atst.app import make_app, make_config from atst.database import db as _db -from .mocks import MOCK_USER import tests.factories as factories @@ -75,7 +74,6 @@ class DummyForm(dict): class DummyField(object): - def __init__(self, data=None, errors=(), raw_data=None): self.data = data self.errors = list(errors) @@ -93,9 +91,11 @@ def dummy_field(): @pytest.fixture -def user_session(monkeypatch): - - def set_user_session(user=MOCK_USER): - monkeypatch.setattr("atst.domain.auth.get_current_user", lambda *args: user) +def user_session(monkeypatch, session): + def set_user_session(user=None): + monkeypatch.setattr( + "atst.domain.auth.get_current_user", + lambda *args: user or factories.UserFactory.build(), + ) return set_user_session diff --git a/tests/domain/test_requests.py b/tests/domain/test_requests.py index d82c8ce4..ebdc64a0 100644 --- a/tests/domain/test_requests.py +++ b/tests/domain/test_requests.py @@ -25,7 +25,7 @@ def test_nonexistent_request_raises(): def test_new_request_has_started_status(): - request = Requests.create(uuid4(), {}) + request = Requests.create(UserFactory.build(), {}) assert request.status == RequestStatus.STARTED @@ -53,6 +53,6 @@ def test_dont_auto_approve_if_no_dollar_value_specified(new_request): def test_exists(session): user_allowed = UserFactory.create() user_denied = UserFactory.create() - request = RequestFactory.create(creator=user_allowed.id) - assert Requests.exists(request.id, user_allowed.id) - assert not Requests.exists(request.id, user_denied.id) + request = RequestFactory.create(creator=user_allowed) + assert Requests.exists(request.id, user_allowed) + assert not Requests.exists(request.id, user_denied) diff --git a/tests/factories.py b/tests/factories.py index 12c41fd4..cf81fa71 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -13,31 +13,6 @@ from atst.models.request_status_event import RequestStatusEvent from atst.domain.roles import Roles -class RequestStatusFactory(factory.alchemy.SQLAlchemyModelFactory): - class Meta: - model = RequestStatusEvent - - -class RequestFactory(factory.alchemy.SQLAlchemyModelFactory): - class Meta: - model = Request - - id = factory.Sequence(lambda x: uuid4()) - status_events = factory.RelatedFactory( - RequestStatusFactory, "request", new_status=RequestStatus.STARTED - ) - body = {} - - -class PENumberFactory(factory.alchemy.SQLAlchemyModelFactory): - class Meta: - model = PENumber - - -class TaskOrderFactory(factory.alchemy.SQLAlchemyModelFactory): - class Meta: - model = TaskOrder - class RoleFactory(factory.alchemy.SQLAlchemyModelFactory): class Meta: @@ -64,3 +39,64 @@ class RequestStatusEventFactory(factory.alchemy.SQLAlchemyModelFactory): model = RequestStatusEvent id = factory.Sequence(lambda x: uuid4()) + + +class RequestFactory(factory.alchemy.SQLAlchemyModelFactory): + class Meta: + model = Request + + id = factory.Sequence(lambda x: uuid4()) + status_events = factory.RelatedFactory( + RequestStatusEventFactory, "request", new_status=RequestStatus.STARTED + ) + creator = factory.SubFactory(UserFactory) + body = factory.LazyAttribute(lambda r: RequestFactory.build_request_body(r.creator)) + + @classmethod + def build_request_body(cls, user, dollar_value=1000000): + return { + "primary_poc": { + "dodid_poc": user.dod_id, + "email_poc": user.email, + "fname_poc": user.first_name, + "lname_poc": user.last_name + }, + "details_of_use": { + "jedi_usage": "adf", + "start_date": "2018-08-08", + "cloud_native": "yes", + "dollar_value": dollar_value, + "dod_component": "us_navy", + "data_transfers": "less_than_100gb", + "jedi_migration": "yes", + "num_software_systems": 1, + "number_user_sessions": 2, + "average_daily_traffic": 1, + "engineering_assessment": "yes", + "technical_support_team": "yes", + "estimated_monthly_spend": 100, + "expected_completion_date": "less_than_1_month", + "rationalization_software_systems": "yes", + "organization_providing_assistance": "in_house_staff" + }, + "information_about_you": { + "citizenship": "United States", + "designation": "military", + "phone_number": "1234567890", + "email_request": user.email, + "fname_request": user.first_name, + "lname_request": user.last_name, + "service_branch": "ads", + "date_latest_training": "2018-08-06" + } + } + + +class PENumberFactory(factory.alchemy.SQLAlchemyModelFactory): + class Meta: + model = PENumber + + +class TaskOrderFactory(factory.alchemy.SQLAlchemyModelFactory): + class Meta: + model = TaskOrder diff --git a/tests/models/test_requests.py b/tests/models/test_requests.py index a1990a35..d6592a25 100644 --- a/tests/models/test_requests.py +++ b/tests/models/test_requests.py @@ -1,4 +1,4 @@ -from tests.factories import RequestFactory +from tests.factories import RequestFactory, UserFactory from atst.domain.requests import Requests, RequestStatus @@ -19,3 +19,52 @@ def test_pending_ccpo_approval_requires_ccpo(): request = Requests.set_status(request, RequestStatus.PENDING_CCPO_APPROVAL) assert Requests.action_required_by(request) == "ccpo" + + +def test_request_has_creator(): + user = UserFactory.create() + request = RequestFactory.create(creator=user) + + assert request.creator == user + + +def test_request_status_started_displayname(): + request = RequestFactory.create() + request = Requests.set_status(request, RequestStatus.STARTED) + + assert request.status_displayname == "Started" + + +def test_request_status_pending_financial_displayname(): + request = RequestFactory.create() + request = Requests.set_status(request, RequestStatus.PENDING_FINANCIAL_VERIFICATION) + + assert request.status_displayname == "Pending Financial Verification" + + +def test_request_status_pending_ccpo_displayname(): + request = RequestFactory.create() + request = Requests.set_status(request, RequestStatus.PENDING_CCPO_APPROVAL) + + assert request.status_displayname == "Pending CCPO Approval" + + +def test_request_status_pending_approved_displayname(): + request = RequestFactory.create() + request = Requests.set_status(request, RequestStatus.APPROVED) + + assert request.status_displayname == "Approved" + + +def test_request_status_pending_expired_displayname(): + request = RequestFactory.create() + request = Requests.set_status(request, RequestStatus.EXPIRED) + + assert request.status_displayname == "Expired" + + +def test_request_status_pending_deleted_displayname(): + request = RequestFactory.create() + request = Requests.set_status(request, RequestStatus.DELETED) + + assert request.status_displayname == "Deleted" diff --git a/tests/routes/test_request_new.py b/tests/routes/test_request_new.py index e402e622..e31aae79 100644 --- a/tests/routes/test_request_new.py +++ b/tests/routes/test_request_new.py @@ -33,7 +33,7 @@ def test_submit_valid_request_form(monkeypatch, client, user_session): def test_owner_can_view_request(client, user_session): user = UserFactory.create() user_session(user) - request = RequestFactory.create(creator=user.id) + request = RequestFactory.create(creator=user) response = client.get("/requests/new/1/{}".format(request.id), follow_redirects=True)