Пример #1
0
    def setUp(self):
        # (if you are copying and pasting, update class title below)
        super(BaseShift, self).setUp()

        # create a span of time and only 2 shifts will exist within it
        today = normalize_to_midnight(datetime.utcnow())
        self.range_start = (today - timedelta(days=4)).isoformat()
        self.range_stop = (today + timedelta(days=4)).isoformat()

        # one unassigned
        unassigned_start = (today + timedelta(days=1, hours=8)).isoformat()
        unassigned_stop = (today + timedelta(days=1, hours=16)).isoformat()
        self.unassigned_shift = self.role.create_shift(start=unassigned_start,
                                                       stop=unassigned_stop)

        # the other assigned
        self.coworker = self.role.create_worker(
            email="*****@*****.**",
            min_hours_per_workweek=20,
            max_hours_per_workweek=40)
        assigned_start = (today - timedelta(days=2, hours=15)).isoformat()
        assigned_stop = (today - timedelta(days=2, hours=7)).isoformat()
        self.assigned_shift = self.role.create_shift(
            start=assigned_start,
            stop=assigned_stop,
            user_id=self.coworker.get_id())
    def test_time_off_request_crud_worker(self):
        self.update_permission_worker()

        # get
        requests = self.worker.get_time_off_requests(
            start=self.range_start, end=self.range_stop)
        assert len(requests) == 1

        # post
        today = normalize_to_midnight(datetime.utcnow())

        # create a time off request for testing against
        date1 = (today + timedelta(days=8)).strftime("%Y-%m-%d")
        date2 = (today + timedelta(days=5)).strftime("%Y-%m-%d")

        default_tz = pytz.timezone("UTC")
        local_tz = pytz.timezone(self.location.data.get("timezone"))

        # can request on a date, but state needs to be left alone
        new_time_off_request = self.worker.create_time_off_request(date=date1)

        request_date = default_tz.localize(
                    iso8601.parse_date(new_time_off_request.data.get("start")
                ) \
                .replace(tzinfo=None)) \
                .astimezone(local_tz) \
                .strftime("%Y-%m-%d")

        assert request_date == date1
        assert new_time_off_request.data.get("state") is None

        # cannot request with it defined
        with pytest.raises(Exception):
            self.worker.create_time_off_request(
                date=date2, minutes_paid=480, state="approved_paid")

        # patch
        # not allowed
        with pytest.raises(Exception):
            self.time_off_request.patch(state="approved_paid", minute_paid=480)

        with pytest.raises(Exception):
            new_time_off_request.patch(state="denied", minutes_paid=0)

        # delete
        new_time_off_request.delete()

        # can delete an unresolved request
        with pytest.raises(Exception):
            self.worker.get_time_off_request(new_time_off_request.get_id())

        # cannot delete once manager has touched it
        self.update_permission_manager()
        self.time_off_request.patch(state="approved_paid", minutes_paid=300)
        self.update_permission_worker()

        with pytest.raises(Exception):
            self.time_off_request.delete()
    def test_time_off_request_crud_manager(self):
        self.update_permission_manager()

        # get
        requests = self.worker.get_time_off_requests(
            start=self.range_start, end=self.range_stop)
        assert len(requests) == 1

        # post
        today = normalize_to_midnight(datetime.utcnow())

        # create a time off request for testing against
        date1 = (today + timedelta(days=8)).strftime("%Y-%m-%d")
        date2 = (today + timedelta(days=5)).strftime("%Y-%m-%d")

        default_tz = pytz.timezone("UTC")
        local_tz = pytz.timezone(self.location.data.get("timezone"))

        # normal
        new_time_off_request = self.worker.create_time_off_request(
            date=date1, minutes_paid=480, state="approved_paid")

        request_date = default_tz.localize(
                    iso8601.parse_date(new_time_off_request.data.get("start")
                ) \
                .replace(tzinfo=None)) \
                .astimezone(local_tz) \
                .strftime("%Y-%m-%d")

        assert request_date == date1
        assert new_time_off_request.data.get("minutes_paid") == 480
        assert new_time_off_request.data.get("state") == "approved_paid"

        # other roles fail for managers
        with pytest.raises(Exception):
            self.other_worker.create_time_off_request(
                date=date2, minutes_paid=0, state="approved_unpaid")

        # patch
        # respond to existing time off request
        self.time_off_request.patch(state="approved_unpaid", minute_paid=0)
        assert self.time_off_request.data.get("minutes_paid") == 0
        assert self.time_off_request.data.get("state") == "approved_unpaid"

        # modify another
        new_time_off_request.patch(state="denied", minutes_paid=0)
        assert new_time_off_request.data.get("minutes_paid") == 0
        assert new_time_off_request.data.get("state") == "denied"

        # delete
        self.time_off_request.delete()
        new_time_off_request.delete()

        with pytest.raises(Exception):
            self.worker.get_time_off_request(self.time_off_request.get_id())

        with pytest.raises(Exception):
            self.worker.get_time_off_request(new_time_off_request.get_id())
Пример #4
0
    def _get_schedule_range(self, role):
        """
        given a Role object, determines the start/stop of its next schedule

        return: (tuple) start, stop
        """

        org = Organization.query.join(Location)\
            .join(Role).filter(Role.id == role.id).first()

        default_tz = get_default_tz()
        local_tz = role.location.timezone_pytz

        last_schedule = Schedule2.query \
            .filter_by(role_id=role.id) \
            .order_by(desc(Schedule2.start)) \
            .first()

        # schedule exists
        if last_schedule:
            start = default_tz.localize(last_schedule.stop)

        # need to create a start for the 1st schedule
        else:

            now = local_tz.localize(normalize_to_midnight(datetime.utcnow()))

            now_utc = now.astimezone(default_tz)
            week_start_index = constants.DAYS_OF_WEEK.index(
                org.day_week_starts)
            adjust_days = ((6 - week_start_index + now.weekday()) %
                           constants.WEEK_LENGTH)

            start = normalize_to_midnight(
                (now_utc - timedelta(days=adjust_days, hours=23)
                 ).astimezone(local_tz)).astimezone(default_tz)

        stop = normalize_to_midnight((start + timedelta(days=constants.WEEK_LENGTH, hours=1)).astimezone(local_tz)) \
            .astimezone(default_tz)

        return start, stop
Пример #5
0
    def setUp(self):
        # (if you are copying and pasting, update class title below)
        super(BaseTimeOffRequest, self).setUp()

        today = normalize_to_midnight(datetime.utcnow())
        self.range_start = today.isoformat()
        self.range_stop = (today + timedelta(days=10)).isoformat()

        # create a time off request for testing against
        next_week_str = (today + timedelta(days=7)).strftime("%Y-%m-%d")
        self.time_off_request = self.worker.create_time_off_request(
            date=next_week_str)
Пример #6
0
    def setUp(self):
        # (if you are copying and pasting, update class title below)
        super(BaseShiftQuery, self).setUp()

        today = normalize_to_midnight(datetime.utcnow())
        self.query_start = (today + timedelta(days=2, hours=8)).isoformat()
        self.query_stop = (today + timedelta(days=2, hours=15)).isoformat()
        self.long_stop = (today + timedelta(days=3, hours=8)).isoformat()

        # the other assigned
        self.coworker = self.role.create_worker(
            email="*****@*****.**",
            min_hours_per_workweek=20,
            max_hours_per_workweek=40)
Пример #7
0
    def setUp(self):
        # (if you are copying and pasting, update class title below)
        super(BaseSchedule, self).setUp()

        today = normalize_to_midnight(datetime.utcnow())
        self.range_start = (today + timedelta(days=7)).isoformat()
        self.range_stop = (today + timedelta(days=14)).isoformat()

        self.demand = {
            "monday": [
                0, 0, 0, 0, 0, 0, 0, 0, 3, 6, 10, 13, 12, 12, 12, 12, 13, 11,
                8, 6, 4, 2, 0, 0
            ],
            "tuesday": [
                0, 0, 0, 0, 0, 0, 0, 0, 2, 6, 9, 9, 9, 10, 12, 13, 13, 11, 7,
                5, 4, 2, 0, 0
            ],
            "wednesday": [
                0, 0, 0, 0, 0, 0, 0, 0, 3, 6, 10, 10, 11, 12, 12, 12, 12, 11,
                9, 6, 5, 2, 0, 0
            ],
            "thursday": [
                0, 0, 0, 0, 0, 0, 0, 0, 3, 6, 7, 11, 11, 11, 9, 9, 10, 9, 5, 3,
                3, 2, 0, 0
            ],
            "friday": [
                0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 7, 9, 9, 10, 12, 12, 12, 8, 7, 5,
                3, 2, 0, 0
            ],
            "saturday": [
                0, 0, 0, 0, 0, 0, 0, 0, 2, 3, 7, 7, 7, 7, 7, 7, 6, 5, 4, 2, 2,
                1, 0, 0
            ],
            "sunday": [
                0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2,
                1, 0, 0
            ]
        }
Пример #8
0
    def is_within_caps(self, user_id):
        """
        does the addition of this shift exceed the user's hourly caps
        """

        role = Role.query.get(self.role_id)
        location = role.location
        org = location.organization
        role_to_user = RoleToUser.query.filter_by(role_id=self.role_id,
                                                  user_id=user_id,
                                                  archived=False).first()

        # archived users should not qualify
        if role_to_user is None:
            return False

        min_seconds_shift_gap = role.min_half_hours_between_shifts / constants.HALF_HOUR_TO_HOUR * constants.SECONDS_PER_HOUR
        max_seconds_workday = role.max_half_hours_per_workday / constants.HALF_HOUR_TO_HOUR * constants.SECONDS_PER_HOUR
        max_seconds_workweek = role_to_user.max_half_hours_per_workweek / constants.HALF_HOUR_TO_HOUR * constants.SECONDS_PER_HOUR

        default_tz = get_default_tz()
        local_tz = role.location.timezone_pytz

        # get schedule that exists during this shift
        schedule = self.get_schedule()

        # schedule may not exist - in which case must artificially determine search bounds and get shifts
        if schedule is None:
            local_start = default_tz.localize(self.start).astimezone(local_tz)

            # these values correspond to what the schedules date range would be
            start = normalize_to_midnight(
                org.get_week_start_from_datetime(local_start)).astimezone(
                    default_tz)
            stop = normalize_to_midnight(
                (start + timedelta(days=constants.WEEK_LENGTH, hours=1)
                 ).astimezone(local_tz)).astimezone(default_tz).replace(
                     tzinfo=None)

            start = start.replace(tzinfo=None)

        # create same variables for parity
        else:
            start = schedule.start
            stop = schedule.stop

        # now get full range for query - need to know beyond just start/stop for
        # measuring consecutive days off
        query_start = start - timedelta(days=role.max_consecutive_workdays)
        query_stop = stop + timedelta(days=role.max_consecutive_workdays)

        # need localized start/end of the week/schedule
        week_start_local = default_tz.localize(start).astimezone(local_tz)
        week_stop_local = default_tz.localize(stop).astimezone(local_tz)

        # need to get all shifts that occur during the schedule
        # but also need those that extend beyond
        shifts = Shift2.query \
            .filter(
                Shift2.role_id == self.role_id,
                Shift2.user_id == user_id,
                Shift2.start >= query_start,
                Shift2.start < query_stop,
                Shift2.id != self.id,
            ).all()

        # time to do some comparisons

        # add self to the shifts and then sort
        # it's important to exclude the shift from the query, and
        # then add it separately there are cases where it could be
        # double counted, which would be bad
        shifts.append(self)
        shifts.sort(key=lambda x: x.start)

        workday_totals = defaultdict(int)
        consecutive_days = 0

        previous_stop = None
        current_date = None
        next_date = None

        for shift in shifts:
            shift_start_local = default_tz.localize(
                shift.start).astimezone(local_tz)
            shift_stop_local = default_tz.localize(
                shift.stop).astimezone(local_tz)

            # increment consecutive days if needed
            if shift_start_local.day == next_date:
                consecutive_days += 1

            # reset it if not consecutive days AND it's not on the same day as already credited
            elif shift_start_local.day != current_date:
                consecutive_days = 0

            # need to collect more data for the shifts that occur during the workweek
            if (start <= shift.start < stop) or (start <= shift.stop <= stop):

                # shift starts and ends on the same day
                if shift_stop_local.day == shift_start_local.day:
                    workday_totals[shift_start_local.day] += int(
                        (shift.stop - shift.start).total_seconds())

                # shift splits between midnight
                else:
                    split = normalize_to_midnight(shift_stop_local)

                    # account for case where shift overlaps from or into another week
                    if week_start_local <= shift_start_local <= week_stop_local:
                        workday_totals[shift_start_local.day] += int(
                            (split - shift_start_local).total_seconds())

                    if week_start_local <= shift_stop_local <= week_stop_local:
                        workday_totals[shift_stop_local.day] += int(
                            (shift_stop_local - split).total_seconds())

            # check time between shifts
            if previous_stop:
                if int(
                    (shift.start -
                     previous_stop).total_seconds()) < min_seconds_shift_gap:
                    return False

            # consecutive days
            if consecutive_days > role.max_consecutive_workdays:
                return False

            previous_stop = shift.stop
            next_date = (shift_start_local + timedelta(days=1)).day
            current_date = shift_start_local.day

        # total hours per workday is too high
        if sum(workday_totals.values()) > max_seconds_workweek:
            return False

        # shift is too long for a given workday
        for key in workday_totals:
            if workday_totals[key] > max_seconds_workday:
                return False

        return True  # Does not defy caps :-)
Пример #9
0
    def get(self, org_id, location_id, role_id, user_id):
        """
        returns timeclock data for a specific user
        """

        parser = reqparse.RequestParser()
        parser.add_argument("active", type=inputs.boolean)
        parser.add_argument("start", type=str)
        parser.add_argument("end", type=str)
        parameters = parser.parse_args()

        # Filter out null values
        parameters = dict(
            (k, v) for k, v in parameters.iteritems() if v is not None)

        org = Organization.query.get_or_404(org_id)
        location = Location.query.get_or_404(location_id)

        default_tz = get_default_tz()
        local_tz = location.timezone_pytz

        timeclocks = Timeclock.query \
            .filter_by(role_id=role_id) \
            .filter_by(user_id=user_id)

        # if searching for active timeclocks, do not include start and end query ranges
        if "active" in parameters:
            timeclocks = timeclocks.filter_by(stop=None)

        # start and end query
        else:

            # check for end 1st
            if "end" in parameters:

                if "start" not in parameters:
                    return {
                        "message":
                        "A start parameter must be given with an end."
                    }, 400

                # ensure good iso formatting
                try:
                    end = iso8601.parse_date(parameters.get("end"))
                except iso8601.ParseError:
                    return {
                        "message":
                        "End time parameter time needs to be in ISO 8601 format"
                    }, 400
                else:
                    end = (end + end.utcoffset()).replace(tzinfo=default_tz)

                timeclocks = timeclocks.filter(Timeclock.start < end)

            # if a start is defined, it must be iso 8601
            if "start" in parameters:

                # make sure start is in right format, and also convert to full iso form
                try:
                    start = iso8601.parse_date(parameters.get("start"))
                except iso8601.ParseError:
                    return {
                        "message":
                        "Start time parameter needs to be in ISO 8601 format"
                    }, 400
                else:
                    start = (start +
                             start.utcoffset()).replace(tzinfo=default_tz)

            # otherwise determine when current week began
            else:
                now = local_tz.localize(datetime.datetime.utcnow())
                start = normalize_to_midnight(
                    org.get_week_start_from_datetime(now)).astimezone(
                        default_tz)

            # add start to query
            timeclocks = timeclocks.filter(Timeclock.start >= start)

        return {
            constants.API_ENVELOPE:
            map(lambda timeclock: marshal(timeclock, timeclock_fields),
                timeclocks.all())
        }
Пример #10
0
    def get(self, org_id, location_id):
        """
        returns all shift data that correlates to a location
        """

        parser = reqparse.RequestParser()
        parser.add_argument("active", type=inputs.boolean)
        parser.add_argument("start", type=str)
        parser.add_argument("end", type=str)
        parameters = parser.parse_args()

        # Filter out null values
        parameters = dict(
            (k, v) for k, v in parameters.iteritems() if v is not None)

        org = Organization.query.get_or_404(org_id)
        location = Location.query.get_or_404(location_id)
        default_tz = get_default_tz()
        local_tz = location.timezone_pytz

        shifts = Shift2.query.join(Role).join(Location).filter(
            Location.id == location_id)

        # if searching for active shifts, do not include start and end query ranges
        if "active" in parameters:
            if "start" in parameters or "end" in parameters:
                return {
                    "message": "Cannot have start or end with active parameter"
                }, 400

            now = datetime.datetime.utcnow()

            shifts = shifts.filter(Shift2.user_id != None, Shift2.start <= now,
                                   Shift2.stop > now)

        # start and end query
        else:

            # check for end 1st
            if "end" in parameters:

                if "start" not in parameters:
                    return {
                        "message":
                        "A start parameter must be given with an end."
                    }, 400

                # ensure good iso formatting
                try:
                    end = iso8601.parse_date(parameters.get("end"))
                except iso8601.ParseError:
                    return {
                        "message":
                        "End time parameter time needs to be in ISO 8601 format"
                    }, 400
                else:
                    end = (end + end.utcoffset()).replace(tzinfo=default_tz)

                shifts = shifts.filter(Shift2.start < end)

            # if a start is defined, it must be iso 8601
            if "start" in parameters:
                # make sure start is in right format, and also convert to full iso form
                try:
                    start = iso8601.parse_date(parameters.get("start"))
                except iso8601.ParseError:
                    return {
                        "message":
                        "Start time parameter needs to be in ISO 8601 format"
                    }, 400
                else:
                    start = (start +
                             start.utcoffset()).replace(tzinfo=default_tz)

            # otherwise determine when current week began
            else:
                now = local_tz.localize(datetime.datetime.utcnow())
                start = normalize_to_midnight(
                    org.get_week_start_from_datetime(now)).astimezone(
                        default_tz)

            # add start to query
            shifts = shifts.filter(Shift2.start >= start)

        return {
            API_ENVELOPE:
            map(lambda shift: marshal(shift, shift_fields), shifts.all())
        }
Пример #11
0
    def get(self, org_id, location_id):
        """
        returns all time off request data that correlates a location
        """

        parser = reqparse.RequestParser()
        parser.add_argument("start", type=str)
        parser.add_argument("end", type=str)
        parser.add_argument("state", type=str)
        parameters = parser.parse_args()

        # Filter out null values
        parameters = dict((k, v) for k, v in parameters.iteritems()
                          if v is not None)

        org = Organization.query.get_or_404(org_id)
        location = Location.query.get_or_404(location_id)
        default_tz = get_default_tz()
        local_tz = location.timezone_pytz

        # prepare query
        time_off_requests = TimeOffRequest.query.join(RoleToUser).join(
            Role).join(Location).filter(Location.id == location_id)

        # start and end query
        # check for end 1st
        if "end" in parameters:

            if "start" not in parameters:
                return {
                    "message": "A start parameter must be given with an end."
                }, 400

            # ensure good iso formatting
            try:
                end = iso8601.parse_date(parameters.get("end"))
            except iso8601.ParseError:
                return {
                    "message":
                    "End time parameter time needs to be in ISO 8601 format"
                }, 400
            else:
                end = (end + end.utcoffset()).replace(tzinfo=default_tz)

            time_off_requests = time_off_requests.filter(
                TimeOffRequest.start < end)

        # if a start is defined, it must be iso 8601
        if "start" in parameters:

            # make sure start is in right format, and also convert to full iso form
            try:
                start = iso8601.parse_date(parameters.get("start"))
            except iso8601.ParseError:
                return {
                    "message":
                    "Start time parameter needs to be in ISO 8601 format"
                }, 400
            else:
                start = (start + start.utcoffset()).replace(tzinfo=default_tz)

        # otherwise determine when current week began
        else:
            now = local_tz.localize(datetime.datetime.utcnow())
            start = normalize_to_midnight(
                org.get_week_start_from_datetime(now)).astimezone(default_tz)

        # add start to query
        time_off_requests = time_off_requests.filter(
            TimeOffRequest.start >= start)

        if "state" in parameters:
            state = parameters.get("state")

            if (state == NULL_TIME_OFF_REQUEST):
                state = None

            time_off_requests = time_off_requests.filter(
                TimeOffRequest.state == state)

        return {
            API_ENVELOPE:
            map(lambda time_off_request: marshal(time_off_request, time_off_request_fields),
                time_off_requests.all())
        }
Пример #12
0
def provision(form):
    # Make the user account

    user = User(
        email=form.email.data.lower().strip(),
        password=form.password.data,
        name=form.name.data.strip(),
        active=False,
        confirmed=False, )

    try:
        db.session.add(user)
        db.session.commit()
    except:
        db.session.rollback()
        raise Exception("Dirty session")
    user.flush_associated_shift_caches()

    # Send activation email
    token = user.generate_confirmation_token(trial=True)
    user.send_email("[Action Required] Activate Your Free Trial",
                    render_template(
                        "email/confirm-trial.html", user=user, token=token),
                    True)

    # Create an org
    organization = Organization(
        name=form.company_name.data,
        day_week_starts=form.day_week_starts.data.lower(),
        enterprise_access=form.enterprise_access.data == "yes",
        plan=form.plan.data,
        trial_days=current_app.config.get("FREE_TRIAL_DAYS"),
        active=True, )
    db.session.add(organization)
    db.session.commit()

    organization.admins.append(user)
    db.session.commit()

    # get timezone
    timezone_name = form.timezone.data.strip()

    if timezone_name not in pytz.all_timezones:
        timezone_name = current_app.config.get("DEFAULT_TIMEZONE")

    timezone = pytz.timezone(timezone_name)
    default_tz = pytz.timezone(current_app.config.get("DEFAULT_TIMEZONE"))

    # Add a location
    l = Location(
        name="Demo - Cafe",
        organization_id=organization.id,
        timezone=timezone_name)
    db.session.add(l)
    db.session.commit()

    # Add two roles
    r_barista = Role(name="Baristas", location_id=l.id)
    r_cashier = Role(name="Cashiers", location_id=l.id)
    db.session.add(r_barista)
    db.session.add(r_cashier)
    db.session.commit()

    barista_user_ids = []
    for email in BARISTAS:
        barista = User.query.filter_by(email=email.lower()).first()
        if barista is None:
            barista = User.create_and_invite(
                email,
                name="Demo User",
                silent=True,  # Silence on dev
            )
        barista_user_ids.append(barista.id)
        db.session.add(RoleToUser(user_id=barista.id, role_id=r_barista.id))
        db.session.commit()

    # Add the current user as a barista too
    db.session.add(RoleToUser(user_id=user.id, role_id=r_barista.id))
    db.session.commit()

    cashier_user_ids = []
    for email in CASHIERS:
        cashier = User.query.filter_by(email=email.lower()).first()
        if cashier is None:
            cashier = User.create_and_invite(
                email,
                name="Demo User",
                silent=True,  # Silence on dev
            )

        cashier_user_ids.append(cashier.id)

        # cashiers are full time, so they inherit max_hours_per_week at 40 instead of 29
        db.session.add(
            RoleToUser(
                user_id=cashier.id,
                role_id=r_cashier.id,
                max_half_hours_per_workweek=80))
        db.session.commit()

    # Load schedule data
    barista_demand = json.loads(
        '{"monday":[0,0,0,0,0,0,0,0,2,2,3,4,4,4,3,2,1,1,0,0,0,0,0,0],"tuesday":[0,0,0,0,0,0,0,0,2,3,4,4,4,4,3,2,1,1,0,0,0,0,0,0],"wednesday":[0,0,0,0,0,0,0,0,2,3,4,4,4,4,3,2,2,1,0,0,0,0,0,0],"thursday":[0,0,0,0,0,0,0,0,2,3,4,4,4,4,3,2,2,1,0,0,0,0,0,0],"friday":[0,0,0,0,0,0,0,0,2,2,3,4,4,4,3,2,1,1,0,0,0,0,0,0],"saturday":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"sunday":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}'
    )
    cashier_demand = json.loads(
        '{"monday":[0,0,0,0,0,0,0,0,1,1,1,2,2,2,2,1,1,1,0,0,0,0,0,0],"tuesday":[0,0,0,0,0,0,0,0,1,1,1,2,2,2,2,1,1,1,0,0,0,0,0,0],"wednesday":[0,0,0,0,0,0,0,0,1,1,2,2,2,2,2,2,1,1,0,0,0,0,0,0],"thursday":[0,0,0,0,0,0,0,0,1,1,2,2,2,2,2,2,1,1,0,0,0,0,0,0],"friday":[0,0,0,0,0,0,0,0,1,1,1,2,2,2,1,1,1,1,0,0,0,0,0,0],"saturday":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"sunday":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}'
    )

    # Have 4 schedules
    done_schedule_start_local = timezone.localize(
        normalize_to_midnight(datetime.utcnow() + timedelta(days=1)))
    done_schedule_start = done_schedule_start_local.astimezone(default_tz)

    # Increment until we get the correct starting day of week
    # (be cautious here due to infinite loop possibility)
    ttl = 7  # Prevent infinite loops
    while (done_schedule_start_local.strftime("%A").lower() !=
           organization.day_week_starts.lower() and ttl > 0):

        done_schedule_start_local = normalize_to_midnight(
            (done_schedule_start + timedelta(days=1, hours=1)
             ).astimezone(timezone))
        done_schedule_start = done_schedule_start_local.astimezone(default_tz)
        ttl -= 1

    if ttl == 0:
        raise Exception("Unable to match day of week")

    done_schedule_start = done_schedule_start_local.astimezone(default_tz)
    done_schedule_stop = normalize_to_midnight(
        (done_schedule_start + timedelta(days=7, hours=1)
         ).astimezone(timezone)).astimezone(default_tz)
    current_schedule_start = normalize_to_midnight(
        (done_schedule_start - timedelta(days=6, hours=23)
         ).astimezone(timezone)).astimezone(default_tz)
    past_schedule_start = normalize_to_midnight(
        (done_schedule_start - timedelta(days=13, hours=23)
         ).astimezone(timezone)).astimezone(default_tz)

    # Make the schedules

    # the previous week
    barista_schedule_past = Schedule2.create(
        role_id=r_barista.id,
        start=past_schedule_start,
        stop=current_schedule_start,
        state="published",
        demand=barista_demand)
    cashier_schedule_past = Schedule2.create(
        role_id=r_cashier.id,
        start=past_schedule_start,
        stop=current_schedule_start,
        state="published",
        demand=cashier_demand)

    # the current week
    barista_schedule_current = Schedule2.create(
        role_id=r_barista.id,
        start=current_schedule_start,
        stop=done_schedule_start,
        state="published",
        demand=barista_demand)
    cashier_schedule_current = Schedule2.create(
        role_id=r_cashier.id,
        start=current_schedule_start,
        stop=done_schedule_start,
        state="published",
        demand=cashier_demand)

    # next week (schedule is published)
    barista_schedule_done = Schedule2.create(
        role_id=r_barista.id,
        start=done_schedule_start,
        stop=done_schedule_stop,
        state="published",
        demand=barista_demand)
    cashier_schedule_done = Schedule2.create(
        role_id=r_cashier.id,
        start=done_schedule_start,
        stop=done_schedule_stop,
        state="published",
        demand=cashier_demand)

    day_mapping = make_day_mapping_dict(organization.day_week_starts)

    # along with each shift, create a timeclock record if the date is before this cutoff
    timeclock_cutoff = normalize_to_midnight(datetime.utcnow())

    # add barista shifts
    for i, set_of_shifts in enumerate(BARISTA_SHIFTS):
        for schedule in [
                barista_schedule_past, barista_schedule_current,
                barista_schedule_done
        ]:

            for shift_tuple in set_of_shifts:
                current_date = schedule.start + timedelta(
                    days=day_mapping[shift_tuple[0]])
                shift_start = current_date + timedelta(hours=shift_tuple[1])
                shift_stop = shift_start + timedelta(hours=shift_tuple[2])

                s = Shift2(
                    role_id=schedule.role_id,
                    user_id=barista_user_ids[i],
                    start=shift_start,
                    stop=shift_stop,
                    published=True)

                db.session.add(s)
                db.session.commit()

                # create a timeclock if appropriate
                if current_date < timeclock_cutoff:

                    # this will make 7% of the timeclocks needed be empty
                    if random.randint(1, 14) == 14:
                        continue

                    start_minutes = random.randrange(-3, 8)
                    start_seconds = random.randrange(-30, 31)

                    stop_minutes = random.randrange(-3, 15)
                    stop_seconds = random.randrange(-30, 31)

                    start = shift_start + timedelta(
                        minutes=start_minutes, seconds=start_seconds)
                    stop = shift_stop + timedelta(
                        minutes=stop_minutes, seconds=stop_seconds)

                    t = Timeclock(
                        role_id=schedule.role_id,
                        user_id=barista_user_ids[i],
                        start=start,
                        stop=stop)

                    db.session.add(t)
                    db.session.commit()

    # add cashier shifts
    for i, set_of_shifts in enumerate(CASHIER_SHIFTS):
        for schedule in [
                cashier_schedule_past, cashier_schedule_current,
                cashier_schedule_done
        ]:

            for shift_tuple in set_of_shifts:
                current_date = schedule.start + timedelta(
                    days=day_mapping[shift_tuple[0]])
                shift_start = current_date + timedelta(hours=shift_tuple[1])
                shift_stop = shift_start + timedelta(hours=shift_tuple[2])

                s = Shift2(
                    role_id=schedule.role_id,
                    user_id=cashier_user_ids[i],
                    start=shift_start,
                    stop=shift_stop,
                    published=True)

                db.session.add(s)
                db.session.commit()

                # create a timeclock if appropriate
                if current_date < timeclock_cutoff:

                    # this will make 7% of the timeclocks needed be empty
                    if random.randint(1, 14) == 14:
                        continue

                    start_minutes = random.randrange(-3, 8)
                    start_seconds = random.randrange(-30, 31)

                    stop_minutes = random.randrange(-3, 15)
                    stop_seconds = random.randrange(-30, 31)

                    start = shift_start + timedelta(
                        minutes=start_minutes, seconds=start_seconds)
                    stop = shift_stop + timedelta(
                        minutes=stop_minutes, seconds=stop_seconds)

                    t = Timeclock(
                        role_id=schedule.role_id,
                        user_id=cashier_user_ids[i],
                        start=start,
                        stop=stop)

                    db.session.add(t)
                    db.session.commit()

    # add shifts for our dear user
    for schedule in [
            barista_schedule_past, barista_schedule_current,
            barista_schedule_done
    ]:

        for shift_tuple in USER_BARISTA_SHIFTS:

            current_date = schedule.start + timedelta(
                days=day_mapping[shift_tuple[0]])
            shift_start = current_date + timedelta(hours=shift_tuple[1])
            shift_stop = shift_start + timedelta(hours=shift_tuple[2])

            s = Shift2(
                role_id=schedule.role_id,
                user_id=user.id,
                start=shift_start,
                stop=shift_stop,
                published=True)

            db.session.add(s)
            db.session.commit()

            # create a timeclock if appropriate
            if current_date < timeclock_cutoff:

                start_minutes = random.randrange(-3, 8)
                start_seconds = random.randrange(-30, 31)

                stop_minutes = random.randrange(-3, 15)
                stop_seconds = random.randrange(-30, 31)

                start = shift_start + timedelta(
                    minutes=start_minutes, seconds=start_seconds)
                stop = shift_stop + timedelta(
                    minutes=stop_minutes, seconds=stop_seconds)

                t = Timeclock(
                    role_id=schedule.role_id,
                    user_id=user.id,
                    start=start,
                    stop=stop)

                db.session.add(t)
                db.session.commit()

    # add unassigned shifts to barista
    for shift_tuple in UNASSIGNED_BARISTA_SHIFTS:
        shift_start = barista_schedule_done.start + timedelta(
            days=day_mapping[shift_tuple[0]], hours=shift_tuple[1])
        shift_stop = shift_start + timedelta(hours=shift_tuple[2])
        s = Shift2(
            role_id=barista_schedule_done.role_id,
            start=shift_start,
            stop=shift_stop,
            published=True)

        db.session.add(s)
        db.session.commit()

    current_app.logger.info(
        "Created a free trial for user %s (id %s) - org %s (id %s)" %
        (user.email, user.id, organization.name, organization.id))
    return
Пример #13
0
    def test_has_overlaps(self):
        # create some past timeclocks for testing
        # all tests are with user 1 unless noted otherwise

        # start from a week ago
        utcnow = datetime.utcnow()
        time_base = normalize_to_midnight(utcnow) - timedelta(days=7)

        # make some timeclocks - all occur on consecutive days
        t1_start = time_base + timedelta(hours=8)
        t1_stop = time_base + timedelta(hours=15)
        timeclock1 = Timeclock(role_id=1,
                               user_id=1,
                               start=t1_start,
                               stop=t1_stop)

        t2_start = time_base + timedelta(days=1, hours=4)
        t2_stop = time_base + timedelta(days=1, hours=11)
        timeclock2 = Timeclock(role_id=1,
                               user_id=1,
                               start=t2_start,
                               stop=t2_stop)

        t3_start = time_base + timedelta(days=2, hours=8)
        t3_stop = time_base + timedelta(days=2, hours=17)
        timeclock3 = Timeclock(role_id=1,
                               user_id=1,
                               start=t3_start,
                               stop=t3_stop)

        # and they just clocked in for a new shift
        timeclock_now = Timeclock(role_id=1, user_id=1, start=utcnow)

        db.session.add(timeclock1)
        db.session.add(timeclock2)
        db.session.add(timeclock3)
        db.session.add(timeclock_now)
        db.session.commit()

        # this timeclock doesn't overlap
        no_overlap_start = time_base + timedelta(days=4, hours=5)
        no_overlap_stop = time_base + timedelta(days=4, hours=12)
        no_overlap_timeclock = Timeclock(role_id=1,
                                         user_id=1,
                                         start=no_overlap_start,
                                         stop=no_overlap_stop)
        self.assertFalse(no_overlap_timeclock.has_overlaps())

        # timeclock starting during one will fail
        # starts during timeclock1 (day 0, 8 - 15)
        start_within_start = time_base + timedelta(hours=12)
        start_within_stop = time_base + timedelta(hours=18)
        tc_start_within = Timeclock(role_id=1,
                                    user_id=1,
                                    start=start_within_start,
                                    stop=start_within_stop)
        self.assertTrue(tc_start_within.has_overlaps())

        # timeclock ending during one will fail
        # ends during timeclock2 (day 1, 4 - 11)
        ends_within_start = time_base + timedelta(days=1, hours=2)
        ends_within_stop = time_base + timedelta(days=1, hours=10)
        tc_ends_within = Timeclock(role_id=1,
                                   user_id=1,
                                   start=ends_within_start,
                                   stop=ends_within_stop)
        self.assertTrue(tc_ends_within.has_overlaps())

        # timeclock within another will fail
        # surrounds timeclock3, (day2, 8 - 17)
        surrounds_start = time_base + timedelta(days=2, hours=6)
        surrounds_stop = time_base + timedelta(days=2, hours=19)
        tc_surrounds = Timeclock(role_id=1,
                                 user_id=1,
                                 start=surrounds_start,
                                 stop=surrounds_stop)
        self.assertTrue(tc_surrounds.has_overlaps())

        # creating a timeclock around an active one will fail
        active_start = utcnow - timedelta(hours=4)
        active_stop = utcnow + timedelta(hours=4)
        tc_during_active = Timeclock(role_id=1,
                                     user_id=1,
                                     start=active_start,
                                     stop=active_stop)
        self.assertTrue(tc_during_active.has_overlaps())
    def get(self, org_id, location_id):
        """
        returns nested timeclock and shift data for the dates occurring
        between a specified start and end day
        """

        parser = reqparse.RequestParser()
        parser.add_argument("startDate", type=str, required=True)
        parser.add_argument("endDate", type=str, required=True)
        parser.add_argument("csv_export", type=inputs.boolean, default=False)
        parameters = parser.parse_args()

        data = {}
        summary = {}
        iso_date = "%Y-%m-%d"
        default_tz = get_default_tz()

        organization = Organization.query.get(org_id)
        location = Location.query.get(location_id)
        roles = Role.query.filter_by(location_id=location_id).all()

        # get start and end values for the query + ensure good iso formatting
        try:
            start_local = iso8601.parse_date(parameters.get("startDate"))
        except iso8601.ParseError:
            return {
                "message":
                "Start time parameter needs to be in ISO 8601 format"
            }, 400

        try:
            end_local = iso8601.parse_date(parameters.get("endDate"))
        except iso8601.ParseError:
            return {
                "message":
                "End time parameter time needs to be in ISO 8601 format"
            }, 400

        # pytz can't can't have iso8601 utc timezone object (needs naive)
        start_local = normalize_to_midnight(start_local).replace(tzinfo=None)
        end_local = normalize_to_midnight(end_local).replace(tzinfo=None)

        # adjust naive/local times for a utc query
        location_timezone = location.timezone_pytz
        start_utc = location_timezone.localize(start_local).astimezone(
            default_tz)

        # NOTE - the query needs to include the whole last day,
        # so add 1 day ahead and make it a <
        end_utc = normalize_to_midnight(
            (location_timezone.localize(end_local).astimezone(default_tz) +
             datetime.timedelta(days=1, hours=1)
             ).astimezone(location_timezone)).astimezone(default_tz)

        shift_query = Shift2.query \
            .filter(
                Shift2.start >= start_utc,
                Shift2.start < end_utc,
            )

        timeclock_query = Timeclock.query\
            .filter(
                Timeclock.start >= start_utc,
                Timeclock.start < end_utc,
                Timeclock.stop != None
            )

        time_off_request_query = TimeOffRequest.query\
            .filter(
                TimeOffRequest.start >= start_utc,
                TimeOffRequest.start < end_utc,
                TimeOffRequest.state.in_(["approved_paid", "approved_unpaid", "sick"]),
            )

        # determine if csv export
        if parameters.get("csv_export"):

            current_app.logger.info(
                "Generating a timeclock csv export for organization %s location %s"
                % (organization.id, location.id))
            g.current_user.track_event("timeclock_csv_export")

            csv_rows = [CSV_HEADER % (location.timezone, location.timezone)]
            download_name = "attendance-%s-%s-%s.csv" % (
                location.name, parameters.get("startDate"),
                parameters.get("endDate"))

            combined_list = []

            for role in roles:
                for role_to_user in role.members:
                    user_timeclocks = timeclock_query.filter_by(
                        role_id=role.id).filter_by(
                            user_id=role_to_user.user_id).order_by(
                                Timeclock.start.asc()).all()

                    user_time_off_requests = time_off_request_query.filter_by(
                        role_to_user_id=role_to_user.id).order_by(
                            TimeOffRequest.start.asc()).all()

                    combined_list += user_timeclocks + user_time_off_requests

            combined_list.sort(key=lambda x: x.start)

            for record in combined_list:
                start_utc = record.start.isoformat()
                start_local = default_tz.localize(
                    record.start).astimezone(location_timezone).isoformat()
                stop_utc = record.stop.isoformat()
                stop_local = default_tz.localize(
                    record.stop).astimezone(location_timezone).isoformat()

                # record will be either a time off request or a timeclock
                if isinstance(record, Timeclock):
                    rtu = RoleToUser.query.filter_by(
                        role_id=record.role_id,
                        user_id=record.user_id).first()
                    user = User.query.get(record.user_id)
                    role = Role.query.get(record.role_id)
                    minutes = int(
                        (record.stop - record.start).total_seconds() / 60)
                    record_type = "Recorded Time"
                    record_state = ""

                # time off requests
                else:
                    rtu = RoleToUser.query.get(record.role_to_user_id)
                    user = rtu.user
                    role = rtu.role
                    minutes = record.minutes_paid
                    record_type = "Time Off"
                    record_state = record.state.replace("_", " ").title()

                csv_rows.append(
                    ",".join(['"%s"'] * len(CSV_HEADER.split(","))) %
                    (user.name if user.name is not None else "",
                     rtu.internal_id
                     or "", user.email, organization.name, location.name,
                     role.name, start_utc, stop_utc, start_local, stop_local,
                     record_type, record_state, minutes))

            response = make_response("\n".join(csv_rows))
            response.headers[
                "Content-Disposition"] = "attachment; filename=%s" % download_name

            return response

        # create a dict with keys for each day of the week needed
        delta = end_local - start_local
        for x in xrange(delta.days + 1):
            data[(start_local +
                  datetime.timedelta(days=x)).strftime(iso_date)] = {}

        # all data is nested underneath each role
        for role in roles:

            # Timeclocks and Time Off Requests
            # timeclock and time off request data is nested underneath
            # each user
            for user in role.members:

                role_user_index = str(role.id) + "-" + str(user.user_id)

                # Timeclocks
                user_timeclocks = timeclock_query.filter_by(
                    role_id=role.id).filter_by(user_id=user.user_id).all()

                # sort each timeclock into correct day bucket
                for timeclock in user_timeclocks:

                    # get localized time for placing in the proper bucket
                    localized_dt = default_tz.localize(
                        timeclock.start).astimezone(location_timezone)
                    local_date = localized_dt.strftime(iso_date)
                    elapsed_time = int(
                        (timeclock.stop - timeclock.start).total_seconds())

                    # add timeclock to user object for the right day
                    if role_user_index in data[local_date]:
                        data[local_date][role_user_index]["timeclocks"].append(
                            marshal(timeclock, timeclock_fields))
                        data[local_date][role_user_index][
                            "logged_time"] += elapsed_time

                    # if user has no records on day, create one
                    else:
                        data[local_date][role_user_index] = {
                            "user_id": user.user_id,
                            "role_id": role.id,
                            "timeclocks":
                            [marshal(timeclock, timeclock_fields)],
                            "time_off_requests": None,
                            "shifts": [],
                            "logged_time": elapsed_time,
                        }

                    if role_user_index in summary:
                        summary[role_user_index]["logged_time"] += elapsed_time
                        summary[role_user_index]["timeclock_count"] += 1
                    else:
                        summary[role_user_index] = {
                            "user_id": user.user_id,
                            "role_id": role.id,
                            "logged_time": elapsed_time,
                            "scheduled_time": 0,
                            "shift_count": 0,
                            "timeclock_count": 1,
                            "time_off_request_count": 0,
                        }

                # Time Off Requests
                # user.id is the role_to_user id
                user_time_off_requests = time_off_request_query.filter_by(
                    role_to_user_id=user.id).all()

                for time_off_request in user_time_off_requests:

                    # get localized time for placing in the proper bucket
                    localized_dt = default_tz.localize(
                        time_off_request.start).astimezone(location_timezone)
                    local_date = localized_dt.strftime(iso_date)

                    # convert minutes_paid to seconds
                    recorded_time = time_off_request.minutes_paid * 60

                    # add time_off_request to user object for the right day
                    if role_user_index in data[local_date]:
                        data[local_date][role_user_index][
                            "time_off_requests"] = marshal(
                                time_off_request, time_off_request_fields)
                        data[local_date][role_user_index][
                            "logged_time"] += recorded_time

                    # if user has no records on day, create one
                    else:
                        data[local_date][role_user_index] = {
                            "user_id":
                            user.user_id,
                            "role_id":
                            role.id,
                            "shifts": [],
                            "timeclocks": [],
                            "time_off_requests":
                            marshal(time_off_request, time_off_request_fields),
                            "logged_time":
                            recorded_time,
                        }

                    if role_user_index in summary:
                        summary[role_user_index][
                            "logged_time"] += recorded_time
                        summary[role_user_index]["time_off_request_count"] += 1
                    else:
                        summary[role_user_index] = {
                            "user_id": user.user_id,
                            "role_id": role.id,
                            "logged_time": recorded_time,
                            "scheduled_time": 0,
                            "shift_count": 0,
                            "timeclock_count": 0,
                            "time_off_request_count": 1,
                        }

            # shifts
            shifts = shift_query \
                .filter(
                    Shift2.role_id == role.id,
                    Shift2.user_id > 0,
                ).all()

            # segment out each shift
            for shift in shifts:
                role_user_index = str(shift.role_id) + "-" + str(shift.user_id)

                # get localized time for placing in the proper bucket
                localized_dt = default_tz.localize(
                    shift.start).astimezone(location_timezone)
                local_date = localized_dt.strftime(iso_date)

                # add shift to user object for the right day
                if role_user_index in data[local_date]:
                    data[local_date][role_user_index]["shifts"].append(
                        marshal(shift, shift_fields))

                # if user has no records on day, create one
                else:
                    data[local_date][role_user_index] = {
                        "user_id": shift.user_id,
                        "role_id": role.id,
                        "timeclocks": [],
                        "time_off_requests": None,
                        "shifts": [marshal(shift, shift_fields)],
                    }

                if role_user_index in summary:
                    summary[role_user_index]["shift_count"] += 1
                    summary[role_user_index]["scheduled_time"] += int(
                        (shift.stop - shift.start).total_seconds())
                else:
                    summary[role_user_index] = {
                        "user_id":
                        shift.user_id,
                        "role_id":
                        role.id,
                        "logged_time":
                        0,
                        "scheduled_time":
                        int((shift.stop - shift.start).total_seconds()),
                        "shift_count":
                        1,
                        "timeclock_count":
                        0,
                        "time_off_request_count":
                        0,
                    }

        # remove user index to flatten
        for key in data:
            data[key] = data[key].values()

        return {API_ENVELOPE: data, "summary": summary.values()}
Пример #15
0
    def post(self, org_id, location_id, role_id, user_id):
        """
        create a new time off request record
        """

        parser = reqparse.RequestParser()
        parser.add_argument("date", type=str, required=True)
        parser.add_argument("state", type=str)
        parser.add_argument("minutes_paid", type=int, default=0)

        parameters = parser.parse_args()

        # Filter out null values
        parameters = dict((k, v) for k, v in parameters.iteritems()
                          if v is not None)

        org = Organization.query.get(org_id)
        location = Location.query.get(location_id)
        rtu = RoleToUser.query.filter_by(
            role_id=role_id, user_id=user_id, archived=False).first_or_404()
        user = User.query.get(user_id)

        user_name = user.email if user.name is None else user.name

        admin_permissions = g.current_user.is_org_admin_or_location_manager(
            org_id, location_id)
        state = parameters.get("state")

        # verify a valid state
        if state not in [
                None, "approved_paid", "approved_unpaid", "sick", "denied"
        ]:
            return {"message": "Invalid time off request state"}, 400

        # non-admins cannot define a state
        if state is not None and not (admin_permissions or
                                      g.current_user.is_sudo()):
            return {
                "message":
                "Only admins can set a state of 'approved_paid', 'approved_unpaid', 'sick', or 'denied'."
            }, 400

        # extract start and stop dates
        default_tz = get_default_tz()
        local_tz = location.timezone_pytz

        try:
            start = iso8601.parse_date(parameters.get("date"))
        except iso8601.ParseError:
            return {"message": "date needs to be in ISO 8601 format"}, 400
        else:
            # apply any offset (there shouldn't be) and then treat as local time
            start = (start + start.utcoffset()).replace(tzinfo=None)

        start_local = local_tz.localize(start)
        start_utc = start_local.astimezone(default_tz)

        # we are using iso8601 to parse the date, but will be restricting it to only the date
        if not check_datetime_is_midnight(start_local):
            return {
                "message": "date must be at exactly midnight in local time"
            }, 400

        # calculate stop
        stop_local = normalize_to_midnight((start_utc + datetime.timedelta(
            days=1, hours=1)).astimezone(local_tz))
        stop_utc = stop_local.astimezone(default_tz)

        duration_seconds = int((stop_utc - start_utc).total_seconds())

        start_utc = start_utc.replace(tzinfo=None)
        stop_utc = stop_utc.replace(tzinfo=None)

        # to consider daylight savings time, check that the duration between start and stop
        # is between 23 and 25 hours in length
        if not (23 * SECONDS_PER_HOUR <= duration_seconds <= 25 *
                SECONDS_PER_HOUR):
            abort(500)

        # finally check on minutes paid
        # these rules also prevent a non-admin from setting minutes_paid
        minutes_paid = parameters.get("minutes_paid")
        if not (0 <= minutes_paid * 60 <= duration_seconds):
            return {
                "message":
                "Cannot set minutes_paid to be greater than the calendar day"
            }, 400

        if minutes_paid > 0 and state == "approved_unpaid":
            return {
                "message":
                "unpaid time off requests cannot have a minutes_paid greater than 0"
            }, 400

        if minutes_paid == 0 and state == "approved_paid":
            return {
                "message":
                "paid time off requests must have a postitive minutes_paid"
            }, 400

        if state is None and minutes_paid != 0:
            return {
                "message":
                "cannot have minutes_paid greater than 0 for time off requests with an undefined state"
            }, 400

        time_off_request = TimeOffRequest(
            role_to_user_id=rtu.id,
            start=start_utc,
            stop=stop_utc,
            state=state,
            minutes_paid=minutes_paid)

        # managers can create pre-approved time off requests
        if time_off_request.state is not None:
            if admin_permissions:
                time_off_request.approver_user_id = g.current_user.id

        # time off requests cannot overlap
        if time_off_request.has_overlaps():
            return {
                "message":
                "This time off request overlaps with another time off request"
            }, 400

        db.session.add(time_off_request)

        try:
            db.session.commit()
        except:
            abort(500)

        if time_off_request.state in [
                "sick", "approved_paid", "approved_unpaid"
        ]:
            time_off_request.unassign_overlapping_shifts()

        # send an email to managers if a worker is the one making the request
        if not (admin_permissions or g.current_user.is_sudo()):
            display_date = start_local.strftime("%A, %B %-d")

            # subject
            subject = "[Action Required] Time off request for %s on %s" % (
                user_name, display_date)

            # calculate start of current week
            week_start_date = org.get_week_start_from_datetime(
                start_local).strftime("%Y-%m-%d")

            # email body
            message = "%s has requested the day off on %s. Please log in to the Manager to approve or deny it:" % (
                user_name, display_date)

            # construct the url
            manager_url = "%s#locations/%s/scheduling/%s" % (url_for(
                'manager.manager_app', org_id=org_id,
                _external=True), location_id, week_start_date)

            # send it
            location.send_manager_email(subject, message, manager_url)

        g.current_user.track_event("created_time_off_request")
        return marshal(time_off_request, time_off_request_fields), 201