fix bugs
This commit is contained in:
parent
b7c35f59e8
commit
0843a369be
3
.gitignore
vendored
3
.gitignore
vendored
@ -211,5 +211,8 @@ rc.py
|
|||||||
|
|
||||||
model/
|
model/
|
||||||
|
|
||||||
|
plugins/*.c
|
||||||
plugin-build/
|
plugin-build/
|
||||||
3rd/
|
3rd/
|
||||||
|
default/
|
||||||
|
plugins/*/*.c
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,15 +1,17 @@
|
|||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
|
import pdb
|
||||||
from rscder.plugins.basic import BasicPlugin
|
from rscder.plugins.basic import BasicPlugin
|
||||||
from PyQt5.QtWidgets import QAction
|
from PyQt5.QtWidgets import QAction
|
||||||
from PyQt5.QtCore import pyqtSignal
|
from PyQt5.QtCore import pyqtSignal
|
||||||
from rscder.utils.project import PairLayer
|
from rscder.utils.project import PairLayer, ResultLayer
|
||||||
from osgeo import gdal, gdal_array
|
from osgeo import gdal, gdal_array
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
import numpy as np
|
import numpy as np
|
||||||
class BasicMethod(BasicPlugin):
|
class BasicMethod(BasicPlugin):
|
||||||
|
|
||||||
message_send = pyqtSignal(str)
|
message_send = pyqtSignal(str)
|
||||||
|
table_result_ok = pyqtSignal(str)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def info():
|
def info():
|
||||||
@ -21,15 +23,17 @@ class BasicMethod(BasicPlugin):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def set_action(self):
|
def set_action(self):
|
||||||
basic_change_detection_menu = self.ctx['basic_change_detection_menu']
|
basic_change_detection_menu = self.ctx['change_detection_menu']
|
||||||
|
|
||||||
basic_diff_method = QAction('差分法')
|
basic_diff_method = QAction('差分法')
|
||||||
basic_change_detection_menu.addAction(basic_diff_method)
|
basic_change_detection_menu.addAction(basic_diff_method)
|
||||||
|
|
||||||
basic_diff_method.setEnabled(False)
|
basic_diff_method.setEnabled(False)
|
||||||
self.basic_diff_method = basic_diff_method
|
self.basic_diff_method = basic_diff_method
|
||||||
|
basic_diff_method.triggered.connect(self.basic_diff_alg)
|
||||||
|
|
||||||
self.message_send.connect(self.send_message)
|
self.message_send.connect(self.send_message)
|
||||||
|
self.table_result_ok.connect(self.on_table_result_ok)
|
||||||
|
|
||||||
self.gap = 128
|
self.gap = 128
|
||||||
|
|
||||||
@ -39,6 +43,23 @@ class BasicMethod(BasicPlugin):
|
|||||||
def send_message(self, s):
|
def send_message(self, s):
|
||||||
self.message_box.info(s)
|
self.message_box.info(s)
|
||||||
|
|
||||||
|
def on_table_result_ok(self, s):
|
||||||
|
with open(s, 'r') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
data_lines = lines[1:]
|
||||||
|
|
||||||
|
if len(data_lines) > 0:
|
||||||
|
data_table = []
|
||||||
|
for l in data_lines:
|
||||||
|
l = l.strip()
|
||||||
|
ls = l.split(',')
|
||||||
|
ls = [float(i) for i in ls]
|
||||||
|
data_table.append(ls)
|
||||||
|
result = ResultLayer(ResultLayer.POINT)
|
||||||
|
result.data = data_table
|
||||||
|
self.result_table.set_data(result)
|
||||||
|
|
||||||
|
|
||||||
def run_basic_diff_alg(self, pth1, pth2, cell_size, out):
|
def run_basic_diff_alg(self, pth1, pth2, cell_size, out):
|
||||||
self.message_send.emit('开始计算差分法')
|
self.message_send.emit('开始计算差分法')
|
||||||
|
|
||||||
@ -70,10 +91,10 @@ class BasicMethod(BasicPlugin):
|
|||||||
if band == 1:
|
if band == 1:
|
||||||
block_data1 = block_data1[None, ...]
|
block_data1 = block_data1[None, ...]
|
||||||
block_data2 = block_data2[None, ...]
|
block_data2 = block_data2[None, ...]
|
||||||
|
# pdb.set_trace()
|
||||||
block_diff = block_data1 - block_data2
|
block_diff = block_data1 - block_data2
|
||||||
block_diff = block_diff.astype(np.float32)
|
block_diff = block_diff.astype(np.float32)
|
||||||
block_diff = block_diff.abs().sum(0)
|
block_diff = np.abs(block_diff).sum(0)
|
||||||
|
|
||||||
min_diff = min(min_diff, block_diff.min())
|
min_diff = min(min_diff, block_diff.min())
|
||||||
max_diff = max(max_diff, block_diff.max())
|
max_diff = max(max_diff, block_diff.max())
|
||||||
@ -82,7 +103,7 @@ class BasicMethod(BasicPlugin):
|
|||||||
self.message_send.emit(f'完成{j}/{yblocks}')
|
self.message_send.emit(f'完成{j}/{yblocks}')
|
||||||
|
|
||||||
out_ds.FlushCache()
|
out_ds.FlushCache()
|
||||||
out_ds = None
|
del out_ds
|
||||||
self.message_send.emit('归一化概率中...')
|
self.message_send.emit('归一化概率中...')
|
||||||
temp_in_ds = gdal.Open(out_tif)
|
temp_in_ds = gdal.Open(out_tif)
|
||||||
|
|
||||||
@ -100,12 +121,15 @@ class BasicMethod(BasicPlugin):
|
|||||||
out_normal_ds.GetRasterBand(1).WriteArray(block_data, *block_xy)
|
out_normal_ds.GetRasterBand(1).WriteArray(block_data, *block_xy)
|
||||||
|
|
||||||
out_normal_ds.FlushCache()
|
out_normal_ds.FlushCache()
|
||||||
out_normal_ds = None
|
del out_normal_ds
|
||||||
self.message_send.emit('完成归一化概率')
|
self.message_send.emit('完成归一化概率')
|
||||||
|
|
||||||
self.message_send.emit('计算变化表格中...')
|
self.message_send.emit('计算变化表格中...')
|
||||||
out_csv = os.path.join(out, 'diff_table.csv')
|
out_csv = os.path.join(out, 'diff_table.csv')
|
||||||
xblocks = xsize // cell_size[0]
|
xblocks = xsize // cell_size[0]
|
||||||
|
|
||||||
|
normal_in_ds = gdal.Open(out_normal_tif)
|
||||||
|
|
||||||
with open(out_csv, 'w') as f:
|
with open(out_csv, 'w') as f:
|
||||||
f.write('x,y,diff,status\n')
|
f.write('x,y,diff,status\n')
|
||||||
for j in range(yblocks):
|
for j in range(yblocks):
|
||||||
@ -113,7 +137,7 @@ class BasicMethod(BasicPlugin):
|
|||||||
block_size = (xsize, cell_size[1])
|
block_size = (xsize, cell_size[1])
|
||||||
if block_xy[1] + block_size[1] > ysize:
|
if block_xy[1] + block_size[1] > ysize:
|
||||||
block_size = (xsize, ysize - block_xy[1])
|
block_size = (xsize, ysize - block_xy[1])
|
||||||
block_data = temp_in_ds.ReadAsArray(*block_xy, *block_size)
|
block_data = normal_in_ds.ReadAsArray(*block_xy, *block_size)
|
||||||
for i in range(xblocks):
|
for i in range(xblocks):
|
||||||
start_x = i * cell_size[0]
|
start_x = i * cell_size[0]
|
||||||
end_x = start_x + cell_size[0]
|
end_x = start_x + cell_size[0]
|
||||||
@ -127,21 +151,26 @@ class BasicMethod(BasicPlugin):
|
|||||||
center_y = center_y * geo[5] + geo [3]
|
center_y = center_y * geo[5] + geo [3]
|
||||||
f.write(f'{center_x},{center_y},{block_data_xy.mean()},1\n')
|
f.write(f'{center_x},{center_y},{block_data_xy.mean()},1\n')
|
||||||
|
|
||||||
|
self.table_result_ok.emit(out_csv)
|
||||||
|
|
||||||
self.message_send.emit('完成计算变化表格')
|
self.message_send.emit('完成计算变化表格')
|
||||||
|
|
||||||
self.message_send.emit('差分法计算完成')
|
self.message_send.emit('差分法计算完成')
|
||||||
|
|
||||||
def basic_diff_alg(self):
|
def basic_diff_alg(self):
|
||||||
# layer_select =
|
# layer_select =
|
||||||
layer:PairLayer = self.project.layers.values()[0]
|
layer:PairLayer = list(self.project.layers.values())[0]
|
||||||
|
|
||||||
img1 = layer.pth2
|
img1 = layer.pth1
|
||||||
img2 = layer.pth2
|
img2 = layer.pth2
|
||||||
|
|
||||||
if not layer.check():
|
if not layer.check():
|
||||||
return
|
return
|
||||||
|
out_dir =os.path.join(self.project.root, 'basic_diff_result')
|
||||||
|
if not os.path.exists(out_dir):
|
||||||
|
os.makedirs(out_dir, exist_ok=True)
|
||||||
|
|
||||||
t = Thread(target=self.run_basic_diff_alg, args=(img1, img2, layer.pth1))
|
t = Thread(target=self.run_basic_diff_alg, args=(img1, img2, layer.cell_size, out_dir))
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
|
|
||||||
|
14
plugins/plugins.yaml
Normal file
14
plugins/plugins.yaml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
- author: RSCDER
|
||||||
|
description: "\u5173\u4E8E"
|
||||||
|
enabled: true
|
||||||
|
module: about
|
||||||
|
name: "\u5173\u4E8E"
|
||||||
|
path: ./plugin-build\about
|
||||||
|
version: 1.0.0
|
||||||
|
- author: RSCDER
|
||||||
|
description: BasicMethod
|
||||||
|
enabled: true
|
||||||
|
module: basic_change
|
||||||
|
name: BasicMethod
|
||||||
|
path: ./plugin-build\basic_change
|
||||||
|
version: 1.0.0
|
@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||||
from PyQt5.QtWidgets import QAction, QActionGroup, QLabel, QFileDialog
|
from PyQt5.QtWidgets import QAction, QActionGroup, QLabel, QFileDialog
|
||||||
from rscder.gui.project import Create
|
from rscder.gui.project import Create
|
||||||
@ -187,7 +188,7 @@ class ActionManager(QtCore.QObject):
|
|||||||
if project.is_init:
|
if project.is_init:
|
||||||
project.save()
|
project.save()
|
||||||
project.clear()
|
project.clear()
|
||||||
project.setup(os.path.join(projec_create.file, projec_create.name + '.prj'))
|
project.setup(projec_create.file, projec_create.name + '.prj')
|
||||||
project.is_init = True
|
project.is_init = True
|
||||||
project.cell_size = projec_create.cell_size
|
project.cell_size = projec_create.cell_size
|
||||||
project.max_memory = projec_create.max_memory
|
project.max_memory = projec_create.max_memory
|
||||||
@ -208,10 +209,13 @@ class ActionManager(QtCore.QObject):
|
|||||||
if Project().is_init:
|
if Project().is_init:
|
||||||
Project().save()
|
Project().save()
|
||||||
|
|
||||||
project_file = QFileDialog.getOpenFileName(self.w_parent, '打开工程', '.', '*.prj')
|
project_file = QFileDialog.getOpenFileName(self.w_parent, '打开工程', Settings.General().last_path, '*.prj')
|
||||||
if project_file[0] != '':
|
if project_file[0] != '':
|
||||||
Project().clear()
|
Project().clear()
|
||||||
Project().setup(project_file[0])
|
parent = str(Path(project_file[0]).parent)
|
||||||
|
Settings.General().last_path = parent
|
||||||
|
name = os.path.basename(project_file[0])
|
||||||
|
Project().setup(parent, name)
|
||||||
|
|
||||||
def project_save(self):
|
def project_save(self):
|
||||||
if Project().is_init:
|
if Project().is_init:
|
||||||
@ -221,7 +225,7 @@ class ActionManager(QtCore.QObject):
|
|||||||
if Project().is_init:
|
if Project().is_init:
|
||||||
Project().save()
|
Project().save()
|
||||||
|
|
||||||
file_open = QFileDialog.getOpenFileNames(self.w_parent, '打开数据', Project().root, '*.*')
|
file_open = QFileDialog.getOpenFileNames(self.w_parent, '打开数据', Settings.General().last_path, '*.*')
|
||||||
if file_open[0] != '':
|
if file_open[0] != '':
|
||||||
if len(file_open[0]) != 2:
|
if len(file_open[0]) != 2:
|
||||||
self.message_box.warning('请选择两个数据文件')
|
self.message_box.warning('请选择两个数据文件')
|
||||||
|
@ -38,8 +38,10 @@ class DoubleCanvas(QWidget):
|
|||||||
self.mapcanva2.update_coordinates_text.connect(self.corr_changed)
|
self.mapcanva2.update_coordinates_text.connect(self.corr_changed)
|
||||||
|
|
||||||
def set_map1_extent():
|
def set_map1_extent():
|
||||||
|
if self.mapcanva2.is_main:
|
||||||
self.mapcanva1.set_extent(self.mapcanva2.extent())
|
self.mapcanva1.set_extent(self.mapcanva2.extent())
|
||||||
def set_map2_extent():
|
def set_map2_extent():
|
||||||
|
if self.mapcanva1.is_main:
|
||||||
self.mapcanva2.set_extent(self.mapcanva1.extent())
|
self.mapcanva2.set_extent(self.mapcanva1.extent())
|
||||||
|
|
||||||
self.mapcanva1.extentsChanged.connect(set_map2_extent)
|
self.mapcanva1.extentsChanged.connect(set_map2_extent)
|
||||||
|
@ -26,7 +26,7 @@ class PluginDialog(QDialog):
|
|||||||
for idx, plugin in enumerate(self.plugins):
|
for idx, plugin in enumerate(self.plugins):
|
||||||
name_item = QTableWidgetItem(plugin['name'])
|
name_item = QTableWidgetItem(plugin['name'])
|
||||||
module_item = QTableWidgetItem(plugin['module'])
|
module_item = QTableWidgetItem(plugin['module'])
|
||||||
enabled_item = QTableWidgetItem()
|
enabled_item = QTableWidgetItem('启用')
|
||||||
enabled_item.setCheckState(Qt.Checked if plugin['enabled'] else Qt.Unchecked)
|
enabled_item.setCheckState(Qt.Checked if plugin['enabled'] else Qt.Unchecked)
|
||||||
|
|
||||||
self.plugin_table.setItem(idx, 0, name_item)
|
self.plugin_table.setItem(idx, 0, name_item)
|
||||||
@ -93,7 +93,8 @@ class PluginDialog(QDialog):
|
|||||||
info = self.plugins.pop(row)
|
info = self.plugins.pop(row)
|
||||||
try:
|
try:
|
||||||
shutil.rmtree(info['path'])
|
shutil.rmtree(info['path'])
|
||||||
except:
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
pass
|
pass
|
||||||
# for idx in self.plugins
|
# for idx in self.plugins
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from pathlib import Path
|
||||||
from PyQt5.QtWidgets import QDialog, QFileDialog, QLineEdit, QPushButton, QVBoxLayout, QHBoxLayout, QLabel, QMessageBox
|
from PyQt5.QtWidgets import QDialog, QFileDialog, QLineEdit, QPushButton, QVBoxLayout, QHBoxLayout, QLabel, QMessageBox
|
||||||
from PyQt5.QtGui import QIcon, QIntValidator
|
from PyQt5.QtGui import QIcon, QIntValidator
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
@ -10,7 +11,7 @@ class Create(QDialog):
|
|||||||
self.setWindowTitle('Create Project')
|
self.setWindowTitle('Create Project')
|
||||||
self.setWindowIcon(QIcon(":/icons/logo.svg"))
|
self.setWindowIcon(QIcon(":/icons/logo.svg"))
|
||||||
|
|
||||||
self.file = str(Settings.General().root)
|
self.file = str(Path(Settings.General().root)/'default')
|
||||||
self.name = '未命名'
|
self.name = '未命名'
|
||||||
self.max_memory = Settings.Project().max_memory
|
self.max_memory = Settings.Project().max_memory
|
||||||
self.cell_size = Settings.Project().cell_size
|
self.cell_size = Settings.Project().cell_size
|
||||||
|
@ -15,9 +15,9 @@ class ResultTable(QtWidgets.QWidget):
|
|||||||
super(ResultTable, self).__init__(parent)
|
super(ResultTable, self).__init__(parent)
|
||||||
# self.tableview = QTableView(self)
|
# self.tableview = QTableView(self)
|
||||||
self.tablewidget = QTableWidget(self)
|
self.tablewidget = QTableWidget(self)
|
||||||
self.tablewidget.setColumnCount(5)
|
self.tablewidget.setColumnCount(4)
|
||||||
self.tablewidget.setRowCount(0)
|
self.tablewidget.setRowCount(0)
|
||||||
self.tablewidget.setHorizontalHeaderLabels(['序号', 'X', 'Y', '概率', '变化'])
|
self.tablewidget.setHorizontalHeaderLabels(['X', 'Y', '概率', '变化'])
|
||||||
self.tablewidget.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
self.tablewidget.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
||||||
|
|
||||||
self.tablewidget.cellDoubleClicked.connect(self.onDoubleClicked)
|
self.tablewidget.cellDoubleClicked.connect(self.onDoubleClicked)
|
||||||
@ -34,27 +34,32 @@ class ResultTable(QtWidgets.QWidget):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def onChanged(self, row, col):
|
def onChanged(self, row, col):
|
||||||
if col == 4:
|
if col == 3:
|
||||||
item_idx = row
|
item_idx = row
|
||||||
item_status = self.tablewidget.item(row, col).checkState() == Qt.Checked
|
item_status = self.tablewidget.item(row, col).checkState() == Qt.Checked
|
||||||
|
if item_status:
|
||||||
|
self.tablewidget.item(row, col).setBackground(Qt.yellow)
|
||||||
|
else:
|
||||||
|
self.tablewidget.item(row, col).setBackground(Qt.green)
|
||||||
self.on_item_changed.emit({'idx':item_idx, 'status':item_status})
|
self.on_item_changed.emit({'idx':item_idx, 'status':item_status})
|
||||||
|
|
||||||
def onClicked(self, row, col):
|
def onClicked(self, row, col):
|
||||||
if col == 4:
|
if col == 3:
|
||||||
self.tablewidget.item(row, col).setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
|
self.tablewidget.item(row, col).setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
|
||||||
|
|
||||||
def onDoubleClicked(self, row, col):
|
def onDoubleClicked(self, row, col):
|
||||||
x = self.tablewidget.item(row, 1).text()
|
x = self.tablewidget.item(row, 0).text()
|
||||||
y = self.tablewidget.item(row, 2).text()
|
y = self.tablewidget.item(row, 1).text()
|
||||||
self.on_item_click.emit({'x':x, 'y':y})
|
self.on_item_click.emit({'x':x, 'y':y})
|
||||||
|
|
||||||
def set_data(self, data:ResultLayer):
|
def set_data(self, data:ResultLayer):
|
||||||
self.tablewidget.setRowCount(len(data.data))
|
self.tablewidget.setRowCount(len(data.data))
|
||||||
|
# print(len(data.data))
|
||||||
|
self.tablewidget.setVerticalHeaderLabels([ str(i+1) for i in range(len(data.data))])
|
||||||
for i, d in enumerate(data.data):
|
for i, d in enumerate(data.data):
|
||||||
self.tablewidget.setItem(i, 0, QTableWidgetItem(str(i+1)))
|
self.tablewidget.setItem(i, 0, QTableWidgetItem(str(d[0]))) # X
|
||||||
self.tablewidget.setItem(i, 1, QTableWidgetItem(str(d[0]))) # X
|
self.tablewidget.setItem(i, 1, QTableWidgetItem(str(d[1]))) # Y
|
||||||
self.tablewidget.setItem(i, 2, QTableWidgetItem(str(d[1]))) # Y
|
self.tablewidget.setItem(i, 2, QTableWidgetItem(str(d[2]))) # 概率
|
||||||
self.tablewidget.setItem(i, 3, QTableWidgetItem(str(d[2]))) # 概率
|
|
||||||
status_item = QTableWidgetItem('变化')
|
status_item = QTableWidgetItem('变化')
|
||||||
if d[3] == 0:
|
if d[3] == 0:
|
||||||
status_item.setBackground(Qt.green)
|
status_item.setBackground(Qt.green)
|
||||||
@ -62,7 +67,7 @@ class ResultTable(QtWidgets.QWidget):
|
|||||||
elif d[3] == 1:
|
elif d[3] == 1:
|
||||||
status_item.setBackground(Qt.yellow)
|
status_item.setBackground(Qt.yellow)
|
||||||
status_item.setCheckState(Qt.Checked)
|
status_item.setCheckState(Qt.Checked)
|
||||||
self.tablewidget.setItem(i, 4, status_item) # 变化
|
self.tablewidget.setItem(i, 3, status_item) # 变化
|
||||||
self.tablewidget.resizeColumnsToContents()
|
self.tablewidget.resizeColumnsToContents()
|
||||||
self.tablewidget.resizeRowsToContents()
|
self.tablewidget.resizeRowsToContents()
|
||||||
self.tablewidget.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
self.tablewidget.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
||||||
|
@ -52,7 +52,8 @@ class Project(QObject):
|
|||||||
self.root = path
|
self.root = path
|
||||||
if name is None:
|
if name is None:
|
||||||
self.file = str(Path(self.root)/'default.prj')
|
self.file = str(Path(self.root)/'default.prj')
|
||||||
|
else:
|
||||||
|
self.file = str(Path(self.root)/name)
|
||||||
if not os.path.exists(self.root):
|
if not os.path.exists(self.root):
|
||||||
os.makedirs(self.root, exist_ok=True)
|
os.makedirs(self.root, exist_ok=True)
|
||||||
if not os.path.exists(self.file):
|
if not os.path.exists(self.file):
|
||||||
|
@ -22,7 +22,7 @@ class Settings(QSettings):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def root(self):
|
def root(self):
|
||||||
_r = './3rd'
|
_r = './plugins'
|
||||||
if not os.path.exists(_r):
|
if not os.path.exists(_r):
|
||||||
os.makedirs(_r)
|
os.makedirs(_r)
|
||||||
return _r
|
return _r
|
||||||
@ -93,12 +93,12 @@ class Settings(QSettings):
|
|||||||
@property
|
@property
|
||||||
def last_path(self):
|
def last_path(self):
|
||||||
with Settings(Settings.General.PRE) as s:
|
with Settings(Settings.General.PRE) as s:
|
||||||
return s.value('last_path', '')
|
return str(s.value('last_path', ''))
|
||||||
|
|
||||||
@last_path.setter
|
@last_path.setter
|
||||||
def last_path(self, value):
|
def last_path(self, value):
|
||||||
with Settings(Settings.General.PRE) as s:
|
with Settings(Settings.General.PRE) as s:
|
||||||
s.setValue('last_path', value)
|
s.setValue('last_path', str(value))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def end_date(self):
|
def end_date(self):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user