class ActivityVersion(EventBaseModel, Period): backref_base_name = "activity_revisions" activity_id = db.Column( db.Integer, db.ForeignKey("activity.id"), index=True, nullable=False ) activity = db.relationship("Activity", backref=backref("revisions")) version = db.Column(db.Integer, nullable=False) context = db.Column(JSONB(none_as_null=True), nullable=True) @property def type(self): return self.activity.type __table_args__ = ( db.UniqueConstraint( "version", "activity_id", name="unique_version_among_same_activity_versions", ), db.Constraint( name="activity_version_start_time_before_reception_time" ), db.Constraint(name="activity_version_end_time_before_reception_time"), db.Constraint(name="activity_version_start_time_before_end_time"), ) def __repr__(self): return f"<Revision [{self.id}] of {self.activity}>"
class Expenditure(UserEventBaseModel, Dismissable): backref_base_name = "expenditures" mission_id = db.Column(db.Integer, db.ForeignKey("mission.id"), index=True, nullable=False) mission = db.relationship("Mission", backref=backref("expenditures")) type = enum_column(ExpenditureType, nullable=False) __table_args__ = ( db.Constraint("no_duplicate_expenditures_per_user_and_mission"), ) def __repr__(self): return f"<Expenditure [{self.id}] : {self.type.value}>"
class OAuth2Token(BaseModel, TokenMixin): __tablename__ = "oauth2_token" client_id = db.Column( db.Integer, db.ForeignKey("oauth2_client.id"), index=True, nullable=False, ) token = db.Column(db.String(255), unique=True, nullable=False) user_id = db.Column(db.Integer, db.ForeignKey("user.id"), index=True, nullable=False) user = db.relationship("User", backref="oauth_tokens") revoked_at = db.Column(DateTimeStoredAsUTC) __table_args__ = (db.Constraint( name="only_one_active_token_per_user_and_client"), ) def get_client_id(self): return self.client_id def get_scope(self): return "" def get_expires_in(self): return 86400 def expires_at(self): return time.time() + 86400 @property def revoked(self): return self.revoked_at is not None
class Activity(UserEventBaseModel, Dismissable, Period): backref_base_name = "activities" mission_id = db.Column(db.Integer, db.ForeignKey("mission.id"), index=True, nullable=False) mission = db.relationship("Mission", backref=backref("activities")) type = enum_column(ActivityType, nullable=False) last_update_time = db.Column(DateTimeStoredAsUTC, nullable=False) editable_fields = {"start_time", "end_time"} __table_args__ = ( db.Constraint(name="no_overlapping_acknowledged_activities"), db.Constraint(name="activity_start_time_before_end_time"), db.Constraint(name="activity_start_time_before_update_time"), db.Constraint(name="activity_end_time_before_update_time"), db.Constraint(name="no_successive_activities_with_same_type"), ) # TODO : add (maybe) # - validator # - version (each version represents a set of changes to the day activities) # OR revises (indicates which prior activity the current one revises) def __repr__(self): return f"<Activity [{self.id}] : {self.type.value}>" def latest_version_number(self): return (max([r.version for r in self.revisions]) if self.revisions else None) def version_at(self, at_time): if self.reception_time > at_time: return None if self.dismissed_at and self.dismissed_at <= at_time: return None return max( [r for r in self.revisions if r.reception_time <= at_time], key=lambda r: r.version, ) def revise( self, revision_time, revision_context=None, bypass_check=False, **updated_props, ): from app.domain.log_activities import handle_activities_update if self.is_dismissed: raise ResourceAlreadyDismissedError("Activity already dismissed") if not set(updated_props.keys()) <= Activity.editable_fields: raise ValueError("Bad arguments to revise method") new = { field: updated_props.get(field, getattr(self, field)) for field in Activity.editable_fields } old = { field: getattr(self, field) for field in Activity.editable_fields } if new == old: app.logger.warning( "No changes detected for the activity", extra={"to_secondary_slack_channel": True}, ) return None with handle_activities_update( submitter=current_user, user=self.user, mission=self.mission, reception_time=revision_time, start_time=new["start_time"], end_time=new["end_time"], bypass_check=bypass_check, ): revision = ActivityVersion( activity=self, reception_time=revision_time, start_time=new["start_time"], end_time=new["end_time"], context=revision_context, version=(self.latest_version_number() or 0) + 1, submitter=current_user, ) db.session.add(revision) for field, value in updated_props.items(): setattr(self, field, value) self.last_update_time = revision_time db.session.add(self) return revision def dismiss(self, dismiss_time=None, context=None): from app.domain.log_activities import handle_activities_update if not dismiss_time: dismiss_time = datetime.now() with handle_activities_update( submitter=current_user, user=self.user, mission=self.mission, reception_time=dismiss_time, start_time=self.start_time, end_time=None, bypass_check=True, reopen_mission_if_needed=False, ): super().dismiss(dismiss_time, context) self.last_update_time = self.dismissed_at
class Employment(UserEventBaseModel, Dismissable): backref_base_name = "employments" is_primary = db.Column(db.Boolean, nullable=True) validation_time = db.Column(DateTimeStoredAsUTC, nullable=True) validation_status = enum_column(EmploymentRequestValidationStatus, nullable=False) start_date = db.Column(db.Date, nullable=False) end_date = db.Column(db.Date, nullable=True) company_id = db.Column(db.Integer, db.ForeignKey("company.id"), index=True, nullable=False) company = db.relationship("Company", backref="employments") has_admin_rights = db.Column(db.Boolean, nullable=True) user_id = db.Column(db.Integer, db.ForeignKey("user.id"), index=True, nullable=True) email = db.Column(db.String(255), nullable=True) invite_token = db.Column(db.String(255), nullable=True, unique=True) __table_args__ = ( db.Constraint(name="only_one_current_primary_enrollment_per_user"), db.Constraint(name="no_simultaneous_enrollments_for_the_same_company"), db.Constraint(name="no_undefined_employment_type_for_user"), ) def __repr__(self): return f"<Employment [{self.id}] of User {self.user_id} in Company {self.company_id}>" @property def is_not_rejected(self): return (self.validation_status != EmploymentRequestValidationStatus.REJECTED) @property def is_acknowledged(self): return (self.validation_status == EmploymentRequestValidationStatus.APPROVED and not self.is_dismissed) def bind(self, user): self.user_id = user.id for email in self.invite_emails: email.user_id = user.id def validate_by(self, user, time=None, reject=False): if not self.user_id == user.id: raise AuthorizationError( "Actor is not authorized to review the employment") if (not self.validation_status == EmploymentRequestValidationStatus.PENDING or self.is_dismissed): raise InvalidResourceError( f"Employment is already {'validated' if self.is_acknowledged else 'dismissed' if self.is_dismissed else 'rejected'}" ) self.validation_status = (EmploymentRequestValidationStatus.APPROVED if not reject else EmploymentRequestValidationStatus.REJECTED) self.validation_time = time if time else datetime.now()