mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 05:14:40 +01:00
Merge branch 'feature/SyncServer_gui_filtering' into feature/1336-allow-multiselection-in-sync-queue
This commit is contained in:
commit
502f30be12
3 changed files with 668 additions and 152 deletions
|
|
@ -1,4 +1,7 @@
|
|||
from Qt import QtCore
|
||||
import attr
|
||||
import abc
|
||||
import six
|
||||
|
||||
from openpype.lib import PypeLogger
|
||||
|
||||
|
|
@ -20,8 +23,111 @@ ProviderRole = QtCore.Qt.UserRole + 2
|
|||
ProgressRole = QtCore.Qt.UserRole + 4
|
||||
DateRole = QtCore.Qt.UserRole + 6
|
||||
FailedRole = QtCore.Qt.UserRole + 8
|
||||
HeaderNameRole = QtCore.Qt.UserRole + 10
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class AbstractColumnFilter:
|
||||
|
||||
def __init__(self, column_name, dbcon=None):
|
||||
self.column_name = column_name
|
||||
self.dbcon = dbcon
|
||||
self._search_variants = []
|
||||
|
||||
def search_variants(self):
|
||||
"""
|
||||
Returns all flavors of search available for this column,
|
||||
"""
|
||||
return self._search_variants
|
||||
|
||||
@abc.abstractmethod
|
||||
def values(self):
|
||||
"""
|
||||
Returns dict of available values for filter {'label':'value'}
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def prepare_match_part(self, values):
|
||||
"""
|
||||
Prepares format valid for $match part from 'values
|
||||
|
||||
Args:
|
||||
values (dict): {'label': 'value'}
|
||||
Returns:
|
||||
(dict): {'COLUMN_NAME': {'$in': ['val1', 'val2']}}
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class PredefinedSetFilter(AbstractColumnFilter):
|
||||
|
||||
def __init__(self, column_name, values):
|
||||
super().__init__(column_name)
|
||||
self._search_variants = ['checkbox']
|
||||
self._values = values
|
||||
if self._values and \
|
||||
list(self._values.keys())[0] == list(self._values.values())[0]:
|
||||
self._search_variants.append('text')
|
||||
|
||||
def values(self):
|
||||
return {k: v for k, v in self._values.items()}
|
||||
|
||||
def prepare_match_part(self, values):
|
||||
return {'$in': list(values.keys())}
|
||||
|
||||
|
||||
class RegexTextFilter(AbstractColumnFilter):
|
||||
|
||||
def __init__(self, column_name):
|
||||
super().__init__(column_name)
|
||||
self._search_variants = ['text']
|
||||
|
||||
def values(self):
|
||||
return {}
|
||||
|
||||
def prepare_match_part(self, values):
|
||||
""" values = {'text1 text2': 'text1 text2'} """
|
||||
if not values:
|
||||
return {}
|
||||
|
||||
regex_strs = set()
|
||||
text = list(values.keys())[0] # only single key always expected
|
||||
for word in text.split():
|
||||
regex_strs.add('.*{}.*'.format(word))
|
||||
|
||||
return {"$regex": "|".join(regex_strs),
|
||||
"$options": 'i'}
|
||||
|
||||
|
||||
class MultiSelectFilter(AbstractColumnFilter):
|
||||
|
||||
def __init__(self, column_name, values=None, dbcon=None):
|
||||
super().__init__(column_name)
|
||||
self._values = values
|
||||
self.dbcon = dbcon
|
||||
self._search_variants = ['checkbox']
|
||||
|
||||
def values(self):
|
||||
if self._values:
|
||||
return {k: v for k, v in self._values.items()}
|
||||
|
||||
recs = self.dbcon.find({'type': self.column_name}, {"name": 1,
|
||||
"_id": -1})
|
||||
values = {}
|
||||
for item in recs:
|
||||
values[item["name"]] = item["name"]
|
||||
return dict(sorted(values.items(), key=lambda it: it[1]))
|
||||
|
||||
def prepare_match_part(self, values):
|
||||
return {'$in': list(values.keys())}
|
||||
|
||||
|
||||
@attr.s
|
||||
class FilterDefinition:
|
||||
type = attr.ib()
|
||||
values = attr.ib(factory=list)
|
||||
|
||||
def pretty_size(value, suffix='B'):
|
||||
for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:
|
||||
if abs(value) < 1024.0:
|
||||
|
|
|
|||
|
|
@ -56,17 +56,31 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel):
|
|||
"""Returns project"""
|
||||
return self._project
|
||||
|
||||
@property
|
||||
def column_filtering(self):
|
||||
return self._column_filtering
|
||||
|
||||
def rowCount(self, _index):
|
||||
return len(self._data)
|
||||
|
||||
def columnCount(self, _index):
|
||||
def columnCount(self, _index=None):
|
||||
return len(self._header)
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
def headerData(self, section, orientation, role=Qt.DisplayRole):
|
||||
if section >= len(self.COLUMN_LABELS):
|
||||
return
|
||||
|
||||
if role == Qt.DisplayRole:
|
||||
if orientation == Qt.Horizontal:
|
||||
return self.COLUMN_LABELS[section][1]
|
||||
|
||||
if role == lib.HeaderNameRole:
|
||||
if orientation == Qt.Horizontal:
|
||||
return self.COLUMN_LABELS[section][0] # return name
|
||||
|
||||
def get_column(self, index):
|
||||
return self.COLUMN_LABELS[index]
|
||||
|
||||
def get_header_index(self, value):
|
||||
"""
|
||||
Returns index of 'value' in headers
|
||||
|
|
@ -103,7 +117,7 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel):
|
|||
self._rec_loaded = 0
|
||||
|
||||
if not representations:
|
||||
self.query = self.get_default_query(load_records)
|
||||
self.query = self.get_query(load_records)
|
||||
representations = self.dbcon.aggregate(self.query)
|
||||
|
||||
self.add_page_records(self.local_site, self.remote_site,
|
||||
|
|
@ -138,7 +152,7 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel):
|
|||
log.debug("fetchMore")
|
||||
items_to_fetch = min(self._total_records - self._rec_loaded,
|
||||
self.PAGE_SIZE)
|
||||
self.query = self.get_default_query(self._rec_loaded)
|
||||
self.query = self.get_query(self._rec_loaded)
|
||||
representations = self.dbcon.aggregate(self.query)
|
||||
self.beginInsertRows(index,
|
||||
self._rec_loaded,
|
||||
|
|
@ -171,7 +185,7 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel):
|
|||
order = -1
|
||||
|
||||
self.sort = {self.SORT_BY_COLUMN[index]: order, '_id': 1}
|
||||
self.query = self.get_default_query()
|
||||
self.query = self.get_query()
|
||||
# import json
|
||||
# log.debug(json.dumps(self.query, indent=4).\
|
||||
# replace('False', 'false').\
|
||||
|
|
@ -180,16 +194,86 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel):
|
|||
representations = self.dbcon.aggregate(self.query)
|
||||
self.refresh(representations)
|
||||
|
||||
def set_filter(self, word_filter):
|
||||
def set_word_filter(self, word_filter):
|
||||
"""
|
||||
Adds text value filtering
|
||||
|
||||
Args:
|
||||
word_filter (str): string inputted by user
|
||||
"""
|
||||
self.word_filter = word_filter
|
||||
self._word_filter = word_filter
|
||||
self.refresh()
|
||||
|
||||
def get_filters(self):
|
||||
"""
|
||||
Returns all available filter editors per column_name keys.
|
||||
"""
|
||||
filters = {}
|
||||
for column_name, _ in self.COLUMN_LABELS:
|
||||
filter_rec = self.COLUMN_FILTERS.get(column_name)
|
||||
if filter_rec:
|
||||
filter_rec.dbcon = self.dbcon
|
||||
filters[column_name] = filter_rec
|
||||
|
||||
return filters
|
||||
|
||||
def get_column_filter(self, index):
|
||||
"""
|
||||
Returns filter object for column 'index
|
||||
|
||||
Args:
|
||||
index(int): index of column in header
|
||||
|
||||
Returns:
|
||||
(AbstractColumnFilter)
|
||||
"""
|
||||
column_name = self._header[index]
|
||||
|
||||
filter_rec = self.COLUMN_FILTERS.get(column_name)
|
||||
if filter_rec:
|
||||
filter_rec.dbcon = self.dbcon # up-to-date db connection
|
||||
|
||||
return filter_rec
|
||||
|
||||
def set_column_filtering(self, checked_values):
|
||||
"""
|
||||
Sets dictionary used in '$match' part of MongoDB aggregate
|
||||
|
||||
Args:
|
||||
checked_values(dict): key:values ({'status':{1:"Foo",3:"Bar"}}
|
||||
|
||||
Modifies:
|
||||
self._column_filtering : {'status': {'$in': [1, 2, 3]}}
|
||||
"""
|
||||
filtering = {}
|
||||
for column_name, dict_value in checked_values.items():
|
||||
column_f = self.COLUMN_FILTERS.get(column_name)
|
||||
if not column_f:
|
||||
continue
|
||||
column_f.dbcon = self.dbcon
|
||||
filtering[column_name] = column_f.prepare_match_part(dict_value)
|
||||
|
||||
self._column_filtering = filtering
|
||||
|
||||
def get_column_filter_values(self, index):
|
||||
"""
|
||||
Returns list of available values for filtering in the column
|
||||
|
||||
Args:
|
||||
index(int): index of column in header
|
||||
|
||||
Returns:
|
||||
(dict) of value: label shown in filtering menu
|
||||
'value' is used in MongoDB query, 'label' is human readable for
|
||||
menu
|
||||
for some columns ('subset') might be 'value' and 'label' same
|
||||
"""
|
||||
filter_rec = self.get_column_filter(index)
|
||||
if not filter_rec:
|
||||
return {}
|
||||
|
||||
return filter_rec.values()
|
||||
|
||||
def set_project(self, project):
|
||||
"""
|
||||
Changes project, called after project selection is changed
|
||||
|
|
@ -251,7 +335,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
("files_count", "Files"),
|
||||
("files_size", "Size"),
|
||||
("priority", "Priority"),
|
||||
("state", "Status")
|
||||
("status", "Status")
|
||||
]
|
||||
|
||||
DEFAULT_SORT = {
|
||||
|
|
@ -259,18 +343,25 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
"_id": 1
|
||||
}
|
||||
SORT_BY_COLUMN = [
|
||||
"context.asset", # asset
|
||||
"context.subset", # subset
|
||||
"context.version", # version
|
||||
"context.representation", # representation
|
||||
"asset", # asset
|
||||
"subset", # subset
|
||||
"version", # version
|
||||
"representation", # representation
|
||||
"updated_dt_local", # local created_dt
|
||||
"updated_dt_remote", # remote created_dt
|
||||
"files_count", # count of files
|
||||
"files_size", # file size of all files
|
||||
"context.asset", # priority TODO
|
||||
"status" # state
|
||||
"status" # status
|
||||
]
|
||||
|
||||
COLUMN_FILTERS = {
|
||||
'status': lib.PredefinedSetFilter('status', lib.STATUS),
|
||||
'subset': lib.RegexTextFilter('subset'),
|
||||
'asset': lib.RegexTextFilter('asset'),
|
||||
'representation': lib.MultiSelectFilter('representation')
|
||||
}
|
||||
|
||||
refresh_started = QtCore.Signal()
|
||||
refresh_finished = QtCore.Signal()
|
||||
|
||||
|
|
@ -297,7 +388,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
files_count = attr.ib(default=None)
|
||||
files_size = attr.ib(default=None)
|
||||
priority = attr.ib(default=None)
|
||||
state = attr.ib(default=None)
|
||||
status = attr.ib(default=None)
|
||||
path = attr.ib(default=None)
|
||||
|
||||
def __init__(self, sync_server, header, project=None):
|
||||
|
|
@ -307,7 +398,10 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
self._project = project
|
||||
self._rec_loaded = 0
|
||||
self._total_records = 0 # how many documents query actually found
|
||||
self.word_filter = None
|
||||
self._word_filter = None
|
||||
self._column_filtering = {}
|
||||
|
||||
self._word_filter = None
|
||||
|
||||
self._initialized = False
|
||||
if not self._project or self._project == lib.DUMMY_PROJECT:
|
||||
|
|
@ -319,12 +413,10 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
self.local_site = self.sync_server.get_active_site(self.project)
|
||||
self.remote_site = self.sync_server.get_remote_site(self.project)
|
||||
|
||||
self.projection = self.get_default_projection()
|
||||
|
||||
self.sort = self.DEFAULT_SORT
|
||||
|
||||
self.query = self.get_default_query()
|
||||
self.default_query = list(self.get_default_query())
|
||||
self.query = self.get_query()
|
||||
self.default_query = list(self.get_query())
|
||||
|
||||
representations = self.dbcon.aggregate(self.query)
|
||||
self.refresh(representations)
|
||||
|
|
@ -359,9 +451,11 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
|
||||
if role == lib.FailedRole:
|
||||
if header_value == 'local_site':
|
||||
return item.state == lib.STATUS[2] and item.local_progress < 1
|
||||
return item.status == lib.STATUS[2] and \
|
||||
item.local_progress < 1
|
||||
if header_value == 'remote_site':
|
||||
return item.state == lib.STATUS[2] and item.remote_progress < 1
|
||||
return item.status == lib.STATUS[2] and \
|
||||
item.remote_progress < 1
|
||||
|
||||
if role == Qt.DisplayRole:
|
||||
# because of ImageDelegate
|
||||
|
|
@ -397,7 +491,6 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
remote_site)
|
||||
|
||||
for repre in result.get("paginatedResults"):
|
||||
context = repre.get("context").pop()
|
||||
files = repre.get("files", [])
|
||||
if isinstance(files, dict): # aggregate returns dictionary
|
||||
files = [files]
|
||||
|
|
@ -420,17 +513,17 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
avg_progress_local = lib.convert_progress(
|
||||
repre.get('avg_progress_local', '0'))
|
||||
|
||||
if context.get("version"):
|
||||
version = "v{:0>3d}".format(context.get("version"))
|
||||
if repre.get("version"):
|
||||
version = "v{:0>3d}".format(repre.get("version"))
|
||||
else:
|
||||
version = "master"
|
||||
|
||||
item = self.SyncRepresentation(
|
||||
repre.get("_id"),
|
||||
context.get("asset"),
|
||||
context.get("subset"),
|
||||
repre.get("asset"),
|
||||
repre.get("subset"),
|
||||
version,
|
||||
context.get("representation"),
|
||||
repre.get("representation"),
|
||||
local_updated,
|
||||
remote_updated,
|
||||
local_site,
|
||||
|
|
@ -449,7 +542,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
self._data.append(item)
|
||||
self._rec_loaded += 1
|
||||
|
||||
def get_default_query(self, limit=0):
|
||||
def get_query(self, limit=0):
|
||||
"""
|
||||
Returns basic aggregate query for main table.
|
||||
|
||||
|
|
@ -461,7 +554,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
'sync_dt' - same for remote side
|
||||
'local_site' - progress of repr on local side, 1 = finished
|
||||
'remote_site' - progress on remote side, calculates from files
|
||||
'state' -
|
||||
'status' -
|
||||
0 - in progress
|
||||
1 - failed
|
||||
2 - queued
|
||||
|
|
@ -481,7 +574,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
if limit == 0:
|
||||
limit = SyncRepresentationSummaryModel.PAGE_SIZE
|
||||
|
||||
return [
|
||||
aggr = [
|
||||
{"$match": self.get_match_part()},
|
||||
{'$unwind': '$files'},
|
||||
# merge potentially unwinded records back to single per repre
|
||||
|
|
@ -584,16 +677,26 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
'paused_local': {'$sum': '$paused_local'},
|
||||
'updated_dt_local': {'$max': "$updated_dt_local"}
|
||||
}},
|
||||
{"$project": self.projection},
|
||||
{"$sort": self.sort},
|
||||
{
|
||||
{"$project": self.projection}
|
||||
]
|
||||
|
||||
if self.column_filtering:
|
||||
aggr.append(
|
||||
{"$match": self.column_filtering}
|
||||
)
|
||||
|
||||
aggr.extend(
|
||||
[{"$sort": self.sort},
|
||||
{
|
||||
'$facet': {
|
||||
'paginatedResults': [{'$skip': self._rec_loaded},
|
||||
{'$limit': limit}],
|
||||
'totalCount': [{'$count': 'count'}]
|
||||
}
|
||||
}
|
||||
]
|
||||
}]
|
||||
)
|
||||
|
||||
return aggr
|
||||
|
||||
def get_match_part(self):
|
||||
"""
|
||||
|
|
@ -614,22 +717,23 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
'files.sites.name': {'$all': [self.local_site,
|
||||
self.remote_site]}
|
||||
}
|
||||
if not self.word_filter:
|
||||
if not self._word_filter:
|
||||
return base_match
|
||||
else:
|
||||
regex_str = '.*{}.*'.format(self.word_filter)
|
||||
regex_str = '.*{}.*'.format(self._word_filter)
|
||||
base_match['$or'] = [
|
||||
{'context.subset': {'$regex': regex_str, '$options': 'i'}},
|
||||
{'context.asset': {'$regex': regex_str, '$options': 'i'}},
|
||||
{'context.representation': {'$regex': regex_str,
|
||||
'$options': 'i'}}]
|
||||
|
||||
if ObjectId.is_valid(self.word_filter):
|
||||
base_match['$or'] = [{'_id': ObjectId(self.word_filter)}]
|
||||
if ObjectId.is_valid(self._word_filter):
|
||||
base_match['$or'] = [{'_id': ObjectId(self._word_filter)}]
|
||||
|
||||
return base_match
|
||||
|
||||
def get_default_projection(self):
|
||||
@property
|
||||
def projection(self):
|
||||
"""
|
||||
Projection part for aggregate query.
|
||||
|
||||
|
|
@ -639,10 +743,10 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel):
|
|||
(dict)
|
||||
"""
|
||||
return {
|
||||
"context.subset": 1,
|
||||
"context.asset": 1,
|
||||
"context.version": 1,
|
||||
"context.representation": 1,
|
||||
"subset": {"$first": "$context.subset"},
|
||||
"asset": {"$first": "$context.asset"},
|
||||
"version": {"$first": "$context.version"},
|
||||
"representation": {"$first": "$context.representation"},
|
||||
"data.path": 1,
|
||||
"files": 1,
|
||||
'files_count': 1,
|
||||
|
|
@ -721,7 +825,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
|
|||
("remote_site", "Remote site"),
|
||||
("files_size", "Size"),
|
||||
("priority", "Priority"),
|
||||
("state", "Status")
|
||||
("status", "Status")
|
||||
]
|
||||
|
||||
PAGE_SIZE = 30
|
||||
|
|
@ -733,10 +837,15 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
|
|||
"updated_dt_local", # local created_dt
|
||||
"updated_dt_remote", # remote created_dt
|
||||
"size", # remote progress
|
||||
"context.asset", # priority TODO
|
||||
"status" # state
|
||||
"size", # priority TODO
|
||||
"status" # status
|
||||
]
|
||||
|
||||
COLUMN_FILTERS = {
|
||||
'status': lib.PredefinedSetFilter('status', lib.STATUS),
|
||||
'file': lib.RegexTextFilter('file'),
|
||||
}
|
||||
|
||||
refresh_started = QtCore.Signal()
|
||||
refresh_finished = QtCore.Signal()
|
||||
|
||||
|
|
@ -759,7 +868,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
|
|||
remote_progress = attr.ib(default=None)
|
||||
size = attr.ib(default=None)
|
||||
priority = attr.ib(default=None)
|
||||
state = attr.ib(default=None)
|
||||
status = attr.ib(default=None)
|
||||
tries = attr.ib(default=None)
|
||||
error = attr.ib(default=None)
|
||||
path = attr.ib(default=None)
|
||||
|
|
@ -772,9 +881,10 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
|
|||
self._project = project
|
||||
self._rec_loaded = 0
|
||||
self._total_records = 0 # how many documents query actually found
|
||||
self.word_filter = None
|
||||
self._word_filter = None
|
||||
self._id = _id
|
||||
self._initialized = False
|
||||
self._column_filtering = {}
|
||||
|
||||
self.sync_server = sync_server
|
||||
# TODO think about admin mode
|
||||
|
|
@ -784,10 +894,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
|
|||
|
||||
self.sort = self.DEFAULT_SORT
|
||||
|
||||
# in case we would like to hide/show some columns
|
||||
self.projection = self.get_default_projection()
|
||||
|
||||
self.query = self.get_default_query()
|
||||
self.query = self.get_query()
|
||||
representations = self.dbcon.aggregate(self.query)
|
||||
self.refresh(representations)
|
||||
|
||||
|
|
@ -821,9 +928,11 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
|
|||
|
||||
if role == lib.FailedRole:
|
||||
if header_value == 'local_site':
|
||||
return item.state == lib.STATUS[2] and item.local_progress < 1
|
||||
return item.status == lib.STATUS[2] and \
|
||||
item.local_progress < 1
|
||||
if header_value == 'remote_site':
|
||||
return item.state == lib.STATUS[2] and item.remote_progress < 1
|
||||
return item.status == lib.STATUS[2] and \
|
||||
item.remote_progress < 1
|
||||
|
||||
if role == Qt.DisplayRole:
|
||||
# because of ImageDelegate
|
||||
|
|
@ -909,7 +1018,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
|
|||
self._data.append(item)
|
||||
self._rec_loaded += 1
|
||||
|
||||
def get_default_query(self, limit=0):
|
||||
def get_query(self, limit=0):
|
||||
"""
|
||||
Gets query that gets used when no extra sorting, filtering or
|
||||
projecting is needed.
|
||||
|
|
@ -923,7 +1032,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
|
|||
if limit == 0:
|
||||
limit = SyncRepresentationSummaryModel.PAGE_SIZE
|
||||
|
||||
return [
|
||||
aggr = [
|
||||
{"$match": self.get_match_part()},
|
||||
{"$unwind": "$files"},
|
||||
{'$addFields': {
|
||||
|
|
@ -1019,7 +1128,16 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
|
|||
]}
|
||||
]}}
|
||||
}},
|
||||
{"$project": self.projection},
|
||||
{"$project": self.projection}
|
||||
]
|
||||
|
||||
if self.column_filtering:
|
||||
aggr.append(
|
||||
{"$match": self.column_filtering}
|
||||
)
|
||||
print(self.column_filtering)
|
||||
|
||||
aggr.extend([
|
||||
{"$sort": self.sort},
|
||||
{
|
||||
'$facet': {
|
||||
|
|
@ -1028,7 +1146,9 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
|
|||
'totalCount': [{'$count': 'count'}]
|
||||
}
|
||||
}
|
||||
]
|
||||
])
|
||||
|
||||
return aggr
|
||||
|
||||
def get_match_part(self):
|
||||
"""
|
||||
|
|
@ -1038,20 +1158,21 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel):
|
|||
Returns:
|
||||
(dict)
|
||||
"""
|
||||
if not self.word_filter:
|
||||
if not self._word_filter:
|
||||
return {
|
||||
"type": "representation",
|
||||
"_id": self._id
|
||||
}
|
||||
else:
|
||||
regex_str = '.*{}.*'.format(self.word_filter)
|
||||
regex_str = '.*{}.*'.format(self._word_filter)
|
||||
return {
|
||||
"type": "representation",
|
||||
"_id": self._id,
|
||||
'$or': [{'files.path': {'$regex': regex_str, '$options': 'i'}}]
|
||||
}
|
||||
|
||||
def get_default_projection(self):
|
||||
@property
|
||||
def projection(self):
|
||||
"""
|
||||
Projection part for aggregate query.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from functools import partial
|
||||
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
from Qt.QtCore import Qt
|
||||
|
|
@ -14,6 +15,7 @@ from openpype.api import get_local_site_id
|
|||
from openpype.lib import PypeLogger
|
||||
|
||||
from avalon.tools.delegates import pretty_timestamp
|
||||
from avalon.vendor import qtawesome
|
||||
|
||||
from openpype.modules.sync_server.tray.models import (
|
||||
SyncRepresentationSummaryModel,
|
||||
|
|
@ -40,6 +42,8 @@ class SyncProjectListWidget(ProjectListWidget):
|
|||
self.local_site = None
|
||||
self.icons = {}
|
||||
|
||||
self.layout().setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
def validate_context_change(self):
|
||||
return True
|
||||
|
||||
|
|
@ -91,7 +95,6 @@ class SyncProjectListWidget(ProjectListWidget):
|
|||
self.project_name = point_index.data(QtCore.Qt.DisplayRole)
|
||||
|
||||
menu = QtWidgets.QMenu()
|
||||
menu.setStyleSheet(style.load_stylesheet())
|
||||
actions_mapping = {}
|
||||
|
||||
if self.sync_server.is_project_paused(self.project_name):
|
||||
|
|
@ -141,16 +144,16 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
message_generated = QtCore.Signal(str)
|
||||
|
||||
default_widths = (
|
||||
("asset", 220),
|
||||
("subset", 190),
|
||||
("version", 55),
|
||||
("representation", 95),
|
||||
("local_site", 170),
|
||||
("remote_site", 170),
|
||||
("asset", 190),
|
||||
("subset", 170),
|
||||
("version", 60),
|
||||
("representation", 145),
|
||||
("local_site", 160),
|
||||
("remote_site", 160),
|
||||
("files_count", 50),
|
||||
("files_size", 60),
|
||||
("priority", 50),
|
||||
("state", 110)
|
||||
("priority", 70),
|
||||
("status", 110)
|
||||
)
|
||||
|
||||
def __init__(self, sync_server, project=None, parent=None):
|
||||
|
|
@ -162,13 +165,16 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
self.representation_id = None
|
||||
self.site_name = None # to pause/unpause representation
|
||||
|
||||
self.filter = QtWidgets.QLineEdit()
|
||||
self.filter.setPlaceholderText("Filter representations..")
|
||||
self.txt_filter = QtWidgets.QLineEdit()
|
||||
self.txt_filter.setPlaceholderText("Quick filter representations..")
|
||||
self.txt_filter.setClearButtonEnabled(True)
|
||||
self.txt_filter.addAction(qtawesome.icon("fa.filter", color="gray"),
|
||||
QtWidgets.QLineEdit.LeadingPosition)
|
||||
|
||||
self._scrollbar_pos = None
|
||||
|
||||
top_bar_layout = QtWidgets.QHBoxLayout()
|
||||
top_bar_layout.addWidget(self.filter)
|
||||
top_bar_layout.addWidget(self.txt_filter)
|
||||
|
||||
self.table_view = QtWidgets.QTableView()
|
||||
headers = [item[0] for item in self.default_widths]
|
||||
|
|
@ -182,8 +188,6 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
QtWidgets.QAbstractItemView.SelectRows)
|
||||
self.table_view.horizontalHeader().setSortIndicator(
|
||||
-1, Qt.AscendingOrder)
|
||||
self.table_view.setSortingEnabled(True)
|
||||
self.table_view.horizontalHeader().setSortIndicatorShown(True)
|
||||
self.table_view.setAlternatingRowColors(True)
|
||||
self.table_view.verticalHeader().hide()
|
||||
|
||||
|
|
@ -195,32 +199,39 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
delegate = ImageDelegate(self)
|
||||
self.table_view.setItemDelegateForColumn(column, delegate)
|
||||
|
||||
for column_name, width in self.default_widths:
|
||||
idx = model.get_header_index(column_name)
|
||||
self.table_view.setColumnWidth(idx, width)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addLayout(top_bar_layout)
|
||||
layout.addWidget(self.table_view)
|
||||
|
||||
self.table_view.doubleClicked.connect(self._double_clicked)
|
||||
self.filter.textChanged.connect(lambda: model.set_filter(
|
||||
self.filter.text()))
|
||||
self.txt_filter.textChanged.connect(lambda: model.set_word_filter(
|
||||
self.txt_filter.text()))
|
||||
self.table_view.customContextMenuRequested.connect(
|
||||
self._on_context_menu)
|
||||
|
||||
model.refresh_started.connect(self._save_scrollbar)
|
||||
model.refresh_finished.connect(self._set_scrollbar)
|
||||
self.table_view.model().modelReset.connect(self._set_selection)
|
||||
model.modelReset.connect(self._set_selection)
|
||||
|
||||
self.model = model
|
||||
|
||||
self.selection_model = self.table_view.selectionModel()
|
||||
self.selection_model.selectionChanged.connect(self._selection_changed)
|
||||
|
||||
horizontal_header = HorizontalHeader(self)
|
||||
|
||||
self.table_view.setHorizontalHeader(horizontal_header)
|
||||
self.table_view.setSortingEnabled(True)
|
||||
|
||||
for column_name, width in self.default_widths:
|
||||
idx = model.get_header_index(column_name)
|
||||
self.table_view.setColumnWidth(idx, width)
|
||||
|
||||
def _selection_changed(self, _new_selection):
|
||||
index = self.selection_model.currentIndex()
|
||||
self._selected_id = \
|
||||
self.table_view.model().data(index, Qt.UserRole)
|
||||
self.model.data(index, Qt.UserRole)
|
||||
|
||||
def _set_selection(self):
|
||||
"""
|
||||
|
|
@ -229,7 +240,7 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
Keep selection during model refresh.
|
||||
"""
|
||||
if self._selected_id:
|
||||
index = self.table_view.model().get_index(self._selected_id)
|
||||
index = self.model.get_index(self._selected_id)
|
||||
if index and index.isValid():
|
||||
mode = QtCore.QItemSelectionModel.Select | \
|
||||
QtCore.QItemSelectionModel.Rows
|
||||
|
|
@ -241,9 +252,9 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
"""
|
||||
Opens representation dialog with all files after doubleclick
|
||||
"""
|
||||
_id = self.table_view.model().data(index, Qt.UserRole)
|
||||
_id = self.model.data(index, Qt.UserRole)
|
||||
detail_window = SyncServerDetailWindow(
|
||||
self.sync_server, _id, self.table_view.model().project)
|
||||
self.sync_server, _id, self.model.project)
|
||||
detail_window.exec()
|
||||
|
||||
def _on_context_menu(self, point):
|
||||
|
|
@ -254,13 +265,12 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
if not point_index.isValid():
|
||||
return
|
||||
|
||||
self.item = self.table_view.model()._data[point_index.row()]
|
||||
self.item = self.model._data[point_index.row()]
|
||||
self.representation_id = self.item._id
|
||||
log.debug("menu representation _id:: {}".
|
||||
format(self.representation_id))
|
||||
|
||||
menu = QtWidgets.QMenu()
|
||||
menu.setStyleSheet(style.load_stylesheet())
|
||||
actions_mapping = {}
|
||||
actions_kwargs_mapping = {}
|
||||
|
||||
|
|
@ -271,7 +281,7 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
|
||||
for site, progress in {local_site: local_progress,
|
||||
remote_site: remote_progress}.items():
|
||||
project = self.table_view.model().project
|
||||
project = self.model.project
|
||||
provider = self.sync_server.get_provider_for_site(project,
|
||||
site)
|
||||
if provider == 'local_drive':
|
||||
|
|
@ -291,17 +301,17 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
else:
|
||||
self.site_name = remote_site
|
||||
|
||||
if self.item.state in [lib.STATUS[0], lib.STATUS[1]]:
|
||||
if self.item.status in [lib.STATUS[0], lib.STATUS[1]]:
|
||||
action = QtWidgets.QAction("Pause")
|
||||
actions_mapping[action] = self._pause
|
||||
menu.addAction(action)
|
||||
|
||||
if self.item.state == lib.STATUS[3]:
|
||||
if self.item.status == lib.STATUS[3]:
|
||||
action = QtWidgets.QAction("Unpause")
|
||||
actions_mapping[action] = self._unpause
|
||||
menu.addAction(action)
|
||||
|
||||
# if self.item.state == lib.STATUS[1]:
|
||||
# if self.item.status == lib.STATUS[1]:
|
||||
# action = QtWidgets.QAction("Open error detail")
|
||||
# actions_mapping[action] = self._show_detail
|
||||
# menu.addAction(action)
|
||||
|
|
@ -337,10 +347,10 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
if to_run:
|
||||
to_run(**to_run_kwargs)
|
||||
|
||||
self.table_view.model().refresh()
|
||||
self.model.refresh()
|
||||
|
||||
def _pause(self):
|
||||
self.sync_server.pause_representation(self.table_view.model().project,
|
||||
self.sync_server.pause_representation(self.model.project,
|
||||
self.representation_id,
|
||||
self.site_name)
|
||||
self.site_name = None
|
||||
|
|
@ -348,7 +358,7 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
|
||||
def _unpause(self):
|
||||
self.sync_server.unpause_representation(
|
||||
self.table_view.model().project,
|
||||
self.model.project,
|
||||
self.representation_id,
|
||||
self.site_name)
|
||||
self.site_name = None
|
||||
|
|
@ -358,7 +368,7 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
# temporary here for testing, will be removed TODO
|
||||
def _add_site(self):
|
||||
log.info(self.representation_id)
|
||||
project_name = self.table_view.model().project
|
||||
project_name = self.model.project
|
||||
local_site_name = get_local_site_id()
|
||||
try:
|
||||
self.sync_server.add_site(
|
||||
|
|
@ -386,15 +396,15 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
try:
|
||||
local_site = get_local_site_id()
|
||||
self.sync_server.remove_site(
|
||||
self.table_view.model().project,
|
||||
self.model.project,
|
||||
self.representation_id,
|
||||
local_site,
|
||||
True)
|
||||
self.message_generated.emit("Site {} removed".format(local_site))
|
||||
except ValueError as exp:
|
||||
self.message_generated.emit("Error {}".format(str(exp)))
|
||||
self.table_view.model().refresh(
|
||||
load_records=self.table_view.model()._rec_loaded)
|
||||
self.model.refresh(
|
||||
load_records=self.model._rec_loaded)
|
||||
|
||||
def _reset_local_site(self):
|
||||
"""
|
||||
|
|
@ -402,11 +412,11 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
redo of upload/download
|
||||
"""
|
||||
self.sync_server.reset_provider_for_file(
|
||||
self.table_view.model().project,
|
||||
self.model.project,
|
||||
self.representation_id,
|
||||
'local')
|
||||
self.table_view.model().refresh(
|
||||
load_records=self.table_view.model()._rec_loaded)
|
||||
self.model.refresh(
|
||||
load_records=self.model._rec_loaded)
|
||||
|
||||
def _reset_remote_site(self):
|
||||
"""
|
||||
|
|
@ -414,18 +424,18 @@ class SyncRepresentationWidget(QtWidgets.QWidget):
|
|||
redo of upload/download
|
||||
"""
|
||||
self.sync_server.reset_provider_for_file(
|
||||
self.table_view.model().project,
|
||||
self.model.project,
|
||||
self.representation_id,
|
||||
'remote')
|
||||
self.table_view.model().refresh(
|
||||
load_records=self.table_view.model()._rec_loaded)
|
||||
self.model.refresh(
|
||||
load_records=self.model._rec_loaded)
|
||||
|
||||
def _open_in_explorer(self, site):
|
||||
if not self.item:
|
||||
return
|
||||
|
||||
fpath = self.item.path
|
||||
project = self.table_view.model().project
|
||||
project = self.model.project
|
||||
fpath = self.sync_server.get_local_file_path(project,
|
||||
site,
|
||||
fpath)
|
||||
|
|
@ -466,8 +476,8 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget):
|
|||
("local_site", 185),
|
||||
("remote_site", 185),
|
||||
("size", 60),
|
||||
("priority", 25),
|
||||
("state", 110)
|
||||
("priority", 60),
|
||||
("status", 110)
|
||||
)
|
||||
|
||||
def __init__(self, sync_server, _id=None, project=None, parent=None):
|
||||
|
|
@ -482,64 +492,73 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget):
|
|||
|
||||
self._selected_id = None
|
||||
|
||||
self.filter = QtWidgets.QLineEdit()
|
||||
self.filter.setPlaceholderText("Filter representation..")
|
||||
self.txt_filter = QtWidgets.QLineEdit()
|
||||
self.txt_filter.setPlaceholderText("Quick filter representation..")
|
||||
self.txt_filter.setClearButtonEnabled(True)
|
||||
self.txt_filter.addAction(qtawesome.icon("fa.filter", color="gray"),
|
||||
QtWidgets.QLineEdit.LeadingPosition)
|
||||
|
||||
self._scrollbar_pos = None
|
||||
|
||||
top_bar_layout = QtWidgets.QHBoxLayout()
|
||||
top_bar_layout.addWidget(self.filter)
|
||||
top_bar_layout.addWidget(self.txt_filter)
|
||||
|
||||
self.table_view = QtWidgets.QTableView()
|
||||
table_view = QtWidgets.QTableView()
|
||||
headers = [item[0] for item in self.default_widths]
|
||||
|
||||
model = SyncRepresentationDetailModel(sync_server, headers, _id,
|
||||
project)
|
||||
self.table_view.setModel(model)
|
||||
self.table_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.table_view.setSelectionMode(
|
||||
table_view.setModel(model)
|
||||
table_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
table_view.setSelectionMode(
|
||||
QtWidgets.QAbstractItemView.SingleSelection)
|
||||
self.table_view.setSelectionBehavior(
|
||||
table_view.setSelectionBehavior(
|
||||
QtWidgets.QTableView.SelectRows)
|
||||
self.table_view.horizontalHeader().setSortIndicator(-1,
|
||||
Qt.AscendingOrder)
|
||||
self.table_view.setSortingEnabled(True)
|
||||
self.table_view.horizontalHeader().setSortIndicatorShown(True)
|
||||
self.table_view.setAlternatingRowColors(True)
|
||||
self.table_view.verticalHeader().hide()
|
||||
table_view.horizontalHeader().setSortIndicator(-1, Qt.AscendingOrder)
|
||||
table_view.horizontalHeader().setSortIndicatorShown(True)
|
||||
table_view.setAlternatingRowColors(True)
|
||||
table_view.verticalHeader().hide()
|
||||
|
||||
column = self.table_view.model().get_header_index("local_site")
|
||||
column = model.get_header_index("local_site")
|
||||
delegate = ImageDelegate(self)
|
||||
self.table_view.setItemDelegateForColumn(column, delegate)
|
||||
table_view.setItemDelegateForColumn(column, delegate)
|
||||
|
||||
column = self.table_view.model().get_header_index("remote_site")
|
||||
column = model.get_header_index("remote_site")
|
||||
delegate = ImageDelegate(self)
|
||||
self.table_view.setItemDelegateForColumn(column, delegate)
|
||||
|
||||
for column_name, width in self.default_widths:
|
||||
idx = model.get_header_index(column_name)
|
||||
self.table_view.setColumnWidth(idx, width)
|
||||
table_view.setItemDelegateForColumn(column, delegate)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addLayout(top_bar_layout)
|
||||
layout.addWidget(self.table_view)
|
||||
layout.addWidget(table_view)
|
||||
|
||||
self.filter.textChanged.connect(lambda: model.set_filter(
|
||||
self.filter.text()))
|
||||
self.table_view.customContextMenuRequested.connect(
|
||||
self._on_context_menu)
|
||||
self.model = model
|
||||
|
||||
self.selection_model = table_view.selectionModel()
|
||||
self.selection_model.selectionChanged.connect(self._selection_changed)
|
||||
|
||||
horizontal_header = HorizontalHeader(self)
|
||||
|
||||
table_view.setHorizontalHeader(horizontal_header)
|
||||
table_view.setSortingEnabled(True)
|
||||
|
||||
for column_name, width in self.default_widths:
|
||||
idx = model.get_header_index(column_name)
|
||||
table_view.setColumnWidth(idx, width)
|
||||
|
||||
self.table_view = table_view
|
||||
|
||||
self.txt_filter.textChanged.connect(lambda: model.set_word_filter(
|
||||
self.txt_filter.text()))
|
||||
table_view.customContextMenuRequested.connect(self._on_context_menu)
|
||||
|
||||
model.refresh_started.connect(self._save_scrollbar)
|
||||
model.refresh_finished.connect(self._set_scrollbar)
|
||||
self.table_view.model().modelReset.connect(self._set_selection)
|
||||
|
||||
self.selection_model = self.table_view.selectionModel()
|
||||
self.selection_model.selectionChanged.connect(self._selection_changed)
|
||||
model.modelReset.connect(self._set_selection)
|
||||
|
||||
def _selection_changed(self):
|
||||
index = self.selection_model.currentIndex()
|
||||
self._selected_id = self.table_view.model().data(index, Qt.UserRole)
|
||||
self._selected_id = self.model.data(index, Qt.UserRole)
|
||||
|
||||
def _set_selection(self):
|
||||
"""
|
||||
|
|
@ -548,7 +567,7 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget):
|
|||
Keep selection during model refresh.
|
||||
"""
|
||||
if self._selected_id:
|
||||
index = self.table_view.model().get_index(self._selected_id)
|
||||
index = self.model.get_index(self._selected_id)
|
||||
if index and index.isValid():
|
||||
mode = QtCore.QItemSelectionModel.Select | \
|
||||
QtCore.QItemSelectionModel.Rows
|
||||
|
|
@ -576,10 +595,9 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget):
|
|||
if not point_index.isValid():
|
||||
return
|
||||
|
||||
self.item = self.table_view.model()._data[point_index.row()]
|
||||
self.item = self.model._data[point_index.row()]
|
||||
|
||||
menu = QtWidgets.QMenu()
|
||||
menu.setStyleSheet(style.load_stylesheet())
|
||||
actions_mapping = {}
|
||||
actions_kwargs_mapping = {}
|
||||
|
||||
|
|
@ -590,7 +608,7 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget):
|
|||
|
||||
for site, progress in {local_site: local_progress,
|
||||
remote_site: remote_progress}.items():
|
||||
project = self.table_view.model().project
|
||||
project = self.model.project
|
||||
provider = self.sync_server.get_provider_for_site(project,
|
||||
site)
|
||||
if provider == 'local_drive':
|
||||
|
|
@ -604,7 +622,7 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget):
|
|||
actions_kwargs_mapping[action] = {'site': site}
|
||||
menu.addAction(action)
|
||||
|
||||
if self.item.state == lib.STATUS[2]:
|
||||
if self.item.status == lib.STATUS[2]:
|
||||
action = QtWidgets.QAction("Open error detail")
|
||||
actions_mapping[action] = self._show_detail
|
||||
menu.addAction(action)
|
||||
|
|
@ -637,12 +655,12 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget):
|
|||
redo of upload/download
|
||||
"""
|
||||
self.sync_server.reset_provider_for_file(
|
||||
self.table_view.model().project,
|
||||
self.model.project,
|
||||
self.representation_id,
|
||||
'local',
|
||||
self.item._id)
|
||||
self.table_view.model().refresh(
|
||||
load_records=self.table_view.model()._rec_loaded)
|
||||
self.model.refresh(
|
||||
load_records=self.model._rec_loaded)
|
||||
|
||||
def _reset_remote_site(self):
|
||||
"""
|
||||
|
|
@ -650,12 +668,12 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget):
|
|||
redo of upload/download
|
||||
"""
|
||||
self.sync_server.reset_provider_for_file(
|
||||
self.table_view.model().project,
|
||||
self.model.project,
|
||||
self.representation_id,
|
||||
'remote',
|
||||
self.item._id)
|
||||
self.table_view.model().refresh(
|
||||
load_records=self.table_view.model()._rec_loaded)
|
||||
self.model.refresh(
|
||||
load_records=self.model._rec_loaded)
|
||||
|
||||
def _open_in_explorer(self, site):
|
||||
if not self.item:
|
||||
|
|
@ -818,3 +836,274 @@ class SyncRepresentationErrorWindow(QtWidgets.QDialog):
|
|||
|
||||
self.setLayout(body_layout)
|
||||
self.setWindowTitle("Sync Representation Error Detail")
|
||||
|
||||
|
||||
class TransparentWidget(QtWidgets.QWidget):
|
||||
clicked = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, column_name, *args, **kwargs):
|
||||
super(TransparentWidget, self).__init__(*args, **kwargs)
|
||||
self.column_name = column_name
|
||||
# self.setStyleSheet("background: red;")
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
self.clicked.emit(self.column_name)
|
||||
|
||||
super(TransparentWidget, self).mouseReleaseEvent(event)
|
||||
|
||||
|
||||
class HorizontalHeader(QtWidgets.QHeaderView):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(HorizontalHeader, self).__init__(QtCore.Qt.Horizontal, parent)
|
||||
self._parent = parent
|
||||
self.checked_values = {}
|
||||
|
||||
self.setModel(self._parent.model)
|
||||
|
||||
self.setSectionsClickable(True)
|
||||
|
||||
self.menu_items_dict = {}
|
||||
self.menu = None
|
||||
self.header_cells = []
|
||||
self.filter_buttons = {}
|
||||
|
||||
self.filter_icon = qtawesome.icon("fa.filter", color="gray")
|
||||
self.filter_set_icon = qtawesome.icon("fa.filter", color="white")
|
||||
|
||||
self.init_layout()
|
||||
|
||||
self._resetting = False
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
"""Keep model synchronized with parent widget"""
|
||||
return self._parent.model
|
||||
|
||||
def init_layout(self):
|
||||
for column_idx in range(self.model.columnCount()):
|
||||
column_name, column_label = self.model.get_column(column_idx)
|
||||
filter_rec = self.model.get_filters().get(column_name)
|
||||
if not filter_rec:
|
||||
continue
|
||||
|
||||
icon = self.filter_icon
|
||||
button = QtWidgets.QPushButton(icon, "", self)
|
||||
|
||||
button.setFixedSize(24, 24)
|
||||
button.setStyleSheet(
|
||||
"QPushButton::menu-indicator{width:0px;}"
|
||||
"QPushButton{border: none;background: transparent;}")
|
||||
button.clicked.connect(partial(self._get_menu,
|
||||
column_name, column_idx))
|
||||
button.setFlat(True)
|
||||
self.filter_buttons[column_name] = button
|
||||
|
||||
def showEvent(self, event):
|
||||
super(HorizontalHeader, self).showEvent(event)
|
||||
|
||||
for i in range(len(self.header_cells)):
|
||||
cell_content = self.header_cells[i]
|
||||
cell_content.setGeometry(self.sectionViewportPosition(i), 0,
|
||||
self.sectionSize(i) - 1, self.height())
|
||||
|
||||
cell_content.show()
|
||||
|
||||
def _set_filter_icon(self, column_name):
|
||||
button = self.filter_buttons.get(column_name)
|
||||
if button:
|
||||
if self.checked_values.get(column_name):
|
||||
button.setIcon(self.filter_set_icon)
|
||||
else:
|
||||
button.setIcon(self.filter_icon)
|
||||
|
||||
def _reset_filter(self, column_name):
|
||||
"""
|
||||
Remove whole column from filter >> not in $match at all (faster)
|
||||
"""
|
||||
self._resetting = True # mark changes to consume them
|
||||
if self.checked_values.get(column_name) is not None:
|
||||
self.checked_values.pop(column_name)
|
||||
self._set_filter_icon(column_name)
|
||||
self._filter_and_refresh_model_and_menu(column_name, True, True)
|
||||
self._resetting = False
|
||||
|
||||
def _apply_filter(self, column_name, values, state):
|
||||
"""
|
||||
Sets 'values' to specific 'state' (checked/unchecked),
|
||||
sends to model.
|
||||
"""
|
||||
if self._resetting: # event triggered by _resetting, skip it
|
||||
return
|
||||
|
||||
self._update_checked_values(column_name, values, state)
|
||||
self._set_filter_icon(column_name)
|
||||
self._filter_and_refresh_model_and_menu(column_name, True, False)
|
||||
|
||||
def _apply_text_filter(self, column_name, items, line_edit):
|
||||
"""
|
||||
Resets all checkboxes, prefers inserted text.
|
||||
"""
|
||||
le_text = line_edit.text()
|
||||
self._update_checked_values(column_name, items, 0) # reset other
|
||||
if self.checked_values.get(column_name) is not None or \
|
||||
le_text == '':
|
||||
self.checked_values.pop(column_name) # reset during typing
|
||||
|
||||
if le_text:
|
||||
self._update_checked_values(column_name, {le_text: le_text}, 2)
|
||||
self._set_filter_icon(column_name)
|
||||
self._filter_and_refresh_model_and_menu(column_name, True, True)
|
||||
|
||||
def _filter_and_refresh_model_and_menu(self, column_name,
|
||||
model=True, menu=True):
|
||||
"""
|
||||
Refresh model and its content and possibly menu for big changes.
|
||||
"""
|
||||
if model:
|
||||
self.model.set_column_filtering(self.checked_values)
|
||||
self.model.refresh()
|
||||
if menu:
|
||||
self._menu_refresh(column_name)
|
||||
|
||||
def _get_menu(self, column_name, index):
|
||||
"""Prepares content of menu for 'column_name'"""
|
||||
menu = QtWidgets.QMenu(self)
|
||||
filter_rec = self.model.get_filters()[column_name]
|
||||
self.menu_items_dict[column_name] = filter_rec.values()
|
||||
|
||||
# text filtering only if labels same as values, not if codes are used
|
||||
if 'text' in filter_rec.search_variants():
|
||||
line_edit = QtWidgets.QLineEdit(menu)
|
||||
line_edit.setClearButtonEnabled(True)
|
||||
line_edit.addAction(self.filter_icon,
|
||||
QtWidgets.QLineEdit.LeadingPosition)
|
||||
|
||||
line_edit.setFixedHeight(line_edit.height())
|
||||
txt = ""
|
||||
if self.checked_values.get(column_name):
|
||||
txt = list(self.checked_values.get(column_name).keys())[0]
|
||||
line_edit.setText(txt)
|
||||
|
||||
action_le = QtWidgets.QWidgetAction(menu)
|
||||
action_le.setDefaultWidget(line_edit)
|
||||
line_edit.textChanged.connect(
|
||||
partial(self._apply_text_filter, column_name,
|
||||
filter_rec.values(), line_edit))
|
||||
menu.addAction(action_le)
|
||||
menu.addSeparator()
|
||||
|
||||
if 'checkbox' in filter_rec.search_variants():
|
||||
action_all = QtWidgets.QAction("All", self)
|
||||
action_all.triggered.connect(partial(self._reset_filter,
|
||||
column_name))
|
||||
menu.addAction(action_all)
|
||||
|
||||
action_none = QtWidgets.QAction("Unselect all", self)
|
||||
state_unchecked = 0
|
||||
action_none.triggered.connect(partial(self._apply_filter,
|
||||
column_name,
|
||||
filter_rec.values(),
|
||||
state_unchecked))
|
||||
menu.addAction(action_none)
|
||||
menu.addSeparator()
|
||||
|
||||
# nothing explicitly >> ALL implicitly >> first time
|
||||
if self.checked_values.get(column_name) is None:
|
||||
checked_keys = self.menu_items_dict[column_name].keys()
|
||||
else:
|
||||
checked_keys = self.checked_values[column_name]
|
||||
|
||||
for value, label in self.menu_items_dict[column_name].items():
|
||||
checkbox = QtWidgets.QCheckBox(str(label), menu)
|
||||
|
||||
# temp
|
||||
checkbox.setStyleSheet("QCheckBox{spacing: 5px;"
|
||||
"padding:5px 5px 5px 5px;}")
|
||||
if value in checked_keys:
|
||||
checkbox.setChecked(True)
|
||||
|
||||
action = QtWidgets.QWidgetAction(menu)
|
||||
action.setDefaultWidget(checkbox)
|
||||
|
||||
checkbox.stateChanged.connect(partial(self._apply_filter,
|
||||
column_name, {value: label}))
|
||||
menu.addAction(action)
|
||||
|
||||
self.menu = menu
|
||||
|
||||
self._show_menu(index, menu)
|
||||
|
||||
def _show_menu(self, index, menu):
|
||||
"""Shows 'menu' under header column of 'index'"""
|
||||
global_pos_point = self.mapToGlobal(
|
||||
QtCore.QPoint(self.sectionViewportPosition(index), 0))
|
||||
menu.setMinimumWidth(self.sectionSize(index))
|
||||
menu.setMinimumHeight(self.height())
|
||||
menu.exec_(QtCore.QPoint(global_pos_point.x(),
|
||||
global_pos_point.y() + self.height()))
|
||||
|
||||
def _menu_refresh(self, column_name):
|
||||
"""
|
||||
Reset boxes after big change - word filtering or reset
|
||||
"""
|
||||
for action in self.menu.actions():
|
||||
if not isinstance(action, QtWidgets.QWidgetAction):
|
||||
continue
|
||||
|
||||
widget = action.defaultWidget()
|
||||
if not isinstance(widget, QtWidgets.QCheckBox):
|
||||
continue
|
||||
|
||||
if not self.checked_values.get(column_name) or \
|
||||
widget.text() in self.checked_values[column_name].values():
|
||||
widget.setChecked(True)
|
||||
else:
|
||||
widget.setChecked(False)
|
||||
|
||||
def _update_checked_values(self, column_name, values, state):
|
||||
"""
|
||||
Modify dictionary of set values in columns for filtering.
|
||||
|
||||
Modifies 'self.checked_values'
|
||||
"""
|
||||
copy_menu_items = dict(self.menu_items_dict[column_name])
|
||||
checked = self.checked_values.get(column_name, copy_menu_items)
|
||||
set_items = dict(values.items()) # prevent dict change during loop
|
||||
for value, label in set_items.items():
|
||||
if state == 2 and label: # checked
|
||||
checked[value] = label
|
||||
elif state == 0 and checked.get(value):
|
||||
checked.pop(value)
|
||||
|
||||
self.checked_values[column_name] = checked
|
||||
|
||||
def paintEvent(self, event):
|
||||
self._fix_size()
|
||||
super(HorizontalHeader, self).paintEvent(event)
|
||||
|
||||
def _fix_size(self):
|
||||
for column_idx in range(self.model.columnCount()):
|
||||
vis_index = self.visualIndex(column_idx)
|
||||
index = self.logicalIndex(vis_index)
|
||||
section_width = self.sectionSize(index)
|
||||
|
||||
column_name = self.model.headerData(column_idx,
|
||||
QtCore.Qt.Horizontal,
|
||||
lib.HeaderNameRole)
|
||||
button = self.filter_buttons.get(column_name)
|
||||
if not button:
|
||||
continue
|
||||
|
||||
pos_x = self.sectionViewportPosition(
|
||||
index) + section_width - self.height()
|
||||
|
||||
pos_y = 0
|
||||
if button.height() < self.height():
|
||||
pos_y = int((self.height() - button.height()) / 2)
|
||||
button.setGeometry(
|
||||
pos_x,
|
||||
pos_y,
|
||||
self.height(),
|
||||
self.height())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue