diff --git a/dolphin/__init__.py b/dolphin/__init__.py index d4deff9d..2b4f11f7 100644 --- a/dolphin/__init__.py +++ b/dolphin/__init__.py @@ -11,7 +11,7 @@ from .controllers.root import Root -__version__ = '0.36.0a4' +__version__ = '0.42.1a7' class Dolphin(Application): diff --git a/dolphin/controllers/event.py b/dolphin/controllers/event.py index 73c9c925..f34b2026 100644 --- a/dolphin/controllers/event.py +++ b/dolphin/controllers/event.py @@ -16,6 +16,7 @@ 'startDate', 'endDate', 'eventTypeId', + 'repeat', ] diff --git a/dolphin/controllers/eventtype.py b/dolphin/controllers/eventtype.py index 3480b39a..abe19289 100644 --- a/dolphin/controllers/eventtype.py +++ b/dolphin/controllers/eventtype.py @@ -78,3 +78,15 @@ def update(self, id): def list(self): return DBSession.query(EventType) + @authorize + @json(prevent_form='709 Form Not Allowed') + @commit + def delete(self, id): + id = int_or_notfound(id) + event_type = DBSession.query(EventType).get(id) + if event_type is None: + raise HTTPNotFound() + + DBSession.delete(event_type) + return event_type + diff --git a/dolphin/controllers/issues.py b/dolphin/controllers/issues.py index 397aa6b5..34647aa5 100644 --- a/dolphin/controllers/issues.py +++ b/dolphin/controllers/issues.py @@ -1,3 +1,4 @@ +import re from datetime import datetime from auditor import context as AuditLogContext @@ -7,17 +8,18 @@ from restfulpy.authorization import authorize from restfulpy.controllers import ModelRestController, JsonPatchControllerMixin from restfulpy.orm import DBSession, commit -from sqlalchemy import and_, exists, select, func, join +from sqlalchemy import and_, exists, select, func, join, or_, Text, cast from ..backends import ChatClient from ..exceptions import StatusRoomMemberAlreadyExist, \ StatusRoomMemberNotFound, StatusChatRoomNotFound, StatusRelatedIssueNotFound, \ - StatusIssueBugMustHaveRelatedIssue, StatusIssueNotFound + StatusIssueBugMustHaveRelatedIssue, StatusIssueNotFound, \ + StatusQueryParameterNotInFormOrQueryString from ..models import Issue, Subscription, Phase, Item, Member, Project, \ RelatedIssue, Subscribable, IssueTag, Tag from ..validators import update_issue_validator, assign_issue_validator, \ issue_move_validator, unassign_issue_validator, issue_relate_validator, \ - issue_unrelate_validator + issue_unrelate_validator, search_issue_validator from .activity import ActivityController from .files import FileController from .phases import PhaseController @@ -28,6 +30,9 @@ UNKNOWN_ASSIGNEE = -1 +TRIAGE_PHASE_ID_PATTERN = re.compile(r'[(,\s]0[,\)\s]|^0$') + + class IssueController(ModelRestController, JsonPatchControllerMixin): __model__ = Issue @@ -168,6 +173,12 @@ def list(self): Item.phase_id, value ) + if TRIAGE_PHASE_ID_PATTERN.search(value): + triage = DBSession.query(Issue) \ + .outerjoin(Item, Item.issue_id == Issue.id) \ + .filter(Item.id == None) + query = query.union(triage) + is_issue_item_joined = True if 'phaseTitle' in context.query: @@ -768,3 +779,33 @@ def _unsee_subscriptions(self, subscriptions): for subscription in subscriptions: subscription.seen_at = None + @authorize + @search_issue_validator + @json + @Issue.expose + def search(self): + query = context.form.get('query') or context.query.get('query') + if query is None: + raise StatusQueryParameterNotInFormOrQueryString() + + query = f'%{query}%' + query = DBSession.query(Issue) \ + .filter(or_( + Issue.title.ilike(query), + Issue.description.ilike(query), + cast(Issue.id, Text).ilike(query) + )) + + if 'unread' in context.query: + query = query \ + .join( + Subscription, + and_( + Subscription.subscribable_id == Issue.id, + Subscription.seen_at.is_(None), + ) + ) \ + .filter(Subscription.member_id == context.identity.id) + + return query + diff --git a/dolphin/controllers/items.py b/dolphin/controllers/items.py index 25d9ebaa..c4196476 100644 --- a/dolphin/controllers/items.py +++ b/dolphin/controllers/items.py @@ -36,3 +36,9 @@ def get(self, id): return item + @authorize + @json(prevent_form='709 Form Not Allowed') + @Item.expose + def list(self): + return DBSession.query(Item) + diff --git a/dolphin/controllers/members.py b/dolphin/controllers/members.py index d46b40c5..75720610 100644 --- a/dolphin/controllers/members.py +++ b/dolphin/controllers/members.py @@ -4,12 +4,14 @@ from restfulpy.controllers import ModelRestController from restfulpy.orm import DBSession, commit from sqlalchemy_media import store_manager +from sqlalchemy import or_ from ..models import Member, Skill, SkillMember, Organization, \ OrganizationMember, Group, GroupMember -from ..exceptions import StatusAlreadyGrantedSkill, StatusSkillNotGrantedYet +from ..exceptions import StatusAlreadyGrantedSkill, StatusSkillNotGrantedYet, \ + StatusQueryParameterNotInFormOrQueryString +from ..validators import search_member_validator from .organization import OrganizationController - from .skill import SkillController @@ -69,6 +71,24 @@ def get(self, id): return member + @authorize + @search_member_validator + @json + @Member.expose + def search(self): + query = context.form.get('query') or context.query.get('query') + if query is None: + raise StatusQueryParameterNotInFormOrQueryString() + + query = f'%{query}%' + query = DBSession.query(Member) \ + .filter(or_( + Member.title.ilike(query), + Member.name.ilike(query) + )) + + return query + class MemberSkillController(ModelRestController): __model__ = Skill diff --git a/dolphin/controllers/releases.py b/dolphin/controllers/releases.py index 37895c6d..9c280c5f 100644 --- a/dolphin/controllers/releases.py +++ b/dolphin/controllers/releases.py @@ -17,7 +17,7 @@ 'description', 'status', 'cutoff', - 'managerReferenceId', + 'managerId', 'launchDate', 'groupId', ] @@ -43,20 +43,17 @@ class ReleaseController(ModelRestController): @commit def create(self): token = context.environ['HTTP_AUTHORIZATION'] - member = DBSession.query(Member) \ - .filter( - Member.reference_id == context.form['managerReferenceId'] - ) \ - .one_or_none() - if member is None: + manager = DBSession.query(Member).get(context.form['managerId']) + if manager is None: raise StatusManagerNotFound() group = DBSession.query(Group).get(context.form.get('groupId')) if group is None: raise StatusGroupNotFound() + creator = Member.current() release = Release() - release.manager_id = member.id + release.manager_id = manager.id release.update_from_request() if release.launch_date < release.cutoff: raise StatusLaunchDateMustGreaterThanCutoffDate() @@ -65,16 +62,16 @@ def create(self): room = chat_client.create_room( release.get_room_title(), token, - member.access_token, + creator.access_token, context.identity.reference_id ) release.room_id = room['id'] try: chat_client.add_member( release.room_id, - member.reference_id, + manager.reference_id, token, - member.access_token + creator.access_token ) except StatusRoomMemberAlreadyExist: @@ -116,11 +113,9 @@ def update(self, id): f'"{form["title"]}" is already exists.' ) - manager_reference_id = context.form.get('managerReferenceId') - if manager_reference_id is not None: - member = DBSession.query(Member) \ - .filter(Member.reference_id == manager_reference_id) \ - .one_or_none() + manager_id = context.form.get('managerId') + if manager_id is not None: + member = DBSession.query(Member).get(manager_id) if member is None: raise StatusManagerNotFound() diff --git a/dolphin/controllers/timecard.py b/dolphin/controllers/timecard.py index 3586b070..5892c37e 100644 --- a/dolphin/controllers/timecard.py +++ b/dolphin/controllers/timecard.py @@ -1,11 +1,11 @@ -from nanohttp import json +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 Timecard -from ..validators import timecard_create_validator +from ..validators import timecard_create_validator, timecard_update_validator class TimecardController(ModelRestController): @@ -24,3 +24,35 @@ def create(self): DBSession.add(time_card) return time_card + @authorize + @json(prevent_form='709 Form Not Allowed') + def get(self, id): + id = int_or_notfound(id) + timecard = DBSession.query(Timecard).get(id) + if timecard is None: + raise HTTPNotFound() + + return timecard + + @authorize + @json(prevent_empty_form='708 Empty Form') + @timecard_update_validator + @commit + def update(self, id): + id = int_or_notfound(id) + timecard = DBSession.query(Timecard).get(id) + if timecard is None: + raise HTTPNotFound() + + timecard.update_from_request() + if timecard.start_date > timecard.end_date: + raise StatusEndDateMustBeGreaterThanStartDate() + + return timecard + + @authorize + @json(prevent_form='709 Form Not Allowed') + @Timecard.expose + def list(self): + return DBSession.query(Timecard) + diff --git a/dolphin/exceptions.py b/dolphin/exceptions.py index 18edfbe8..4464b81d 100644 --- a/dolphin/exceptions.py +++ b/dolphin/exceptions.py @@ -195,3 +195,11 @@ class StatusStartDateIsNull(HTTPKnownStatus): class StatusEndDateIsNull(HTTPKnownStatus): status = '906 End Date Is Null' + +class StatusRepeatNotInForm(HTTPKnownStatus): + status = '911 Repeat Not In Form' + + +class StatusQueryParameterNotInFormOrQueryString(HTTPKnownStatus): + status = '912 Query Parameter Not In Form Or Query String' + diff --git a/dolphin/middleware_callback.py b/dolphin/middleware_callback.py index 692dd5a5..f0e58a23 100644 --- a/dolphin/middleware_callback.py +++ b/dolphin/middleware_callback.py @@ -1,9 +1,10 @@ +import traceback from datetime import datetime import ujson from auditor.logentry import ChangeAttributeLogEntry, InstantiationLogEntry, \ AppendLogEntry, RemoveLogEntry, RequestLogEntry -from nanohttp import context +from nanohttp import context, HTTPStatus from restfulpy.datetimehelpers import format_datetime from dolphin.backends import ChatClient @@ -18,7 +19,6 @@ def callback(audit_log): if audit_log[-1].status == '200 OK' and len(audit_log) > 1: chat_client = ChatClient() member = Member.current() - # FIXME: We will rollback if cannot send a message successfully for log in audit_log: if isinstance(log, ChangeAttributeLogEntry): message = dict( @@ -55,11 +55,19 @@ def callback(audit_log): ) if not isinstance(log, RequestLogEntry): - chat_client.send_message( - room_id=log.object_.room_id, - body=ujson.dumps(message), - mimetype=AUDIT_LOG_MIMETYPE, - token=context.environ['HTTP_AUTHORIZATION'], - x_access_token=member.access_token, - ) + try: + chat_client.send_message( + room_id=log.object_.room_id, + body=ujson.dumps(message), + mimetype=AUDIT_LOG_MIMETYPE, + token=context.environ['HTTP_AUTHORIZATION'], + x_access_token=member.access_token, + ) + + # This exception passed because after consulting with + # Mr.Mardani, decision made: This exception will be + # resolved when the dolphin and jaguar merge together + except HTTPStatus: + traceback.print_exc() + pass diff --git a/dolphin/migration/versions/07d212f7a1d7_.py b/dolphin/migration/versions/07d212f7a1d7_.py new file mode 100644 index 00000000..be0ea6fb --- /dev/null +++ b/dolphin/migration/versions/07d212f7a1d7_.py @@ -0,0 +1,47 @@ +"""empty message +Revision ID: 07d212f7a1d7 +Revises: 26d0e0ee2536 +Create Date: 2019-05-02 12:20:25.784006 +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '07d212f7a1d7' +down_revision = '26d0e0ee2536' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.execute( + "CREATE TYPE event_repeat AS ENUM ('yearly', 'monthly', 'never');" + ) + op.execute( + "ALTER TABLE event ADD repeat event_repeat;" + ) + op.drop_column('event', 'description') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.execute( + "ALTER TABLE event DROP COLUMN repeat;" + ) + op.execute( + "DROP TYPE event_repeat;" + ) + op.add_column( + 'event', + sa.Column( + 'description', + sa.VARCHAR(), + autoincrement=False, + nullable=True + ) + ) + # ### end Alembic commands ### + diff --git a/dolphin/migration/versions/26d0e0ee2536_.py b/dolphin/migration/versions/26d0e0ee2536_.py new file mode 100644 index 00000000..aa7583db --- /dev/null +++ b/dolphin/migration/versions/26d0e0ee2536_.py @@ -0,0 +1,51 @@ +"""empty message +Revision ID: 26d0e0ee2536 +Revises: 0ecdd82ae075 +Create Date: 2019-04-29 11:16:35.270598 +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '26d0e0ee2536' +down_revision = '0ecdd82ae075' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + 'event_type', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('title', sa.Unicode(), nullable=False), + sa.Column('description', sa.Unicode(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table( + 'event', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('event_type_id', sa.Integer(), nullable=False), + sa.Column('title', sa.Unicode(), nullable=False), + sa.Column('start_date', sa.DateTime(), nullable=False), + sa.Column('end_date', sa.DateTime(), nullable=False), + sa.Column('description', sa.Unicode(), nullable=True), + sa.ForeignKeyConstraint(['event_type_id'], ['event_type.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.add_column( + 'release', + sa.Column('group_id', sa.Integer(), nullable=False) + ) + op.create_foreign_key(None, 'release', 'group', ['group_id'], ['id']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'release', type_='foreignkey') + op.drop_column('release', 'group_id') + op.drop_table('event') + op.drop_table('event_type') + # ### end Alembic commands ### diff --git a/dolphin/migration/versions/a2410e75c467_.py b/dolphin/migration/versions/a2410e75c467_.py new file mode 100644 index 00000000..5d552149 --- /dev/null +++ b/dolphin/migration/versions/a2410e75c467_.py @@ -0,0 +1,58 @@ +"""empty message + +Revision ID: a2410e75c467 +Revises: 07d212f7a1d7 +Create Date: 2019-05-09 14:54:59.879316 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'a2410e75c467' +down_revision = '07d212f7a1d7' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + 'timecard', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('start_date', sa.DateTime(), nullable=False), + sa.Column('end_date', sa.DateTime(), nullable=False), + sa.Column('estimated_time', sa.Integer(), nullable=False), + sa.Column('summary', sa.Unicode(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.add_column('item', sa.Column('description', sa.String(), nullable=True)) + op.add_column('item', sa.Column('end_time', sa.DateTime(), nullable=True)) + op.add_column('item', sa.Column('start_time', sa.DateTime(), nullable=True)) + op.add_column( + 'item', + sa.Column( + 'status', + sa.Enum( + 'to-do', + 'in-progress', + 'on-hold', + 'delayed', + 'complete', + name='item_status' + ), + nullable=True + ) + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('item', 'status') + op.drop_column('item', 'start_time') + op.drop_column('item', 'end_time') + op.drop_column('item', 'description') + op.drop_table('timecard') + # ### end Alembic commands ### diff --git a/dolphin/models/__init__.py b/dolphin/models/__init__.py index 51939cf6..7fb1b8a5 100644 --- a/dolphin/models/__init__.py +++ b/dolphin/models/__init__.py @@ -20,6 +20,6 @@ from .skill import Skill, SkillMember from .activity import Activity from .eventtype import EventType -from .event import Event from .timecard import Timecard +from .event import Event, event_repeats diff --git a/dolphin/models/event.py b/dolphin/models/event.py index 33667004..8c89d963 100644 --- a/dolphin/models/event.py +++ b/dolphin/models/event.py @@ -1,8 +1,15 @@ -import datetime +from datetime import datetime from restfulpy.orm import Field, DeclarativeBase, OrderingMixin, \ FilteringMixin, PaginationMixin, relationship -from sqlalchemy import Integer, Unicode, DateTime, ForeignKey +from sqlalchemy import Integer, Unicode, DateTime, ForeignKey, Enum + + +event_repeats = [ + 'yearly', + 'monthly', + 'never', +] class Event(OrderingMixin, FilteringMixin, PaginationMixin, DeclarativeBase): @@ -22,7 +29,7 @@ class Event(OrderingMixin, FilteringMixin, PaginationMixin, DeclarativeBase): ForeignKey('event_type.id'), python_type=int, watermark='lorem ipsum', - label='Lorem ipsum', + label='Type', nullable=False, not_none=True, readonly=False, @@ -32,13 +39,15 @@ class Event(OrderingMixin, FilteringMixin, PaginationMixin, DeclarativeBase): Unicode, max_length=50, min_length=1, - label='lorem ipsum', + label='Name', watermark='lorem ipsum', example='lorem ipsum', nullable=False, not_none=True, required=True, python_type=str, + readonly=False, + message='Lorem ipsum', ) start_date= Field( DateTime, @@ -53,6 +62,8 @@ class Event(OrderingMixin, FilteringMixin, PaginationMixin, DeclarativeBase): nullable=False, not_none=True, required=True, + readonly=False, + message='Lorem ipsum', ) end_date = Field( DateTime, @@ -67,18 +78,19 @@ class Event(OrderingMixin, FilteringMixin, PaginationMixin, DeclarativeBase): nullable=False, not_none=True, required=True, + readonly=False, + message='Lorem ipsum', ) - description = Field( - Unicode, - min_length=1, - max_length=512, - label='Description', - watermark='Lorem Ipsum', - not_none=False, - nullable=True, - required=False, + repeat = Field( + Enum(*event_repeats, name='event_repeat'), python_type=str, - example='Lorem Ipsum', + label='Repeat', + not_none=True, + required=True, + nullable=False, + example='Lorem ipsum', + watermark='Lorem ipsum', + message='Lorem ipsum', ) event_type = relationship( 'EventType', diff --git a/dolphin/models/eventtype.py b/dolphin/models/eventtype.py index 32689049..b85be983 100644 --- a/dolphin/models/eventtype.py +++ b/dolphin/models/eventtype.py @@ -44,6 +44,7 @@ class EventType(OrderingMixin, FilteringMixin, PaginationMixin, 'Event', back_populates='event_type', protected=True, + cascade='delete', ) def __repr__(self): # pragma: no cover diff --git a/dolphin/models/issue.py b/dolphin/models/issue.py index a4550034..9e528f81 100644 --- a/dolphin/models/issue.py +++ b/dolphin/models/issue.py @@ -142,7 +142,7 @@ class Issue(OrderingMixin, FilteringMixin, PaginationMixin, ModifiedByMixin, watermark='Choose a status', not_none=True, required=False, - default='on-hold', + default='to-do', example='lorem ipsum', ) priority = Field( diff --git a/dolphin/models/item.py b/dolphin/models/item.py index 6621eecd..0a028f77 100644 --- a/dolphin/models/item.py +++ b/dolphin/models/item.py @@ -1,10 +1,23 @@ +from datetime import datetime from restfulpy.orm import Field, DeclarativeBase, relationship -from restfulpy.orm.mixins import TimestampMixin -from sqlalchemy import Integer, ForeignKey, UniqueConstraint +from restfulpy.orm.mixins import TimestampMixin, OrderingMixin, \ + FilteringMixin, PaginationMixin +from sqlalchemy import Integer, ForeignKey, UniqueConstraint, DateTime, Enum, \ + String -class Item(TimestampMixin, DeclarativeBase): +item_statuses = [ + 'to-do', + 'in-progress', + 'on-hold', + 'delayed', + 'complete', +] + + +class Item(TimestampMixin, OrderingMixin, FilteringMixin, PaginationMixin, + DeclarativeBase): __tablename__ = 'item' id = Field( @@ -18,10 +31,89 @@ class Item(TimestampMixin, DeclarativeBase): example=1, protected=False, ) - - phase_id = Field(Integer, ForeignKey('phase.id')) - issue_id = Field(Integer, ForeignKey('issue.id')) - member_id = Field(Integer, ForeignKey('member.id')) + start_time = Field( + DateTime, + python_type=datetime, + label='Start Date', + pattern= + r'^(\d{4})-(0[1-9]|1[012]|[1-9])-(0[1-9]|[12]\d{1}|3[01]|[1-9])' + r'(T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)?(Z)?)?$', + pattern_description='ISO format and format like "yyyy-mm-dd" is valid', + example='2018-02-02T1:12:12.000Z', + nullable=True, + not_none=False, + required=False, + readonly=True, + ) + end_time = Field( + DateTime, + python_type=datetime, + label='Target Date', + pattern= + r'^(\d{4})-(0[1-9]|1[012]|[1-9])-(0[1-9]|[12]\d{1}|3[01]|[1-9])' + r'(T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)?(Z)?)?$', + pattern_description='ISO format and format like "yyyy-mm-dd" is valid', + example='2018-02-02T1:12:12.000Z', + nullable=True, + not_none=False, + required=False, + readonly=True, + ) + status = Field( + Enum(*item_statuses, name='item_status'), + python_type=str, + default='to-do', + label='Status', + watermark='Choose a status', + nullable=True, + required=False, + example='Lorem Ipsum' + ) + description = Field( + String, + min_length=1, + max_length=512, + label='Description', + watermark='Enter the description', + not_none=False, + nullable=True, + required=False, + python_type=str, + example='Lorem Ipsum' + ) + phase_id = Field( + Integer, + ForeignKey('phase.id'), + python_type=int, + nullable=False, + watermark='Choose a phase', + label='Phase', + not_none=True, + required=True, + example='Lorem Ipsum' + ) + issue_id = Field( + Integer, + ForeignKey('issue.id'), + python_type=int, + nullable=False, + watermark='Choose an issue', + label='Issue', + not_none=True, + required=True, + example='Lorem Ipsum' + ) + member_id = Field( + Integer, + ForeignKey('member.id'), + python_type=int, + nullable=False, + watermark='Choose a member', + label='Member', + not_none=True, + required=True, + example='Lorem Ipsum' + ) issues = relationship( 'Issue', @@ -30,3 +122,4 @@ class Item(TimestampMixin, DeclarativeBase): ) UniqueConstraint(phase_id, issue_id, member_id) + diff --git a/dolphin/models/project.py b/dolphin/models/project.py index fa295041..a91e4916 100644 --- a/dolphin/models/project.py +++ b/dolphin/models/project.py @@ -175,6 +175,7 @@ class Project(ModifiedByMixin, OrderingMixin, FilteringMixin, PaginationMixin, .where(status == 'active') ) + @hybrid_property def boarding(self): if self.status == 'on-hold': @@ -269,6 +270,13 @@ def iter_metadata_fields(cls): example='Lorem Ipsum', message='Lorem Ipsum', ) + yield MetadataField( + name='releaseCutoff', + key='release_cutoff', + label='Release Cutoff', + required=False, + readonly=True + ) def to_dict(self): project_dict = super().to_dict() @@ -276,6 +284,7 @@ def to_dict(self): project_dict['isSubscribed'] = True if self.is_subscribed else False project_dict['dueDate'] = self.due_date.isoformat() \ if self.due_date else None + project_dict['releaseCutoff'] = self.release_cutoff.isoformat() return project_dict def get_room_title(self): @@ -284,6 +293,8 @@ def get_room_title(self): @classmethod def __declare_last__(cls): + from . import Release + super().__declare_last__() observe( cls, @@ -297,4 +308,8 @@ def __declare_last__(cls): 'secondary_manager_id', ] ) + cls.release_cutoff = column_property( + select([Release.cutoff]) + .where(Release.id == cls.release_id) + ) diff --git a/dolphin/models/release.py b/dolphin/models/release.py index e0a9092a..3e056a36 100644 --- a/dolphin/models/release.py +++ b/dolphin/models/release.py @@ -48,8 +48,8 @@ class Release(ModifiedMixin, FilteringMixin, OrderingMixin, PaginationMixin, label='Manager', nullable=False, not_none=True, - readonly=True, - required=True + readonly=False, + required=True, ) room_id = Field( Integer, @@ -153,19 +153,8 @@ def iter_metadata_fields(cls): watermark='Lorem Ipsum', message='Lorem Ipsum', ) - yield MetadataField( - name='managerReferenceId', - key='manager_reference_id', - label='Lorem Ipsum', - required=True, - not_none=True, - readonly=False, - watermark='Lorem Ipsum', - example='Lorem Ipsum', - message='Lorem Ipsum', - ) - def __repr__(self): + def __repr__(self): # pragma: no cover return f'\tTitle: {self.title}\n' def get_room_title(self): diff --git a/dolphin/models/timecard.py b/dolphin/models/timecard.py index e358dc12..e0722c8e 100644 --- a/dolphin/models/timecard.py +++ b/dolphin/models/timecard.py @@ -1,4 +1,4 @@ -import datetime +from datetime import datetime from restfulpy.orm import Field, DeclarativeBase, OrderingMixin, \ FilteringMixin, PaginationMixin diff --git a/dolphin/tests/test_draftissue_finalize.py b/dolphin/tests/test_draftissue_finalize.py index af706dca..f32a249b 100644 --- a/dolphin/tests/test_draftissue_finalize.py +++ b/dolphin/tests/test_draftissue_finalize.py @@ -133,7 +133,7 @@ def test_finalize(self): f'Define an issue', f'/apiv1/draftissues/id: {self.draft_issue1.id}', f'FINALIZE', - form=dict( + json=dict( title='Defined issue', status='in-progress', description='A description for defined issue', @@ -157,72 +157,78 @@ def test_finalize(self): assert isinstance(logs[0], InstantiationLogEntry) assert isinstance(logs[1], RequestLogEntry) - when('Priority value not in form', form=Remove('priority')) + when( + 'Status is null', + json=given | dict(status=None, title='new title') + ) + assert status == 200 + + when('Priority value not in form', json=Remove('priority')) assert status == '768 Priority Not In Form' - when('Invalid the priority value', form=Update(priority='lorem')) + when('Invalid the priority value', json=Update(priority='lorem')) assert status == '767 Invalid priority, only one of "low, '\ 'normal, high" will be accepted' when( 'Project id not in form', - form=given - 'projectId' | dict(title='New title') + json=given - 'projectId' | dict(title='New title') ) assert status == '713 Project Id Not In Form' when( 'Project not found with string type', - form=given | dict(projectId='Alphabetical', title='New title') + json=given | dict(projectId='Alphabetical', title='New title') ) assert status == '714 Invalid Project Id Type' when( 'Project not found with integer type', - form=given | dict(projectId=0, title='New title') + json=given | dict(projectId=0, title='New title') ) assert status == '601 Project not found with id: 0' when( 'Relate issue not found with string type', - form=Update(relatedIssueId='Alphabetical', title='New title') + json=Update(relatedIssueId='Alphabetical', title='New title') ) assert status == '722 Invalid Issue Id Type' when( 'Relate to issue not found with integer type', - form=Update(relatedIssueId=0, title='New title') + json=Update(relatedIssueId=0, title='New title') ) assert status == 647 assert status.text.startswith('relatedIssue With Id') when( 'Title is not in form', - form=given - 'title' + json=given - 'title' ) assert status == '710 Title Not In Form' when( 'Title format is wrong', - form=given | dict(title=' Invalid Format ') + json=given | dict(title=' Invalid Format ') ) assert status == '747 Invalid Title Format' when( 'Title is repetitive', - form=Update(title='First issue') + json=Update(title='First issue') ) assert status == '600 Another issue with title: "First issue" '\ 'is already exists.' when( 'Title length is more than limit', - form=given | dict(title=((128 + 1) * 'a')) + json=given | dict(title=((128 + 1) * 'a')) ) assert status == '704 At Most 128 Characters Are Valid For Title' when( 'Description length is less than limit', - form=given | dict( + json=given | dict( description=((8192 + 1) * 'a'), title=('Another title') ) @@ -232,7 +238,7 @@ def test_finalize(self): when( 'Due date format is wrong', - form=given | dict( + json=given | dict( dueDate='20-20-20', title='Another title' ) @@ -241,25 +247,25 @@ def test_finalize(self): when( 'Due date is not in form', - form=given - 'dueDate' | dict(title='Another title') + json=given - 'dueDate' | dict(title='Another title') ) assert status == '711 Due Date Not In Form' when( 'Kind is not in form', - form=given - 'kind' | dict(title='Another title') + json=given - 'kind' | dict(title='Another title') ) assert status == '718 Kind Not In Form' when( 'Days is not in form', - form=given - 'days' | dict(title='Another title') + json=given - 'days' | dict(title='Another title') ) assert status == '720 Days Not In Form' when( 'Days type is wrong', - form=given | dict( + json=given | dict( days='Alphabetical', title='Another title' ) @@ -268,14 +274,14 @@ def test_finalize(self): when( 'Invalid kind value is in form', - form=given | dict(kind='enhancing', title='Another title') + json=given | dict(kind='enhancing', title='Another title') ) assert status == '717 Invalid kind, only one of "feature, bug" '\ 'will be accepted' when( 'Invalid status value is in form', - form=given | dict(status='progressing') | \ + json=given | dict(status='progressing') | \ dict(title='Another title') ) assert status == '705 Invalid status, only one of "to-do, ' \ @@ -283,7 +289,7 @@ def test_finalize(self): when( 'Trying to pass with invalid form parameters', - form=Update(a=1) + json=Update(a=1) ) assert status == '707 Invalid field, only following fields are ' \ 'accepted: title, description, kind, days, status, projectId, ' \ @@ -295,7 +301,7 @@ def test_finalize(self): when( 'Trying to pass draft issue bug without related issue', url_parameters=dict(id=self.draft_issue2.id), - form=Update( + json=Update( title='Another title', kind='bug' ) @@ -305,35 +311,35 @@ def test_finalize(self): with chat_server_status('404 Not Found'): when( 'Chat server is not found', - form=given | dict(title='Another title') + json=given | dict(title='Another title') ) assert status == '617 Chat Server Not Found' with chat_server_status('503 Service Not Available'): when( 'Chat server is not available', - form=given | dict(title='Another title') + json=given | dict(title='Another title') ) assert status == '800 Chat Server Not Available' with chat_server_status('500 Internal Service Error'): when( 'Chat server faces with internal error', - form=given | dict(title='Another title') + json=given | dict(title='Another title') ) assert status == '801 Chat Server Internal Error' with chat_server_status('615 Room Already Exists'): when( 'Chat server faces with internal error', - form=given | dict(title='Another title') + json=given | dict(title='Another title') ) assert status == 200 with chat_server_status('604 Already Added To Target'): when( 'Chat server faces with internal error', - form=given | dict(title='Awesome project') + json=given | dict(title='Awesome project') ) assert status == 200 diff --git a/dolphin/tests/test_event_add.py b/dolphin/tests/test_event_add.py index 2b90f3d7..0f10fcff 100644 --- a/dolphin/tests/test_event_add.py +++ b/dolphin/tests/test_event_add.py @@ -31,6 +31,7 @@ def test_add(self): self.login(self.member.email) title = 'Event 1' description = 'A description for an event' + repeat = 'never' start_date = datetime.datetime.now().isoformat() end_date = datetime.datetime.now().isoformat() @@ -43,7 +44,7 @@ def test_add(self): eventTypeId=self.event_type.id, startDate=start_date, endDate=end_date, - description=description, + repeat=repeat, ), ): assert status == 200 @@ -51,7 +52,7 @@ def test_add(self): assert response.json['title'] == title assert response.json['startDate'] == start_date assert response.json['endDate'] == end_date - assert response.json['description'] == description + assert response.json['repeat'] == repeat when('Trying to pass without form parameters', json={}) assert status == '708 Empty Form' @@ -80,13 +81,6 @@ def test_add(self): ) assert status == '727 Title Is None' - when( - 'Description length is less than limit', - json=given | dict(description=(512 + 1) * 'a'), - ) - assert status == '703 At Most 512 Characters Are Valid For ' \ - 'Description' - when( 'Trying to pass without start date', json=given - 'startDate', @@ -130,6 +124,19 @@ def test_add(self): ) assert status == '794 Type Id Not In Form' + when( + 'Trying to pass without repeat', + json=given - 'repeat' + ) + assert status == '911 Repeat Not In Form' + + when( + 'Invalid repeat value is in form', + json=given | dict(repeat='a') + ) + assert status == '910 Invalid Repeat, only one of ' \ + '"yearly, monthly, never" will be accepted' + when('Request is not authorized', authorization=None) assert status == 401 diff --git a/dolphin/tests/test_event_get.py b/dolphin/tests/test_event_get.py index 8835630e..1c522e79 100644 --- a/dolphin/tests/test_event_get.py +++ b/dolphin/tests/test_event_get.py @@ -30,6 +30,7 @@ def mockup(cls): start_date=datetime.datetime.now().isoformat(), end_date=datetime.datetime.now().isoformat(), event_type=event_type, + repeat='never', ) session.add(cls.event1) session.commit() diff --git a/dolphin/tests/test_event_list.py b/dolphin/tests/test_event_list.py index 550e2d12..d89c0b02 100644 --- a/dolphin/tests/test_event_list.py +++ b/dolphin/tests/test_event_list.py @@ -29,6 +29,7 @@ def mockup(cls): start_date=datetime.datetime.now().isoformat(), end_date=datetime.datetime.now().isoformat(), event_type=event_type, + repeat='never', ) session.add(event1) @@ -37,6 +38,7 @@ def mockup(cls): start_date=datetime.datetime.now().isoformat(), end_date=datetime.datetime.now().isoformat(), event_type=event_type, + repeat='never', ) session.add(event2) @@ -45,6 +47,7 @@ def mockup(cls): start_date=datetime.datetime.now().isoformat(), end_date=datetime.datetime.now().isoformat(), event_type=event_type, + repeat='never', ) session.add(event3) session.commit() diff --git a/dolphin/tests/test_event_metadata.py b/dolphin/tests/test_event_metadata.py new file mode 100644 index 00000000..07bb9f31 --- /dev/null +++ b/dolphin/tests/test_event_metadata.py @@ -0,0 +1,56 @@ +from bddrest.authoring import status, response + +from dolphin.tests.helpers import LocalApplicationTestCase + + +class TestEvent(LocalApplicationTestCase): + + def test_metadata(self): + with self.given( + 'Test metadata verb', + '/apiv1/events', + 'METADATA' + ): + assert status == 200 + + fields = response.json['fields'] + + assert fields['id']['label'] is not None + assert fields['id']['minimum'] is not None + assert fields['id']['name'] is not None + assert fields['id']['key'] is not None + assert fields['id']['notNone'] is not None + assert fields['id']['required'] is not None + assert fields['id']['readonly'] is not None + assert fields['id']['primaryKey'] is not None + + assert fields['title']['label'] is not None + assert fields['title']['notNone'] is not None + assert fields['title']['required'] is not None + assert fields['title']['maxLength'] is not None + assert fields['title']['readonly'] is not None + assert fields['title']['watermark'] is not None + assert fields['title']['example'] is not None + assert fields['title']['message'] is not None + assert fields['title']['notNone'] is not None + assert fields['title']['type'] is not None + + assert fields['startDate']['label'] is not None + assert fields['startDate']['notNone'] is not None + assert fields['startDate']['required'] is not None + assert fields['startDate']['readonly'] is not None + assert fields['startDate']['watermark'] is not None + assert fields['startDate']['example'] is not None + assert fields['startDate']['message'] is not None + assert fields['startDate']['notNone'] is not None + assert fields['title']['type'] is not None + + assert fields['endDate']['label'] is not None + assert fields['endDate']['notNone'] is not None + assert fields['endDate']['required'] is not None + assert fields['endDate']['readonly'] is not None + assert fields['endDate']['watermark'] is not None + assert fields['endDate']['example'] is not None + assert fields['endDate']['message'] is not None + assert fields['endDate']['notNone'] is not None + diff --git a/dolphin/tests/test_event_update.py b/dolphin/tests/test_event_update.py index c0bb482c..2873477f 100644 --- a/dolphin/tests/test_event_update.py +++ b/dolphin/tests/test_event_update.py @@ -36,6 +36,7 @@ def mockup(cls): start_date=datetime.datetime.now().isoformat(), end_date=datetime.datetime.now().isoformat(), event_type=event_type1, + repeat='never', ) session.add(cls.event1) @@ -44,6 +45,7 @@ def mockup(cls): start_date=datetime.datetime.now().isoformat(), end_date=datetime.datetime.now().isoformat(), event_type=event_type1, + repeat='yearly', ) session.add(cls.event2) session.commit() @@ -51,7 +53,7 @@ def mockup(cls): def test_add(self): self.login(self.member.email) title = 'New event' - description = 'A description for an event' + repeat = 'monthly' start_date = datetime.datetime.now().isoformat() end_date = datetime.datetime.now().isoformat() @@ -63,7 +65,7 @@ def test_add(self): title=title, startDate=start_date, endDate=end_date, - description=description, + repeat=repeat, eventTypeId=self.event_type2.id, ), ): @@ -72,7 +74,7 @@ def test_add(self): assert response.json['title'] == title assert response.json['startDate'] == start_date assert response.json['endDate'] == end_date - assert response.json['description'] == description + assert response.json['repeat'] == repeat assert response.json['eventTypeId'] == self.event_type2.id when('Trying to pass without form parameters', json={}) @@ -96,13 +98,6 @@ def test_add(self): ) assert status == '727 Title Is None' - when( - 'Description length is less than limit', - json=given | dict(description=(512 + 1) * 'a'), - ) - assert status == '703 At Most 512 Characters Are Valid For ' \ - 'Description' - when( 'Start date format is wrong', json=given | dict(startDate='30-20-20') @@ -128,6 +123,13 @@ def test_add(self): when('The event-type not found', json=given | dict(eventTypeId=0)) assert status == '658 Event Type Not Found' + when( + 'Invalid repeat value is in form', + json=given | dict(repeat='a') + ) + assert status == '910 Invalid Repeat, only one of ' \ + '"yearly, monthly, never" will be accepted' + when('Request is not authorized', authorization=None) assert status == 401 diff --git a/dolphin/tests/test_eventtype_delete.py b/dolphin/tests/test_eventtype_delete.py new file mode 100644 index 00000000..609624f8 --- /dev/null +++ b/dolphin/tests/test_eventtype_delete.py @@ -0,0 +1,79 @@ +import datetime + +from bddrest import status, response, when + +from dolphin.models import Member, EventType, Event +from dolphin.tests.helpers import LocalApplicationTestCase, oauth_mockup_server + + +class TestEventType(LocalApplicationTestCase): + + @classmethod + 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) + + cls.event_type = EventType( + title='Type1', + ) + session.add(cls.event_type) + + cls.event = Event( + title='Event1', + event_type=cls.event_type, + start_date=datetime.datetime.now().isoformat(), + end_date=datetime.datetime.now().isoformat(), + repeat='never', + ) + session.add(cls.event) + session.commit() + + def test_delete(self): + self.login(self.member.email) + + with oauth_mockup_server(), self.given( + f'Deleting an event type', + f'/apiv1/eventtypes/id: {self.event_type.id}', + f'DELETE', + ): + assert status == 200 + assert response.json['id'] == self.event_type.id + + session = self.create_session() + assert not session.query(EventType) \ + .filter(EventType.id == self.event_type.id) \ + .one_or_none() + + assert not session.query(Event) \ + .filter(Event.id == self.event.id) \ + .one_or_none() + + when( + 'Intended event type with string type not found', + url_parameters=dict(id='Alphabetical') + ) + assert status == 404 + + when( + 'Intended event type not found', + url_parameters=dict(id=0) + ) + assert status == 404 + + when( + 'Form parameter is sent with request', + form=dict(a='a') + ) + assert status == '709 Form Not Allowed' + + when('Request is not authorized', authorization=None) + assert status == 401 + diff --git a/dolphin/tests/test_issue_list.py b/dolphin/tests/test_issue_list.py index a03058e2..fa02c513 100644 --- a/dolphin/tests/test_issue_list.py +++ b/dolphin/tests/test_issue_list.py @@ -475,6 +475,12 @@ def test_list(self): assert status == 200 assert len(response.json) == 1 + when( + 'filter by triage phase id', + query=dict(phaseId='IN(0,1)') + ) + assert len(response.json) == 2 + when('Request is not authorized', authorization=None) assert status == 401 diff --git a/dolphin/tests/test_issue_search.py b/dolphin/tests/test_issue_search.py new file mode 100644 index 00000000..13d95380 --- /dev/null +++ b/dolphin/tests/test_issue_search.py @@ -0,0 +1,191 @@ +from nanohttp import context +from nanohttp.contexts import Context +from bddrest.authoring import status, response, when, given +from auditor.context import Context as AuditLogContext + +from dolphin.models import Member, Release, Project, Issue, Group, Workflow, \ + Skill, Subscription +from dolphin.tests.helpers import LocalApplicationTestCase, oauth_mockup_server + + +class TestIssue(LocalApplicationTestCase): + + @classmethod + @AuditLogContext(dict()) + def mockup(cls): + session = cls.create_session() + cls.member = Member( + email='member@example.com', + title='member', + access_token='access token 1', + reference_id=2 + ) + workflow = Workflow(title='Default') + skill = Skill(title='First Skill') + 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.member, + room_id=0, + group=group, + ) + + cls.project = Project( + release=release, + workflow=workflow, + group=group, + manager=cls.member, + title='My first project', + description='A decription for my project', + room_id=1 + ) + + with Context(dict()): + context.identity = cls.member + + cls.issue1 = Issue( + project=cls.project, + title='First issue', + description='This is description of first issue', + status='in-progress', + due_date='2020-2-20', + kind='feature', + days=1, + room_id=2, + ) + session.add(cls.issue1) + + cls.issue2 = Issue( + project=cls.project, + title='Second issue', + description='This is description of second issue', + status='to-do', + due_date='2016-2-20', + kind='feature', + days=2, + room_id=3, + ) + session.add(cls.issue2) + + cls.issue3 = Issue( + project=cls.project, + title='Third issue', + description='This is description of third issue', + status='on-hold', + due_date='2020-2-20', + kind='feature', + days=3, + room_id=4, + ) + session.add(cls.issue3) + + cls.issue4 = Issue( + project=cls.project, + title='Fourth issue', + description='This is description of fourth issue', + status='complete', + due_date='2020-2-20', + kind='feature', + days=3, + room_id=4, + ) + session.add(cls.issue4) + session.flush() + + subscription1 = Subscription( + subscribable_id=cls.issue1.id, + member_id=cls.member.id, + ) + session.add(subscription1) + session.commit() + + def test_search_issue(self): + self.login(self.member.email) + + with oauth_mockup_server(), self.given( + 'Search for a issue by submitting form', + '/apiv1/issues', + 'SEARCH', + form=dict(query='Fir'), + ): + assert status == 200 + assert len(response.json) == 1 + assert response.json[0]['title'] == self.issue1.title + + when('Search without query parameter', form=given - 'query') + assert status == '912 Query Parameter Not In Form Or Query String' + + when( + 'Search string must be less than 20 charecters', + form=given | dict(query=(50 + 1) * 'a') + ) + assert status == '704 At Most 50 Characters Valid For Title' + + when( + 'Try to sort the response', + query=dict(sort='id'), + form=given | dict(query='issue') + ) + assert len(response.json) == 4 + assert response.json[0]['id'] < response.json[1]['id'] + + when( + 'Trying ro sort the response in descend ordering', + query=dict(sort='-id'), + form=given | dict(query='issue') + ) + assert len(response.json) == 4 + assert response.json[0]['id'] > response.json[1]['id'] + + when('Filtering the response', query=dict(title=self.issue1.title)) + assert len(response.json) == 1 + assert response.json[0]['title'] == self.issue1.title + + when( + 'Trying to filter the response ignoring the title', + query=dict(title=f'!{self.issue1.title}'), + form=given | dict(query='issue') + ) + assert len(response.json) == 3 + + when( + 'Testing pagination', + query=dict(take=1, skip=1), + form=given | dict(query='issue') + ) + assert len(response.json) == 1 + + when( + 'Sort before pagination', + query=dict(sort='-id', take=3, skip=1), + form=given | dict(query='issue') + ) + assert len(response.json) == 3 + assert response.json[0]['id'] > response.json[1]['id'] + + when( + 'Filtering unread issues', + query=dict(unread='true'), + form=given | dict(query='issue') + ) + assert len(response.json) == 1 + assert response.json[0]['id'] == self.issue1.id + + def test_request_with_query_string(self): + self.login(self.member.email) + + with oauth_mockup_server(), self.given( + 'Test request using query string', + '/apiv1/issues', + 'SEARCH', + query=dict(query='Sec') + ): + assert status == 200 + assert len(response.json) == 1 + + when('An unauthorized search', authorization=None) + assert status == 401 + diff --git a/dolphin/tests/test_item_list.py b/dolphin/tests/test_item_list.py new file mode 100644 index 00000000..8da05c69 --- /dev/null +++ b/dolphin/tests/test_item_list.py @@ -0,0 +1,184 @@ +from bddrest import status, response, when, given +from auditor.context import Context as AuditLogContext + +from dolphin.models import Member, Group, Workflow, Skill, Phase, Release, \ + Project, Issue, Item +from dolphin.tests.helpers import LocalApplicationTestCase, oauth_mockup_server + + +class TestListGroup(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 + ) + + cls.member2 = Member( + title='Second Member', + email='member2@example.com', + access_token='access token 2', + phone=987654321, + reference_id=3 + ) + + workflow = Workflow(title='Default') + session.add(workflow) + + skill = Skill(title='First Skill') + cls.phase1 = Phase( + title='backlog', + order=-1, + workflow=workflow, + skill=skill, + ) + session.add(cls.phase1) + + cls.phase2 = Phase( + title='Triage', + order=1, + workflow=workflow, + skill=skill, + ) + session.add(cls.phase2) + + cls.phase3 = Phase( + title='Development', + order=1, + workflow=workflow, + skill=skill, + ) + session.add(cls.phase3) + + 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=0, + group=group, + ) + + project = Project( + release=release, + workflow=workflow, + group=group, + manager=cls.member2, + title='My first project', + description='A decription for my project', + room_id=1 + ) + + cls.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=2 + ) + session.add(cls.issue1) + + cls.issue2 = Issue( + project=project, + title='Second issue', + description='This is description of second issue', + due_date='2020-2-20', + kind='feature', + days=1, + room_id=3 + ) + session.add(cls.issue2) + + cls.issue3 = Issue( + project=project, + title='Third issue', + description='This is description of second issue', + due_date='2020-2-20', + kind='feature', + days=1, + room_id=4 + ) + session.add(cls.issue3) + session.flush() + + cls.item1 = Item( + issue_id=cls.issue1.id, + phase_id=cls.phase1.id, + member_id=cls.member1.id, + ) + session.add(cls.item1) + session.flush() + + cls.item2 = Item( + issue_id=cls.issue2.id, + phase_id=cls.phase3.id, + member_id=cls.member2.id, + ) + session.add(cls.item2) + session.flush() + + cls.item3 = Item( + issue_id=cls.issue3.id, + phase_id=cls.phase2.id, + member_id=cls.member1.id, + ) + session.add(cls.item3) + session.commit() + + def test_list_item(self): + self.login(self.member1.email) + + with oauth_mockup_server(), self.given( + 'List item', + '/apiv1/items', + 'LIST', + ): + assert status == 200 + assert len(response.json) == 3 + + when('Sort by id', query=dict(sort='id')) + assert status == 200 + assert response.json[0]['id'] == self.item1.id + assert response.json[1]['id'] == self.item2.id + assert response.json[2]['id'] == self.item3.id + + when('Reverse sort by id', query=dict(sort='-id')) + assert status == 200 + assert response.json[0]['id'] == self.item3.id + assert response.json[1]['id'] == self.item2.id + assert response.json[2]['id'] == self.item1.id + + when('Filter by id', query=dict(id=f'{self.item1.id}')) + assert len(response.json) == 1 + assert response.json[0]['id'] == self.item1.id + + when('Filter by id', query=dict(id=f'!{self.item1.id}')) + assert len(response.json) == 2 + + when( + 'Paginate item', + query=dict(sort='id', take=1, skip=2) + ) + assert response.json[0]['id'] == self.item3.id + + when( + 'Manipulate sorting and pagination', + query=dict(sort='-id', take=1, skip=2) + ) + assert response.json[0]['id'] == self.item1.id + + when('Request is not authorized', authorization=None) + assert status == 401 + diff --git a/dolphin/tests/test_item_metadata.py b/dolphin/tests/test_item_metadata.py new file mode 100644 index 00000000..b0713478 --- /dev/null +++ b/dolphin/tests/test_item_metadata.py @@ -0,0 +1,52 @@ +from bddrest.authoring import status, response + +from dolphin.tests.helpers import LocalApplicationTestCase + + +class TestItem(LocalApplicationTestCase): + + def test_metadata(self): + with self.given( + 'Test metadata verb', + '/apiv1/items', + 'METADATA' + ): + assert status == 200 + + fields = response.json['fields'] + + assert fields['id']['label'] is not None + assert fields['id']['minimum'] is not None + assert fields['id']['example'] is not None + assert fields['id']['name'] is not None + assert fields['id']['key'] is not None + assert fields['id']['notNone'] is not None + assert fields['id']['required'] is not None + assert fields['id']['readonly'] is not None + assert fields['id']['protected'] is not None + assert fields['id']['primaryKey'] is not None + + assert fields['phaseId']['label'] is not None + assert fields['phaseId']['name'] is not None + assert fields['phaseId']['type'] is not None + assert fields['phaseId']['required'] is not None + assert fields['phaseId']['watermark'] is not None + assert fields['phaseId']['notNone'] is not None + assert fields['phaseId']['watermark'] is not None + + assert fields['issueId']['label'] is not None + assert fields['issueId']['name'] is not None + assert fields['issueId']['type'] is not None + assert fields['issueId']['required'] is not None + assert fields['issueId']['watermark'] is not None + assert fields['issueId']['notNone'] is not None + assert fields['issueId']['watermark'] is not None + + assert fields['memberId']['label'] is not None + assert fields['memberId']['name'] is not None + assert fields['memberId']['type'] is not None + assert fields['memberId']['required'] is not None + assert fields['memberId']['watermark'] is not None + assert fields['memberId']['notNone'] is not None + assert fields['memberId']['watermark'] is not None + diff --git a/dolphin/tests/test_member_search.py b/dolphin/tests/test_member_search.py new file mode 100644 index 00000000..2ef6e252 --- /dev/null +++ b/dolphin/tests/test_member_search.py @@ -0,0 +1,122 @@ +from bddrest.authoring import status, response, when, given + +from dolphin.models import Member +from dolphin.tests.helpers import LocalApplicationTestCase, oauth_mockup_server + + +class TestMember(LocalApplicationTestCase): + + @classmethod + def mockup(cls): + session = cls.create_session() + + cls.member1 = Member( + title='First Member', + email='member1@example.com', + access_token='access token 1', + phone=123987465, + reference_id=3 + ) + session.add(cls.member1) + + cls.member2 = Member( + title='Second Member', + email='member2@example.com', + access_token='access token', + phone=1287465, + reference_id=4 + ) + session.add(cls.member2) + + member3 = Member( + title='Third Member', + email='member3@example.com', + access_token='access token', + phone=1287456, + reference_id=5 + ) + session.add(member3) + session.commit() + + def test_search(self): + self.login(self.member1.email) + + with oauth_mockup_server(), self.given( + 'Search for a member by submitting form', + '/apiv1/members', + 'SEARCH', + form=dict(query='Sec'), + ): + assert status == 200 + assert len(response.json) == 1 + assert response.json[0]['title'] == self.member2.title + + when('Search without query parameter', form=given - 'query') + assert status == '912 Query Parameter Not In Form Or Query String' + + when( + 'Search string must be less than 20 charecters', + form=given | dict(query=(50 + 1) * 'a') + ) + assert status == '704 At Most 50 Characters Valid For Title' + + when( + 'Try to sort the response', + query=dict(sort='id'), + form=given | dict(query='member') + ) + assert len(response.json) == 3 + assert response.json[0]['id'] < response.json[1]['id'] + + when( + 'Trying ro sort the response in descend ordering', + query=dict(sort='-id'), + form=given | dict(query='member') + ) + assert len(response.json) == 3 + assert response.json[0]['id'] > response.json[1]['id'] + + when( + 'Filtering the response', + query=dict(title=self.member2.title) + ) + assert len(response.json) == 1 + assert response.json[0]['title'] == self.member2.title + + when( + 'Trying to filter the response ignoring the title', + query=dict(title=f'!{self.member2.title}'), + form=given | dict(query='member') + ) + assert len(response.json) == 2 + + when( + 'Testing pagination', + query=dict(take=1, skip=1), + form=given | dict(query='member') + ) + assert len(response.json) == 1 + + when( + 'Sort before pagination', + query=dict(sort='-id', take=2, skip=1), + form=given | dict(query='member') + ) + assert len(response.json) == 2 + assert response.json[0]['id'] > response.json[1]['id'] + + def test_request_with_query_string(self): + self.login(self.member1.email) + + with oauth_mockup_server(), self.given( + 'Test request using query string', + '/apiv1/members', + 'SEARCH', + query=dict(query='Sec') + ): + assert status == 200 + assert len(response.json) == 1 + + when('An unauthorized search', authorization=None) + assert status == 401 + diff --git a/dolphin/tests/test_project_create.py b/dolphin/tests/test_project_create.py index 28a48697..f5fcba35 100644 --- a/dolphin/tests/test_project_create.py +++ b/dolphin/tests/test_project_create.py @@ -1,3 +1,5 @@ +from datetime import datetime + from auditor import MiddleWare from auditor.context import Context as AuditLogContext from auditor.logentry import RequestLogEntry, InstantiationLogEntry @@ -34,11 +36,11 @@ def mockup(cls): cls.group = Group(title='Public', public=True) - release1 = Release( + cls.release1 = Release( title='My first release', description='A decription for my first release', - cutoff='2030-2-20', - launch_date='2030-2-20', + cutoff='2030-02-20T00:00:00', + launch_date='2030-02-20T00:00:00', manager=cls.member, room_id=0, group=cls.group, @@ -47,7 +49,7 @@ def mockup(cls): project1 = Project( workflow=cls.workflow, group=cls.group, - release=release1, + release=cls.release1, manager=cls.member, title='My first project', description='A decription for my project', @@ -83,6 +85,7 @@ def test_create(self): assert response.json['dueDate'] == None assert response.json['managerId'] == self.member.id assert response.json['secondaryManagerId'] is None + assert response.json['releaseCutoff'] == self.release1.cutoff created_project_id = response.json['id'] created_project = session.query(Project).get(created_project_id) diff --git a/dolphin/tests/test_project_list.py b/dolphin/tests/test_project_list.py index 53e4ecba..3dcc076f 100644 --- a/dolphin/tests/test_project_list.py +++ b/dolphin/tests/test_project_list.py @@ -287,8 +287,8 @@ def test_list(self): assert len(response.json) == 4 assert response.json[0]['title'] == self.project3.title assert response.json[1]['title'] == self.project2.title - assert response.json[2]['title'] == self.project1.title - assert response.json[3]['title'] == self.project4.title + assert response.json[2]['title'] == self.project4.title + assert response.json[3]['title'] == self.project1.title with self.given( 'Filter projects', @@ -337,10 +337,9 @@ def test_list(self): query=dict(boarding='IN(on-time,frozen)') ) assert status == 200 - assert len(response.json) == 3 - assert response.json[0]['title'] == self.project1.title - assert response.json[1]['title'] == self.project2.title - assert response.json[2]['title'] == self.project4.title + assert len(response.json) == 2 + assert response.json[0]['title'] == self.project2.title + assert response.json[1]['title'] == self.project4.title with self.given( 'Project pagination', diff --git a/dolphin/tests/test_project_metadata.py b/dolphin/tests/test_project_metadata.py index cdec7fb7..edee6b0a 100644 --- a/dolphin/tests/test_project_metadata.py +++ b/dolphin/tests/test_project_metadata.py @@ -84,3 +84,8 @@ def test_metadata(self): assert fields['boardingValue']['required'] is not None assert fields['boardingValue']['readonly'] is not None + assert fields['releaseCutoff']['label'] is not None + assert fields['releaseCutoff']['name'] is not None + assert fields['releaseCutoff']['required'] is not None + assert fields['releaseCutoff']['readonly'] is not None + diff --git a/dolphin/tests/test_release_create.py b/dolphin/tests/test_release_create.py index 8cb9fd39..aa5aa832 100644 --- a/dolphin/tests/test_release_create.py +++ b/dolphin/tests/test_release_create.py @@ -57,7 +57,7 @@ def test_create(self): description='Decription for my release', cutoff='2030-2-20', launchDate='2030-2-20', - managerReferenceId=self.member.reference_id, + managerId=self.member.id, groupId=self.group.id, ) ): @@ -67,7 +67,7 @@ def test_create(self): assert response.json['cutoff'] == '2030-02-20T00:00:00' assert response.json['launchDate'] == '2030-02-20T00:00:00' assert response.json['status'] is None - assert response.json['managerId'] == self.member.reference_id + assert response.json['managerId'] == self.member.id assert response.json['roomId'] is not None assert len(response.json['projects']) == 0 @@ -170,22 +170,22 @@ def test_create(self): '"in-progress, on-hold, delayed, complete" will be accepted' when( - 'Manager reference id is null', - json=Update(title='New Release', managerReferenceId=None) + 'Manager id is null', + json=Update(title='New Release', managerId=None) ) - assert status == '778 Manager Reference Id Is Null' + assert status == '778 Manager Id Is Null' when( 'Manager is not found', - json=Update(title='New Release', managerReferenceId=0) + json=Update(title='New Release', managerId=0) ) assert status == '608 Manager Not Found' when( - 'Maneger reference id is not in form', - json=given - 'managerReferenceId' | dict(title='New Release') + 'Maneger id is not in form', + json=given - 'managerId' | dict(title='New Release') ) - assert status == '777 Manager Reference Id Not In Form' + assert status == '777 Manager Id Not In Form' when( 'Group id is null', @@ -214,7 +214,7 @@ def test_create(self): ) assert status == '707 Invalid field, only following fields are ' \ 'accepted: title, description, status, cutoff, ' \ - 'managerReferenceId, launchDate, groupId' + 'managerId, launchDate, groupId' with chat_server_status('404 Not Found'): when( diff --git a/dolphin/tests/test_release_update.py b/dolphin/tests/test_release_update.py index 67f2a6be..c398e76c 100644 --- a/dolphin/tests/test_release_update.py +++ b/dolphin/tests/test_release_update.py @@ -80,7 +80,7 @@ def test_update(self): cutoff='2030-2-21', launchDate='2030-2-21', status='in-progress', - managerReferenceId=self.member2.reference_id, + managerId=self.member2.id, groupId=self.group2.id, ) ): @@ -172,14 +172,14 @@ def test_update(self): '"in-progress, on-hold, delayed, complete" will be accepted' when( - 'Manager reference id is null', - json=Update(title='New Release', managerReferenceId=None) + 'Manager id is null', + json=Update(title='New Release', managerId=None) ) - assert status == '778 Manager Reference Id Is Null' + assert status == '778 Manager Id Is Null' when( 'Manager is not found', - json=Update(title='New Release', managerReferenceId=0) + json=Update(title='New Release', managerId=0) ) assert status == '608 Manager Not Found' diff --git a/dolphin/tests/test_timecard_get.py b/dolphin/tests/test_timecard_get.py new file mode 100644 index 00000000..1c614b46 --- /dev/null +++ b/dolphin/tests/test_timecard_get.py @@ -0,0 +1,64 @@ +import datetime + +from bddrest import status, response, when + +from dolphin.models import Member, Timecard +from dolphin.tests.helpers import LocalApplicationTestCase, oauth_mockup_server + + +class TestTimecard(LocalApplicationTestCase): + + @classmethod + 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) + + cls.timecard = Timecard( + start_date=datetime.datetime.now().isoformat(), + end_date=datetime.datetime.now().isoformat(), + estimated_time=2, + summary='Summary for timecard', + ) + session.add(cls.timecard) + session.commit() + + def test_get(self): + self.login(self.member.email) + + with oauth_mockup_server(), self.given( + f'Get a timecard', + f'/apiv1/timecards/id: {self.timecard.id}', + f'GET', + ): + assert status == 200 + assert response.json['id'] == self.timecard.id + + when( + 'Intended group with string type not found', + url_parameters=dict(id='Alphabetical') + ) + assert status == 404 + + when( + 'Intended group not found', + url_parameters=dict(id=0) + ) + assert status == 404 + + when( + 'Form parameter is sent with request', + form=dict(parameter='Invalid form parameter') + ) + assert status == '709 Form Not Allowed' + + when('Request is not authorized', authorization=None) + assert status == 401 + diff --git a/dolphin/tests/test_timecard_list.py b/dolphin/tests/test_timecard_list.py new file mode 100644 index 00000000..9da66f74 --- /dev/null +++ b/dolphin/tests/test_timecard_list.py @@ -0,0 +1,82 @@ +import datetime + +from bddrest import when, response, status + +from dolphin.models import Member, Timecard +from dolphin.tests.helpers import LocalApplicationTestCase, oauth_mockup_server + + +class TestTimecard(LocalApplicationTestCase): + + @classmethod + 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=2, + ) + session.add(cls.member) + + timecard1 = Timecard( + start_date=datetime.datetime.now().isoformat(), + end_date=datetime.datetime.now().isoformat(), + estimated_time=1, + summary='Summary for timecard1', + ) + session.add(timecard1) + + timecard2 = Timecard( + start_date=datetime.datetime.now().isoformat(), + end_date=datetime.datetime.now().isoformat(), + estimated_time=2, + summary='Summary for timecard2', + ) + session.add(timecard2) + + timecard3 = Timecard( + start_date=datetime.datetime.now().isoformat(), + end_date=datetime.datetime.now().isoformat(), + estimated_time=3, + summary='Summary for timecard3', + ) + session.add(timecard3) + session.commit() + + def test_list(self): + self.login(self.member.email) + + with oauth_mockup_server(), self.given( + 'List of timecards', + '/apiv1/timecards', + 'LIST', + ): + assert status == 200 + assert len(response.json) == 3 + + 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 + + when('Sorting the response descending', query=dict(sort='-id')) + assert response.json[0]['id'] > response.json[1]['id'] == 2 + + when('Trying pagination response', query=dict(take=1)) + assert response.json[0]['id'] == 1 + assert len(response.json) == 1 + + when('Trying pagination with skip', query=dict(take=1, skip=1)) + assert response.json[0]['id'] == 2 + assert len(response.json) == 1 + + when('Trying filtering response', query=dict(id=1)) + assert response.json[0]['id'] == 1 + assert len(response.json) == 1 + + when('Request is not authorized', authorization=None) + assert status == 401 + diff --git a/dolphin/tests/test_timecard_metadata.py b/dolphin/tests/test_timecard_metadata.py new file mode 100644 index 00000000..56a80132 --- /dev/null +++ b/dolphin/tests/test_timecard_metadata.py @@ -0,0 +1,48 @@ +from bddrest.authoring import status, response + +from dolphin.tests.helpers import LocalApplicationTestCase + + +class TestTimecard(LocalApplicationTestCase): + + def test_metadata(self): + with self.given( + 'Test metadata verb', + '/apiv1/timecards', + 'METADATA' + ): + assert status == 200 + fields = response.json['fields'] + + assert fields['id']['label'] is not None + assert fields['id']['minimum'] is not None + assert fields['id']['name'] is not None + assert fields['id']['key'] is not None + assert fields['id']['notNone'] is not None + assert fields['id']['required'] is not None + assert fields['id']['readonly'] is not None + assert fields['id']['primaryKey'] is not None + + assert fields['startDate']['label'] is not None + assert fields['startDate']['watermark'] is not None + assert fields['startDate']['pattern'] is not None + assert fields['startDate']['example'] is not None + assert fields['startDate']['name'] is not None + assert fields['startDate']['notNone'] is not None + assert fields['startDate']['required'] is not None + + assert fields['endDate']['label'] is not None + assert fields['endDate']['watermark'] is not None + assert fields['endDate']['pattern'] is not None + assert fields['endDate']['example'] is not None + assert fields['endDate']['name'] is not None + assert fields['endDate']['notNone'] is not None + assert fields['endDate']['required'] is not None + + assert fields['estimatedTime']['label'] is not None + assert fields['estimatedTime']['watermark'] is not None + assert fields['estimatedTime']['example'] is not None + assert fields['estimatedTime']['name'] is not None + assert fields['estimatedTime']['notNone'] is not None + assert fields['estimatedTime']['required'] is not None + diff --git a/dolphin/tests/test_timecard_update.py b/dolphin/tests/test_timecard_update.py new file mode 100644 index 00000000..621ca43d --- /dev/null +++ b/dolphin/tests/test_timecard_update.py @@ -0,0 +1,123 @@ +import datetime + +from bddrest import status, response, when, given + +from dolphin.models import Member, Timecard +from dolphin.tests.helpers import LocalApplicationTestCase, oauth_mockup_server + + +class TestTimecard(LocalApplicationTestCase): + + @classmethod + 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) + + cls.timecard = Timecard( + start_date=datetime.datetime.now().isoformat(), + end_date=datetime.datetime.now().isoformat(), + estimated_time=3, + summary='The summary for a time card', + ) + session.add(cls.timecard) + session.commit() + + def test_update(self): + self.login(self.member.email) + start_date = datetime.datetime.now().isoformat() + end_date = datetime.datetime.now().isoformat() + summary = 'Some summary' + + with oauth_mockup_server(), self.given( + f'Updating a timecard', + f'/apiv1/timecards/id: {self.timecard.id}', + f'UPDATE', + json=dict( + startDate=start_date, + endDate=end_date, + estimatedTime=2, + summary=summary, + ), + ): + assert status == 200 + assert response.json['id'] is not None + assert response.json['startDate'] == start_date + assert response.json['endDate'] == end_date + assert response.json['estimatedTime'] == 2 + assert response.json['summary'] == summary + + when('Trying to pass without form parameters', json={}) + assert status == '708 Empty Form' + + when( + 'Intended timecard with string type not found', + url_parameters=dict(id='Alphabetical') + ) + assert status == 404 + + when( + 'Intended timecard with integer type not found', + url_parameters=dict(id=0) + ) + assert status == 404 + + when( + 'Summary length is less than limit', + json=given | dict(summary=(1024 + 1) * 'a'), + ) + assert status == '902 At Most 1024 Characters Are Valid For ' \ + 'Summary' + + when( + 'Estimated time type is wrong', + json=given | dict(estimatedTime='time'), + ) + assert status == '900 Invalid Estimated Time Type' + + when( + 'Start date format is wrong', + json=given | dict(startDate='30-20-20') + ) + assert status == '791 Invalid Start Date Format' + + when( + 'End date format is wrong', + json=given | dict(endDate='30-20-20') + ) + assert status == '790 Invalid End Date Format' + + when('Start date is null', json=given | dict(startDate=None)) + assert status == '905 Start Date Is Null' + + when('End date is null', json=given | dict(endDate=None)) + assert status == '906 End Date Is Null' + + when( + 'Estimated time is null', + json=given | dict(estimatedTime=None) + ) + assert status == '904 Estimated Time Is Null' + + when('Summary is null', json=given | dict(summary=None)) + assert status == '903 Summary Is Null' + + when( + 'End date must be greater than start date', + json=given | dict( + startDate=end_date, + endDate=start_date, + ) + ) + assert status == '657 End Date Must Be Greater Than Start Date' + + when('Request is not authorized', authorization=None) + assert status == 401 + diff --git a/dolphin/validators.py b/dolphin/validators.py index 08b4d767..43c9f5ae 100644 --- a/dolphin/validators.py +++ b/dolphin/validators.py @@ -3,15 +3,16 @@ from nanohttp import validate, HTTPStatus, context from restfulpy.orm import DBSession -from .models import * -from .models.organization import roles from .exceptions import StatusResourceNotFound, StatusRepetitiveTitle, \ StatusRelatedIssueNotFound, StatusEventTypeNotFound, \ StatusInvalidStartDateFormat, StatusInvalidEndDateFormat, \ StatusLimitedCharecterForSummary, StatusInvalidEstimatedTimeType, \ StatusSummaryNotInForm, StatusEstimatedTimeNotInForm, \ StatusEndDateNotInForm, StatusStartDateNotInForm, StatusSummaryIsNull, \ - StatusEstimatedTimeIsNull, StatusStartDateIsNull, StatusEndDateIsNull + StatusEstimatedTimeIsNull, StatusStartDateIsNull, StatusEndDateIsNull, \ + StatusRepeatNotInForm +from .models import * +from .models.organization import roles TITLE_PATTERN = re.compile(r'^(?!\s).*[^\s]$') @@ -92,6 +93,17 @@ def project_accessible_validator(projectId, project, field): return projectId +def event_repeat_value_validator(repeat, project, field): + form = context.form + if 'repeat' in form and form['repeat'] not in event_repeats: + raise HTTPStatus( + f'910 Invalid Repeat, only one of ' \ + f'"{", ".join(event_repeats)}" will be accepted' + ) + + return repeat + + def project_status_value_validator(status, project, field): form = context.form if 'status' in form and form['status'] not in project_statuses: @@ -139,7 +151,9 @@ def kind_value_validator(kind, project, field): def issue_status_value_validator(status, project, field): form = context.form - if 'status' in form and form['status'] not in issue_statuses: + 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' @@ -320,10 +334,10 @@ def event_exists_validator(title, project, field): status=dict( callback=release_status_value_validator ), - managerReferenceId=dict( + managerId=dict( type_=(int, '608 Manager Not Found'), - required='777 Manager Reference Id Not In Form', - not_none='778 Manager Reference Id Is Null', + required='777 Manager Id Not In Form', + not_none='778 Manager Id Is Null', ), launchDate=dict( pattern=(DATETIME_PATTERN, '784 Invalid Launch Date Format'), @@ -351,9 +365,9 @@ def event_exists_validator(title, project, field): status=dict( callback=release_status_value_validator ), - managerReferenceId=dict( + managerId=dict( type_=(int, '608 Manager Not Found'), - not_none='778 Manager Reference Id Is Null', + not_none='778 Manager Id Is Null', ), launchDate=dict( pattern=(DATETIME_PATTERN, '784 Invalid Launch Date Format'), @@ -819,11 +833,9 @@ def event_exists_validator(title, project, field): event_add_validator = validate( - description=dict( - max_length=( - 512, - '703 At Most 512 Characters Are Valid For Description' - ), + repeat=dict( + required=StatusRepeatNotInForm, + callback=event_repeat_value_validator, ), eventTypeId=dict( required='794 Type Id Not In Form', @@ -862,11 +874,8 @@ def event_exists_validator(title, project, field): event_update_validator = validate( - description=dict( - max_length=( - 512, - '703 At Most 512 Characters Are Valid For Description' - ), + repeat=dict( + callback=event_repeat_value_validator, ), eventTypeId=dict( not_none='798 Event Type Id Is Null', @@ -908,3 +917,37 @@ def event_exists_validator(title, project, field): ), ) + +timecard_update_validator = validate( + summary=dict( + max_length=(1024, StatusLimitedCharecterForSummary), + not_none=StatusSummaryIsNull, + ), + startDate=dict( + pattern=(DATETIME_PATTERN, StatusInvalidStartDateFormat), + not_none=StatusStartDateIsNull, + ), + endDate=dict( + pattern=(DATETIME_PATTERN, StatusInvalidEndDateFormat), + not_none=StatusEndDateIsNull, + ), + estimatedTime=dict( + type_=(int, StatusInvalidEstimatedTimeType), + not_none=StatusEstimatedTimeIsNull, + ), +) + + +search_member_validator = validate( + query=dict( + max_length=(50, '704 At Most 50 Characters Valid For Title'), + ) +) + + +search_issue_validator = validate( + query=dict( + max_length=(50, '704 At Most 50 Characters Valid For Title'), + ) +) +