示例#1
0
class Blob(Model):
    """
  Model for storing large file content.

  Files are stored on-disk, named after their uuid. Repository is located in
  instance folder/data/files.
  """
    __tablename__ = "blob"
    query_class = BlobQuery

    id = Column(Integer(), primary_key=True, autoincrement=True)
    uuid = Column(UUID(), unique=True, nullable=False, default=uuid.uuid4)
    meta = Column(JSONDict(), nullable=False, default=dict)

    def __init__(self, value=None, *args, **kwargs):
        super(Blob, self).__init__(*args, **kwargs)
        if self.uuid is None:
            self.uuid = uuid.uuid4()

        if self.meta is None:
            self.meta = dict()

        if value is not None:
            self.value = value

    @property
    def file(self):
        """
    Return :class:`pathlib.Path` object used for storing value
    """
        from abilian.services.repository import session_repository as repository
        return repository.get(self, self.uuid)

    @property
    def size(self):
        """
    Return size in bytes of value
    """
        f = self.file
        return f.stat().st_size if f is not None else 0

    @property
    def value(self):
        """
    Binary value content
    """
        v = self.file
        return v.open('rb').read() if v is not None else v

    @value.setter
    def value(self, value, encoding='utf-8'):
        """
    Store binary content to applications's repository and update
    `self.meta['md5']`.

    :param:content: string, bytes, or any object with a `read()` method
    :param:encoding: encoding to use when content is unicode
    """
        from abilian.services.repository import session_repository as repository
        repository.set(self, self.uuid, value)
        self.meta['md5'] = unicode(hashlib.md5(self.value).hexdigest())

        if hasattr(value, 'filename'):
            filename = getattr(value, 'filename')
            if isinstance(filename, bytes):
                filename = filename.decode('utf-8')
            self.meta['filename'] = filename

        if hasattr(value, 'content_type'):
            self.meta['mimetype'] = getattr(value, 'content_type')

    @value.deleter
    def value(self):
        """
    Remove value from repository
    """
        from abilian.services.repository import session_repository as repository
        repository.delete(self, self.uuid)

    @property
    def md5(self):
        """
    Return md5 from meta, or compute it if absent
    """
        md5 = self.meta.get('md5')
        if md5 is None:
            md5 = unicode(hashlib.md5(self.value).hexdigest())

        return md5

    def __nonzero__(self):
        """
    A blob is considered null if it has no file
    """
        return self.file is not None and self.file.exists()
示例#2
0
class Blob(Model):
    """Model for storing large file content.

    Files are stored on-disk, named after their uuid. Repository is
    located in instance folder/data/files.
    """

    __tablename__ = "blob"

    id = Column(Integer(), primary_key=True, autoincrement=True)
    uuid = Column(UUID(), unique=True, nullable=False, default=uuid.uuid4)
    meta = Column(JSONDict(), nullable=False, default=dict)

    def __init__(self, value=None, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if self.uuid is None:
            self.uuid = uuid.uuid4()

        if self.meta is None:
            self.meta = {}

        if value is not None:
            self.value = value

    @property
    def file(self) -> Optional[Path]:
        """Return :class:`pathlib.Path` object used for storing value."""
        from abilian.services.repository import session_repository as repository

        # pyre-fixme[7]: Expected `Optional[Path]` but got `Optional[Union[Path,
        #  object]]`.
        # pyre-fixme[6]: Expected `UUID` for 2nd param but got `Column`.
        return repository.get(self, self.uuid)

    @property
    def size(self) -> int:
        """Return size in bytes of value."""
        f = self.file
        return f.stat().st_size if f is not None else 0

    @property
    def value(self) -> bytes:
        """Binary value content."""
        v = self.file
        # pyre-fixme[7]: Expected `bytes` but got `Optional[AnyStr]`.
        return v.open("rb").read() if v is not None else v

    @value.setter
    def value(self, value: bytes):
        """Store binary content to applications's repository and update
        `self.meta['md5']`.

        :param:content: bytes, or any object with a `read()` method
        :param:encoding: encoding to use when content is Unicode
        """
        from abilian.services.repository import session_repository as repository

        repository.set(self, self.uuid, value)
        self.meta["md5"] = str(hashlib.md5(self.value).hexdigest())

        if hasattr(value, "filename"):
            filename = value.filename
            if isinstance(filename, bytes):
                filename = filename.decode("utf-8")
            self.meta["filename"] = filename

        if hasattr(value, "content_type"):
            self.meta["mimetype"] = value.content_type

    @value.deleter
    def value(self) -> None:
        """Remove value from repository."""
        from abilian.services.repository import session_repository as repository

        # pyre-fixme[6]: Expected `UUID` for 2nd param but got `Column`.
        repository.delete(self, self.uuid)

    @property
    def md5(self) -> str:
        """Return md5 from meta, or compute it if absent."""
        md5 = self.meta.get("md5")
        if md5 is None:
            md5 = str(hashlib.md5(self.value).hexdigest())

        return md5

    def __bool__(self) -> bool:
        """A blob is considered falsy if it has no file."""
        return self.file is not None and self.file.exists()

    # Py3k compat
    __nonzero__ = __bool__
示例#3
0
class DummyModel2(Entity):
    list_attr = Column(JSONList())
    dict_attr = Column(JSONDict())
    uuid = Column(UUID())

    query: Query
示例#4
0
class Config(Entity):
    __tablename__ = "config"
    __indexable__ = False

    data = Column(JSONDict(), default=dict)
示例#5
0
class OrgUnit(Entity):
    __tablename__ = "orgunit"
    __indexable__ = False
    query_class = OrgUnitQuery

    type = Column(Enum(*TYPE_ENUM, name="orgunit_type"), nullable=False)

    dn = Column(String, unique=True, index=True)
    nom = Column(Unicode, nullable=False, unique=True)
    sigle = Column(Unicode, default="", nullable=False)

    parent_id = Column(Integer, ForeignKey("orgunit.id"))
    parent = relationship(
        "OrgUnit",
        primaryjoin=remote(Entity.id) == foreign(parent_id),
        backref=backref("children",
                        lazy="joined",
                        cascade="all, delete-orphan"),
    )

    wf_settings = Column(JSONDict(), default=dict)

    permettre_reponse_directe = Column(Boolean)
    permettre_soummission_directe = Column(Boolean)

    def __init__(self, **kw):
        super().__init__(**kw)
        self.wf_settings = {}

    def __unicode__(self):
        return f"<OrgUnit type='{self.type}' nom='{self.nom}' id={self.id}>"

    __str__ = __unicode__

    # def __repr__(self):
    #     return unicode(self).encode('utf8')

    @property
    def sigle_ou_nom(self) -> str:
        return self.sigle or self.nom

    @property
    def depth(self) -> int:
        if self.type == EQUIPE:
            return 4
        if self.type == DEPARTEMENT:
            return 3
        if self.type == LABORATOIRE:
            return 2
        if self.type == UFR:
            return 1
        if self.type == POLE_DE_RECHERCHE:
            return 0
        # Should not happen
        return 0

    @property
    def path(self) -> list[str]:
        t = [""] * 5
        for p in self.parents + [self]:
            t[p.depth] = p.nom
        return t

    @property
    def parents(self) -> list[OrgUnit]:
        p = self
        result = []
        while True:
            p = p.parent
            if not p:
                break
            result.append(p)
        result.reverse()
        return result

    def descendants(self) -> list[OrgUnit]:
        if self.type == EQUIPE:
            return []
        if self.type == DEPARTEMENT:
            return list(self.children)

        children = self.children
        result = list(children)
        for c in children:
            result += c.descendants()
        return result

    def get_contacts_dgrtt(self):
        from .mapping_dgrtt import MappingDgrtt

        return MappingDgrtt.query.get_for_ou(self)

    def get_members_with_role(self,
                              role_type: RoleType | None) -> list[Profile]:
        roles = roles_service.get_roles(role_type=role_type, target=self)
        result = [r.profile for r in roles if r.profile]
        return result

    def get_directeurs(self) -> list[Profile]:
        from .roles import RoleType

        result = self.get_members_with_role(RoleType.DIRECTION)
        assert all(p.has_role("directeur") for p in result)

        # On met le "vrai" directeur en premier
        result = [p for p in result if p.is_directeur
                  ] + [p for p in result if not p.is_directeur]

        return result

    def get_gestionnaires(self) -> list[Profile]:
        from .roles import RoleType

        result = self.get_members_with_role(RoleType.GDL)
        assert all(p.has_role("gestionnaire") for p in result)
        return result

    def get_administrateurs(self) -> list[Profile]:
        from .roles import RoleType

        result = self.get_members_with_role(RoleType.ALL)
        assert all(p.has_role("all") for p in result)
        return result

    @property
    def direction(self) -> list[Profile]:
        return self.get_directeurs()

    @property
    def gestionnaires(self) -> list[Profile]:
        return self.get_gestionnaires()

    @property
    def administrateurs(self):
        return self.get_administrateurs()

    def set_roles(self, users: list[Profile], role_type: RoleType) -> None:
        roles = roles_service.get_roles(role_type=role_type, target=self)
        for role in roles:
            db.session.delete(role)
        db.session.flush()
        for user in users:
            roles_service.grant_role(user, role_type, self)

    @property
    def directeur(self) -> Profile | None:

        direction = self.get_directeurs()
        direction = [d for d in direction if d.is_directeur]
        return toolz.get(0, direction, None)

    @property
    def adresse(self):
        if self.directeur and self.directeur.adresse:
            return self.directeur.adresse
        return ""

    def validate(self) -> None:
        if self.type == POLE_DE_RECHERCHE:
            assert self.parent is None

        elif self.type == UFR:
            assert self.parent.type == POLE_DE_RECHERCHE

        elif self.type == LABORATOIRE:
            assert self.parent.type in (POLE_DE_RECHERCHE, UFR)

        elif self.type == DEPARTEMENT:
            assert self.parent.type == LABORATOIRE

        elif self.type == EQUIPE:
            assert self.parent.type in (LABORATOIRE, DEPARTEMENT)

        elif self.type == BUREAU_DGRTT:
            assert self.parent is None

        else:
            raise AssertionError()

    def get_labo(self) -> OrgUnit | None:
        if self.type == LABORATOIRE:
            return self
        if not self.parent:
            return None
        if self.parent.type == LABORATOIRE:
            return self.parent
        if self.parent.parent.type == LABORATOIRE:
            return self.parent.parent
        raise AssertionError("Should not happen")

    @property
    def laboratoire(self) -> OrgUnit | None:
        try:
            return self.get_labo()
        except Exception:
            return None

    @property
    def equipe(self) -> OrgUnit | None:
        if self.type == EQUIPE:
            return self
        return None

    @property
    def departement(self) -> OrgUnit | None:
        if self.type == DEPARTEMENT:
            return self
        if self.type == EQUIPE and self.parent.type == DEPARTEMENT:
            return self.parent
        return None

    @property
    def ufr(self) -> OrgUnit | None:
        if self.type == POLE_DE_RECHERCHE:
            return None
        if self.type == UFR:
            return self

        assert self.laboratoire
        labo: OrgUnit = self.laboratoire
        parent = labo.parent
        if parent.type == UFR:
            return parent
        return None

    @property
    def pole(self) -> OrgUnit | None:
        if self.type == POLE_DE_RECHERCHE:
            return self
        if self.type == UFR:
            return self.parent

        assert self.laboratoire
        labo: OrgUnit = self.laboratoire
        parent: OrgUnit = labo.parent
        if parent.type == POLE_DE_RECHERCHE:
            return parent
        return parent.parent

    def get_membres(self) -> list[Profile]:
        if self.type not in [LABORATOIRE, EQUIPE, DEPARTEMENT]:
            return []

        labo = self.get_labo()
        if labo:
            membres_du_labo = {m for m in labo.membres if m.active}
        else:
            membres_du_labo = set()

        if self.type == LABORATOIRE:
            membres = list(membres_du_labo)

        elif self.type == EQUIPE:
            membres = [m for m in membres_du_labo if m.sous_structure == self]

        else:
            # self.type == DEPARTEMENT
            membres_dict = {
                m
                for m in membres_du_labo if m.sous_structure == self
            }
            for equipe in self.children:
                membres_dict.update(equipe.get_membres())
            membres = list(membres_dict)

        def sorter(profile: Profile) -> tuple[str, str]:
            return profile.nom, profile.prenom

        return sorted(membres, key=sorter)

    def wf_must_validate(self, type: str) -> bool:
        assert type in [LABORATOIRE, DEPARTEMENT, EQUIPE]

        if self.type == LABORATOIRE:
            return True

        wf_settings = self.wf_settings
        if type == LABORATOIRE:
            return wf_settings.get("validation_labo", True)
        elif type == DEPARTEMENT:
            return wf_settings.get("validation_dept", True)
        else:
            # type == EQUIPE
            return wf_settings.get("validation_equipe", True)
示例#6
0
class Demande(IdMixin, TimestampedMixin, Indexable, db.Model):
    __tablename__ = "demande"
    query_class = DemandeQuery

    type = Column(Enum(*TYPE_ENUM, name="demande_type"), nullable=False, index=True)
    nom = Column(Unicode, default="", nullable=False, info=SEARCHABLE)
    name = Column(Unicode, default="", nullable=False, info=SEARCHABLE)

    no_infolab = Column(
        Unicode, default="", server_default="", nullable=False, info=SEARCHABLE
    )
    no_eotp = Column(
        Unicode, default="", server_default="", nullable=False, info=SEARCHABLE
    )

    data = Column(JSONDict(), default=dict)
    past_versions = Column(JSONList(), default=list)
    form_state = Column(JSONDict(), default=dict)
    attachments = Column(JSONDict(), default=dict)
    feuille_cout = Column(JSONDict(), default=dict)

    #: Date de validation par la hiérarchie
    date_effective = Column(Date, nullable=True)

    #: Seules les demandes actives apparaissent dans le workflow.
    #: Les autres sont considérées comme archivées.
    active = Column(Boolean, default=True, nullable=False)
    editable = Column(Boolean, default=True, nullable=False)

    # Les acteurs de la demande:

    #: id du porteur de la demande
    porteur_id = Column(Integer, ForeignKey(Profile.id), index=True)
    #: Le porteur de la demande
    porteur = relationship(Profile, foreign_keys=[porteur_id])

    #: id du gestionnaire de la demande (GDL)
    gestionnaire_id = Column(Integer, ForeignKey(Profile.id), index=True)
    #: Le gestionnaire de la demande
    gestionnaire = relationship(Profile, foreign_keys=[gestionnaire_id])

    #: id du gestionnaire de la demande (GDL)
    contact_dgrtt_id = Column(Integer, ForeignKey(Profile.id), index=True)
    #: Le gestionnaire de la demande
    contact_dgrtt = relationship(Profile, foreign_keys=[contact_dgrtt_id])

    structure_id = Column(Integer, ForeignKey(OrgUnit.id), index=True)
    structure = relationship(OrgUnit, foreign_keys=[structure_id])

    # Variables liées au workflow
    wf_state = Column(WF_ENUM, default=EN_EDITION.id, nullable=False, index=True)

    wf_date_derniere_action = Column(DateTime, nullable=False)

    #: nombre de jours de retard (0 si pas de retard)
    wf_retard = Column(Integer, nullable=False, default=0)

    wf_history = Column(JSONList(), default=list)

    wf_data = Column(JSONDict(), default=dict)

    wf_stage_id = Column(Integer, ForeignKey(OrgUnit.id), index=True, nullable=True)
    wf_stage = relationship(
        OrgUnit, primaryjoin=remote(Entity.id) == foreign(wf_stage_id)
    )

    #: id de la personne responsable de la tâche en cours
    wf_current_owner_id = Column(
        Integer, ForeignKey(Profile.id), index=True, nullable=True
    )
    #: la personne responsable de la tâche en cours
    wf_current_owner = relationship(
        Profile, primaryjoin=remote(Entity.id) == foreign(wf_current_owner_id)
    )

    __mapper_args__ = {"polymorphic_identity": "", "polymorphic_on": type}

    def __init__(self, **kw):
        super().__init__(**kw)

        assert self.porteur or self.gestionnaire
        if self.porteur:
            self.structure = self.porteur.structure

        self.data = {}
        self.attachments = {}
        self.form_state = {"fields": []}
        self.versions = []
        self.wf_state = EN_EDITION.id
        self.wf_history = []
        self.wf_data = {}
        if not self.created_at:
            self.created_at = datetime.utcnow()
        self.wf_date_derniere_action = self.created_at

    def __repr__(self) -> str:
        return f"<{self.__class__.__name__} with id={self.id}>"

    def log_creation(self, actor: Profile) -> None:
        if not self.wf_history:
            message = f"Demande créée par l'utilisateur {actor.full_name}"
            log_entry = {
                "date": datetime.now().strftime("%d %b %Y %H:%M:%S"),
                "actor_id": actor.id,
                "message": message,
                "note": "",
            }
            self.wf_history = [log_entry]

    def clone(self) -> Demande:
        nouvelle_demande = Demande(
            nom=self.nom,
            type=self.type,
            wf_state=EN_EDITION.id,
            porteur=self.porteur,
            gestionnaire=self.gestionnaire,
        )
        nouvelle_demande.data = self.data.copy()
        nouvelle_demande.form_state = self.form_state.copy()
        return nouvelle_demande

    @property
    def date_debut(self):
        return None

    @property
    def age(self):
        if not self.date_effective:
            return 0
        dt = date.today() - self.date_effective
        return int(dt.days)

    @property
    def retard(self):
        if not self.wf_date_derniere_action or not self.active:
            return 0

        # TODO: implémenter la logique de jours ouvrés
        dt = datetime.utcnow() - self.wf_date_derniere_action
        return int(dt.days)

    def update_retard(self):
        # TODO: implémenter la logique de jours ouvrés
        dt = datetime.utcnow() - self.wf_date_derniere_action
        self.wf_retard = int(dt.days)

    def nom_par_defaut(self) -> str:
        return self.type + " sans nom"

    #
    # Accessors / properties
    #
    def __getattr__(self, name: str) -> Any:
        if name.startswith("_"):
            raise AttributeError(f"object has no attribute '{name}'")

        data = getattr(self, "data", None)
        if data and name in data:
            return data[name]

        raise AttributeError(f"object has no attribute '{name}'")

    def has_same_data(self, data: dict[str, Any]) -> bool:
        """Return True if the current version is the same as the given data."""
        current_data = self.data

        for k in set(current_data.keys()) | set(data.keys()):
            if k in ["csrf_token"]:
                continue
            if k.startswith("html-"):
                continue
            current_value = current_data.get(k)
            new_value = data.get(k)
            if current_value != new_value:
                return False
        return True

    @property
    def contact(self) -> Profile:
        return self.gestionnaire or self.porteur

    @property
    def directeur_name(self) -> str:
        if self.laboratoire and self.laboratoire.directeur:
            return self.laboratoire.directeur.full_name
        return ""

    @property
    def laboratoire(self) -> OrgUnit:
        structure = self.structure
        if not structure:
            if self.porteur:
                self.structure = self.porteur.structure
            elif self.gestionnaire:
                self.structure = self.gestionnaire.laboratoire
            structure = self.structure

        return structure.laboratoire

    @property
    def owners(self) -> Collection[Profile]:
        owners = []
        if self.gestionnaire:
            owners.append(self.gestionnaire)
        if self.porteur:
            owners.append(self.porteur)
        return owners

    @property
    def contributeurs(self) -> Collection[Profile]:
        return []

    def is_editable_by(self, user: Profile):
        # FIXME
        return True
        # return self.editable and user in [self.gestionnaire, self.porteur]

    def feuille_cout_is_editable_by(self, user: Profile) -> bool:
        return self.active and user in [
            self.gestionnaire,
            self.porteur,
            self.contact_dgrtt,
        ]

    #
    # Workflow
    #
    def get_workflow(self, user: Profile | None = None) -> LabsterWorkflow:
        return LabsterWorkflow(self, user)

    def get_state(self, user: Profile | None = None) -> State:
        workflow = self.get_workflow(user)
        return workflow.current_state()

    def current_owners(self) -> list[Profile]:
        return self.get_workflow().current_owners()

    @property
    def date_soumission(self) -> date | None:
        for entry in self.wf_history:
            if entry.get("transition") == "SOUMETTRE":
                return dateutil.parser.parse(entry["date"]).date()

        return None

    @property
    def date_finalisation(self) -> date | None:
        final_states = ["CONFIRMER_FINALISATION_DGRTT", "ABANDONNER", "REJETER_DGRTT"]
        for entry in self.wf_history:
            if entry.get("transition") in final_states:
                return dateutil.parser.parse(entry["date"]).date()

        return None

    #
    # Data validation / manipulation
    #
    def validate(self) -> Validation:
        return Validation(self, self.get_errors(), self.get_extra_errors())

    def get_errors(self) -> list[Any]:
        errors = []
        form_state = self.form_state
        fields = form_state["fields"]
        for field_name, field_value in self.data.items():
            if field_name not in fields:
                continue
            field = fields[field_name]

            visible = field.get("visible")
            required = field.get("required")

            if visible and required and not field_value:
                errors.append(field["name"])
                continue

        return errors

    def get_extra_errors(self):
        return []

    def is_valid(self) -> bool:
        validation = self.validate()
        return validation.ok

    @property
    def errors(self):
        return self.validate().errors

    def update_data(self, data: dict) -> None:
        self.increase_version()
        self.data.update(data)

        self.update_nom()
        self.post_update()

    # new_name = data.get('nom', None)
    # if new_name is not None and new_name != self.name:
    #     self.name = new_name

    def post_update(self) -> None:
        new_porteur_uid = self.data.get("porteur", None)

        if new_porteur_uid is not None:
            try:
                new_porteur = Profile.query.get_by_uid(new_porteur_uid)
                self.porteur = new_porteur
                self.structure = new_porteur.structure
            except Exception:
                # TODO: better solution to deal with tests.
                pass

    def increase_version(self) -> None:
        if self.data:
            self.past_versions.append(
                (self.data, datetime.utcnow().strftime("%d %b %Y %H:%M:%S"))
            )

    # Pièces jointes
    @property
    def pieces_jointes(self) -> list[dict]:
        result = []
        for k, v in sorted(self.attachments.items()):
            if isinstance(v, dict):
                creator = Profile.query.get_by_uid(v["creator"])
                d = {"id": v["id"], "name": k, "creator": creator}

                date_str = v.get("date")
                if date_str:
                    d["date"] = iso8601.parse_date(date_str)
                else:
                    d["date"] = None
            else:
                d = {"id": v, "name": k, "creator": None, "date": None}
            id = d["id"]
            blob = Blob.query.get(id)
            if blob:
                result.append(d)

        return result