class TaxOffice(Actor): class Meta: proxy = True # data subfields tax_office_name = SubfieldWrapper("uname") code = ValueSubfield("data", int) account_number = ValueSubfield("data", int)
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)
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)
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()
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
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)
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")
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])
class Countable(Model): class Meta: proxy = True # data subfields count = ValueSubfield("data", float, null=False, create_only=True)
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"]
class Country(Model): class Meta: proxy = True uname = UnameSubfield(create_only=True, alias="국가명") code = ValueSubfield("data", str, alias="국가코드")
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
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