Exemple #1
0
class TaxOffice(Actor):
    class Meta:
        proxy = True

    # data subfields
    tax_office_name = SubfieldWrapper("uname")
    code = ValueSubfield("data", int)
    account_number = ValueSubfield("data", int)
Exemple #2
0
class File(Model):
    class Meta:
        proxy = True

    uname = DerivedUnameSubfield(default=lambda self: "{}/{}".format(self.blob.id, self.name))
    # data subfields
    name = ValueSubfield("data", str, null=False, create_only=True, normalize=normalize_file_name)

    # relational subfields
    blob = ForeignKeySubfield("data", Type.Blob)

    # computed subfields
    file_hash = ValueSubfield("computed", str, null=False)
    ext = ValueSubfield("computed", str, null=False)
    size = ValueSubfield("computed", int, null=False)

    def __init__(self, *args, **kwargs):
        file = kwargs.pop("file", None)
        super().__init__(*args, **kwargs)
        self._file = None
        if file:
            self.file = file

    def compute_file_hash(self):
        return self.blob.file_hash

    def compute_ext(self):
        return self.blob.ext

    def compute_size(self):
        return self.blob.size

    # property
    @property
    def file(self):
        return self.blob.file

    @file.setter
    def file(self, v):
        assert isinstance(v, DjangoFile)
        if not self.name:
            self.name = v.name

        file_hash = compute_file_hash(v)
        try:
            blob = Blob.objects.get(uname=file_hash)
        except Blob.DoesNotExist:
            blob = Blob.objects.create(uname=file_hash, file=v)
        self.blob = blob

    @cached_property
    def file_path(self):
        return self.blob.file_path

    @property
    def file_abs_path(self):
        return os.path.join(settings.MEDIA_ROOT, self.file_path)
Exemple #3
0
class Blob(Model):
    class Meta:
        proxy = True

    # data subfields
    uname = UnameSubfield(null=False, create_only=True, alias="file_hash")
    name = ValueSubfield("data", str, null=False, create_only=True, normalize=normalize_file_name)
    ext = ValueSubfield("data", str, null=False, create_only=True, normalize=normalize_ext)
    size = ValueSubfield("data", int, null=False, create_only=True, check=lambda v: v >= 1)

    def __init__(self, *args, **kwargs):
        file = kwargs.pop("file", None)
        super().__init__(*args, **kwargs)
        self._file = None
        if file:
            self.file = file

    def onchange_uname(self, old, new):
        assert old is None
        assert new is not None

    # property
    @property
    def file(self):
        assert self.uname
        if self._file:
            try:
                self._file.open()
            except ValueError:
                self._file = None
        if not self._file:
            self._file = default_storage.open(self.file_path)
        return self._file

    @file.setter
    def file(self, v):
        assert self._file is None
        assert isinstance(v, DjangoFile)
        self._file = v
        assert self.uname, "Blob 세팅 시에는 반드시 file_hash(uname) 을 먼저 세팅해 주어야 합니다."
        self.name = name = v.name
        self.ext = name.split(".")[-1] if "." in name else ""
        self.size = v.size

    @cached_property
    def file_path(self):
        file_hash = self.uname
        assert file_hash
        return "{}/{}/{}".format(file_hash[0:2], file_hash[2:4], file_hash)

    def on_syncdb_insert(self):
        # 단위 테스트에서는 DB 가 리셋되기 때문에 같은 hash 의 파일이 계속 write 시도될 수가 있음. 이를 회피
        if settings.IS_UNIT_TEST and default_storage.exists(self.file_path):
            return
        # DB 에 insert 가 끝난 후 File 을 생성. 에러 발생 시 DB 는 롤백이 되므로
        default_storage.save(self.file_path, self.file)
Exemple #4
0
class HumanIdentifier(Model):
    class Meta:
        proxy = True

    # data subfield
    encrypted_password = ValueSubfield("data", str)
    password_expire_date = ValueSubfield("data", datetime)

    # relational subfields
    container = ForeignKeySubfield("data", Type.Human, alias="human")

    def prepare(self):
        password = None
        return password

    def authenticate(self, password):
        if not self.check_password(password):
            raise PasswordNotMatchException("password 가 일치하지 않습니다.")
        password_expire_date = self.password_expire_date
        if password_expire_date and password_expire_date < now():
            raise PasswordExpiredException("패스워드가 만료되었습니다.")
        human = self.human
        assert isinstance(human, Human)
        tran = TransactionManager.get_transaction()
        if tran.login_user is not None and tran.login_user != human:
            tran.logout()
        tran.login(human)
        return human

    @property
    def password(self):
        raise AssertionError("password 조회는 불가능합니다.")

    @password.setter
    def password(self, v):
        self.set_password(v)

    def set_password(self, password):
        self.encrypted_password = make_password(password)

    def check_password(self, password):
        encrypted_password = self.encrypted_password
        if not password or not encrypted_password:
            raise PasswordNotMatchException("적절한 password 가 아닙니다.")
        return check_password(password, encrypted_password)

    def expire_password(self):
        self.password_expire_date = now() - timedelta(1)
        self.save()
Exemple #5
0
class Post(Model):
    class Meta:
        proxy = True

    author = SubfieldWrapper("owner")
    title = ValueSubfield("data", str)
    content = ValueSubfield("data", str)

    container = ForeignKeySubfield("data",
                                   Type.Board,
                                   null=False,
                                   create_only=True,
                                   alias="board")

    @property
    def published_date(self):
        return self.created_date
Exemple #6
0
class SubDummy(Dummy):
    class Meta:
        proxy = True

    alias_test1 = ValueSubfield("data", str, alias="sda")
    alias_test3 = ForeignKeySubfield("data",
                                     Type.DummyContainer2,
                                     alias="wrapper_alias3")
    alias_test4 = ForeignKeySubfield("data", Type.DummyContainer2)
Exemple #7
0
class DummyContainer(Model):
    class Meta:
        proxy = True

    # data subfields
    uname = DerivedUnameSubfield(default=lambda self: self.uname_source)
    uname_source = ValueSubfield("data", str)

    # relational subfields
    dummies = ReverseForeignKeySubfield("computed", Type.Dummy, "container")
    properties = ReverseForeignKeySubfield("computed", Type.Dummy,
                                           "proprietor")
Exemple #8
0
class LocalTaxGovernment(Actor):
    class Meta:
        proxy = True

    local_tax_government_name = SubfieldWrapper("uname")
    office_address = ValueSubfield("data", str)
    addresses = ReverseForeignKeySubfield("computed", Type.Address,
                                          "local_tax_government")

    @property
    def headquarter_position_name(self):
        local_tax_government_name = self.local_tax_government_name
        if local_tax_government_name.endswith("시청"):
            return "{}".format(local_tax_government_name.replace("시청", "시장"))
        elif local_tax_government_name.endswith("군청"):
            return "{}".format(local_tax_government_name.replace("군청", "군수"))
        elif local_tax_government_name.endswith("구청"):
            return "{}".format(local_tax_government_name.replace("구청", "구청장"))
        elif local_tax_government_name.endswith("도청"):
            return "{}".format(local_tax_government_name.replace("도청", "도지사"))
        else:
            raise NotImplementedError(
                "headquarter_position_name() 이 구현되지 않은 지방세관할청 타입입니다. : {}".
                format(self.local_tax_government_name))

    @classmethod
    def get_by_human_address(cls, human_address):
        splited_human_address = human_address.split()
        for token_count in range(4, 0, -1):
            address = Address.objects.filter(address=cls.join_address(
                splited_human_address, token_count)).first("-id")
            if address:
                return address.local_tax_government
        return None

    @classmethod
    def join_address(cls, splited_address, count):
        return " ".join(splited_address[:count])
Exemple #9
0
class Countable(Model):
    class Meta:
        proxy = True

    # data subfields
    count = ValueSubfield("data", float, null=False, create_only=True)
Exemple #10
0
class Address(Model):
    class Meta:
        proxy = True

    # data subfields
    uname = UnameSubfield(normalize=normalize_address, alias="address")
    tax_office_name = ValueSubfield(
        "data", str, default=lambda self: self.get_tax_office_name())
    road_address = ValueSubfield("data",
                                 str,
                                 default=lambda self: self.get_road_address())
    지번_address = ValueSubfield("data",
                               str,
                               default=lambda self: self.get_지번_address())
    road_name = ValueSubfield("data",
                              str,
                              default=lambda self: self.get_road_name())
    # TODO: expire, onchange 기능확장 후 수정
    zip_code = ValueSubfield("data",
                             str,
                             default=lambda self: self.get_zip_code())

    local_tax_government = ForeignKeySubfield("data", Type.LocalTaxGovernment)

    @property
    def tax_office(self):
        tax_office, _ = TaxOffice.objects.get_or_create(
            uname=self.tax_office_name)
        return tax_office

    @cached_property
    def _juso_res_json(self):
        # TODO: api 콜은 여기가 아니라 따로 caller 로 빼기
        url = "http://www.juso.go.kr/addrlink/addrLinkApi.do?"
        splited_address = self.address.split()
        # 미리 세팅한 이유는 4개 미만인경우 dict 를 만들어서 넘겨줘야 하기때문
        # 도로명주소, 지번주소, 우편번호, 도로명(도로이름)
        result = {
            "results": {
                "juso": [{
                    "roadAddr": self.address,
                    "jibunAddr": self.address,
                    "zipNo": None,
                    "rn": None
                }]
            }
        }
        params = {
            "resultType": "json",
            "confmKey": settings.ADDERSS_SEARCH_API_KEY
        }
        if len(splited_address) < 4:
            if (not splited_address[-1].endswith("동")) or (
                    1 <= len(splited_address) < 3):
                return result
            # TODO: 리팩토링
            params["keyword"] = self.address
            res = requests_get(url, params=params)
            result = json_loads(res.text)
        else:
            for token_count in range(4, len(splited_address) + 1):
                # TODO: 리팩토링
                params["keyword"] = " ".join(splited_address[:token_count])
                res = requests_get(url, params=params)
                json_res = json_loads(res.text)
                if json_res["results"]["juso"]:
                    result = json_res
                else:
                    break
        # TODO: 3어절로 검색하면 주렁주렁 붙는데 이에대한처리 추가
        return result

    # 도로이름
    # TODO: 리팩토링 -> get_road_name 이랑 구현이 완전 동일, 단지 zipNo 인지 rn 인지의 차이뿐
    def get_road_name(self):
        juso_res_json = self._juso_res_json
        rn_list = [data["rn"] for data in juso_res_json["results"]["juso"]]
        if len(set(rn_list)) > 1:
            file_log(
                "2개 이상의 도로가 검색되었습니다. 검색 결과 중 첫번째 도로가 임의로 선택되었습니다. : {}".format(
                    self.address))
        return juso_res_json["results"]["juso"][0]["rn"]

    def select_one_tax_office_name(self, soup):
        # TODO: 제대로 구현 -> 검색결과가 여러개인경우 normalize_address 의 값이랑 매칭해서 1개의 값을 리턴
        # 여러개의 결과 중 하나 선택 : 운서로 라고 검색했는데 인천에 운서로와 대구에 운서로 이렇게 2개의 결과가 존재하여 이중에 하나를 선택하는 함수
        return soup.select_one(
            "body > div:nth-of-type(2) > table > tr:nth-of-type(1) > td:nth-of-type(2) > a"
        ).text

    # 세무서 (국세)
    def get_tax_office_name(self):
        road_name = self.road_name
        url = "https://www.nts.go.kr/wtsuser/about_semuFind.asp"
        params = {
            "search_type": "R",
            "keyword": road_name.encode("euc-kr"),
            "target": "search_semu"
        }
        res = requests_get(url, params=params)
        soup = BeautifulSoup(res.content.decode("euc-kr"), features="lxml")
        tax_office_name = self.select_one_tax_office_name(soup)
        return tax_office_name

    # 우편번호
    # TODO: 리팩토링 -> get_road_name 이랑 구현이 완전 동일, 단지 zipNo 인지 rn 인지의 차이뿐
    # TODO: 우편번호 조회방식 개선
    def get_zip_code(self):
        juso_res_json = self._juso_res_json
        rn_list = [data["rn"] for data in juso_res_json["results"]["juso"]]
        if len(set(rn_list)) == 1:
            return juso_res_json["results"]["juso"][0]["zipNo"]
        elif len(set(rn_list)) == 0:
            return "검색실패"
        elif len(set(rn_list)) >= 2:
            return "2개이상"
        else:
            assert AssertionError

    def get_road_address(self):
        juso_res_json = self._juso_res_json
        if len(juso_res_json["results"]["juso"]) > 1:
            file_log(
                "2개 이상의 도로명 주소가 검색되었습니다. 검색 결과 중 첫번째 도로명 주소가 임의로 선택되었습니다. : {}"
                .format(self.address))
        return juso_res_json["results"]["juso"][0]["roadAddr"]

    def get_지번_address(self):
        juso_res_json = self._juso_res_json
        if len(juso_res_json["results"]["juso"]) > 1:
            file_log(
                "2개 이상의 지번 주소가 검색되었습니다. 검색 결과 중 첫번째 지번 주소가 임의로 선택되었습니다. : {}".
                format(self.address))
        return juso_res_json["results"]["juso"][0]["jibunAddr"]
Exemple #11
0
class Country(Model):
    class Meta:
        proxy = True

    uname = UnameSubfield(create_only=True, alias="국가명")
    code = ValueSubfield("data", str, alias="국가코드")
Exemple #12
0
class Model(AbstractModel, metaclass=ModelBase):
    UNIQUE_KEY_SUBFIELD_NAMES = ("id", "uname", "uri", "uri_hash", "computed_uri_hash")
    PSEUDO_KEY_SUBFIELD_NAMES = ("name",)

    class Meta:
        base_manager_name = "objects"
        unique_together = (
            ("status", "type", "id"),
            ("computed_owner", "status", "type", "id"),
            ("computed_container", "status", "type", "id"),
            ("computed_proprietor", "status", "type", "id"),
        )
        indexes = (GinIndex(fields=("computed_search_array",)),)

    # class part
    objects = ModelManager()
    my_type = CachedClassProperty("_get_my_type", is_freeze=False)
    types = CachedClassProperty("_get_types")
    super_types = CachedClassProperty("_get_super_types")
    subfield_defaults = CachedClassProperty("_get_subfield_defaults")
    required_filters = ({"status", "type"},)
    uri_format = CachedClassProperty("_get_uri_format", is_freeze=False)
    field_names_needs_object_setter = CachedClassProperty("_get_field_names_needs_object_setter")
    is_agent = False

    # system field
    type = IntegerField(null=False)
    optimistic_lock_count = IntegerField(null=False)

    # json field
    data = JSONField(null=False, blank=True)
    computed = JSONField(null=False, blank=True)
    raw = JSONField(null=True, blank=True)

    # data subfields
    created_date = ValueSubfield("data", datetime)
    uname = UnameSubfield()  # intrinsic key (mutable)
    name = ValueSubfield("data", str)  # loose key (mutable)

    # computed subfields
    uri = ValueSubfield(
        "computed",
        str,
        check=lambda v: v[:5] == "/uri/" and v[-1] == "/",
        filter=lambda cls, v: {"computed_uri_hash": compute_hash_uuid(v)},
    )
    uri_hash = ValueSubfield("computed", UUID, filter=lambda cls, v: {"computed_uri_hash": v})

    # relational subfields
    creator = ForeignKeySubfield("data", Type.Actor, create_only=True)
    owner = ForeignKeySubfield("data", Type.Actor)
    container = ForeignKeySubfield("data", Type.Model)
    elements = ReverseForeignKeySubfield("computed", Type.Model, "container")
    proprietor = ForeignKeySubfield("data", Type.Model)
    properties = ReverseForeignKeySubfield("computed", Type.Model, "proprietor")

    # computed field
    computed_uri_hash = UUIDField(unique=True, null=True)  # derived from uname (intrinsic key)
    computed_owner = ForeignKey(
        "Model", related_name="computed_possessions", null=True, on_delete=PROTECT, db_index=False
    )
    computed_container = ForeignKey(
        "Model", related_name="computed_elements", null=True, on_delete=PROTECT, db_index=False
    )
    computed_proprietor = ForeignKey(
        "Model", related_name="computed_properties", null=True, on_delete=PROTECT, db_index=False
    )
    computed_search_array = ArrayField(BigIntegerField(), null=True)

    # class member variable
    _is_initialized = False  # instance member variable 이나 __init__() 전에 setattr 이 호출될 수 있어 세팅함

    @cached_property
    def intrinsic_content_subfield_names(self):
        return tuple(
            k
            for k, v in self.subfields["_total"].items()
            if not isinstance(v, ForeignKeySubfield)
            and not isinstance(v, ReverseForeignKeySubfield)
            and not isinstance(v, SubfieldWrapper)
            and k not in ("created_date", "uname", "uri", "uri_hash")
        )

    def is_same_content(self, other):
        if self.my_type != other.my_type:
            return False
        for subfield_name in self.intrinsic_content_subfield_names:
            if getattr(self, subfield_name) != getattr(other, subfield_name):
                return False
        return True

    def __init__(self, *args, **kwargs):
        assert not self.__class__.__dict__.get("ABSTRACT", False), "인스턴스화가 불가능한 모델입니다."
        self._is_initialized = False
        self._force_changer_count = 0
        super().__init__(*args, subfield_kwargs=kwargs)

    def init_variables(self):
        super().init_variables()
        self._is_initialized = True
        self._mjson_revert_patch = {"data": {}, "computed": {}}
        self._old_uri = None
        if settings.IS_UNIT_TEST:
            self._mjson_on_init = deepcopy(self.mjson)

    @property
    def last_transaction_date(self):
        return get_datetime_from_key(self.last_transaction)

    @classmethod
    def _get_my_type(cls):
        return getattr(Type, cls.__name__)

    @classmethod
    def _get_types(cls):
        my_type = cls.my_type
        if my_type == Type.Model:
            return []
        sub_model_types = list(my_type.sub_types)
        return [my_type] + sub_model_types

    @classmethod
    def _get_super_types(cls):
        return [parent.my_type for parent in cls.mro()[1:] if issubclass(parent, Model)]

    @classmethod
    def _get_subfield_defaults(cls):
        total = {"data": {}, "computed": {"_mjson_revert_patch": None}}
        for k, v in cls.subfields["data"].items():
            total["data"][k] = json_encode(v.default)
        for k, v in cls.subfields["computed"].items():
            total["computed"][k] = json_encode(v.default)
        return total

    @classmethod
    def _get_uri_format(cls):
        app_name = cls.__module__.split(".")[0]
        cls_name = cls.__name__
        header = "/uri/{}/{}/".format(app_name.lower(), cls_name.lower())
        return header + "{}/"

    @classmethod
    def _get_field_names_needs_object_setter(cls):
        return cls.subfield_names + cls.model_level_field_names + cls.only_column_names

    def __str__(self):
        return "({}, {}, {}, {}, {})".format(
            self.type, self.id, self.uri or self.uname or self.name, self.status.name, id(self)
        )

    def _pre_create(self):
        super()._pre_create()
        my_type = self.my_type
        assert my_type is not None, "Type 이 정의되어 있어야 합니다."
        self.type = my_type
        default = json_deepcopy_with_callable(self.subfield_defaults)
        self.data = default["data"]
        self.computed = default["computed"]
        self.optimistic_lock_count = 0
        self.data["created_date"] = str(now())

    def _init_subfields(self, **kwargs):
        for k, v in kwargs.items():
            setattr(self, k, v)

    def on_create(self):
        super().on_create()
        for subfield_name, subfield in self.subfields["data"].items():
            encoded = self.data[subfield_name]
            if encoded and not callable(encoded):
                self.onchange_subfield("data", subfield_name, None, json_decode(encoded, subfield.subfield_type))

    def _init_no_inited_subfields(self):
        for k, v in self.data.items():
            if v is None and k[0] != "_":
                subfield = self.get_subfield(k)
                assert subfield.null, console_log("{} 는 null 이 허용되지 않습니다.".format(subfield.subfield_name))
            elif callable(v):
                self.data[k] = json_encode(safe_call(v, self))
        for k, v in self.computed.items():
            if v is None and k[0] != "_":
                subfield = self.get_subfield(k)
                assert subfield.null
            elif callable(v):
                self.computed[k] = json_encode(safe_call(v, self))

    def _pre_syncdb_insert(self, tran):
        super()._pre_syncdb_insert(tran)
        self._init_no_inited_subfields()

    def take_optimistic_lock(self):
        if self.status in (Status.CREATING, Status.NEW):
            return
        with ForceChanger(self):
            self.optimistic_lock_count += 1
        self.save()

    # 절대 함부로 사용하지 말 것. 정말 특수한 경우가 아니라면 활용할 일이 없음
    def _set_fields(self, **kwargs):
        # version
        assert "version" not in kwargs
        old_version = self.version
        self.__dict__["version"] = old_version + 1
        kwargs["version"] = old_version + 1
        # set fields
        for k, v in kwargs.items():
            self.__dict__[k] = v
            if settings.IS_UNIT_TEST:
                self._mjson_on_init[k] = v
        # DB
        result = self.__class__.objects.filter(id=self.id, version=old_version).update(**kwargs)
        if result == 0:
            raise OptimisticLockException
        assert result == 1

    def set_working(self):
        assert self.is_in_writable_transaction(), "해당 instance 가 수정 가능한 Transaction 내에 없습니다."
        if self.status == Status.NEW:
            tran = TransactionManager.get_transaction()
            self._syncdb_insert(tran)
        assert self.status in (Status.NORMAL, Status.DIRTY)
        self._set_fields(status=Status.WORKING)

    def rollback_working(self):
        assert self.status is Status.WORKING, console_log("Status 가 WORKING 일때만 호출이 가능합니다.")
        self._set_fields(status=Status.NORMAL)

    def take_exclusive_lock(self, nowait=False):
        if self.status in (Status.CREATING, Status.NEW):
            return
        manager = self.__class__.objects
        queryset = manager.select_for_update(nowait=nowait).filter(id=self.id)
        assert queryset._result_cache is None or len(queryset._result_cache) == 0
        queryset._fetch_all()
        assert len(queryset._result_cache) == 1

    @classmethod
    def from_db_impl(cls, db, field_names, values):
        t = Type(values[field_names.index("type")])
        # override cls
        cls_t = t.model
        # almost same with DjangoModel.from_db()
        if len(values) != len(cls_t._meta.concrete_fields):
            values_iter = iter(values)
            values = [next(values_iter) if f.attname in field_names else DEFERRED for f in cls_t._meta.concrete_fields]
        new = cls_t(*values)
        new._is_initialized = False
        new._state.adding = False
        new._state.db = db
        # for only Model
        assert new.type == cls_t.my_type
        new.type = Type(new.type)
        new.status = Status(new.status)
        new.init_variables()
        new.patch_json_field()
        return new

    # TODO : schema 만 바뀐 경우에 save() 가 가능하도록 하는 구현 부분 리팩토링
    def patch_json_field(self):
        if self.status == Status.DELETED:
            return
        assert self.status in (Status.NORMAL, Status.WORKING), self.status
        total_default = self.subfield_defaults
        _schema_changed = False
        old_status = self.__dict__["status"]
        for json_field_name in total_default:
            json_field = self.__dict__[json_field_name]
            field_default = total_default[json_field_name]
            for key, value in field_default.items():
                if key not in json_field:
                    if callable(value):
                        value = safe_call(value, self)
                    json_field[key] = json_deepcopy_with_callable(value)
                    _schema_changed = True
                    self.__dict__["status"] = Status.DIRTY
                    self._mjson_revert_patch.update(
                        {"status": json_encode(old_status), json_field_name: {"_schema_changed": _schema_changed}}
                    )
            for k in [k for k in json_field.keys() if k not in field_default]:
                del json_field[k]
                _schema_changed = True
                self.__dict__["status"] = Status.DIRTY
                self._mjson_revert_patch.update(
                    {"status": json_encode(old_status), json_field_name: {"_schema_changed": _schema_changed}}
                )
        if _schema_changed and settings.IS_UNIT_TEST:
            self._mjson_on_init = deepcopy(self.mjson)
            self._mjson_on_init["status"] = json_encode(old_status)

    def patch(self, **kwargs):
        for subfield_name, value in kwargs.items():
            setattr(self, subfield_name, value)

    def assert_changeable(self, field_name=None):
        super().assert_changeable(field_name)
        if field_name:
            assert field_name not in ("id", "type"), "id, type field 는 절대 수정할 수 없습니다."
            if field_name == "raw":
                assert self.status in (Status.CREATING, Status.NEW), "raw 컬럼은 최초 생성시에만 세팅 가능합니다."
            elif settings.IS_UNIT_TEST:
                assert field_name == "data" or self._force_changer_count >= 1, console_log(
                    "data field 를 제외하면 기본적으로 수정 금지입니다 : {}".format(field_name)
                )
            else:
                # for django admin
                assert (
                    field_name in ("data", "computed") or self._force_changer_count >= 1
                ), "data field 를 제외하면 기본적으로 수정 금지입니다. computed field 는 수정되지 않고 무시됩니다."

    def __setattr__(self, field_name, value):
        # __dict__ setter
        if field_name[0] == "_" or not self._is_initialized:
            self.__dict__[field_name] = value
            return
        # object setter
        # TODO : 모두 확정된 후 로컬상수변수로 대체하여 튜닝
        if field_name in self.field_names_needs_object_setter:
            return object.__setattr__(self, field_name, value)
        # for overrides super().delete()
        if field_name == "id" and value is None:
            return
        # set field
        old = getattr(self, field_name)
        if old != value:
            # for django admin
            if value is None and field_name in ("data", "computed"):
                return
            # check pre condition
            self.assert_changeable(field_name)
            # change value
            if field_name == "data":
                for subfield, subvalue in value.items():
                    setattr(self, subfield, subvalue)
            elif field_name == "status":
                assert old.check_route(value), "해당 status 변경은 허용되지 않습니다: {} --> {}".format(old, value)
                self.__dict__["status"] = value
            elif field_name == "computed":
                assert not settings.IS_UNIT_TEST, "computed field 직접 세팅은 허용되지 않습니다."
                # for django admin
                console_log("try change of computed field is ignored")
            else:
                if self.status in (Status.NORMAL, Status.WORKING):
                    self.__setattr__("status", Status.DIRTY)
                self.__dict__[field_name] = value
            # make revert patch
            if field_name not in self._mjson_revert_patch and field_name in self.field_names:
                self._mjson_revert_patch[field_name] = json_encode(old)
            # raw
            if field_name == "raw":
                assert self.status in (Status.CREATING, Status.NEW)
                self.init_data_by_raw()

    # for cached queryset filter
    def __getattr__(self, key):
        # dot operator 처리 순서 상 이미 __dict__ 는 check 가 된 상태임
        if key[0] == "_":
            raise AttributeError(key)

        parts = key.split("__")
        field_name = parts[0]

        # TODO : ComplexSubfield 에 대해 2-depth 까지만 허용되고 있는데, 이를 확장
        # json field
        if field_name in ("data", "computed", "raw"):
            assert len(parts) in (2, 3)
            subfield_name = parts[1]
            assert getattr(self.__class__, subfield_name).field_name == field_name
            result = getattr(self, subfield_name)
            if len(parts) == 3:
                result = result[parts[2]]
            return result
        # subfield
        elif field_name in self.subfield_names:
            assert len(parts) in (2,)
            subfield_name = parts[0]
            result = getattr(self, subfield_name)[parts[1]]
            return result
        else:
            raise AttributeError("{}.{}".format(self.__class__.__name__, key))

    def get_modified_field_names(self):
        return [field_name for field_name, value in self._mjson_revert_patch.items() if value != {}]

    def create_history(self):
        column_names = [f for f in self.column_names if not f.startswith("computed") and not f.startswith("raw")]
        field_names_str = ",".join(column_names)
        tran = TransactionManager.get_transaction()
        query = "insert into base_modelhistory ({},{}) select {},{} from base_model where id=%s".format(
            "history_transaction", field_names_str, tran.id, field_names_str
        )
        instance_id = self.id
        run_sql(query, params=(instance_id,))

    def on_nosave(self):
        if settings.IS_UNIT_TEST:
            assert self.mjson == self._mjson_on_init, "subfield 를 통하지 않는 등 잘못된 방식으로 필드가 수정되었습니다."
        super().on_nosave()

    def _syncdb_update(self, tran, update_fields=None):
        assert update_fields is None
        update_fields = self.get_modified_field_names()
        assert len(update_fields) >= 2, "수정된 필드가 없는데 syncdb_update() 가 호출되었습니다."
        assert "status" in update_fields and "status" in self._mjson_revert_patch, "status 필드가 적절한 방식으로 수정되지 않았습니다."
        assert (
            "version" not in update_fields and "last_transaction" not in update_fields
        ), "system 필드가 적절한 방식으로 수정되지 않았습니다."
        with ForceChanger(self):
            self.version += 1
            self.last_transaction = tran.id
        if settings.IS_UNIT_TEST:
            mjson_reverted = patch_mjson(deepcopy(self.mjson), MappingProxyType(self._mjson_revert_patch))
            # TODO : schema 만 바뀐 경우에 save() 가 가능하도록 하는 구현 부분 리팩토링
            if "_schema_changed" in mjson_reverted["data"]:
                del mjson_reverted["data"]["_schema_changed"]
            if "_schema_changed" in mjson_reverted["computed"]:
                del mjson_reverted["computed"]["_schema_changed"]
            assert mjson_reverted == self._mjson_on_init, "subfield 를 통하지 않는 등 잘못된 방식으로 필드가 수정되었습니다."
        self.create_history()
        self.computed["_mjson_revert_patch"] = self._mjson_revert_patch
        update_fields.extend(["version", "last_transaction", "computed"])
        super()._syncdb_update(tran, update_fields=update_fields)

    def _mark_delete(self):
        with ForceChanger(self):
            self.computed_uri_hash = None
        super()._mark_delete()

    def on_delete(self):
        subfields = self.subfields
        for rf in subfields["sources"]:
            setattr(self, rf, None)
        for rf in subfields["targets"]:
            setattr(self, rf, None)

    def _syncdb_delete(self, tran):
        self.create_history()
        super()._syncdb_delete(tran)

    def _destroy(self, using=None, keep_parents=True):
        self.create_history()
        super()._destroy(using=None, keep_parents=True)

    def _process_expire_onchange(self, subfield_name=None):
        if subfield_name is None:
            for k, v in self.subfields["data"].items():
                if v.expire == Expire.ON_CHANGE:
                    v.__set__(self, v.default(self))
        else:
            subfields = self.subfields
            if subfield_name in subfields["_dependents"]:
                depended = subfields["_dependents"][subfield_name]
                for v in depended:
                    if v.expire == Expire.ON_CHANGE:
                        v.__set__(self, safe_call(v.default, self))

    def _process_dependent_computed(self, subfield_name):
        subfields = self.subfields
        if subfield_name in subfields["_dependents"]:
            for v in subfields["_dependents"][subfield_name]:
                if v.field_name == "computed":
                    self.compute(v.subfield_name)

    @set_invalid_on_exception
    def onchange_subfield(self, field_name, subfield_name, old, new):
        assert old != new, "old 와 new 값이 같은 경우는 원천적으로 발생되지 않도록 해야 합니다."
        old_status, _mjson_revert_patch, subfields = (self.status, self._mjson_revert_patch, self.subfields)
        subfield = subfields[field_name][subfield_name]
        assert not isinstance(subfield, SubfieldWrapper)
        if old_status == Status.CREATING:
            # TODO : save() 호출 자체를 없애고 나면 이것도 없애기
            # 이때는 on_create 이후 별도로 onchange 이벤트를 일괄 발생시킴
            return
        if subfield_name not in _mjson_revert_patch[field_name]:
            _mjson_revert_patch[field_name][subfield_name] = json_encode(old)
        with ForceChanger(self):
            if old_status not in (Status.NEW, Status.NO_SYNC):
                self.status = Status.DIRTY
            self._process_dependent_computed(subfield_name)
            # original
            onchange_func_name = ONCHANGE_FUNC_NAME.format(subfield_name)
            func = getattr(self, onchange_func_name, None)
            func and func(old, new)
            # wrappers
            for wrapper in subfields["_wrapper_reverse"].get(subfield_name, []):
                onchange_func_name = ONCHANGE_FUNC_NAME.format(wrapper.wrapper_subfield_name)
                func = getattr(self, onchange_func_name, None)
                func and func(old, new)
        self._process_expire_onchange(subfield_name)

    def compute(self, subfield_name):
        # set to computed json
        old_status = self.status
        compute_func = getattr(self, COMPUTE_FUNC_NAME.format(subfield_name))
        value = safe_call(compute_func)
        setattr(self, subfield_name, value)
        # set to computed_field
        computed_field_name = COMPUTED_FIELD_NAME.format(subfield_name)
        if computed_field_name in self.field_names:
            setattr(self, computed_field_name, value)
        if old_status in (Status.NORMAL, Status.WORKING) and self.status == Status.DIRTY:
            self.save()

    def _compute_total(self):
        for subfield_name in self.computed:
            if subfield_name[0] != "_":
                self.compute(subfield_name)

    def compute_uri(self):
        return self.convert_uri(self.uname)

    def compute_all(self):
        for subfield_name in self.subfields["computed"]:
            self.compute(subfield_name)

    @classmethod
    def convert_uri(cls, uname):
        if not uname:
            return None
        uri = cls.uri_format.format(uname if uname[0] != "/" else uname[1:])
        return uri if uri[-2] != "/" else uri[:-1]

    def onchange_uri(self, old, new):
        tran = TransactionManager.get_transaction()
        assert not old or old in tran.uri_mapping, "Transaction.uri_mapping 에 기존 uri 값이 존재하지 않습니다."
        if new in tran.uri_mapping:
            if self.status == Status.NEW:
                tran.remove(self)
            raise DuplicateUriException
        self._old_uri = old
        tran.set(self)

    def compute_uri_hash(self):
        return compute_hash_uuid(self.uri)

    def init_data_by_raw(self):
        pass
Exemple #13
0
class Dummy(Model):
    class Meta:
        proxy = True

    # data subfields
    d = DictSubfield("data", {"a": int, "b": int, "c": datetime})
    temp = ValueSubfield("data", int)
    patent = DictSubfield("data", {"raw": str, "plain": str, "html": str})
    monitors = ListSubfield("data", [int])
    modified_dates = ListSubfield("data", [datetime])
    test_status = ValueSubfield("data", Status)
    default_dict_test = DictSubfield("data", {"default_test": str},
                                     default={"default_test": "test"})
    tax_office_name = ValueSubfield(
        "data", str, default=lambda self: self.get_tax_office_name())
    check_test = ValueSubfield("data",
                               int,
                               default=10,
                               check=lambda v: v >= 2,
                               null=False)
    check_test2 = ValueSubfield("data",
                                int,
                                default=10,
                                check=lambda v: v >= 2)
    wrapper_test1 = ValueSubfield("data", int, alias="wrapper_test2")
    bool_test = ValueSubfield("data", bool, normalize=convert_bool)
    create_only_test = ValueSubfield("data", int, create_only=True)
    alias_test1 = ValueSubfield("data", str, alias="wrapper_alias")
    alias_test2 = ValueSubfield("data", str, alias="wrapper_alias2")
    result_labels = DictSubfield(
        "data", {
            "STA_y": str,
            "STA_prob": float,
            "STA2_y": str,
            "STA2_prob": float,
            "TTS_y": str,
            "TTS_prob": float
        })

    # computed subfields
    patent_summary = DictSubfield("computed", {"len": int, "summary": str})
    # default 에 callable 을 넘길 때는 self 를 인자로 받는 lambda 로 싸서 넘김
    monitors_count = ValueSubfield(
        "computed", int, default=lambda self: self.compute_monitors_count())

    # relational subfields
    container = ForeignKeySubfield("data", Type.DummyContainer)
    group = ForeignKeySubfield("data", Type.Dummy)
    members = ReverseForeignKeySubfield("computed", Type.Dummy, "group")
    proprietor = ForeignKeySubfield("data", Type.DummyContainer)
    alias_test3 = ForeignKeySubfield("data",
                                     Type.DummyContainer,
                                     alias="wrapper_alias3")
    alias_test4 = ForeignKeySubfield("data",
                                     Type.DummyContainer,
                                     alias="wrapper_alias4")

    def compute_patent_summary(self):
        patent = self.patent
        if not patent:
            return {}
        raw = self.patent.raw
        return {"len": raw and len(raw), "summary": raw and raw[0]}

    def compute_monitors_count(self):
        return len(self.monitors)

    def get_tax_office_name(self):
        return "동수원세무서"

    def onchange_wrapper_test2(self, old, new):
        self.__dict__["_wrapper_test"] = True