diff --git a/dolphin/__init__.py b/dolphin/__init__.py index 2b4f11f7..44e3c056 100644 --- a/dolphin/__init__.py +++ b/dolphin/__init__.py @@ -11,7 +11,7 @@ from .controllers.root import Root -__version__ = '0.42.1a7' +__version__ = '0.43.1a7' class Dolphin(Application): @@ -84,10 +84,10 @@ def __init__(self, application_name='dolphin', root=Root()): version=__version__ ) - def insert_basedata(self, *args): + def insert_basedata(self, *args):# pragma: no cover basedata.insert() - def insert_mockup(self, *args): + def insert_mockup(self, *args):# pragma: no cover mockup.insert() def register_cli_launchers(self, subparsers): diff --git a/dolphin/controllers/dailyreport.py b/dolphin/controllers/dailyreport.py index 832e3ddc..7904ea64 100644 --- a/dolphin/controllers/dailyreport.py +++ b/dolphin/controllers/dailyreport.py @@ -1,69 +1,8 @@ -from datetime import datetime - -from nanohttp import json, int_or_notfound, HTTPNotFound -from restfulpy.authorization import authorize from restfulpy.controllers import ModelRestController -from restfulpy.orm import DBSession, commit -from ..exceptions import StatusEndDateMustBeGreaterThanStartDate from ..models import Dailyreport -from ..validators import dailyreport_create_validator, \ - dailyreport_update_validator class DailyreportController(ModelRestController): __model__ = Dailyreport - @authorize - @json( - prevent_empty_form='708 Empty Form', - form_whitelist=( - ['hours', 'note', 'itemId'], - '707 Invalid field, only following fields are accepted: ' \ - 'hours, note and itemId' - ) - ) - @dailyreport_create_validator - @commit - def create(self): - dailyreport = Dailyreport() - dailyreport.update_from_request() - dailyreport.date = datetime.now().date() - DBSession.add(dailyreport) - return dailyreport - - @authorize - @json(prevent_form='709 Form Not Allowed') - def get(self, id): - id = int_or_notfound(id) - dailyreport = DBSession.query(Dailyreport).get(id) - if dailyreport is None: - raise HTTPNotFound() - - return dailyreport - - @authorize - @json( - prevent_empty_form='708 Empty Form', - form_whitelist=( - ['hours', 'note'], - '707 Invalid field, only following fields are accepted: hours, note' - ) - ) - @dailyreport_update_validator - @commit - def update(self, id): - id = int_or_notfound(id) - dailyreport = DBSession.query(Dailyreport).get(id) - if dailyreport is None: - raise HTTPNotFound() - - dailyreport.update_from_request() - return dailyreport - - @authorize - @json(prevent_form='709 Form Not Allowed') - @Dailyreport.expose - def list(self): - return DBSession.query(Dailyreport) - diff --git a/dolphin/controllers/issues.py b/dolphin/controllers/issues.py index ddac90f0..b046fc7b 100644 --- a/dolphin/controllers/issues.py +++ b/dolphin/controllers/issues.py @@ -83,24 +83,6 @@ def _get_issue(self, id): return issue - def _ensure_room(self, title, token, access_token): - create_room_error = 1 - room = None - while create_room_error is not None: - try: - room = ChatClient().create_room( - title, - token, - access_token, - context.identity.reference_id - ) - create_room_error = None - except StatusChatRoomNotFound: - # FIXME: Cover here - create_room_error = 1 - - return room - @authorize @json( prevent_empty_form='708 No Parameter Exists In The Form', @@ -397,6 +379,7 @@ def subscribe(self, id=None): seen_at=datetime.utcnow() ) DBSession.add(subscription) + DBSession.flush() try: chat_client.add_member( @@ -413,18 +396,6 @@ def subscribe(self, id=None): # room. pass - try: - DBSession.flush() - - except: - chat_client.kick_member( - issue.room_id, - context.identity.reference_id, - token, - member.access_token - ) - raise - return issue @authorize @@ -450,6 +421,7 @@ def unsubscribe(self, id): raise HTTPStatus('612 Not Subscribed Yet') DBSession.delete(subscription) + DBSession.flush() chat_client = ChatClient() try: @@ -466,18 +438,6 @@ def unsubscribe(self, id): # to room. pass - try: - DBSession.flush() - - except: - chat_client.add_member( - issue.room_id, - context.identity.reference_id, - token, - member.access_token - ) - raise - return issue @authorize diff --git a/dolphin/controllers/items.py b/dolphin/controllers/items.py index c4196476..e64255f5 100644 --- a/dolphin/controllers/items.py +++ b/dolphin/controllers/items.py @@ -1,44 +1,165 @@ +from datetime import datetime + from nanohttp import json, context, HTTPNotFound, int_or_notfound from restfulpy.authorization import authorize from restfulpy.controllers import ModelRestController from restfulpy.orm import DBSession, commit +from sqlalchemy import select, func + +from ..models import Item, Dailyreport, Event +from ..validators import update_item_validator, dailyreport_update_validator, \ + estimate_item_validator +from ..exceptions import StatusEndDateMustBeGreaterThanStartDate + + +FORM_WHITLELIST = [ + 'startDate', + 'endDate', + 'estimatedHours', +] -from dolphin.models import Item -from dolphin.validators import update_item_validator + +FORM_WHITELIST_STRING = ', '.join(FORM_WHITLELIST) class ItemController(ModelRestController): __model__ = Item + def __call__(self, *remaining_path): + if len(remaining_path) > 1 and remaining_path[1] == 'dailyreports': + id = int_or_notfound(remaining_path[0]) + item = self._get_item(id) + return ItemDailyreportController(item=item)(*remaining_path[2:]) + + return super().__call__(*remaining_path) + + def _get_item(self, id): + item = DBSession.query(Item).filter(Item.id == id).one_or_none() + if item is None: + raise HTTPNotFound() + + return item + @authorize - @json - @update_item_validator - @Item.expose - @commit - def update(self, id): + @json(prevent_form='709 Form Not Allowed') + def get(self, id): id = int_or_notfound(id) - item = DBSession.query(Item).get(id) if not item: raise HTTPNotFound() - item.status = context.form['status'] return item @authorize @json(prevent_form='709 Form Not Allowed') @Item.expose - def get(self, id): + def list(self): + resource = Member.current() + query = DBSession.query(Item).filter(Item.member_id == resource.id) + active_item_cte = select([func.max(Item.id).label('max_item_id')]) \ + .group_by(Item.issue_id) \ + .cte() + + if 'zone' in context.query: + if context.query['zone'] == 'needEstimate': + query = query.filter(Item.id == active_item_cte.c.max_item_id) \ + .filter(Item.estimated_hours.is_(None)) + + elif context.query['zone'] == 'upcomingNuggets': + query = query.filter(Item.id == active_item_cte.c.max_item_id) \ + .filter(Item.start_date > datetime.now()) + + elif context.query['zone'] == 'inProcessNuggets': + query = query.filter(Item.id == active_item_cte.c.max_item_id) \ + .filter(Item.start_date < datetime.now()) + + return query + + @authorize + @json( + prevent_empty_form='708 Empty Form', + form_whitelist=( + FORM_WHITLELIST, + f'707 Invalid field, only following fields are accepted: ' + f'{FORM_WHITELIST_STRING}' + ) + ) + @estimate_item_validator + @commit + def estimate(self, id): id = int_or_notfound(id) + item = DBSession.query(Item).get(id) if not item: raise HTTPNotFound() + item.update_from_request() + if item.start_date > item.end_date: + raise StatusEndDateMustBeGreaterThanStartDate() + return item + +class ItemDailyreportController(ModelRestController): + __model__ = Dailyreport + + def __init__(self, item): + self.item = item + + def _create_dailyreport_if_needed(self): + if Event.isworkingday(DBSession): + is_dailyreport_exists = DBSession.query(Dailyreport) \ + .filter( + Dailyreport.date == datetime.now().date(), + Dailyreport.item_id == self.item.id + ) \ + .one_or_none() + + if not is_dailyreport_exists: + dailyreport = Dailyreport( + date=datetime.now().date(), + item_id=self.item.id, + ) + DBSession.add(dailyreport) + return dailyreport + @authorize @json(prevent_form='709 Form Not Allowed') - @Item.expose + @commit + def get(self, id): + id = int_or_notfound(id) + self._create_dailyreport_if_needed() + dailyreport = DBSession.query(Dailyreport).get(id) + if dailyreport is None: + raise HTTPNotFound() + + return dailyreport + + @authorize + @json( + prevent_empty_form='708 Empty Form', + form_whitelist=( + ['hours', 'note'], + '707 Invalid field, only following fields are accepted: hours, note' + ) + ) + @dailyreport_update_validator + @commit + def update(self, id): + id = int_or_notfound(id) + dailyreport = DBSession.query(Dailyreport).get(id) + if dailyreport is None: + raise HTTPNotFound() + + dailyreport.update_from_request() + return dailyreport + + @authorize + @json(prevent_form='709 Form Not Allowed') + @Dailyreport.expose + @commit def list(self): - return DBSession.query(Item) + self._create_dailyreport_if_needed() + return DBSession.query(Dailyreport) \ + .filter(Dailyreport.item_id == self.item.id) diff --git a/dolphin/controllers/organization.py b/dolphin/controllers/organization.py index b6ff5fbb..29e6c4c7 100644 --- a/dolphin/controllers/organization.py +++ b/dolphin/controllers/organization.py @@ -47,6 +47,7 @@ def __call__(self, *remaining_paths): return super().__call__(*remaining_paths) @authorize + @store_manager(DBSession) @json(prevent_empty_form=True) @organization_create_validator @Organization.expose @@ -61,6 +62,7 @@ def create(self): member = Member.current() organization = Organization( title=context.form.get('title'), + logo=context.form.get('logo'), ) DBSession.add(organization) DBSession.flush() diff --git a/dolphin/controllers/phases.py b/dolphin/controllers/phases.py index 4b99aafc..14d2c17f 100644 --- a/dolphin/controllers/phases.py +++ b/dolphin/controllers/phases.py @@ -37,10 +37,6 @@ def __call__(self, *remaining_paths): return super().__call__(*remaining_paths) - def __init__(self, workflow=None, issue=None): - self.workflow = workflow - self.issue = issue - def _get_phase(self, id): id = int_or_notfound(id) @@ -50,29 +46,6 @@ def _get_phase(self, id): return phase - @authorize - @json(prevent_form='709 Form Not Allowed') - @Phase.expose - def list(self): - query = DBSession.query(Phase) \ - .filter(Phase.workflow_id == self.workflow.id) - return query - - @authorize - @json - @Phase.expose - @commit - def set(self, id): - id = int_or_notfound(id) - - phase = DBSession.query(Phase).get(id) - if phase is None: - raise HTTPNotFound() - - phase.issues.append(self.issue) - DBSession.add(phase) - return phase - @authorize @json(form_whitelist=( FORM_WHITELIST, @@ -127,37 +100,8 @@ def get(self, id): return phase @authorize - @phase_validator - @json - @commit - def create(self): - form = context.form - self._check_title_repetition( - workflow=self.workflow, - title=form['title'] - ) - self._check_order_repetition( - workflow=self.workflow, - order=form['order'], - ) - phase = Phase() - phase.update_from_request() - phase.workflow = self.workflow - phase.skill_id = form['skillId'] - DBSession.add(phase) - return phase - - def _check_title_repetition(self, workflow, title): - phase = DBSession.query(Phase) \ - .filter(Phase.title == title, Phase.workflow_id == workflow.id) \ - .one_or_none() - if phase is not None: - raise HTTPStatus('600 Repetitive Title') - - def _check_order_repetition(self, workflow, order): - phase = DBSession.query(Phase) \ - .filter(Phase.order == order, Phase.workflow_id == workflow.id) \ - .one_or_none() - if phase is not None: - raise HTTPStatus('615 Repetitive Order') + @json(prevent_form='709 Form Not Allowed') + @Phase.expose + def list(self): + return DBSession.query(Phase) diff --git a/dolphin/controllers/root.py b/dolphin/controllers/root.py index 76bf0d09..da8c9417 100644 --- a/dolphin/controllers/root.py +++ b/dolphin/controllers/root.py @@ -6,7 +6,6 @@ import dolphin from .issues import IssueController -from .items import ItemController from .members import MemberController from .oauth2 import OAUTHController from .projects import ProjectController @@ -26,6 +25,7 @@ from .eventtype import EventTypeController from .event import EventController from .dailyreport import DailyreportController +from .items import ItemController here = abspath(dirname(__file__)) @@ -38,7 +38,6 @@ class Apiv1(RestController, JsonPatchControllerMixin): projects = ProjectController() members = MemberController() issues = IssueController() - items = ItemController() tokens = TokenController() oauth2 = OAUTHController() organizations = OrganizationController() @@ -55,6 +54,7 @@ class Apiv1(RestController, JsonPatchControllerMixin): eventtypes = EventTypeController() events = EventController() dailyreports = DailyreportController() + items = ItemController() @json def version(self): diff --git a/dolphin/controllers/skill.py b/dolphin/controllers/skill.py index 228fdc2e..ea0279a8 100644 --- a/dolphin/controllers/skill.py +++ b/dolphin/controllers/skill.py @@ -52,6 +52,8 @@ def create(self): def update(self, id): id = int_or_notfound(id) skill = DBSession.query(Skill).get(id) + if skill is None: + raise HTTPNotFound() if DBSession.query(Skill) \ .filter( @@ -61,9 +63,6 @@ def update(self, id): .one_or_none(): raise StatusRepetitiveTitle() - if skill is None: - raise HTTPNotFound() - skill.update_from_request() DBSession.add(skill) return skill diff --git a/dolphin/controllers/workflows.py b/dolphin/controllers/workflows.py index 7bee68be..4e77688b 100644 --- a/dolphin/controllers/workflows.py +++ b/dolphin/controllers/workflows.py @@ -1,11 +1,25 @@ from nanohttp import json, context, HTTPNotFound, int_or_notfound, HTTPStatus -from restfulpy.controllers import ModelRestController +from restfulpy.controllers import ModelRestController, RestController from restfulpy.authorization import authorize from restfulpy.orm import DBSession, commit -from ..models import Workflow +from ..models import Workflow, Phase, Skill from .phases import PhaseController -from ..validators import workflow_create_validator, workflow_update_validator +from ..validators import workflow_create_validator, workflow_update_validator, \ + phase_update_validator, phase_validator +from ..exceptions import StatusRepetitiveTitle, StatusRepetitiveOrder, \ + StatusSkillNotFound + + +FORM_WHITELIST_PHASE = [ + 'title', + 'order', + 'skillId', + 'description', +] + + +FORM_WHITELISTS_STRING_PHASE = ', '.join(FORM_WHITELIST_PHASE) class WorkflowController(ModelRestController): @@ -14,7 +28,7 @@ class WorkflowController(ModelRestController): def __call__(self, *remaining_paths): if len(remaining_paths) > 1 and remaining_paths[1] == 'phases': workflow = self._get_workflow(remaining_paths[0]) - return PhaseController(workflow)(*remaining_paths[2:]) + return WorkflowPhaseController(workflow)(*remaining_paths[2:]) return super().__call__(*remaining_paths) @@ -82,5 +96,97 @@ def update(self, id): raise HTTPStatus(f'600 Repetitive Title') workflow.update_from_request() - return workflow + return workflow + + +class WorkflowPhaseController(RestController): + + def __init__(self, workflow): + self.workflow = workflow + + @authorize + @json(prevent_form='709 Form Not Allowed') + @Phase.expose + def list(self): + query = DBSession.query(Phase) \ + .filter(Phase.workflow_id == self.workflow.id) + return query + + @authorize + @json(form_whitelist=( + FORM_WHITELIST_PHASE, + f'707 Invalid field, only following fields are accepted: ' + f'{FORM_WHITELISTS_STRING_PHASE}' + )) + @phase_update_validator + @commit + def update(self, id): + id = int_or_notfound(id) + form = context.form + phase = DBSession.query(Phase).get(id) + if phase is None: + raise HTTPNotFound() + + is_repetitive_title = DBSession.query(Phase) \ + .filter( + Phase.title == context.form.get('title'), + Phase.workflow_id == self.workflow.id + ) \ + .one_or_none() + if phase.title != context.form.get('title') \ + and is_repetitive_title is not None: + raise StatusRepetitiveTitle() + + is_repetitive_order = DBSession.query(Phase) \ + .filter( + Phase.order == context.form.get('order'), + Phase.workflow_id == self.workflow.id + ) \ + .one_or_none() + if phase.order != context.form.get('order') \ + and is_repetitive_order is not None: + raise StatusRepetitiveOrder() + + if 'skillId' in form and not DBSession.query(Skill) \ + .filter(Skill.id == form.get('skillId')) \ + .one_or_none(): + raise StatusSkillNotFound() + + phase.update_from_request() + return phase + + @authorize + @phase_validator + @json + @commit + def create(self): + form = context.form + self._check_title_repetition( + workflow=self.workflow, + title=form['title'] + ) + self._check_order_repetition( + workflow=self.workflow, + order=form['order'], + ) + phase = Phase() + phase.update_from_request() + phase.workflow = self.workflow + phase.skill_id = form['skillId'] + DBSession.add(phase) + return phase + + def _check_title_repetition(self, workflow, title): + phase = DBSession.query(Phase) \ + .filter(Phase.title == title, Phase.workflow_id == workflow.id) \ + .one_or_none() + if phase is not None: + raise HTTPStatus('600 Repetitive Title') + + def _check_order_repetition(self, workflow, order): + phase = DBSession.query(Phase) \ + .filter(Phase.order == order, Phase.workflow_id == workflow.id) \ + .one_or_none() + if phase is not None: + raise HTTPStatus('615 Repetitive Order') diff --git a/dolphin/exceptions.py b/dolphin/exceptions.py index 4267c1d3..43d87fe9 100644 --- a/dolphin/exceptions.py +++ b/dolphin/exceptions.py @@ -83,7 +83,6 @@ def __init__(self, related_issue_id): class StatusResourceNotFound(HTTPKnownStatus): - def __init__(self, resource_id): self.status = f'609 Resource not found with id: {resource_id}' @@ -160,14 +159,80 @@ class StatusLimitedCharecterForNote(HTTPKnownStatus): status = '902 At Most 1024 Characters Are Valid For Note' -class StatusInvalidHoursType(HTTPKnownStatus): - status = '900 Invalid Hours Type' +class StatusInvalidEstimatedHoursType(HTTPKnownStatus): + status = '900 Invalid Estimated Hours Type' class StatusSummaryNotInForm(HTTPKnownStatus): status = '799 Summary Not In Form' +class StatusReleaseNotFound(HTTPKnownStatus): + status = '607 Release Not Found' + + +class StatusInvalidReleaseIdType(HTTPKnownStatus): + status = '750 Invalid Release Id Type' + + +class StatusInvalidStatusValue(HTTPKnownStatus): + def __init__(self, statuses_values): + self.status = f'705 Invalid status value, only one of ' \ + f'"{", ".join(statuses_values)}" will be accepted' + + +class StatusProjectNotFound(HTTPKnownStatus): + status = '601 Project Not Found' + + +class StatusHiddenProjectIsNotEditable(HTTPKnownStatus): + status = '746 Hidden Project Is Not Editable' + + +class StatusPhaseNotFound(HTTPKnownStatus): + status = '613 Phase Not Found' + + +class StatusInvalidWorkflowIdType(HTTPKnownStatus): + status = '743 Invalid Workflow Id Type' + + +class StatusWorkflowNotFound(HTTPKnownStatus): + status = '616 Workflow Not Found' + + +class StatusInvalidRoleValue(HTTPKnownStatus): + status = '756 Invalid Role Value' + + +class StatusTitleIsNull(HTTPKnownStatus): + status = '727 Title Is Null' + + +class StatusMaxLenghtForTitle(HTTPKnownStatus): + + def __init__(self, lenght): + self.status = f'704 At Most {lenght} Characters Are Valid For Title' + + +class StatusEventTypeIdIsNull(HTTPKnownStatus): + status = '798 Event Type Id Is Null' + + +class StatusMaxLenghtForDescription(HTTPKnownStatus): + def __init__(self, lenght): + self.status = f'703 At Most {lenght} Characters Are Valid For '\ + 'Description' + + +class StatusTitleNotInForm(HTTPKnownStatus): + status = '710 Title Not In Form' + + +class StatusEndDateNotInForm(HTTPKnownStatus): + status = '793 End Date Not IN Form' + + class StatusStartDateNotInForm(HTTPKnownStatus): status = '792 Start Date Not In Form' @@ -176,16 +241,16 @@ class StatusEndDateNotInForm(HTTPKnownStatus): status = '793 End Date Not In Form' -class StatusEstimatedTimeNotInForm(HTTPKnownStatus): - status = '901 Estimated Time Not In Form' +class StatusEstimatedHoursNotInForm(HTTPKnownStatus): + status = '901 Estimated Hours Not In Form' class StatusNoteIsNull(HTTPKnownStatus): status = '903 Note Is Null' -class StatusEstimatedTimeIsNull(HTTPKnownStatus): - status = '904 Estimated Time Is Null' +class StatusEstimatedHoursIsNull(HTTPKnownStatus): + status = '904 Estimated Hours Is Null' class StatusStartDateIsNull(HTTPKnownStatus): @@ -207,3 +272,219 @@ class StatusQueryParameterNotInFormOrQueryString(HTTPKnownStatus): class StatusHoursMustBeGreaterThanZero(HTTPKnownStatus): status = '914 Hours Must Be Greater Than 0' + +class StatusTypeIdNotInForm(HTTPKnownStatus): + status = '794 Type Id Not In Form' + + +class StatusInvalidOrderType(HTTPKnownStatus): + status = '741 Invalid Order Type' + + +class StatusInvalidSkillIdType(HTTPKnownStatus): + status = '788 Invalid Skill Id Type' + + +class StatusOrderNotInForm(HTTPKnownStatus): + status = '742 Order Not In Form' + + +class StatusInvalidTargetIssueIdType(HTTPKnownStatus): + status = '781 Invalid Target Issue Id Type' + + +class StatusTargetIssueIdNotInForm(HTTPKnownStatus): + status = '780 Target Issue Id Not In Form' + + +class StatusTargetIssueIdIsNull(HTTPKnownStatus): + status = '779 Target Issue Id Is Null' + + +class StatusInvalidTitleFormat(HTTPKnownStatus): + status = '747 Invalid Title Format' + + +class StatusInvalidMemberIdType(HTTPKnownStatus): + status = '736 Invalid Member Id Type' + + +class StatusMemberIdNotInForm(HTTPKnownStatus): + status = '735 Member Id Not In Form' + + +class StatusMemberIdIsNull(HTTPKnownStatus): + status = '774 Member Id Is Null' + + +class StatusInvalidProjectIdType(HTTPKnownStatus): + status = '714 Invalid Project Id Type' + + +class StatusProjectIdNotInForm(HTTPKnownStatus): + status = '713 Project Id Not In Form' + + +class StatusAuthorizationCodeNotInForm(HTTPKnownStatus): + status = '762 Authorization Code Not In Form' + + +class StatusInvalidOrganizationIdType(HTTPKnownStatus): + status = '763 Invalid Organization Id Type' + + +class StatusOrganizationIdNotINForm(HTTPKnownStatus): + status = '761 Organization Id Not In Form' + + +class StatusTokenNotInForm(HTTPKnownStatus): + status = '757 Token Not In Form' + + +class StatusRedirectUriNotInForm(HTTPKnownStatus): + status = '766 Redirect Uri Not In From' + + +class StatusApplicationIdNotInForm(HTTPKnownStatus): + status = '764 Application Id Not In Form' + + +class StatusScopesNotInForm(HTTPKnownStatus): + status = '765 Scopes Not In Form' + + +class StatusRoleNotInForm(HTTPKnownStatus): + status = '755 Role Not In Form' + + +class StatusInvalidEmailFormat(HTTPKnownStatus): + status = '754 Invalid Email Format' + + +class StatusEmailNotInForm(HTTPKnownStatus): + status = '753 Email Not In Form' + + +class StatusInvalidPhaseIdType(HTTPKnownStatus): + status = '738 Invalid Phase Id Type' + + +class StatusPhaseIdNotInForm(HTTPKnownStatus): + status = '737 Phase Id Not In Form' + + +class StatusInvalidCutoffFormat(HTTPKnownStatus): + status = '702 Invalid Cutoff Format' + + +class StatusCutoffNotInForm(HTTPKnownStatus): + status = '712 Cutoff Not In Form' + + +class StatusFileNotInForm(HTTPKnownStatus): + status = '758 File Not In Form' + + +class StatusPhaseIdIsNull(HTTPKnownStatus): + status = '770 Phase Id Is Null' + + +class StatusInvalidResourceIdType(HTTPKnownStatus): + status = '716 Invalid Resource Id Type' + + +class StatusResourceIdNotInForm(HTTPKnownStatus): + status = '715 Resource Id Not In Form' + + +class StatusResourceIdIsNull(HTTPKnownStatus): + status = '769 Resource Id Is Null' + + +class StatusStatusNotInForm(HTTPKnownStatus): + status = '719 Status Not In Form' + + +class StatusInvalidDaysType(HTTPKnownStatus): + status = '721 Invalid Days Type' + + +class StatusInvalidDueDateFormat(HTTPKnownStatus): + status ='701 Invalid Due Date Format' + + +class StatusManagerReferenceIdNotInForm(HTTPKnownStatus): + status = '777 Manager Reference Id Not In Form' + + +class StatusManagerReferenceIdIsNull(HTTPKnownStatus): + status = '778 Manager Reference Id Is Null' + + +class StatusInvalidLaunchDateFormat(HTTPKnownStatus): + status = '784 Invalid Launch Date Format' + + +class StatusLaunchDateNotInForm(HTTPKnownStatus): + status = '783 Launch Date Not In Form' + + +class StatusInvalidGroupIdType(HTTPKnownStatus): + status = '797 Invalid Group Id Type' + + +class StatusGroupIdNotInForm(HTTPKnownStatus): + status = '795 Group Id Not In Form' + + +class StatusGroupIdIsNull(HTTPKnownStatus): + status = '796 Group Id Is Null' + + +class StatusManagerIdNotInForm(HTTPKnownStatus): + status = '786 Manager Id Not In Form' + + +class StatusManagerIdIsNull(HTTPKnownStatus): + status = '785 Manager Id Is Null' + + +class StatusIssueIdIsNull(HTTPKnownStatus): + status = '775 Issue Id Is Null' + + +class StatusInvalidIssueIdType(HTTPKnownStatus): + status = '722 Invalid Issue Id Type' + + +class StatusDaysNotInForm(HTTPKnownStatus): + status = '720 Days Not In Form' + + +class StatusKindNotInForm(HTTPKnownStatus): + status = '718 Kind Not In Form' + + +class StatusDueDateNotInForm(HTTPKnownStatus): + status = '711 Due Date Not In Form' + + +class StatusPriorityNotInForm(HTTPKnownStatus): + status = '768 Priority Not In Form' + + +class StatusInvalidPriority(HTTPKnownStatus): + def __init__(self, issue_priorities): + self.status = f'767 Invalid priority, only one of ' \ + f'"{", ".join(issue_priorities)}" will be accepted' + + +class StatusInvalidKind(HTTPKnownStatus): + def __init__(self, issue_kinds): + self.status = f'717 Invalid kind, only one of ' \ + f'"{", ".join(issue_kinds)}" will be accepted' + + +class StatusInvalidHoursType(HTTPKnownStatus): + status = '915 Invalid Hours Type' + diff --git a/dolphin/migration/versions/84d5fef5976a_.py b/dolphin/migration/versions/84d5fef5976a_.py new file mode 100644 index 00000000..0c216cfe --- /dev/null +++ b/dolphin/migration/versions/84d5fef5976a_.py @@ -0,0 +1,80 @@ +"""empty message + +Revision ID: 84d5fef5976a +Revises: a2410e75c467 +Create Date: 2019-05-13 14:33:29.207279 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '84d5fef5976a' +down_revision = 'a2410e75c467' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + 'dailyreport', + sa.Column('item_id', sa.Integer(), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('date', sa.Date(), nullable=False), + sa.Column('hours', sa.Integer(), nullable=True), + sa.Column('note', sa.Unicode(), nullable=True), + sa.ForeignKeyConstraint(['item_id'], ['item.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.drop_table('timecard') + op.add_column( + 'item', + sa.Column('estimated_hours', sa.Integer(), nullable=True) + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('item', 'estimated_hours') + op.create_table( + 'timecard', + sa.Column( + 'id', + sa.INTEGER(), + autoincrement=True, + nullable=False + ), + sa.Column( + 'start_date', + postgresql.TIMESTAMP(), + autoincrement=False, + nullable=False + ), + sa.Column( + 'end_date', + postgresql.TIMESTAMP(), + autoincrement=False, + nullable=False + ), + sa.Column( + 'estimated_time', + sa.INTEGER(), + autoincrement=False, + nullable=False + ), + sa.Column( + 'summary', + sa.VARCHAR(), + autoincrement=False, + nullable=False + ), + sa.PrimaryKeyConstraint( + 'id', + name='timecard_pkey' + ) + ) + op.drop_table('dailyreport') + # ### end Alembic commands ### diff --git a/dolphin/models/dailyreport.py b/dolphin/models/dailyreport.py index 55abb789..dee35504 100644 --- a/dolphin/models/dailyreport.py +++ b/dolphin/models/dailyreport.py @@ -56,7 +56,7 @@ class Dailyreport(OrderingMixin, FilteringMixin, PaginationMixin, \ Unicode, min_length=1, max_length=1024, - label='Lorem Isum', + label='Notes', watermark='Lorem Ipsum', not_none=False, nullable=True, diff --git a/dolphin/models/issue.py b/dolphin/models/issue.py index 9e528f81..999c7737 100644 --- a/dolphin/models/issue.py +++ b/dolphin/models/issue.py @@ -169,7 +169,7 @@ class Issue(OrderingMixin, FilteringMixin, PaginationMixin, ModifiedByMixin, 'Project', foreign_keys=[project_id], back_populates='issues', - protected=True + protected=False ) members = relationship( 'Member', @@ -193,7 +193,7 @@ class Issue(OrderingMixin, FilteringMixin, PaginationMixin, ModifiedByMixin, items = relationship( 'Item', - protected=False, + protected=True, order_by=Item.created_at, ) @@ -354,11 +354,25 @@ def iter_metadata_fields(cls): ) def to_dict(self, include_relations=True): + items_list = [] + item_fields = ( + 'memberId', + 'phaseId', + 'createdAt', + ) + for item in self.items: + items_list.append(dict( + memberId=item.member_id, + phaseId=item.phase_id, + createdAt=item.created_at, + )) + issue_dict = super().to_dict() issue_dict['boarding'] = self.boarding issue_dict['isSubscribed'] = True if self.is_subscribed else False issue_dict['seenAt'] \ = self.seen_at.isoformat() if self.seen_at else None + issue_dict['items'] = items_list if include_relations: issue_dict['relations'] = [] diff --git a/dolphin/models/item.py b/dolphin/models/item.py index eb7bbe6d..6b0e024c 100644 --- a/dolphin/models/item.py +++ b/dolphin/models/item.py @@ -1,6 +1,7 @@ from datetime import datetime from restfulpy.orm import Field, DeclarativeBase, relationship +from restfulpy.orm.metadata import MetadataField from restfulpy.orm.mixins import TimestampMixin, OrderingMixin, \ FilteringMixin, PaginationMixin from sqlalchemy import Integer, ForeignKey, UniqueConstraint, DateTime, Enum, \ @@ -34,7 +35,7 @@ class Item(TimestampMixin, OrderingMixin, FilteringMixin, PaginationMixin, example=1, protected=False, ) - start_time = Field( + start_date = Field( DateTime, python_type=datetime, label='Start Date', @@ -46,9 +47,9 @@ class Item(TimestampMixin, OrderingMixin, FilteringMixin, PaginationMixin, nullable=True, not_none=False, required=False, - readonly=True, + readonly=False, ) - end_time = Field( + end_date = Field( DateTime, python_type=datetime, label='Target Date', @@ -60,7 +61,7 @@ class Item(TimestampMixin, OrderingMixin, FilteringMixin, PaginationMixin, nullable=True, not_none=False, required=False, - readonly=True, + readonly=False, ) estimated_hours = Field( Integer, @@ -128,10 +129,11 @@ class Item(TimestampMixin, OrderingMixin, FilteringMixin, PaginationMixin, example='Lorem Ipsum' ) - issues = relationship( + issue = relationship( 'Issue', foreign_keys=issue_id, - back_populates='items' + back_populates='items', + protected=False, ) dailyreports = relationship( 'Dailyreport', @@ -146,7 +148,30 @@ class Item(TimestampMixin, OrderingMixin, FilteringMixin, PaginationMixin, UniqueConstraint(phase_id, issue_id, member_id) def to_dict(self): + issue_fields = ( + 'title', + 'kind', + 'status', + 'priority', + 'boarding', + ) + issue_dict = {i: getattr(self.issue, i) for i in issue_fields} + item_dict = super().to_dict() item_dict['hoursWorked'] = self.hours_worked return item_dict + @classmethod + def iter_metadata_fields(cls): + yield from super().iter_metadata_fields() + yield MetadataField( + name='issue', + key='issue', + label='Lorem Ipsun', + required=False, + readonly=True, + watermark='Lorem Ipsum', + example='Lorem Ipsum', + message='Lorem Ipsun', + ) + diff --git a/dolphin/models/organization.py b/dolphin/models/organization.py index 39b6920b..c1793c54 100644 --- a/dolphin/models/organization.py +++ b/dolphin/models/organization.py @@ -20,6 +20,12 @@ ] +AVATAR_CONTENT_TYPES = [ + 'image/jpeg', + 'image/png', +] + + class OrganizationMember(DeclarativeBase): __tablename__ = 'organization_member' @@ -229,13 +235,22 @@ def logo(self, value): raise HTTPStatus(f'625 {e}') except AspectRatioValidationError as e: - raise HTTPStatus(f'622 {e}') + raise HTTPStatus( + '622 Invalid aspect ratio Only 1/1 is accepted.' + ) except ContentTypeValidationError as e: - raise HTTPStatus(f'623 {e}') + raise HTTPStatus( + f'623 Invalid content type, Valid options are: '\ + f'{", ".join(type for type in AVATAR_CONTENT_TYPES)}' + ) except MaximumLengthIsReachedError as e: - raise HTTPStatus(f'624 {e}') + max_length = settings.attachments.organizations.logos.max_length + raise HTTPStatus( + f'624 Cannot store files larger than: '\ + f'{max_length * 1024} bytes' + ) else: self._logo = None @@ -245,7 +260,7 @@ def to_dict(self): organization['logo'] = self.logo return organization - def __repr__(self): + def __repr__(self):# pragma: no cover return f'\tTitle: {self.title}\n' @classmethod diff --git a/dolphin/tests/stuff/logo-150x100.jpg b/dolphin/tests/stuff/logo-150x100.jpg new file mode 100644 index 00000000..d1f1c8bb Binary files /dev/null and b/dolphin/tests/stuff/logo-150x100.jpg differ diff --git a/dolphin/tests/stuff/logo-225x225.jpg b/dolphin/tests/stuff/logo-225x225.jpg new file mode 100644 index 00000000..c9d6dbc1 Binary files /dev/null and b/dolphin/tests/stuff/logo-225x225.jpg differ diff --git a/dolphin/tests/stuff/logo-50x50.jpg b/dolphin/tests/stuff/logo-50x50.jpg new file mode 100644 index 00000000..a12b5d80 Binary files /dev/null and b/dolphin/tests/stuff/logo-50x50.jpg differ diff --git a/dolphin/tests/stuff/logo-550x550.jpg b/dolphin/tests/stuff/logo-550x550.jpg new file mode 100644 index 00000000..d42318f8 Binary files /dev/null and b/dolphin/tests/stuff/logo-550x550.jpg differ diff --git a/dolphin/tests/stuff/maximum-length-30.jpg b/dolphin/tests/stuff/maximum-length-30.jpg new file mode 100644 index 00000000..857dbcb5 Binary files /dev/null and b/dolphin/tests/stuff/maximum-length-30.jpg differ diff --git a/dolphin/tests/stuff/test.pdf b/dolphin/tests/stuff/test.pdf new file mode 100644 index 00000000..95b1560f Binary files /dev/null and b/dolphin/tests/stuff/test.pdf differ diff --git a/dolphin/tests/test_dailyreport_create.py b/dolphin/tests/test_dailyreport_create.py deleted file mode 100644 index b415c51b..00000000 --- a/dolphin/tests/test_dailyreport_create.py +++ /dev/null @@ -1,148 +0,0 @@ -from datetime import datetime - -from bddrest import status, response, when, given -from auditor.context import Context as AuditLogContext - -from dolphin.models import Member, Workflow, Skill, Group, Phase, Release, \ - Project, Issue, Item -from dolphin.tests.helpers import LocalApplicationTestCase, oauth_mockup_server - - -class TestDailyreport(LocalApplicationTestCase): - - @classmethod - @AuditLogContext(dict()) - def mockup(cls): - session = cls.create_session() - - cls.member = Member( - title='First Member', - email='member1@example.com', - access_token='access token 1', - phone=123456789, - reference_id=1, - ) - session.add(cls.member) - - workflow = Workflow(title='Default') - skill = Skill(title='First Skill') - group = Group(title='default') - - phase = Phase( - title='backlog', - order=-1, - workflow=workflow, - skill=skill, - ) - session.add(phase) - - release = Release( - title='My first release', - description='A decription for my first release', - cutoff='2030-2-20', - launch_date='2030-2-20', - manager=cls.member, - room_id=0, - group=group, - ) - - project = Project( - release=release, - workflow=workflow, - group=group, - manager=cls.member, - title='My first project', - description='A decription for my project', - room_id=1 - ) - - issue = Issue( - project=project, - title='First issue', - description='This is description of first issue', - due_date='2020-2-20', - kind='feature', - days=1, - room_id=2 - ) - session.add(issue) - session.flush() - - cls.item = Item( - issue_id=issue.id, - phase_id=phase.id, - member_id=cls.member.id, - ) - session.add(cls.item) - session.commit() - - def test_create(self): - self.login(self.member.email) - form = dict( - note='Some summary', - hours=2, - itemId=self.item.id, - ) - - with oauth_mockup_server(), self.given( - 'Creating a dailyreport', - '/apiv1/dailyreports', - 'CREATE', - json=form - ): - assert status == 200 - assert response.json['id'] is not None - assert response.json['date'] == str(datetime.now().date()) - assert response.json['hours'] == form['hours'] - assert response.json['note'] == form['note'] - assert response.json['itemId'] == form['itemId'] - - when( - 'Item id is null', - json=given | dict(itemId=None) - ) - assert status == '913 Item Id Is Null' - - when( - 'Item id is not in form', - json=given - 'itemId' - ) - assert status == '732 Item Id Not In Form' - - when( - 'Item is not found', - json=given | dict(itemId=0) - ) - assert status == '660 Item Not Found' - - when('Trying to pass without form parameters', json={}) - assert status == '708 Empty Form' - - when( - 'Invalid parameter is in form', - json=given | dict(parameter='invalid parameter') - ) - assert status == '707 Invalid field, only following fields are ' \ - 'accepted: hours, note and itemId' - - when( - 'Note length is less than limit', - json=given | dict(note=(1024 + 1) * 'a'), - ) - assert status == '902 At Most 1024 Characters Are Valid For Note' - - when( - 'Hours value type is wrong', - json=given | dict(hours='a') - ) - assert status == '900 Invalid Hours Type' - - when( - 'Hours value is less then 1', - json=given | dict(hours=0) - ) - assert status == '914 Hours Must Be Greater Than 0' - - when('Request is not authorized', authorization=None) - assert status == 401 - diff --git a/dolphin/tests/test_dailyreport_get.py b/dolphin/tests/test_dailyreport_get.py index 089c09d7..1c00540b 100644 --- a/dolphin/tests/test_dailyreport_get.py +++ b/dolphin/tests/test_dailyreport_get.py @@ -1,6 +1,6 @@ -import datetime +from datetime import datetime -from bddrest import status, response, when +from bddrest import status, response, when, given from auditor.context import Context as AuditLogContext from dolphin.models import Member, Dailyreport, Workflow, Skill, Group, Release, \ @@ -76,7 +76,7 @@ def mockup(cls): session.add(cls.item) cls.dailyreport = Dailyreport( - date=datetime.datetime.now().date(), + date=datetime.strptime('2019-1-2', '%Y-%m-%d').date(), hours=3, note='The note for a daily report', item=cls.item, @@ -86,24 +86,35 @@ def mockup(cls): def test_get(self): self.login(self.member.email) + session = self.create_session() with oauth_mockup_server(), self.given( f'Get a dailyreport', - f'/apiv1/dailyreports/id: {self.dailyreport.id}', + f'/apiv1/items/item_id: {self.item.id}/' + f'dailyreports/id: {self.dailyreport.id}', f'GET', ): assert status == 200 assert response.json['id'] == self.dailyreport.id + assert session.query(Dailyreport) \ + .filter(Dailyreport.date == datetime.now().date()) \ + .one() + + when( + 'The item in not found', + url_parameters=given | dict(item_id=0) + ) + assert status == 404 when( 'Intended group with string type not found', - url_parameters=dict(id='Alphabetical') + url_parameters=given | dict(id='Alphabetical') ) assert status == 404 when( 'Intended group not found', - url_parameters=dict(id=0) + url_parameters=given | dict(id=0) ) assert status == 404 diff --git a/dolphin/tests/test_dailyreport_list.py b/dolphin/tests/test_dailyreport_list.py index 0faba45e..81a7cac3 100644 --- a/dolphin/tests/test_dailyreport_list.py +++ b/dolphin/tests/test_dailyreport_list.py @@ -1,4 +1,4 @@ -import datetime +from datetime import datetime from bddrest import when, response, status from auditor.context import Context as AuditLogContext @@ -67,57 +67,61 @@ def mockup(cls): session.add(issue) session.flush() - item = Item( + cls.item = Item( issue_id=issue.id, phase_id=phase.id, member_id=cls.member.id, ) - session.add(item) + session.add(cls.item) dailyreport1 = Dailyreport( - date=datetime.datetime.now().date(), + date=datetime.strptime('2019-1-2', '%Y-%m-%d').date(), hours=1, note='note for dailyreport1', - item=item, + item=cls.item, ) session.add(dailyreport1) dailyreport2 = Dailyreport( - date=datetime.datetime.now().date(), + date=datetime.strptime('2019-1-3', '%Y-%m-%d').date(), hours=2, note='note for dailyreport2', - item=item, + item=cls.item, ) session.add(dailyreport2) dailyreport3 = Dailyreport( - date=datetime.datetime.now().date(), + date=datetime.strptime('2019-1-4', '%Y-%m-%d').date(), hours=3, note='note for dailyreport3', - item=item, + item=cls.item, ) session.add(dailyreport3) session.commit() def test_list(self): self.login(self.member.email) + session = self.create_session() with oauth_mockup_server(), self.given( 'List of dailyreports', - '/apiv1/dailyreports', + f'/apiv1/items/item_id: {self.item.id}/dailyreports', 'LIST', ): assert status == 200 - assert len(response.json) == 3 + assert len(response.json) == 4 + assert session.query(Dailyreport) \ + .filter(Dailyreport.date == datetime.now().date()) \ + .one() when('The request with form parameter', form=dict(param='param')) assert status == '709 Form Not Allowed' when('Trying to sorting response', query=dict(sort='id')) - assert response.json[0]['id'] < response.json[1]['id'] == 2 + assert response.json[0]['id'] < response.json[1]['id'] when('Sorting the response descending', query=dict(sort='-id')) - assert response.json[0]['id'] > response.json[1]['id'] == 2 + assert response.json[0]['id'] > response.json[1]['id'] when('Trying pagination response', query=dict(take=1)) assert response.json[0]['id'] == 1 diff --git a/dolphin/tests/test_timecard_update.py b/dolphin/tests/test_dailyreport_update.py similarity index 92% rename from dolphin/tests/test_timecard_update.py rename to dolphin/tests/test_dailyreport_update.py index 24a0bc27..28d9e2ef 100644 --- a/dolphin/tests/test_timecard_update.py +++ b/dolphin/tests/test_dailyreport_update.py @@ -68,18 +68,18 @@ def mockup(cls): session.add(issue) session.flush() - item = Item( + cls.item = Item( issue_id=issue.id, phase_id=phase.id, member_id=cls.member.id, ) - session.add(item) + session.add(cls.item) cls.dailyreport = Dailyreport( date=datetime.datetime.now().date(), hours=3, note='The note for a daily report', - item=item, + item=cls.item, ) session.add(cls.dailyreport) session.commit() @@ -93,7 +93,7 @@ def test_update(self): with oauth_mockup_server(), self.given( f'Updating a dailyreport', - f'/apiv1/dailyreports/id: {self.dailyreport.id}', + f'/apiv1/items/item_id: {self.item.id}/dailyreports/id: {self.dailyreport.id}', f'UPDATE', json=form, ): @@ -107,7 +107,7 @@ def test_update(self): when( 'Intended timecard with string type not found', - url_parameters=dict(id='Alphabetical') + url_parameters=given | dict(id='Alphabetical') ) assert status == 404 @@ -128,7 +128,7 @@ def test_update(self): 'Hours value type is wrong', json=given | dict(hours='a') ) - assert status == '900 Invalid Hours Type' + assert status == '915 Invalid Hours Type' when( 'Hours value is less then 1', diff --git a/dolphin/tests/test_draftissue_finalize.py b/dolphin/tests/test_draftissue_finalize.py index f32a249b..f93a68a0 100644 --- a/dolphin/tests/test_draftissue_finalize.py +++ b/dolphin/tests/test_draftissue_finalize.py @@ -1,9 +1,12 @@ +from nanohttp.contexts import Context +from nanohttp import context from auditor import MiddleWare from auditor.context import Context as AuditLogContext from auditor.logentry import RequestLogEntry, InstantiationLogEntry from bddrest import status, response, Update, when, given, Remove from dolphin import Dolphin +from dolphin.middleware_callback import callback as auditor_callback from dolphin.models import Issue, Project, Workflow, Phase, Tag, \ DraftIssue, Organization, OrganizationMember, Group, Release, Skill, Resource from dolphin.tests.helpers import LocalApplicationTestCase, \ @@ -13,6 +16,7 @@ def callback(audit_logs): global logs logs = audit_logs + auditor_callback(audit_logs) class TestIssue(LocalApplicationTestCase): @@ -74,56 +78,61 @@ def mockup(cls): room_id=1 ) - issue1 = Issue( - project=cls.project, - title='First issue', - description='This is description of first issue', - due_date='2020-2-20', - kind='feature', - days=1, - room_id=2 - ) - session.add(issue1) + with Context(dict()): + context.identity = cls.member - cls.draft_issue1 = DraftIssue() - session.add(cls.draft_issue1) + cls.issue1 = Issue( + project=cls.project, + title='First issue', + description='This is description of first issue', + due_date='2020-2-20', + kind='feature', + days=1, + room_id=2 + ) + session.add(cls.issue1) - organization = Organization( - title='organization-title', - ) - session.add(organization) - session.flush() + cls.draft_issue1 = DraftIssue() + session.add(cls.draft_issue1) - organization_member = OrganizationMember( - organization_id=organization.id, - member_id=cls.member.id, - role='owner', - ) - session.add(organization_member) + organization = Organization( + title='organization-title', + ) + session.add(organization) + session.flush() - cls.tag1 = Tag( - title='tag 1', - organization_id=organization.id, - ) - session.add(cls.tag1) + cls.draft_issue1.related_issues = [cls.issue1] - cls.tag2 = Tag( - title='tag 2', - organization_id=organization.id, - ) - session.add(cls.tag2) + organization_member = OrganizationMember( + organization_id=organization.id, + member_id=cls.member.id, + role='owner', + ) + session.add(organization_member) - cls.tag3 = Tag( - title='tag 3', - organization_id=organization.id, - ) - session.add(cls.tag3) + cls.tag1 = Tag( + title='tag 1', + organization_id=organization.id, + ) + session.add(cls.tag1) - cls.draft_issue1.tags = [cls.tag1, cls.tag2] + cls.tag2 = Tag( + title='tag 2', + organization_id=organization.id, + ) + session.add(cls.tag2) - cls.draft_issue2 = DraftIssue() - session.add(cls.draft_issue2) - session.commit() + cls.tag3 = Tag( + title='tag 3', + organization_id=organization.id, + ) + session.add(cls.tag3) + + cls.draft_issue1.tags = [cls.tag1, cls.tag2] + + cls.draft_issue2 = DraftIssue() + session.add(cls.draft_issue2) + session.commit() def test_finalize(self): self.login(self.member.email) @@ -170,6 +179,20 @@ def test_finalize(self): assert status == '767 Invalid priority, only one of "low, '\ 'normal, high" will be accepted' + when( + 'Draft issue with string type not found', + url_parameters=dict(id='Alphabetical'), + json=given | dict(title='New title') + ) + assert status == 404 + + when( + 'Draft issue not found', + url_parameters=dict(id=0), + json=given | dict(title='New title') + ) + assert status == 404 + when( 'Project id not in form', json=given - 'projectId' | dict(title='New title') @@ -186,7 +209,7 @@ def test_finalize(self): 'Project not found with integer type', json=given | dict(projectId=0, title='New title') ) - assert status == '601 Project not found with id: 0' + assert status == '601 Project Not Found' when( 'Relate issue not found with string type', @@ -201,6 +224,12 @@ def test_finalize(self): assert status == 647 assert status.text.startswith('relatedIssue With Id') + when( + 'Include related issue', + json=Update(relatedIssueId=self.issue1.id, title='New title') + ) + assert status == 200 + when( 'Title is not in form', json=given - 'title' @@ -217,8 +246,7 @@ def test_finalize(self): 'Title is repetitive', json=Update(title='First issue') ) - assert status == '600 Another issue with title: "First issue" '\ - 'is already exists.' + assert status == '600 Repetitive Title' when( 'Title length is more than limit', @@ -284,7 +312,7 @@ def test_finalize(self): json=given | dict(status='progressing') | \ dict(title='Another title') ) - assert status == '705 Invalid status, only one of "to-do, ' \ + assert status == '705 Invalid status value, only one of "to-do, ' \ 'in-progress, on-hold, complete, done" will be accepted' when( diff --git a/dolphin/tests/test_draftissue_relate.py b/dolphin/tests/test_draftissue_relate.py index c675dc84..d6f8d06f 100644 --- a/dolphin/tests/test_draftissue_relate.py +++ b/dolphin/tests/test_draftissue_relate.py @@ -105,7 +105,7 @@ def test_relate(self): 'Trying to pass with none issue id', json=dict(targetIssueId=None) ) - assert status == '779 Target Issue Id Is None' + assert status == '779 Target Issue Id Is Null' when( 'Trying to pass with invalid issue id type', diff --git a/dolphin/tests/test_event_add.py b/dolphin/tests/test_event_add.py index 0f10fcff..1aaa3910 100644 --- a/dolphin/tests/test_event_add.py +++ b/dolphin/tests/test_event_add.py @@ -73,13 +73,13 @@ def test_add(self): 'Title length is more than limit', json=given | dict(title=((50 + 1) * 'a')) ) - assert status == '704 At Most 50 Characters Valid For Title' + assert status == '704 At Most 50 Characters Are Valid For Title' when( 'Trying to pass with none title', json=given | dict(title=None) ) - assert status == '727 Title Is None' + assert status == '727 Title Is Null' when( 'Trying to pass without start date', diff --git a/dolphin/tests/test_event_update.py b/dolphin/tests/test_event_update.py index 2873477f..01f60914 100644 --- a/dolphin/tests/test_event_update.py +++ b/dolphin/tests/test_event_update.py @@ -90,13 +90,13 @@ def test_add(self): 'Title length is more than limit', json=given | dict(title=((50 + 1) * 'a')) ) - assert status == '704 At Most 50 Characters Valid For Title' + assert status == '704 At Most 50 Characters Are Valid For Title' when( 'Trying to pass with none title', json=given | dict(title=None) ) - assert status == '727 Title Is None' + assert status == '727 Title Is Null' when( 'Start date format is wrong', @@ -130,6 +130,15 @@ def test_add(self): assert status == '910 Invalid Repeat, only one of ' \ '"yearly, monthly, never" will be accepted' + when('Event not found', url_parameters=dict(id=0)) + assert status == 404 + + when( + 'Intended event with string type not found', + url_parameters=dict(id='Alphabetical') + ) + assert status == 404 + when('Request is not authorized', authorization=None) assert status == 401 diff --git a/dolphin/tests/test_eventtype_create.py b/dolphin/tests/test_eventtype_create.py index 637b4a39..7f868d79 100644 --- a/dolphin/tests/test_eventtype_create.py +++ b/dolphin/tests/test_eventtype_create.py @@ -57,13 +57,13 @@ def test_create(self): 'Title length is more than limit', json=dict(title=((50 + 1) * 'a')) ) - assert status == '704 At Most 50 Characters Valid For Title' + assert status == '704 At Most 50 Characters Are Valid For Title' when( 'Trying to pass with none title', json=dict(title=None) ) - assert status == '727 Title Is None' + assert status == '727 Title Is Null' when( 'Description length is less than limit', diff --git a/dolphin/tests/test_eventtype_update.py b/dolphin/tests/test_eventtype_update.py index 6f70ad26..e6e67df1 100644 --- a/dolphin/tests/test_eventtype_update.py +++ b/dolphin/tests/test_eventtype_update.py @@ -74,7 +74,7 @@ def test_update(self): 'Title length is more than limit', json=given | dict(title=(50 + 1) * 'a') ) - assert status == '704 At Most 50 Characters Valid For Title' + assert status == '704 At Most 50 Characters Are Valid For Title' when('Invalid parameter is in the form', json=dict(a='a')) assert status == '707 Invalid field, only following fields are ' \ @@ -87,7 +87,7 @@ def test_update(self): 'Trying to pass with none title', json=dict(title=None) ) - assert status == '727 Title Is None' + assert status == '727 Title Is Null' when( 'Description length is less than limit', diff --git a/dolphin/tests/test_group_add.py b/dolphin/tests/test_group_add.py index 21c84dff..0d69e0da 100644 --- a/dolphin/tests/test_group_add.py +++ b/dolphin/tests/test_group_add.py @@ -64,6 +64,15 @@ def test_add(self): when('Trying to pass without form parameters', json={}) assert status == '708 Empty Form' + when('Group not found', url_parameters=dict(id=0)) + assert status == 404 + + when( + 'Intended group with string type not found', + url_parameters=dict(id='Alphabetical') + ) + assert status == 404 + when( 'Member is not found', json=dict(memberId=0), diff --git a/dolphin/tests/test_group_create.py b/dolphin/tests/test_group_create.py index 9ba1b888..cc3b3724 100644 --- a/dolphin/tests/test_group_create.py +++ b/dolphin/tests/test_group_create.py @@ -68,7 +68,7 @@ def test_create(self): 'Trying to pass with none title', json=dict(title=None) ) - assert status == '727 Title Is None' + assert status == '727 Title Is Null' when( 'Description length is less than limit', diff --git a/dolphin/tests/test_group_remove.py b/dolphin/tests/test_group_remove.py index 0b33344e..0c6b49e5 100644 --- a/dolphin/tests/test_group_remove.py +++ b/dolphin/tests/test_group_remove.py @@ -64,6 +64,15 @@ def test_remove(self): when('Trying to pass without form parameters', json={}) assert status == '708 Empty Form' + when('Group not found', url_parameters=dict(id=0)) + assert status == 404 + + when( + 'Intended group with string type not found', + url_parameters=dict(id='Alphabetical') + ) + assert status == 404 + when( 'Member is not found', json=dict(memberId=0), diff --git a/dolphin/tests/test_group_update.py b/dolphin/tests/test_group_update.py index d471caa3..7b2b08ef 100644 --- a/dolphin/tests/test_group_update.py +++ b/dolphin/tests/test_group_update.py @@ -78,7 +78,7 @@ def test_update(self): 'Title length is more than limit', json=given | dict(title=(50 + 1) * 'a') ) - assert status == '704 At Most 50 Characters Valid For Title' + assert status == '704 At Most 50 Characters Are Valid For Title' when('Invalid parameter is in the form', json=dict(a='a')) assert status == '707 Invalid field, only following fields are ' \ diff --git a/dolphin/tests/test_invitation_create.py b/dolphin/tests/test_invitation_create.py index e167e73c..0297c718 100644 --- a/dolphin/tests/test_invitation_create.py +++ b/dolphin/tests/test_invitation_create.py @@ -148,13 +148,13 @@ def test_invite(self): 'Trying to pass without application id parameter in form', form=Remove('applicationId') ) - assert status == '764 Application Id Not In form' + assert status == '764 Application Id Not In Form' when( 'Trying to pass without redirect uri parameter in form', form=Remove('redirectUri') ) - assert status == '766 Redirect Uri Not In form' + assert status == '766 Redirect Uri Not In From' when( 'The user already in this organization', diff --git a/dolphin/tests/test_issue_assign.py b/dolphin/tests/test_issue_assign.py index 05a39fd0..a7bb6b0d 100644 --- a/dolphin/tests/test_issue_assign.py +++ b/dolphin/tests/test_issue_assign.py @@ -151,7 +151,7 @@ def test_assign(self): assert status == '716 Invalid Resource Id Type' when('Phase not found', form=Update(phaseId=0)) - assert status == '613 Phase not found with id: 0' + assert status == '613 Phase Not Found' when('Phase id is not in form', form=Remove('phaseId')) assert status == '737 Phase Id Not In Form' diff --git a/dolphin/tests/test_issue_get.py b/dolphin/tests/test_issue_get.py index 7883b9a8..65af1cef 100644 --- a/dolphin/tests/test_issue_get.py +++ b/dolphin/tests/test_issue_get.py @@ -1,7 +1,8 @@ from auditor.context import Context as AuditLogContext from bddrest import status, response, when -from dolphin.models import Issue, Member, Workflow, Group, Project, Release +from dolphin.models import Issue, Member, Workflow, Group, Project, Release, \ + Skill, Phase, Item from dolphin.tests.helpers import LocalApplicationTestCase, oauth_mockup_server @@ -22,6 +23,7 @@ def mockup(cls): session.add(member) workflow = Workflow(title='default') + skill = Skill(title='First Skill') group = Group(title='default') release = Release( @@ -34,7 +36,7 @@ def mockup(cls): group=group, ) - project = Project( + cls.project = Project( release=release, workflow=workflow, group=group, @@ -45,7 +47,7 @@ def mockup(cls): ) cls.issue = Issue( - project=project, + project=cls.project, title='First issue', description='This is description of first issue', due_date='2020-2-20', @@ -54,6 +56,22 @@ def mockup(cls): room_id=2 ) session.add(cls.issue) + + cls.phase = Phase( + workflow=workflow, + title='Backlog', + order=1, + skill=skill, + ) + session.add(cls.phase) + session.flush() + + cls.item = Item( + member_id=member.id, + phase_id=cls.phase.id, + issue_id=cls.issue.id, + ) + session.add(cls.item) session.commit() def test_get(self): @@ -66,7 +84,9 @@ def test_get(self): ): assert status == 200 assert response.json['id'] == self.issue.id - assert response.json['title'] == 'First issue' + assert response.json['title'] == self.issue.title + assert response.json['project']['id'] == self.project.id + assert response.json['items'][0]['phaseId'] == self.phase.id when( 'Intended project with string type not found', diff --git a/dolphin/tests/test_issue_move.py b/dolphin/tests/test_issue_move.py index 936d088b..3e8b2d01 100644 --- a/dolphin/tests/test_issue_move.py +++ b/dolphin/tests/test_issue_move.py @@ -1,11 +1,16 @@ +from auditor import MiddleWare from auditor.context import Context as AuditLogContext from bddrest import status, when, given, response +from dolphin import Dolphin +from dolphin.middleware_callback import callback as auditor_callback from dolphin.models import Issue, Project, Member, Workflow, Group, Release -from dolphin.tests.helpers import LocalApplicationTestCase, oauth_mockup_server +from dolphin.tests.helpers import LocalApplicationTestCase, \ + oauth_mockup_server, chat_mockup_server class TestIssue(LocalApplicationTestCase): + __application__ = MiddleWare(Dolphin(), auditor_callback) @classmethod @AuditLogContext(dict()) @@ -83,7 +88,7 @@ def mockup(cls): def test_move(self): self.login('member1@example.com') - with oauth_mockup_server(), self.given( + with oauth_mockup_server(), chat_mockup_server(), self.given( f'Move a issue', f'/apiv1/issues/id: {self.issue1.id}', f'MOVE', @@ -117,7 +122,7 @@ def test_move(self): 'Intended project with integer type not found', form=dict(projectId=0), ) - assert status == '601 Project not found with id: 0' + assert status == '601 Project Not Found' when( 'Trying to pass with hidden project', diff --git a/dolphin/tests/test_issue_relate.py b/dolphin/tests/test_issue_relate.py index 903c362e..5fd94730 100644 --- a/dolphin/tests/test_issue_relate.py +++ b/dolphin/tests/test_issue_relate.py @@ -127,7 +127,7 @@ def test_relate(self): 'Trying to pass with none issue id', json=dict(targetIssueId=None) ) - assert status == '779 Target Issue Id Is None' + assert status == '779 Target Issue Id Is Null' when( 'Trying to pass with invalid issue id type', diff --git a/dolphin/tests/test_issue_unassign.py b/dolphin/tests/test_issue_unassign.py index 5ba10ee6..3c0d580d 100644 --- a/dolphin/tests/test_issue_unassign.py +++ b/dolphin/tests/test_issue_unassign.py @@ -128,7 +128,7 @@ def test_unassign(self): 'Intended phase with integer type not found', form=Update(phaseId=0) ) - assert status == '613 Phase not found with id: 0' + assert status == '613 Phase Not Found' when('Phase id is not in form', form=Remove('phaseId')) assert status == '737 Phase Id Not In Form' diff --git a/dolphin/tests/test_issue_unrelate.py b/dolphin/tests/test_issue_unrelate.py index a60208c1..d3fd3ad3 100644 --- a/dolphin/tests/test_issue_unrelate.py +++ b/dolphin/tests/test_issue_unrelate.py @@ -150,7 +150,7 @@ def test_unrelate(self): 'Related issue is none', json=dict(targetIssueId=None) ) - assert status == '779 Target Issue Id Is None' + assert status == '779 Target Issue Id Is Null' when( 'Trying to pass with invalid issue id type', diff --git a/dolphin/tests/test_issue_update.py b/dolphin/tests/test_issue_update.py index 4e69f9e9..3b0b71bc 100644 --- a/dolphin/tests/test_issue_update.py +++ b/dolphin/tests/test_issue_update.py @@ -6,13 +6,16 @@ from nanohttp.contexts import Context from dolphin import Dolphin +from dolphin.middleware_callback import callback as auditor_callback from dolphin.models import Issue, Project, Member, Workflow, Group, Release -from dolphin.tests.helpers import LocalApplicationTestCase, oauth_mockup_server +from dolphin.tests.helpers import LocalApplicationTestCase, \ + oauth_mockup_server, chat_mockup_server def callback(audit_logs): global logs logs = audit_logs + auditor_callback(audit_logs) class TestIssue(LocalApplicationTestCase): @@ -101,7 +104,7 @@ def __init__(self, member): days=4, priority='high', ) - with oauth_mockup_server(), self.given( + with oauth_mockup_server(), chat_mockup_server(), self.given( 'Update a issue', f'/apiv1/issues/id:{self.issue2.id}', 'UPDATE', @@ -190,7 +193,7 @@ def __init__(self, member): form=given + dict(status='progressing') | \ dict(title='Another title') ) - assert status == '705 Invalid status, only one of "to-do, ' \ + assert status == '705 Invalid status value, only one of "to-do, ' \ 'in-progress, on-hold, complete, done" will be accepted' assert status.text.startswith('Invalid status') @@ -201,6 +204,11 @@ def __init__(self, member): assert status == '767 Invalid priority, only one of "low, '\ 'normal, high" will be accepted' + when( + 'ProjectId in form', + form=Update(projectId=self.issue2.project.id) + ) + assert status == 200 when( 'Invalid parameter is in the form', diff --git a/dolphin/tests/test_item_estimate.py b/dolphin/tests/test_item_estimate.py new file mode 100644 index 00000000..e2aa39ca --- /dev/null +++ b/dolphin/tests/test_item_estimate.py @@ -0,0 +1,197 @@ +from datetime import datetime + +from auditor.context import Context as AuditLogContext +from bddrest import status, response, when, given + +from dolphin.models import Project, Member, Workflow, Group, Release, Skill, \ + Phase, Issue, Item +from dolphin.tests.helpers import LocalApplicationTestCase, oauth_mockup_server + + +class TestItem(LocalApplicationTestCase): + + @classmethod + @AuditLogContext(dict()) + def mockup(cls): + session = cls.create_session() + + cls.member1 = Member( + title='First Member', + email='member1@example.com', + access_token='access token 1', + phone=123456789, + reference_id=2 + ) + + workflow = Workflow(title='Default') + session.add(workflow) + + skill = Skill(title='First Skill') + phase1 = Phase( + title='backlog', + order=-1, + workflow=workflow, + skill=skill, + ) + session.add(phase1) + + group = Group(title='default') + + release = Release( + title='My first release', + description='A decription for my first release', + cutoff='2030-2-20', + launch_date='2030-2-20', + manager=cls.member1, + room_id=1, + group=group, + ) + + project = Project( + release=release, + workflow=workflow, + group=group, + manager=cls.member1, + title='My first project', + description='A decription for my project', + room_id=2 + ) + + issue1 = Issue( + project=project, + title='First issue', + description='This is description of first issue', + due_date='2020-2-20', + kind='feature', + days=1, + room_id=3 + ) + session.add(issue1) + + issue2 = Issue( + project=project, + title='Second issue', + description='This is description of second issue', + due_date='2020-2-22', + kind='feature', + days=1, + room_id=4 + ) + session.add(issue2) + session.flush() + + cls.item1 = Item( + issue_id=issue1.id, + phase_id=phase1.id, + member_id=cls.member1.id, + ) + session.add(cls.item1) + + cls.item2 = Item( + issue_id=issue2.id, + phase_id=phase1.id, + member_id=cls.member1.id, + ) + session.add(cls.item2) + session.commit() + + def test_estimate(self): + self.login(self.member1.email) + json = dict( + startDate=datetime.strptime('2019-2-2', '%Y-%m-%d').isoformat(), + endDate=datetime.strptime('2019-2-3', '%Y-%m-%d').isoformat(), + estimatedHours=3, + ) + + with oauth_mockup_server(), self.given( + 'Estimating a item', + f'/apiv1/items/id: {self.item1.id}', + 'ESTIMATE', + json=json + ): + assert status == 200 + assert response.json['id'] == self.item1.id + assert response.json['estimatedHours'] == json['estimatedHours'] + assert response.json['startDate'] == json['startDate'] + assert response.json['endDate'] == json['endDate'] + + when( + 'Intended item with string type not found', + url_parameters=dict(id='Alphabetical') + ) + assert status == 404 + + when( + 'Intended item with string type not found', + url_parameters=dict(id=100) + ) + assert status == 404 + + when( + 'Form is empty', + json=dict() + ) + assert status == '708 Empty Form' + + when( + 'Form parameter is sent with request', + json=dict(parameter='Invalid form parameter') + ) + assert status == '707 Invalid field, only following fields are ' \ + 'accepted: startDate, endDate, estimatedHours' + + when( + 'Start date is less than end date', + json=given | dict( + endDate=datetime \ + .strptime('2019-1-2', '%Y-%m-%d') \ + .isoformat() + ) + ) + assert status == '657 End Date Must Be Greater Than Start Date' + + when( + 'Start date is not in form', + json=given - 'startDate' + ) + assert status == '792 Start Date Not In Form' + + when( + 'Start date is not in form', + json=given | dict(startDate=None) + ) + assert status == '905 Start Date Is Null' + + when( + 'End date is not in form', + json=given - 'endDate' + ) + assert status == '793 End Date Not In Form' + + when( + 'End date is not in form', + json=given | dict(endDate=None) + ) + assert status == '906 End Date Is Null' + + when( + 'Estimated hours field is not in form', + json=given - 'estimatedHours' + ) + assert status == '901 Estimated Hours Not In Form' + + when( + 'Estimated hours is null', + json=given | dict(estimatedHours=None) + ) + assert status == '904 Estimated Hours Is Null' + + when( + 'Estimated hours type is wrong', + json=given | dict(estimatedHours='invalid type') + ) + assert status == '900 Invalid Estimated Hours Type' + + when('Request is not authorized', authorization=None) + assert status == 401 + diff --git a/dolphin/tests/test_item_get.py b/dolphin/tests/test_item_get.py index 27efdbdf..c395beff 100644 --- a/dolphin/tests/test_item_get.py +++ b/dolphin/tests/test_item_get.py @@ -1,3 +1,5 @@ +from datetime import datetime + from auditor.context import Context as AuditLogContext from bddrest import status, response, when @@ -55,20 +57,20 @@ def mockup(cls): room_id=1 ) - issue1 = Issue( + cls.issue1 = Issue( project=project, title='First issue', description='This is description of first issue', - due_date='2020-2-20', + due_date=datetime.strptime('2020-2-20', '%Y-%m-%d'), kind='feature', days=1, room_id=2 ) - session.add(issue1) + session.add(cls.issue1) session.flush() cls.item = Item( - issue_id=issue1.id, + issue_id=cls.issue1.id, phase_id=phase1.id, member_id=cls.member1.id, ) @@ -86,6 +88,13 @@ def test_get(self): assert status == 200 assert response.json['id'] == self.item.id + issue = response.json['issue'] + assert issue['title'] == self.issue1.title + assert issue['kind'] == self.issue1.kind + assert issue['status'] == self.issue1.status + assert issue['priority'] == self.issue1.priority + assert issue['boarding'] == self.issue1.boarding + when( 'Intended item with string type not found', url_parameters=dict(id='Alphabetical') diff --git a/dolphin/tests/test_item_list.py b/dolphin/tests/test_item_list.py index 8da05c69..592631e3 100644 --- a/dolphin/tests/test_item_list.py +++ b/dolphin/tests/test_item_list.py @@ -1,3 +1,5 @@ +from datetime import datetime + from bddrest import status, response, when, given from auditor.context import Context as AuditLogContext @@ -124,7 +126,10 @@ def mockup(cls): cls.item2 = Item( issue_id=cls.issue2.id, phase_id=cls.phase3.id, - member_id=cls.member2.id, + member_id=cls.member1.id, + start_date=datetime.strptime('2020-2-2', '%Y-%m-%d'), + end_date=datetime.strptime('2020-2-3', '%Y-%m-%d'), + estimated_hours=3, ) session.add(cls.item2) session.flush() @@ -133,6 +138,9 @@ def mockup(cls): issue_id=cls.issue3.id, phase_id=cls.phase2.id, member_id=cls.member1.id, + start_date=datetime.strptime('2018-2-2', '%Y-%m-%d'), + end_date=datetime.strptime('2020-2-3', '%Y-%m-%d'), + estimated_hours=3, ) session.add(cls.item3) session.commit() @@ -167,6 +175,27 @@ def test_list_item(self): when('Filter by id', query=dict(id=f'!{self.item1.id}')) assert len(response.json) == 2 + when( + 'Filter by `needEstimate` zone', + query=dict(zone='needEstimate') + ) + assert len(response.json) == 1 + assert response.json[0]['id'] == self.item1.id + + when( + 'Filter by `upcomingNuggets` zone', + query=dict(zone='upcomingNuggets') + ) + assert len(response.json) == 1 + assert response.json[0]['id'] == self.item2.id + + when( + 'Filter by `inProcessNuggets` zone', + query=dict(zone='inProcessNuggets') + ) + assert len(response.json) == 1 + assert response.json[0]['id'] == self.item3.id + when( 'Paginate item', query=dict(sort='id', take=1, skip=2) diff --git a/dolphin/tests/test_organization_create.py b/dolphin/tests/test_organization_create.py index 3e8f80c4..3cde4be2 100644 --- a/dolphin/tests/test_organization_create.py +++ b/dolphin/tests/test_organization_create.py @@ -1,9 +1,23 @@ +import io +from os.path import dirname, abspath, join + +from nanohttp import settings from bddrest.authoring import when, status, response, given from dolphin.models import Member, Organization, OrganizationMember from dolphin.tests.helpers import LocalApplicationTestCase, oauth_mockup_server +TEST_DIR = abspath(dirname(__file__)) +STUFF_DIR = join(TEST_DIR, 'stuff') +VALID_LOGO_PATH = join(STUFF_DIR, 'logo-225x225.jpg') +INVALID_FORMAT_LOGO_PATH = join(STUFF_DIR, 'test.pdf') +INVALID_MAXIMUM_SIZE_LOGO_PATH = join(STUFF_DIR, 'logo-550x550.jpg') +INVALID_MINIMUM_SIZE_LOGO_PATH = join(STUFF_DIR, 'logo-50x50.jpg') +INVALID_RATIO_LOGO_PATH = join(STUFF_DIR, 'logo-150x100.jpg') +INVALID_MAXMIMUM_LENGTH_LOGO_PATH = join(STUFF_DIR, 'maximum-length-30.jpg') + + class TestOrganization(LocalApplicationTestCase): @classmethod @@ -35,12 +49,13 @@ def mockup(cls): def test_create(self): title = 'My-organization' self.login(email='member1@example.com') + settings.attachments.organizations.logos.max_length = 30 with oauth_mockup_server(), self.given( 'The organization has successfully created', '/apiv1/organizations', 'CREATE', - form=dict(title=title) + multipart=dict(title=title) ): assert status == 200 assert response.json['title'] == title @@ -52,25 +67,71 @@ def test_create(self): when( 'The organization title is exist', - form=dict(title='organization-title') + multipart=dict(title='organization-title') ) assert status == '600 Repetitive Title' - when('The title format is invalid', form=dict(title='my organ')) + when( + 'The title format is invalid', + multipart=dict(title='my organ') + ) assert status == '747 Invalid Title Format' when( 'The length of title is too long', - form=dict(title=(50 + 1) * 'a') + multipart=dict(title=(50 + 1) * 'a') ) assert status == '704 At Most 50 Characters Are Valid For Title' - when('The title not in form', form=given - 'title' + dict(a='a')) + when( + 'The title not in form', + multipart=given - 'title' + dict(a='a') + ) assert status == '710 Title Not In Form' - when('Trying to pass with empty form', form={}) + when('Trying to pass with empty form', multipart={}) assert status == '400 Empty Form' + with open(INVALID_MAXIMUM_SIZE_LOGO_PATH, 'rb') as f: + when( + 'The logo size is exceeded the maximum size', + multipart=dict(title='newtitle', logo=io.BytesIO(f.read())) + ) + assert status == '625 Maximum allowed width is: 200, '\ + 'but the 550 is given.' + + with open(INVALID_MINIMUM_SIZE_LOGO_PATH, 'rb') as f: + when( + 'The logo size is less than minimum size', + multipart=dict(title='newtitle', logo=io.BytesIO(f.read())) + ) + assert status == '625 Minimum allowed width is: 100, '\ + 'but the 50 is given.' + + with open(INVALID_RATIO_LOGO_PATH, 'rb') as f: + when( + 'Aspect ratio of the logo is invalid', + multipart=dict(title='newtitle', logo=io.BytesIO(f.read())) + ) + assert status == '622 Invalid aspect ratio Only ' \ + '1/1 is accepted.' + + with open(INVALID_FORMAT_LOGO_PATH, 'rb') as f: + when( + 'Format of the avatar is invalid', + multipart=dict(title='newtitle', logo=io.BytesIO(f.read())) + ) + assert status == '623 Invalid content type, Valid options '\ + 'are: image/jpeg, image/png' + + with open(INVALID_MAXMIMUM_LENGTH_LOGO_PATH, 'rb') as f: + when( + 'The maxmimum length of avatar is invalid', + multipart=dict(title='newtitle', logo=io.BytesIO(f.read())) + ) + assert status == '624 Cannot store files larger than: '\ + '30720 bytes' + when('Trying with an unauthorized member', authorization=None) assert status == 401 diff --git a/dolphin/tests/test_phase_create.py b/dolphin/tests/test_phase_create.py index f914d4d8..eab4d115 100644 --- a/dolphin/tests/test_phase_create.py +++ b/dolphin/tests/test_phase_create.py @@ -100,7 +100,7 @@ def test_create(self): 'Title length is more than limit', json=given | dict(title=(50 + 1) * 'a') ) - assert status == '704 At Most 50 Characters Valid For Title' + assert status == '704 At Most 50 Characters Are Valid For Title' when( 'Title length is more than limit', @@ -109,7 +109,7 @@ def test_create(self): assert status == '703 At Most 512 Characters Are Valid For Description' when('Title is not in form', json=given - 'title') - assert status == '610 Title Not In Form' + assert status == '710 Title Not In Form' when( 'Order type is wrong', diff --git a/dolphin/tests/test_phase_get.py b/dolphin/tests/test_phase_get.py index df801e26..d7174391 100644 --- a/dolphin/tests/test_phase_get.py +++ b/dolphin/tests/test_phase_get.py @@ -36,8 +36,7 @@ def test_get(self): self.login(self.member.email) with oauth_mockup_server(), self.given( f'Getting a phase', - f'/apiv1/workflows/workflow_id: {self.workflow.id}' \ - f'/phases/phase_id: {self.phase.id}', + f'/apiv1/phases/phase_id: {self.phase.id}', f'GET', ): assert status == 200 diff --git a/dolphin/tests/test_phase_update.py b/dolphin/tests/test_phase_update.py index 4d78a314..0d42d383 100644 --- a/dolphin/tests/test_phase_update.py +++ b/dolphin/tests/test_phase_update.py @@ -82,6 +82,18 @@ def test_update(self): ) assert status == '600 Repetitive Title' + when( + 'Trying to pass using id is alphabetical', + url_parameters=Update(id='not-integer') + ) + assert status == 404 + + when( + 'Phase not exit with this id', + url_parameters=Update(id=0) + ) + assert status == 404 + when( 'Order is repetitive', json=given | dict(order=self.phase2.order) @@ -92,7 +104,7 @@ def test_update(self): 'Title length is more than limit', json=given | dict(title=(50 + 1) * 'a') ) - assert status == '704 At Most 50 Characters Valid For Title' + assert status == '704 At Most 50 Characters Are Valid For Title' when( 'Title length is more than limit', diff --git a/dolphin/tests/test_phases_list.py b/dolphin/tests/test_phases_list.py index 54c0fe4b..4421c276 100644 --- a/dolphin/tests/test_phases_list.py +++ b/dolphin/tests/test_phases_list.py @@ -9,14 +9,14 @@ class TestListPhase(LocalApplicationTestCase): @classmethod def mockup(cls): session = cls.create_session() - member = Member( + cls.member = Member( title='First Member', email='member1@example.com', access_token='access token 1', phone=123456789, reference_id=2 ) - session.add(member) + session.add(cls.member) skill = Skill(title='First Skill') cls.triage = Phase(title='triage', order=0, skill=skill) @@ -71,3 +71,38 @@ def test_list_phases(self): when('Try to pass an Unauthorized request', authorization=None) assert status == 401 + def test_list_phases_without_workflow(self): + self.login(self.member.email) + + with oauth_mockup_server(), self.given( + 'List all phases', + '/apiv1/phases', + 'LIST', + ): + assert status == 200 + assert len(response.json) == 2 + + when( + 'Try to send a form in the request', + form=dict(parameter='form parameter') + ) + assert status == '709 Form Not Allowed' + + when('Try to sort the response', query=dict(sort='id')) + assert len(response.json) == 2 + assert response.json[0]['id'] == 1 + + when('Sorting the response descending', query=dict(sort='-id')) + assert response.json[0]['id'] == 2 + + when('Testing pagination', query=dict(take=1, skip=1)) + assert len(response.json) == 1 + assert response.json[0]['order'] == -1 + + when('Filtering the response', query=dict(id=self.triage.id)) + assert len(response.json) == 1 + assert response.json[0]['title'] == self.triage.title + + when('Try to pass an Unauthorized request', authorization=None) + assert status == 401 + diff --git a/dolphin/tests/test_project_create.py b/dolphin/tests/test_project_create.py index f5fcba35..9bf841a1 100644 --- a/dolphin/tests/test_project_create.py +++ b/dolphin/tests/test_project_create.py @@ -6,6 +6,7 @@ from bddrest import status, response, when, Remove, given, Update from dolphin import Dolphin +from dolphin.middleware_callback import callback as auditor_callback from dolphin.models import Project, Member, Workflow, Release, Group from dolphin.tests.helpers import LocalApplicationTestCase, \ oauth_mockup_server, chat_mockup_server, chat_server_status @@ -14,6 +15,7 @@ def callback(audit_logs): global logs logs = audit_logs + auditor_callback(audit_logs) class TestProject(LocalApplicationTestCase): @@ -137,6 +139,12 @@ def test_create(self): ) assert status == 200 + when( + 'Group id is not in form', + json=given - 'groupId' | dict(title='New Project1') + ) + assert status == 200 + when( 'Workflow id is in form but not found(alphabetical)', json=given | dict(title='New title', workflowId='Alphabetical') @@ -147,7 +155,7 @@ def test_create(self): 'Workflow id is in form but not found(numeric)', json=given | dict(title='New title', workflowId=0) ) - assert status == '616 Workflow not found with id: 0' + assert status == '616 Workflow Not Found' when( 'Title format is wrong', @@ -159,8 +167,7 @@ def test_create(self): 'Title is repetetive', json=given | dict(title='My first project') ) - assert status == '600 Another project with title: My first '\ - 'project is already exists.' + assert status == '600 Repetitive Title' when( 'Release ID type is wrong', @@ -172,7 +179,7 @@ def test_create(self): 'Release not found with integer type', json=given | dict(releaseId=0, title='New title') ) - assert status == '607 Release not found with id: 0' + assert status == '607 Release Not Found' when( 'Title is not in form', diff --git a/dolphin/tests/test_project_update.py b/dolphin/tests/test_project_update.py index 4636055e..42b2e316 100644 --- a/dolphin/tests/test_project_update.py +++ b/dolphin/tests/test_project_update.py @@ -7,6 +7,7 @@ from nanohttp import context from dolphin import Dolphin +from dolphin.middleware_callback import callback as auditor_callback from dolphin.models import Project, Member, Workflow, Group, Release from dolphin.tests.helpers import LocalApplicationTestCase, \ oauth_mockup_server, chat_mockup_server @@ -15,6 +16,7 @@ def callback(audit_logs): global logs logs = audit_logs + auditor_callback(audit_logs) class TestProject(LocalApplicationTestCase): diff --git a/dolphin/tests/test_projectattachment_add.py b/dolphin/tests/test_projectattachment_add.py index a1f8ae0a..66b6e6f9 100644 --- a/dolphin/tests/test_projectattachment_add.py +++ b/dolphin/tests/test_projectattachment_add.py @@ -93,6 +93,11 @@ def test_add_attachment(self): ) assert status == '704 At Most 128 Characters Are Valid For Title' + when( + 'The project not exist with this id', + url_parameters=dict(id=0) + ) + assert status == 404 with open(maximum_image_path, 'rb') as f: when( diff --git a/dolphin/tests/test_release_create.py b/dolphin/tests/test_release_create.py index aa5aa832..e085ac1e 100644 --- a/dolphin/tests/test_release_create.py +++ b/dolphin/tests/test_release_create.py @@ -4,6 +4,7 @@ from bddrest import status, response, when, Remove, given, Update from dolphin import Dolphin +from dolphin.middleware_callback import callback as auditor_callback from dolphin.models import Member, Release, Group from dolphin.tests.helpers import LocalApplicationTestCase, \ oauth_mockup_server, chat_mockup_server, chat_server_status @@ -12,6 +13,7 @@ def callback(audit_logs): global logs logs = audit_logs + auditor_callback(audit_logs) class TestRelease(LocalApplicationTestCase): @@ -98,8 +100,7 @@ def test_create(self): 'Title is repetetive', json=given | dict(title='My first release') ) - assert status == '600 Another release with title: My first '\ - 'release is already exists.' + assert status == '600 Repetitive Title' when( 'Description length is less than limit', diff --git a/dolphin/tests/test_skill_update.py b/dolphin/tests/test_skill_update.py index 754cc6cd..71986215 100644 --- a/dolphin/tests/test_skill_update.py +++ b/dolphin/tests/test_skill_update.py @@ -50,6 +50,15 @@ def test_update(self): assert response.json['id'] is not None assert response.json['description'] is not None + when('Skill not found', url_parameters=dict(id=0)) + assert status == 404 + + when( + 'Intended skill with string type not found', + url_parameters=dict(id='Alphabetical'), + ) + assert status == 404 + when( 'Trying to send title which intended skill already has', json=dict(title=title), diff --git a/dolphin/tests/test_tag_create.py b/dolphin/tests/test_tag_create.py index a670c0a5..21cf695a 100644 --- a/dolphin/tests/test_tag_create.py +++ b/dolphin/tests/test_tag_create.py @@ -86,7 +86,7 @@ def test_create(self): 'Trying to pass with none title', json=dict(title=None) ) - assert status == '727 Title Is None' + assert status == '727 Title Is Null' when( 'Description length is less than limit', diff --git a/dolphin/tests/test_tag_update.py b/dolphin/tests/test_tag_update.py index 4ad8d4be..ad1fee87 100644 --- a/dolphin/tests/test_tag_update.py +++ b/dolphin/tests/test_tag_update.py @@ -88,6 +88,15 @@ def test_update(self): assert response.json['description'] == description assert response.json['id'] == self.tag1.id + when('Tag not found', url_parameters=dict(id=0)) + assert status == 404 + + when( + 'Intended tag with string type not found', + url_parameters=dict(id='Alphabetical') + ) + assert status == 404 + when( 'Title is repetitive', json=dict(title=self.tag2.title) diff --git a/dolphin/tests/test_tokens.py b/dolphin/tests/test_tokens.py new file mode 100644 index 00000000..8b1abe43 --- /dev/null +++ b/dolphin/tests/test_tokens.py @@ -0,0 +1,36 @@ +import time + +import pytest +from nanohttp import settings, HTTPStatus + +from dolphin.tokens import OrganizationInvitationToken +from dolphin.tests.helpers import LocalApplicationTestCase + + +class TestTokens(LocalApplicationTestCase): + + def test_invitaion_token(self): + + # Create access token using dump and load methods + payload = dict(a=1, b=2) + invitation_token = OrganizationInvitationToken(payload) + dump = invitation_token.dump() + load = OrganizationInvitationToken.load(dump.decode()) + assert load.payload == payload + + # Trying to load token using bad signature token + with pytest.raises( + HTTPStatus('626 Malformed Token').__class__ + ): + load = OrganizationInvitationToken.load('token') + + # Trying to load token when token is expired + with pytest.raises( + HTTPStatus('627 Token Expired').__class__ + ): + settings.organization_invitation.max_age = 0.3 + invitation_token = OrganizationInvitationToken(payload) + dump = invitation_token.dump() + time.sleep(1) + load = OrganizationInvitationToken.load(dump.decode()) + diff --git a/dolphin/tests/test_workflow_create.py b/dolphin/tests/test_workflow_create.py index 9c2fb875..67d4bf6a 100644 --- a/dolphin/tests/test_workflow_create.py +++ b/dolphin/tests/test_workflow_create.py @@ -71,7 +71,7 @@ def test_create(self): 'Trying to pass with none title', json=dict(title=None) ) - assert status == '727 Title Is None' + assert status == '727 Title Is Null' when( 'Description length is less than limit', diff --git a/dolphin/tests/test_workflow_update.py b/dolphin/tests/test_workflow_update.py index b22ede75..1462ac2f 100644 --- a/dolphin/tests/test_workflow_update.py +++ b/dolphin/tests/test_workflow_update.py @@ -51,6 +51,15 @@ def test_update(self): assert response.json['title'] == title assert response.json['description'] == description + when('Workflow not found', url_parameters=dict(id=0)) + assert status == 404 + + when( + 'Intended workflow with string type not found', + url_parameters=dict(id='Alphabetical') + ) + assert status == 404 + when('There is no parameters in form', json={}) assert status == '708 Empty Form' diff --git a/dolphin/validators.py b/dolphin/validators.py index 11f2d6a0..fc9aa25f 100644 --- a/dolphin/validators.py +++ b/dolphin/validators.py @@ -3,14 +3,7 @@ from nanohttp import validate, HTTPStatus, context, int_or_notfound from restfulpy.orm import DBSession -from .exceptions import StatusResourceNotFound, StatusRepetitiveTitle, \ - StatusRelatedIssueNotFound, StatusEventTypeNotFound, \ - StatusInvalidStartDateFormat, StatusInvalidEndDateFormat, \ - StatusLimitedCharecterForNote, StatusHoursMustBeGreaterThanZero, \ - StatusSummaryNotInForm, StatusEstimatedTimeNotInForm, \ - StatusEndDateNotInForm, StatusStartDateNotInForm, StatusNoteIsNull, \ - StatusEstimatedTimeIsNull, StatusStartDateIsNull, StatusEndDateIsNull, \ - StatusRepeatNotInForm, StatusInvalidHoursType +from .exceptions import * from .models import * from .models.organization import roles @@ -34,14 +27,12 @@ def release_exists_validator(releaseId, project, field): try: releaseId = int(releaseId) except (TypeError, ValueError): - raise HTTPStatus('750 Invalid Release Id Type') + raise StatusInvalidReleaseIdType() if 'releaseId' in form and not DBSession.query(Release) \ .filter(Release.id == releaseId) \ .one_or_none(): - raise HTTPStatus( - f'607 Release not found with id: {context.form["releaseId"]}' - ) + raise StatusReleaseNotFound() return releaseId @@ -49,10 +40,7 @@ def release_exists_validator(releaseId, project, field): def release_status_value_validator(status, project, field): form = context.form if 'status' in form and form['status'] not in release_statuses: - raise HTTPStatus( - f'705 Invalid status value, only one of ' \ - f'"{", ".join(release_statuses)}" will be accepted' - ) + raise StatusInvalidStatusValue(statuses_values=release_statuses) return form['status'] @@ -61,9 +49,8 @@ def release_not_exists_validator(title, project, field): release = DBSession.query(Release).filter(Release.title == title) \ .one_or_none() if release is not None: - raise HTTPStatus( - f'600 Another release with title: {title} is already exists.' - ) + raise StatusRepetitiveTitle() + return title @@ -72,9 +59,8 @@ def project_not_exists_validator(title, project, field): project = DBSession.query(Project).filter(Project.title == title) \ .one_or_none() if project is not None: - raise HTTPStatus( - f'600 Another project with title: {title} is already exists.' - ) + raise StatusRepetitiveTitle() + return title @@ -83,12 +69,10 @@ def project_accessible_validator(projectId, project, field): project = DBSession.query(Project) \ .filter(Project.id == context.form['projectId']).one_or_none() if not project: - raise HTTPStatus( - f'601 Project not found with id: {context.form["projectId"]}' - ) + raise StatusProjectNotFound() if project.is_deleted: - raise HTTPStatus('746 Hidden Project Is Not Editable') + raise StatusHiddenProjectIsNotEditable() return projectId @@ -107,10 +91,7 @@ def event_repeat_value_validator(repeat, project, field): def project_status_value_validator(status, project, field): form = context.form if 'status' in form and form['status'] not in project_statuses: - raise HTTPStatus( - f'705 Invalid status value, only one of ' \ - f'"{", ".join(project_statuses)}" will be accepted' - ) + raise StatusInvalidStatusValue(statuses_values=project_statuses) return form['status'] @@ -122,9 +103,8 @@ def issue_not_exists_validator(title, project, field): for issue in project.issues: if issue.title == title: - raise HTTPStatus( - f'600 Another issue with title: "{title}" is already exists.' - ) + raise StatusRepetitiveTitle() + return title @@ -142,32 +122,22 @@ def relate_to_issue_exists_validator(relatedIssueId, container, field): def kind_value_validator(kind, project, field): form = context.form if 'kind' in form and form['kind'] not in issue_kinds: - raise HTTPStatus( - f'717 Invalid kind, only one of ' \ - f'"{", ".join(issue_kinds)}" will be accepted' - ) + raise StatusInvalidKind(issue_kinds) return form['kind'] def issue_status_value_validator(status, project, field): form = context.form - if 'status' in form \ - and form['status'] is not None and \ - form['status'] not in issue_statuses: - raise HTTPStatus( - f'705 Invalid status, only one of ' \ - f'"{", ".join(issue_statuses)}" will be accepted' - ) + if 'status' in form and form['status'] not in issue_statuses: + raise StatusInvalidStatusValue(statuses_values=issue_statuses) + return form['status'] def issue_priority_value_validator(priority, project, field): form = context.form if 'priority' in form and form['priority'] not in issue_priorities: - raise HTTPStatus( - f'767 Invalid priority, only one of ' \ - f'"{", ".join(issue_priorities)}" will be accepted' - ) + raise StatusInvalidPriority(issue_priorities) return form['priority'] @@ -177,12 +147,12 @@ def phase_exists_validator(phaseId, project, field): try: phaseId = int(phaseId) except (TypeError, ValueError): - raise HTTPStatus(f'613 Phase not found with id: {form["phaseId"]}') + raise StatusPhaseNotFound() if 'phaseId' in form and not DBSession.query(Phase) \ .filter(Phase.id == form['phaseId']) \ .one_or_none(): - raise HTTPStatus(f'613 Phase not found with id: {form["phaseId"]}') + raise StatusPhaseNotFound() return phaseId @@ -192,12 +162,12 @@ def workflow_exists_validator(workflowId, project, field): try: workflowId = int(workflowId) except (TypeError, ValueError): - raise HTTPStatus('743 Invalid Workflow Id Type') + raise StatusInvalidWorkflowIdType() if not DBSession.query(Workflow) \ .filter(Workflow.id == workflowId) \ .one_or_none(): - raise HTTPStatus(f'616 Workflow not found with id: {workflowId}') + raise StatusWorkflowNotFound() return workflowId @@ -205,10 +175,8 @@ def workflow_exists_validator(workflowId, project, field): def item_status_value_validator(status, project, field): form = context.form if 'status' in form and form['status'] not in item_statuses: - raise HTTPStatus( - f'705 Invalid status value, only one of ' \ - f'"{", ".join(item_statuses)}" will be accepted' - ) + raise StatusInvalidStatusValue(statuses_values=item_statuses) + return form['status'] @@ -234,15 +202,13 @@ def resource_exists_validator(resourceId, project, field): .filter(Resource.id == form['resourceId']) \ .one_or_none() if not resource: - raise HTTPStatus( - f'609 Resource not found with id: {form["resourceId"]}' - ) + raise StatusResourceNotFound(resource_id=context.form['resourceId']) return resourceId def organization_value_of_role_validator(role, container, field): if context.form.get('role') not in roles: - raise HTTPStatus('756 Invalid Role Value') + raise StatusInvalidRoleValue() return role @@ -251,7 +217,7 @@ def group_exists_validator(title, project, field): group = DBSession.query(Group).filter(Group.title == title).one_or_none() if group is not None: - raise HTTPStatus('600 Repetitive Title') + raise StatusRepetitiveTitle() return title @@ -262,7 +228,7 @@ def workflow_exists_validator_by_title(title, project, field): .filter(Workflow.title == title) \ .one_or_none() if workflow is not None: - raise HTTPStatus('600 Repetitive Title') + raise StatusRepetitiveTitle() return title @@ -284,7 +250,7 @@ def tag_exists_validator(title, project, field): def skill_exists_validator(title, project, field): skill = DBSession.query(Skill).filter(Skill.title == title).one_or_none() if skill is not None: - raise HTTPStatus('600 Repetitive Title') + raise StatusRepetitiveTitle() return title @@ -329,17 +295,17 @@ def item_exists_validator(item_id, container, field): release_validator = validate( title=dict( - required='710 Title Not In Form', - max_length=(128, '704 At Most 128 Characters Are Valid For Title'), - pattern=(TITLE_PATTERN, '747 Invalid Title Format'), + required=StatusTitleNotInForm, + max_length=(128, StatusMaxLenghtForTitle(128)), + pattern=(TITLE_PATTERN, StatusInvalidTitleFormat), callback=release_not_exists_validator ), description=dict( - max_length=(8192, '703 At Most 8192 Characters Are Valid For Description') + max_length=(8192, StatusMaxLenghtForDescription(8192)) ), cutoff=dict( - pattern=(DATETIME_PATTERN, '702 Invalid Cutoff Format'), - required='712 Cutoff Not In Form' + pattern=(DATETIME_PATTERN, StatusInvalidCutoffFormat), + required=StatusCutoffNotInForm ), status=dict( callback=release_status_value_validator @@ -350,27 +316,28 @@ def item_exists_validator(item_id, container, field): not_none='778 Manager Id Is Null', ), launchDate=dict( - pattern=(DATETIME_PATTERN, '784 Invalid Launch Date Format'), - required='783 Launch Date Not In Form' + pattern=(DATETIME_PATTERN, StatusInvalidLaunchDateFormat), + required=StatusLaunchDateNotInForm ), groupId=dict( - type_=(int, '797 Invalid Group Id Type'), - required='795 Group Id Not In Form', - not_none='796 Group Id Is Null', + type_=(int, StatusInvalidGroupIdType), + required=StatusGroupIdNotInForm, + not_none=StatusGroupIdIsNull, ), ) update_release_validator = validate( title=dict( - max_length=(128, '704 At Most 128 Characters Are Valid For Title'), - pattern=(TITLE_PATTERN, '747 Invalid Title Format'), + max_length=(128, StatusMaxLenghtForTitle(128)), + pattern=(TITLE_PATTERN, StatusInvalidTitleFormat), + ), description=dict( - max_length=(8192, '703 At Most 8192 Characters Are Valid For Description') + max_length=(8192, StatusMaxLenghtForDescription(8192)) ), cutoff=dict( - pattern=(DATETIME_PATTERN, '702 Invalid Cutoff Format'), + pattern=(DATETIME_PATTERN, StatusInvalidCutoffFormat), ), status=dict( callback=release_status_value_validator @@ -380,24 +347,24 @@ def item_exists_validator(item_id, container, field): not_none='778 Manager Id Is Null', ), launchDate=dict( - pattern=(DATETIME_PATTERN, '784 Invalid Launch Date Format'), + pattern=(DATETIME_PATTERN, StatusInvalidLaunchDateFormat), ), groupId=dict( - type_=(int, '797 Invalid Group Id Type'), - not_none='796 Group Id Is Null', + type_=(int, StatusInvalidGroupIdType), + not_none= StatusGroupIdIsNull, ), ) project_validator = validate( title=dict( - required='710 Title Not In Form', + required=StatusTitleNotInForm, callback=project_not_exists_validator, - max_length=(128, '704 At Most 128 Characters Are Valid For Title'), - pattern=(TITLE_PATTERN, '747 Invalid Title Format'), + max_length=(128, StatusMaxLenghtForTitle(128)), + pattern=(TITLE_PATTERN, StatusInvalidTitleFormat), ), description=dict( - max_length=(8192, '703 At Most 8192 Characters Are Valid For Description') + max_length=(8192, StatusMaxLenghtForDescription(8192)) ), status=dict( callback=project_status_value_validator @@ -409,40 +376,40 @@ def item_exists_validator(item_id, container, field): callback=release_exists_validator ), managerId=dict( - type_=(int, '608 Manager Not Found'), - required='786 Manager Id Not In Form', - not_none='785 Manager Id Is Null', + type_=(int, StatusManagerNotFound), + required=StatusManagerIdNotInForm, + not_none=StatusManagerIdIsNull, ), secondaryManagerId=dict( - type_=(int, '650 Secondary Manager Not Found'), + type_=(int, StatusSecondaryManagerNotFound), ), ) update_project_validator = validate( title=dict( - max_length=(128, '704 At Most 128 Characters Are Valid For Title'), - pattern=(TITLE_PATTERN, '747 Invalid Title Format'), + max_length=(128, StatusMaxLenghtForTitle(128)), + pattern=(TITLE_PATTERN, StatusInvalidTitleFormat), ), description=dict( - max_length=(8192, '703 At Most 8192 Characters Are Valid For Description') + max_length=(8192, StatusMaxLenghtForDescription(8192)) ), status=dict( callback=project_status_value_validator ), secondaryManagerId=dict( - type_=(int, '650 Secondary Manager Not Found'), + type_=(int, StatusSecondaryManagerNotFound), ), managerId=dict( - type_=(int, '608 Manager Not Found'), - not_none='785 Manager Id Is Null', + type_=(int, StatusManagerNotFound), + not_none=StatusManagerIdIsNull, ), ) draft_issue_define_validator = validate( relatedIssueId=dict( - type_=(int, '722 Invalid Issue Id Type'), + type_=(int, StatusInvalidIssueIdType), callback=relate_to_issue_exists_validator, ), ) @@ -450,41 +417,41 @@ def item_exists_validator(item_id, container, field): draft_issue_finalize_validator = validate( priority=dict( - required='768 Priority Not In Form', + required=StatusPriorityNotInForm, callback=issue_priority_value_validator ), projectId=dict( - required='713 Project Id Not In Form', - type_=(int, '714 Invalid Project Id Type'), + required=StatusProjectIdNotInForm, + type_=(int, StatusInvalidProjectIdType), callback=project_accessible_validator, ), title=dict( - required='710 Title Not In Form', - max_length=(128, '704 At Most 128 Characters Are Valid For Title'), - pattern=(TITLE_PATTERN, '747 Invalid Title Format'), + required=StatusTitleNotInForm, + max_length=(128, StatusMaxLenghtForTitle(128)), + pattern=(TITLE_PATTERN, StatusInvalidTitleFormat), callback=issue_not_exists_validator ), description=dict( - max_length=(8192, '703 At Most 8192 Characters Are Valid For Description') + max_length=(8192, StatusMaxLenghtForDescription(8192)) ), dueDate=dict( - pattern=(DATETIME_PATTERN, '701 Invalid Due Date Format'), - required='711 Due Date Not In Form' + pattern=(DATETIME_PATTERN, StatusInvalidDueDateFormat), + required=StatusDueDateNotInForm ), kind=dict( - required='718 Kind Not In Form', + required=StatusKindNotInForm, callback=kind_value_validator ), status=dict( callback=issue_status_value_validator ), days=dict( - type_=(int, '721 Invalid Days Type'), - required='720 Days Not In Form' + type_=(int, StatusInvalidDaysType), + required=StatusDaysNotInForm ), relatedIssueId=dict( - type_=(int, '722 Invalid Issue Id Type'), - not_none='775 Issue Id Is None', + type_=(int, StatusInvalidIssueIdType), + not_none=StatusIssueIdIsNull, callback=relate_to_issue_exists_validator, ), ) @@ -492,14 +459,14 @@ def item_exists_validator(item_id, container, field): update_issue_validator = validate( title=dict( - max_length=(128, '704 At Most 128 Characters Are Valid For Title'), - pattern=(TITLE_PATTERN, '747 Invalid Title Format'), + max_length=(128, StatusMaxLenghtForTitle(128)), + pattern=(TITLE_PATTERN, StatusInvalidTitleFormat), ), description=dict( - max_length=(8192, '703 At Most 8192 Characters Are Valid For Description') + max_length=(8192, StatusMaxLenghtForDescription(8192)) ), dueDate=dict( - pattern=(DATETIME_PATTERN, '701 Invalid Due Date Format'), + pattern=(DATETIME_PATTERN, StatusInvalidDueDateFormat), ), kind=dict( callback=kind_value_validator @@ -508,7 +475,7 @@ def item_exists_validator(item_id, container, field): callback=issue_status_value_validator ), days=dict( - type_=(int, '721 Invalid Days Type'), + type_=(int, StatusInvalidDaysType), ), priority=dict( callback=issue_priority_value_validator, @@ -518,7 +485,7 @@ def item_exists_validator(item_id, container, field): update_item_validator = validate( status=dict( - required='719 Status Not In Form', + required=StatusStatusNotInForm, callback=item_status_value_validator ) ) @@ -526,13 +493,13 @@ def item_exists_validator(item_id, container, field): assign_issue_validator = validate( memberId=dict( - not_none='769 Resource Id Is None', - type_=(int, '716 Invalid Resource Id Type'), + not_none=StatusResourceIdIsNull, + type_=(int, StatusInvalidResourceIdType), callback=member_exists_validator ), phaseId=dict( - required='737 Phase Id Not In Form', - type_=(int, '738 Invalid Phase Id Type'), + required=StatusPhaseIdNotInForm, + type_=(int, StatusInvalidPhaseIdType), callback=phase_exists_validator ), status=dict( @@ -549,15 +516,15 @@ def item_exists_validator(item_id, container, field): unassign_issue_validator = validate( memberId=dict( - not_none='769 Resource Id Is None', - required='715 Resource Id Not In Form', - type_=(int, '716 Invalid Resource Id Type'), + not_none=StatusResourceIdIsNull, + required=StatusResourceIdNotInForm, + type_=(int, StatusInvalidResourceIdType), callback=member_exists_validator ), phaseId=dict( - not_none='770 Phase Id Is None', - required='737 Phase Id Not In Form', - type_=(int, '738 Invalid Phase Id Type'), + not_none=StatusPhaseIdIsNull, + required=StatusPhaseIdNotInForm, + type_=(int, StatusInvalidPhaseIdType), callback=phase_exists_validator ) ) @@ -565,56 +532,56 @@ def item_exists_validator(item_id, container, field): organization_create_validator = validate( title=dict( - required='710 Title Not In Form', - max_length=(50,'704 At Most 50 Characters Are Valid For Title'), - pattern=(ORGANIZATION_TITLE_PATTERN, '747 Invalid Title Format'), + required=StatusTitleNotInForm, + max_length=(50, StatusMaxLenghtForTitle(50)), + pattern=(ORGANIZATION_TITLE_PATTERN, StatusInvalidTitleFormat), ), ) organization_invite_validator = validate( email=dict( - required='753 Email Not In Form', - pattern=(USER_EMAIL_PATTERN, '754 Invalid Email Format') + required=StatusEmailNotInForm, + pattern=(USER_EMAIL_PATTERN, StatusInvalidEmailFormat) ), role=dict( - required='755 Role Not In Form', + required=StatusRoleNotInForm, callback=organization_value_of_role_validator, ), scopes=dict( - required='765 Scopes Not In Form' + required=StatusScopesNotInForm ), applicationId=dict( - required='764 Application Id Not In form' + required=StatusApplicationIdNotInForm ), redirectUri=dict( - required='766 Redirect Uri Not In form' + required=StatusRedirectUriNotInForm ), ) organization_join_validator = validate( token=dict( - required='757 Token Not In Form', + required=StatusTokenNotInForm, ), ) token_obtain_validator = validate( organizationId=dict( - required='761 Organization Id Not In Form', - type_=(int, '763 Invalid Organization Id Type') + required=StatusOrganizationIdNotINForm, + type_=(int, StatusInvalidOrganizationIdType) ), authorizationCode=dict( - required='762 Authorization Code Not In Form' + required=StatusAuthorizationCodeNotInForm, ), ) issue_move_validator = validate( projectId=dict( - required='713 Project Id Not In Form', - type_=(int, '714 Invalid Project Id Type'), + required=StatusProjectIdNotInForm, + type_=(int, StatusInvalidProjectIdType), callback=project_accessible_validator, ), ) @@ -622,11 +589,11 @@ def item_exists_validator(item_id, container, field): attachment_validator = validate( title=dict( - max_length=(128, '704 At Most 128 Characters Are Valid For Title'), - pattern=(TITLE_PATTERN, '747 Invalid Title Format'), + max_length=(128, StatusMaxLenghtForTitle(128)), + pattern=(TITLE_PATTERN, StatusInvalidTitleFormat), ), attachment=dict( - required='758 File Not In Form' + required=StatusFileNotInForm ) ) @@ -634,14 +601,13 @@ def item_exists_validator(item_id, container, field): group_create_validator = validate( description=dict( max_length=( - 8192, - '703 At Most 8192 Characters Are Valid For Description' + 8192, StatusMaxLenghtForDescription(8192) ) ), title=dict( - not_none='727 Title Is None', - required='710 Title Not In Form', - max_length=(128, '704 At Most 128 Characters Are Valid For Title'), + not_none=StatusTitleIsNull, + required=StatusTitleNotInForm, + max_length=(128, StatusMaxLenghtForTitle(128)), callback=group_exists_validator, ) ) @@ -650,31 +616,30 @@ def item_exists_validator(item_id, container, field): group_update_validator = validate( description=dict( max_length=( - 8192, - '703 At Most 8192 Characters Are Valid For Description' + 8192, StatusMaxLenghtForDescription(8192) ) ), title=dict( - not_none='727 Title Is None', - max_length=(50, '704 At Most 50 Characters Valid For Title'), + not_none=StatusTitleIsNull, + max_length=(50, StatusMaxLenghtForTitle(50)), ) ) group_add_validator = validate( memberId=dict( - not_none='774 Member Id Is Null', - required='735 Member Id Not In Form', - type_=(int , '736 Invalid Member Id Type'), + not_none=StatusMemberIdIsNull, + required=StatusMemberIdNotInForm, + type_=(int , StatusInvalidMemberIdType), ), ) group_remove_validator = validate( memberId=dict( - not_none='774 Member Id Is Null', - required='735 Member Id Not In Form', - type_=(int , '736 Invalid Member Id Type'), + not_none=StatusMemberIdIsNull, + required=StatusMemberIdNotInForm, + type_=(int , StatusInvalidMemberIdType), ), ) @@ -682,15 +647,14 @@ def item_exists_validator(item_id, container, field): workflow_create_validator = validate( description=dict( max_length=( - 8192, - '703 At Most 8192 Characters Are Valid For Description' + 8192, StatusMaxLenghtForDescription(8192) ) ), title=dict( - not_none='727 Title Is None', - required='710 Title Not In Form', - max_length=(50, '704 At Most 50 Characters Are Valid For Title'), - pattern=(WORKFLOW_TITLE_PATTERN, '747 Invalid Title Format'), + not_none=StatusTitleIsNull, + required=StatusTitleNotInForm, + max_length=(50, StatusMaxLenghtForTitle(50)), + pattern=(WORKFLOW_TITLE_PATTERN, StatusInvalidTitleFormat), callback=workflow_exists_validator_by_title, ) ) @@ -699,14 +663,13 @@ def item_exists_validator(item_id, container, field): tag_create_validator = validate( description=dict( max_length=( - 8192, - '703 At Most 8192 Characters Are Valid For Description' + 8192, StatusMaxLenghtForDescription(8192) ) ), title=dict( - not_none='727 Title Is None', - required='710 Title Not In Form', - max_length=(50, '704 At Most 50 Characters Are Valid For Title'), + not_none=StatusTitleIsNull, + required=StatusTitleNotInForm, + max_length=(50, StatusMaxLenghtForTitle(50)), callback=tag_exists_validator, ) ) @@ -715,40 +678,39 @@ def item_exists_validator(item_id, container, field): tag_update_validator = validate( description=dict( max_length=( - 8192, - '703 At Most 8192 Characters Are Valid For Description' + 8192, StatusMaxLenghtForDescription(8192) ) ), title=dict( - not_none='727 Title Is Null', - max_length=(50, '704 At Most 50 Characters Are Valid For Title'), + not_none=StatusTitleIsNull, + max_length=(50, StatusMaxLenghtForTitle(50)), ) ) issue_relate_validator = validate( targetIssueId=dict( - not_none='779 Target Issue Id Is None', - required='780 Target Issue Id Not In Form', - type_=(int, '781 Invalid Target Issue Id Type'), + not_none=StatusTargetIssueIdIsNull, + required=StatusTargetIssueIdNotInForm, + type_=(int, StatusInvalidTargetIssueIdType), ) ) issue_unrelate_validator = validate( targetIssueId=dict( - not_none='779 Target Issue Id Is None', - required='780 Target Issue Id Not In Form', - type_=(int, '781 Invalid Target Issue Id Type'), + not_none=StatusTargetIssueIdIsNull, + required=StatusTargetIssueIdNotInForm, + type_=(int, StatusInvalidTargetIssueIdType), ) ) draft_issue_relate_validator = validate( targetIssueId=dict( - not_none='779 Target Issue Id Is None', - required='780 Target Issue Id Not In Form', - type_=(int, '781 Invalid Target Issue Id Type'), + not_none=StatusTargetIssueIdIsNull, + required=StatusTargetIssueIdNotInForm, + type_=(int, StatusInvalidTargetIssueIdType), ) ) @@ -756,14 +718,13 @@ def item_exists_validator(item_id, container, field): skill_create_validator = validate( description=dict( max_length=( - 512, - '703 At Most 512 Characters Are Valid For Description' + 512, StatusMaxLenghtForDescription(512), ), ), title = dict( - required='710 Title Not In Form', - not_none='727 Title Is Null', - max_length=(50, '704 At Most 50 Characters Are Valid For Title'), + required=StatusTitleNotInForm, + not_none=StatusTitleIsNull, + max_length=(50, StatusMaxLenghtForTitle(50)), callback=skill_exists_validator, ), ) @@ -772,13 +733,12 @@ def item_exists_validator(item_id, container, field): skill_update_validator = validate( description=dict( max_length=( - 512, - '703 At Most 512 Characters Are Valid For Description' + 512, StatusMaxLenghtForDescription(512), ), ), title = dict( - not_none='727 Title Is Null', - max_length=(50, '704 At Most 50 Characters Are Valid For Title'), + not_none=StatusTitleIsNull, + max_length=(50, StatusMaxLenghtForTitle(50)), ), ) @@ -786,32 +746,30 @@ def item_exists_validator(item_id, container, field): workflow_update_validator = validate( description=dict( max_length=( - 8192, - '703 At Most 8192 Characters Are Valid For Description' + 8192, StatusMaxLenghtForDescription(8192) ), ), title = dict( - not_none='727 Title Is Null', - max_length=(50, '704 At Most 50 Characters Are Valid For Title'), + not_none=StatusTitleIsNull, + max_length=(50, StatusMaxLenghtForTitle(50)), ), ) phase_update_validator = validate( skillId=dict( - type_=(int, '788 Invalid Skill Id Type'), + type_=(int, StatusInvalidSkillIdType), ), order=dict( - type_=(int, '741 Invalid Order Type'), + type_=(int, StatusInvalidOrderType), ), title=dict( - not_none='727 Title Is Null', - max_length=(50, '704 At Most 50 Characters Valid For Title'), + not_none=StatusTitleIsNull, + max_length=(50, StatusMaxLenghtForTitle(50)), ), description=dict( max_length=( - 512, - '703 At Most 512 Characters Are Valid For Description' + 512, StatusMaxLenghtForDescription(512), ), ) ) @@ -819,17 +777,16 @@ def item_exists_validator(item_id, container, field): phase_validator = validate( title=dict( - required='610 Title Not In Form', - max_length=(50, '704 At Most 50 Characters Valid For Title'), + required=StatusTitleNotInForm, + max_length=(50, StatusMaxLenghtForTitle(50)), ), order=dict( - required='742 Order Not In Form', - type_=(int, '741 Invalid Order Type'), + required=StatusOrderNotInForm, + type_=(int, StatusInvalidOrderType), ), description=dict( max_length=( - 512, - '703 At Most 512 Characters Are Valid For Description' + 512, StatusMaxLenghtForDescription(512), ), ) ) @@ -838,14 +795,13 @@ def item_exists_validator(item_id, container, field): eventtype_create_validator = validate( description=dict( max_length=( - 512, - '703 At Most 512 Characters Are Valid For Description' + 512, StatusMaxLenghtForDescription(512), ), ), title=dict( - required='710 Title Not In Form', - not_none='727 Title Is None', - max_length=(50, '704 At Most 50 Characters Valid For Title'), + required=StatusTitleNotInForm, + not_none=StatusTitleIsNull, + max_length=(50, StatusMaxLenghtForTitle(50)), callback=eventtype_exists_validator_by_title ), ) @@ -857,22 +813,22 @@ def item_exists_validator(item_id, container, field): callback=event_repeat_value_validator, ), eventTypeId=dict( - required='794 Type Id Not In Form', - not_none='798 Event Type Id Is Null', + required=StatusTypeIdNotInForm, + not_none=StatusEventTypeIdIsNull, callback=eventtype_exists_validator_by_id, ), startDate=dict( - required='792 Start Date Not In Form', + required=StatusStartDateNotInForm, pattern=(DATETIME_PATTERN, StatusInvalidStartDateFormat), ), endDate=dict( - required='793 End Date Not In Form', + required=StatusEndDateNotInForm, pattern=(DATETIME_PATTERN, StatusInvalidEndDateFormat), ), title=dict( - required='710 Title Not In Form', - not_none='727 Title Is None', - max_length=(50, '704 At Most 50 Characters Valid For Title'), + required=StatusTitleNotInForm, + not_none=StatusTitleIsNull, + max_length=(50, StatusMaxLenghtForTitle(50)), callback=event_exists_validator, ), ) @@ -881,14 +837,14 @@ def item_exists_validator(item_id, container, field): eventtype_update_validator = validate( description=dict( max_length=( - 512, - '703 At Most 512 Characters Are Valid For Description' - ) + 512, StatusMaxLenghtForDescription(512), + ), ), + title=dict( - not_none='727 Title Is None', - max_length=(50, '704 At Most 50 Characters Valid For Title'), - ) + not_none=StatusTitleIsNull, + max_length=(50, StatusMaxLenghtForTitle(50)) + ), ) @@ -897,7 +853,7 @@ def item_exists_validator(item_id, container, field): callback=event_repeat_value_validator, ), eventTypeId=dict( - not_none='798 Event Type Id Is Null', + not_none=StatusEventTypeIdIsNull, callback=eventtype_exists_validator_by_id, ), startDate=dict( @@ -907,8 +863,8 @@ def item_exists_validator(item_id, container, field): pattern=(DATETIME_PATTERN, StatusInvalidEndDateFormat), ), title=dict( - not_none='727 Title Is None', - max_length=(50, '704 At Most 50 Characters Valid For Title'), + not_none=StatusTitleIsNull, + max_length=(50, StatusMaxLenghtForTitle(50)), ), ) @@ -953,3 +909,22 @@ def item_exists_validator(item_id, container, field): ) ) + +estimate_item_validator = validate( + startDate=dict( + required=StatusStartDateNotInForm, + pattern=(DATETIME_PATTERN, StatusInvalidStartDateFormat), + not_none=StatusStartDateIsNull, + ), + endDate=dict( + required=StatusEndDateNotInForm, + pattern=(DATETIME_PATTERN, StatusInvalidEndDateFormat), + not_none=StatusEndDateIsNull, + ), + estimatedHours=dict( + required=StatusEstimatedHoursNotInForm, + type_=(int, StatusInvalidEstimatedHoursType), + not_none=StatusEstimatedHoursIsNull, + ) +) +