예제 #1
0
    def delete(self, org_id, location_id, user_id):
        """removes the user from the management position"""

        organization = Organization.query.get_or_404(org_id)
        location = Location.query.get_or_404(location_id)
        user = User.query.get_or_404(user_id)

        if not user.is_location_manager(location_id):
            return {"message": "User does not exist or is not a manager"}, 404

        location.managers.remove(user)

        try:
            db.session.commit()
        except Exception as exception:
            db.session.rollback()
            current_app.logger.exception(str(exception))
            abort(400)

        alert_email(
            user, "You have been removed as a %s manager at %s" %
            (location.name, organization.name),
            "You have been removed as a manager at the %s location of %s" %
            (location.name, organization.name))

        return {}, 204
예제 #2
0
    def _deactivate_expired_organizations(self):
        orgs_to_deactivate = Organization.query\
            .filter_by(active=True)\
            .filter(
                and_(
                    or_(
                        Organization.paid_until == None,
                        Organization.paid_until < func.now()
                    ),
                    func.timestampdiff(
                        sqltext("SECOND"),
                        Organization.created_at,
                        func.now(),
                    ) > (Organization.trial_days * constants.SECONDS_PER_DAY),
                )
            )

        for org in orgs_to_deactivate:
            manager_url = url_for('manager.manager_app',
                                  org_id=org.id,
                                  _external=True) + "#settings"

            # alert admins of deactivation
            for admin in org.admins:
                alert_email(
                    admin,
                    "[Action Required] %s scheduling is on hold" % org.name,
                    "In order to continue scheduling, please set up billing at:<br><a href='%s'>%s</a>"
                    % (manager_url, manager_url))

            org.active = False
            current_app.logger.info(
                "Deactivated org %s because it is unpaid and the trial is over"
                % org.id)
            db.session.commit()
예제 #3
0
    def post(self, org_id, location_id):
        """ Add a user as a manager either by user id or email """
        parser = reqparse.RequestParser()
        parser.add_argument("email", type=str)
        parser.add_argument("id", type=int)
        parser.add_argument("name", type=str)
        parameters = parser.parse_args(strict=True)

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

        if parameters.get("id") is not None:
            user = User.query.get_or_404(parameters.get("id"))
        elif parameters.get("email") is not None:
            email = parameters.get("email")
            if "@" not in email:
                abort(400)

            # Check if user has email
            user = User.query.filter_by(email=email.lower()).first()
            # otherwise invite by email
            if user is None:
                user = User.create_and_invite(email, parameters.get("name"),
                                              organization.name)
        else:
            return {"message": "unable to identify user"}, 400

        if user.is_location_manager(location_id):
            return {"message": "user already an admin"}, 400

        location.managers.append(user)

        try:
            db.session.commit()
        except Exception as exception:
            db.session.rollback()
            current_app.logger.exception(str(exception))
            abort(400)

        alert_email(
            user, "You have been added as a %s manager at %s" %
            (location.name, organization.name),
            "You have been added as a manager at the %s location of %s." %
            (location.name, organization.name))

        return marshal(user, user_fields), 201
예제 #4
0
    def _close_abandoned_timeclocks(self):
        """Close timeclocks if they have been open for more than 23 hours """

        cutoff_start = datetime.utcnow() - timedelta(
            hours=constants.MAX_TIMECLOCK_HOURS)

        timeclocks_to_close = Timeclock.query\
            .filter(
                Timeclock.stop==None,
                cutoff_start > Timeclock.start,
            )\
            .all()

        for timeclock in timeclocks_to_close:
            timeclock.stop = timeclock.start + timedelta(
                hours=constants.MAX_TIMECLOCK_HOURS)
            db.session.commit()
            current_app.logger.info(
                "Closed abandoned timeclock id %s (user %s)" %
                (timeclock.id, timeclock.user_id))

            user = User.query.get(timeclock.user_id)
            alert_email(
                user, "You have been automatically clocked out",
                "You have been clocked in for over %s hours, so you have been automatically clocked out."
                % constants.MAX_TIMECLOCK_HOURS)

            if user.phone_number:
                user.send_sms(
                    "You have been automatically clocked out by Staffjoy after being clocked in for %s hours"
                    % constants.MAX_TIMECLOCK_HOURS)
            role = Role.query.get(timeclock.role_id)
            location = Location.query.get(role.location_id)
            org = Organization.query.get(location.organization_id)

            location.send_manager_email(
                "[Action Required] %s forgot to clock out" % user.name,
                "%s (%s) forgot to clock out of their last shift, so Staffjoy just automatically clocked them out after %s hours. Please review and adjust their timeclock on the %s attendance page."
                % (user.name, user.email, constants.MAX_TIMECLOCK_HOURS,
                   location.name),
                url_for("manager.manager_app", org_id=org.id, _external=True) +
                "#locations/%s/attendance" % location.id)
예제 #5
0
    def delete(self, org_id, user_id):
        organization = Organization.query.get_or_404(org_id)
        user = User.query.get_or_404(user_id)

        if not user.is_org_admin(org_id):
            return {"message": "user does not exist or not an admin"}, 404

        organization.admins.remove(user)
        try:
            db.session.commit()
        except Exception as exception:
            db.session.rollback()
            current_app.logger.exception(str(exception))
            abort(400)

        # Force send because this could have security implications
        alert_email(
            user,
            "You have been removed as an administator of %s on Staffjoy" %
            organization.name,
            "You have been removed as an organization administrator of the Staffjoy account for %s."
            % organization.name,
            force_send=True)
        return {}, 204
예제 #6
0
    def post(self, org_id, location_id, role_id):
        """ Add a user as a worker either by user id or email """

        role = Role.query.get_or_404(role_id)
        org = Organization.query.get_or_404(org_id)

        parser = reqparse.RequestParser()
        parser.add_argument("email", type=str)
        parser.add_argument("id", type=int)
        parser.add_argument("min_hours_per_workweek", type=int, required=True)
        parser.add_argument("max_hours_per_workweek", type=int, required=True)
        parser.add_argument("name", type=str)
        parser.add_argument("internal_id", type=str)
        parser.add_argument("working_hours", type=str)
        parameters = parser.parse_args(strict=True)

        if parameters.get("id") is not None:
            user = User.query.get_or_404(parameters.get("id"))
        elif parameters.get("email") is not None:
            email = parameters.get("email")

            # Check if valid email
            if "@" not in email:
                abort(400)

            # Check if user has email
            user = User.query.filter_by(email=email.lower().strip()).first()

            # otherwise invite by email
            if user is None:
                user = User.create_and_invite(email, parameters.get("name"),
                                              org.name)
        else:
            return {"message": "unable to identify user"}, 400

        # get min/max workweek values
        min_half_hours_per_workweek = parameters.get(
            "min_hours_per_workweek") * 2
        max_half_hours_per_workweek = parameters.get(
            "max_hours_per_workweek") * 2

        if min_half_hours_per_workweek > max_half_hours_per_workweek:
            return {
                "message":
                "min_hours_per_workweek cannot be greater than max_hours_per_workweek"
            }, 400

        if not (0 <= min_half_hours_per_workweek <= 336):
            return {
                "message": "min_hours_per_workweek cannot be less than 0"
            }, 400

        if not (0 <= max_half_hours_per_workweek <= 336):
            return {
                "message": "max_hours_per_workweek cannot be greater than 168"
            }, 400

        # check if the user already is in the role
        membership = RoleToUser.query.filter_by(role_id=role_id).filter_by(
            user_id=user.id).first()

        if membership:
            if membership.archived == False:
                return {"message": "user already in role"}, 400

            membership.archived = False

        else:
            membership = RoleToUser()
            membership.user = user
            membership.role = role

        membership.min_half_hours_per_workweek = min_half_hours_per_workweek
        membership.max_half_hours_per_workweek = max_half_hours_per_workweek

        # internal_id in post? I'll allow it
        if parameters.get("internal_id") is not None:
            membership.internal_id = parameters["internal_id"]

        if parameters.get("working_hours") is not None:
            try:
                working_hours = json.loads(parameters.get("working_hours"))
            except:
                return {
                    "message": "Unable to parse working hours json body"
                }, 400
            if working_hours is None:
                return {
                    "message": "Unable to parse working hours json body"
                }, 400
            if not verify_days_of_week_struct(working_hours, True):
                return {
                    "message": "working hours is improperly formatted"
                }, 400

            membership.working_hours = json.dumps(working_hours)

        try:
            db.session.commit()
        except Exception as exception:
            db.session.rollback()
            current_app.logger.exception(str(exception))
            abort(400)

        location = Location.query.get(location_id)
        organization = Organization.query.get(org_id)

        alert_email(
            user, "You have been added to %s on Staffjoy" % organization.name,
            "%s is using Staffjoy to manage its workforce, and you have been added to the team <b>%s</b> at the <b>%s</b> location."
            % (
                organization.name,
                role.name,
                location.name,
            ))

        data = {}
        data.update(marshal(user, user_fields))
        data.update(marshal(membership, role_to_user_fields))
        g.current_user.track_event("added_role_member")

        return data, 201
예제 #7
0
    def delete(self, org_id, location_id, role_id, user_id):
        user = User.query.get_or_404(user_id)
        role = Role.query.get_or_404(role_id)

        assoc = RoleToUser.query.filter_by(user_id=user.id,
                                           role_id=role.id).first()
        if assoc is None:
            abort(404)

        if assoc.archived:
            abort(400)

        assoc.archived = True

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

        location = Location.query.get(location_id)
        organization = Organization.query.get(org_id)

        # Set future shifts to unassigned
        # Be careful to not unassign them from other orgs!
        future_shifts = Shift2.query.filter(
            Shift2.user_id == user.id,
            Shift2.role_id == role_id,
            Shift2.start > datetime.datetime.utcnow(),
        ).all()

        for shift in future_shifts:
            shift.user_id = None

            # clear cache too
            schedule = Schedule2.query \
                .filter(
                    Schedule2.role_id == role_id,
                    Schedule2.start <= shift.start,
                    Schedule2.stop > shift.start,
                ).first()

            if schedule is not None:
                Shifts2Cache.delete(schedule.id)

        # deny future time off requests that are open
        future_time_off_requests = TimeOffRequest.query \
            .filter_by(role_to_user_id=assoc.id) \
            .filter_by(state=None) \
            .filter(
                TimeOffRequest.start > datetime.datetime.utcnow(),
            ) \
            .all()

        for time_off_request in future_time_off_requests:
            time_off_request.state = "denied"

        # unassign all recurring shifts
        recurring_shifts = RecurringShift.query \
            .filter_by(
                role_id=role_id,
                user_id=user_id
            ) \
            .all()

        for recurring_shift in recurring_shifts:
            current_app.logger.info(
                "Setting recurring shift %s to unassigned because user %s is being removed from role %s"
                % (recurring_shift.id, user_id, role_id))
            recurring_shift.user_id = None

        # close open timeclocks
        timeclocks = Timeclock.query \
            .filter_by(
                role_id=role_id,
                user_id=user_id,
                stop=None
            ) \
            .all()

        for timeclock in timeclocks:
            original_start = timeclock.start
            original_stop = timeclock.stop

            timeclock.stop = datetime.datetime.utcnow()
            current_app.logger.info(
                "Closing timeclock %s because user %s is being removed from role %s"
                % (timeclock.id, user_id, role_id))

            alert_timeclock_change(timeclock, org_id, location_id, role_id,
                                   original_start, original_stop, user,
                                   g.current_user)

        alert_email(
            user,
            "You have been removed from a team at %s" % organization.name,
            "You have been removed from the team <b>%s</b> at the <b>%s</b> location of <b>%s</b>. This may happen as the scheduling manager changes your role or location."
            % (role.name, location.name, organization.name),
            force_send=True)
        g.current_user.track_event("deleted_role_member")
        return {}, 204
예제 #8
0
    def patch(self, org_id, location_id, role_id, user_id,
              time_off_request_id):
        """
        modifies an existing time_off_request record
        NOTE that start and stop cannot be modified
        """

        parser = reqparse.RequestParser()
        parser.add_argument("state", type=str)
        parser.add_argument("minutes_paid", type=int)

        parameters = parser.parse_args()

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

        time_off_request = TimeOffRequest.query.get_or_404(time_off_request_id)
        role_to_user = RoleToUser.query.get(time_off_request.role_to_user_id)
        user = User.query.get(user_id)
        location = Location.query.get(location_id)
        org = Organization.query.get(org_id)

        # verify for state
        state = parameters.get("state", time_off_request.state)

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

        if "state" in parameters:
            # state can be set to None - which get parsed through as an empty string
            if not parameters["state"]:
                changes["state"] = None
                changes["approver_user_id"] = None
            else:
                changes["state"] = parameters["state"]

                # log the approver if its an administrator
                if g.current_user.is_org_admin_or_location_manager(
                        org_id, location_id):
                    changes["approver_user_id"] = g.current_user.id
                else:
                    changes["approver_user_id"] = None

        # verification for minutes_paid
        minutes_paid = parameters.get("minutes_paid",
                                      time_off_request.minutes_paid)
        duration = int(
            (time_off_request.stop - time_off_request.start).total_seconds())

        if not (0 <= minutes_paid * 60 <= duration):
            return {
                "message":
                "minutes_paid must be within the duration of the 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

        if "minutes_paid" in parameters:
            changes["minutes_paid"] = parameters["minutes_paid"]

        for change, value in changes.iteritems():
            try:
                setattr(time_off_request, change, value)
                db.session.commit()
            except Exception as exception:
                db.session.rollback()
                current_app.logger.exception(str(exception))
                abort(400)

        g.current_user.track_event("time_off_request_modified")

        # unassign shifts that overlap
        if changes.get("state") is not None and time_off_request.state in [
                "sick", "approved_paid", "approved_unpaid"
        ]:
            time_off_request.unassign_overlapping_shifts()

        # send an email to the user whenever the approved state changes
        # only send an email for unarchived workers and
        # the time off request is in the future
        if changes.get("state") is not None \
            and not role_to_user.archived \
            and time_off_request.start > datetime.datetime.utcnow():

            default_tz = get_default_tz()
            local_tz = location.timezone_pytz

            start_local = default_tz.localize(
                time_off_request.start).astimezone(local_tz)
            display_date = start_local.strftime("%A, %B %-d")

            # calculate myschedules url
            week_start_date = org.get_week_start_from_datetime(
                start_local).strftime("%Y-%m-%d")

            myschedules_url = "%s#week/%s" % (url_for(
                'myschedules.myschedules_app',
                org_id=org_id,
                location_id=location_id,
                role_id=role_id,
                user_id=user_id,
                _external=True), week_start_date)

            # prepare subject and starting body - each state can be fine tuned
            if time_off_request.state == "denied":
                subject = "Your time off request on %s has been denied" % display_date
                body = "Your time off request on %s has been denied" % display_date

            elif time_off_request.state == "approved_paid":
                subject = "Your time off request on %s has been approved" % display_date
                body = "You have been approved for paid time off on %s" % display_date

            elif time_off_request.state == "approved_unpaid":
                subject = "Your time off request on %s has been approved" % display_date
                body = "You have been approved for unpaid time off on %s" % display_date

            elif time_off_request.state == "sick":
                subject = "Your time off request on %s has been approved" % display_date
                body = "You have been approved to take a sick day on %s" % display_date

            # add in approval info if it is avilable
            if time_off_request.approver_user_id:
                approval_user = User.query.get(
                    time_off_request.approver_user_id)
                approval_name = approval_user.email if approval_user.name is None else approval_user.email
                body += " by %s." % approval_name
            else:
                body += "."

            body += " Visit My Schedules to learn more:<br><a href=\"%s\">%s</a>" % (
                myschedules_url, myschedules_url)

            alert_email(user, subject, body)

        return changes