chg: [user-account] chat user-accounts: show usernames list + usernames timeline

This commit is contained in:
terrtia 2024-07-09 13:04:18 +02:00
parent ced00d14cb
commit c7204d5bbd
No known key found for this signature in database
GPG key ID: 1E1B1F50D84613D0
8 changed files with 204 additions and 4 deletions

View file

@ -465,6 +465,22 @@ def get_user_account_nb_all_week_messages(user_id, chats, subchannels):
nb_day += 1 nb_day += 1
return stats return stats
def get_user_account_usernames_timeline(subtype, user_id):
user_account = UsersAccount.UserAccount(user_id, subtype)
usernames = user_account.get_usernames_history()
if usernames:
for row in usernames:
row['obj'] = row['obj'].rsplit(':', 1)[1]
if row['start'] > row['end']:
t = row['start']
row['start'] = row['end']
row['end'] = t
if row['start'] == row['end']:
row['end'] = row['end'] + 1
row['start'] = row['start'] * 1000
row['end'] = row['end'] * 1000
return usernames
def get_user_account_chats_chord(subtype, user_id): def get_user_account_chats_chord(subtype, user_id):
nb = {} nb = {}
user_account = UsersAccount.UserAccount(user_id, subtype) user_account = UsersAccount.UserAccount(user_id, subtype)
@ -733,7 +749,7 @@ def api_get_user_account(user_id, instance_uuid, translation_target=None):
user_account = UsersAccount.UserAccount(user_id, instance_uuid) user_account = UsersAccount.UserAccount(user_id, instance_uuid)
if not user_account.exists(): if not user_account.exists():
return {"status": "error", "reason": "Unknown user-account"}, 404 return {"status": "error", "reason": "Unknown user-account"}, 404
meta = user_account.get_meta({'chats', 'icon', 'info', 'subchannels', 'threads', 'translation', 'username', 'username_meta'}, translation_target=translation_target) meta = user_account.get_meta({'chats', 'icon', 'info', 'subchannels', 'threads', 'translation', 'username', 'usernames', 'username_meta'}, translation_target=translation_target)
if meta['chats']: if meta['chats']:
meta['chats'] = get_user_account_chats_meta(user_id, meta['chats'], meta['subchannels']) meta['chats'] = get_user_account_chats_meta(user_id, meta['chats'], meta['subchannels'])
return meta, 200 return meta, 200

View file

@ -132,7 +132,15 @@ class UserAccount(AbstractSubtypeObject):
return self._get_timeline_username().get_last_obj_id() return self._get_timeline_username().get_last_obj_id()
def get_usernames(self): def get_usernames(self):
return self._get_timeline_username().get_objs_ids() usernames = []
names = self._get_timeline_username().get_objs_ids()
for name in names:
_, subtype, obj_id = name.split(':', 2)
usernames.append({'type': 'username', 'subtype': subtype, 'id': obj_id})
return usernames
def get_usernames_history(self):
return self._get_timeline_username().get_objs()
def update_username_timeline(self, username_global_id, timestamp): def update_username_timeline(self, username_global_id, timestamp):
self._get_timeline_username().add_timestamp(timestamp, username_global_id) self._get_timeline_username().add_timestamp(timestamp, username_global_id)

View file

@ -112,9 +112,29 @@ class Timeline:
for block in r_meta.zrange(f'line:{self.id}:{self.name}', 0, -1): for block in r_meta.zrange(f'line:{self.id}:{self.name}', 0, -1):
if block: if block:
if block.startswith('start:'): if block.startswith('start:'):
print(self._get_block_obj_global_id(block[6:]))
objs.add(self._get_block_obj_global_id(block[6:])) objs.add(self._get_block_obj_global_id(block[6:]))
return objs return objs
def get_objs(self):
objs = []
blocks = r_meta.zrange(f'line:{self.id}:{self.name}', 0, -1, withscores=True)
for i in range(0, len(blocks), 2):
block1, score1 = blocks[i]
block2, score2 =blocks[i + 1]
score1 = int(score1)
score2 = int(score2)
if block1.startswith('start:'):
start = score1
end = score2
obj = self._get_block_obj_global_id(block1[6:])
else:
start = score2
end = score1
obj = self._get_block_obj_global_id(block2[6:])
objs.append({'obj': obj, 'start': start, 'end': end})
return objs
# def get_objs_ids(self): # def get_objs_ids(self):
# objs = {} # objs = {}
# last_obj_id = None # last_obj_id = None

View file

@ -288,6 +288,9 @@ def objects_user_account():
if target == "Don't Translate": if target == "Don't Translate":
target = None target = None
user_account = chats_viewer.api_get_user_account(user_id, instance_uuid, translation_target=target) user_account = chats_viewer.api_get_user_account(user_id, instance_uuid, translation_target=target)
# print()
# print(user_account[0]['usernames'])
# print()
if user_account[1] != 200: if user_account[1] != 200:
return create_json_response(user_account[0], user_account[1]) return create_json_response(user_account[0], user_account[1])
else: else:
@ -297,6 +300,15 @@ def objects_user_account():
ail_tags=Tag.get_modal_add_tags(user_account['id'], user_account['type'], user_account['subtype']), ail_tags=Tag.get_modal_add_tags(user_account['id'], user_account['type'], user_account['subtype']),
translation_languages=languages, translation_target=target) translation_languages=languages, translation_target=target)
@chats_explorer.route("/objects/user-account_usernames_timeline_json", methods=['GET']) # TODO API
@login_required
@login_read_only
def objects_user_account_usernames_timeline_json():
subtype = request.args.get('subtype')
user_id = request.args.get('id')
json_graph = chats_viewer.get_user_account_usernames_timeline(subtype, user_id)
return jsonify(json_graph)
@chats_explorer.route("/objects/user-account_chats_chord_json", methods=['GET']) # TODO API @chats_explorer.route("/objects/user-account_chats_chord_json", methods=['GET']) # TODO API
@login_required @login_required
@login_read_only @login_read_only

View file

@ -8,6 +8,10 @@
const create_directed_chord_diagram = (container_id, data, min_value= 0, max_value = -1, fct_mouseover, fct_mouseout, options) => { const create_directed_chord_diagram = (container_id, data, min_value= 0, max_value = -1, fct_mouseover, fct_mouseout, options) => {
if(!Object.keys(data.data).length){
return;
}
// Filter data by value between target and source // Filter data by value between target and source
if (min_value > 0) { if (min_value > 0) {
data.data = data.data.filter(function(value) { data.data = data.data.filter(function(value) {

119
var/www/static/js/d3/timeline_basic.js vendored Normal file
View file

@ -0,0 +1,119 @@
//Requirement: - D3v7
// - jquery
// container_id = #container_id "data": [{"obj": username, "start": 1111111100000, "end": 2222222200000}, ...]
// tooltip = d3 tooltip object
const create_timeline_basic = (container_id, data) => {
if(!Object.keys(data).length){
return;
}
const width = 800;
const height = 100;
const margin = { top: 10, right: 10, bottom: 40, left: 40 };
const colorScale = d3.scaleOrdinal(d3.schemeCategory10)
.domain(data.map(d => d.obj));
const xScale = d3.scaleTime()
.domain([d3.min(data, d => d.start), d3.max(data, d => d.end)])
.range([margin.left, width - margin.right]);
const svg = d3.select(container_id)
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
const tooltip = d3.select("body")
.append("div")
.attr("class", "tooltip_basic_timeline")
.style("opacity", 0)
//d3.select(".tooltip")
.style("position", "absolute")
.style("text-align", "center")
.style("padding", " 8px")
.style("font", "sans-serif")
.style("font-size", "12px")
.style("background", "whitesmoke")
.style("border", "0px")
.style("border-radius", "8px")
.style("pointer-events", "none");
// date-time format
const dateTimeFormat = d3.timeFormat("%Y-%m-%d %H:%M");
svg.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x", d => xScale(d.start))
.attr("y", 20)
.attr("width", d => xScale(d.end) - xScale(d.start))
.attr("height", 20)
.attr("fill", d => colorScale(d.obj))
.on("mouseover", function(event, d) {
tooltip.transition()
.duration(200)
.style("opacity", .9);
tooltip.html(`${d.obj}<br>⏳ ${dateTimeFormat(new Date(d.start))}<br>⌛ ${dateTimeFormat(new Date(d.end))}`)
.style("left", (event.pageX + 5) + "px")
.style("top", (event.pageY - 28) + "px");
})
.on("mousemove", function(event) {
tooltip.style("left", (event.pageX + 5) + "px")
.style("top", (event.pageY - 28) + "px");
})
.on("mouseout", function() {
tooltip.transition()
.duration(500)
.style("opacity", 0);
});
svg.selectAll("text")
.data(data)
.enter()
.append("text")
.attr("x", d => xScale(d.start) + 5)
.attr("y", 35)
.text(d => d.obj)
.attr("fill", "white")
.style("pointer-events", "none");
// Date format
const dateFormat = d3.timeFormat("%Y-%m-%d");
// x-axis
const xAxis = d3.axisBottom(xScale)
.ticks(d3.timeMonth.every(1))
.tickFormat(dateFormat);
svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(xAxis)
.selectAll("text")
.attr("transform", "rotate(-45)")
.style("text-anchor", "end");
const startDate = new Date(d3.min(data, d => d.start));
const endDate = new Date(d3.max(data, d => d.end));
svg.append("text")
.attr("x", margin.left)
.attr("y", height - margin.bottom + 14)
.style("text-anchor", "end")
.style("font-size", "10px")
.attr("transform", `rotate(-90, ${margin.left}, ${height - margin.bottom + 10})`)
.text(dateFormat(startDate));
svg.append("text")
.attr("x", width - margin.right)
.attr("y", height - margin.bottom + 14)
.style("text-anchor", "end")
.style("font-size", "10px")
.attr("transform", `rotate(-90, ${width - margin.right}, ${height - margin.bottom + 10})`)
.text(dateFormat(endDate));
//return svg.node();
}

View file

@ -19,7 +19,7 @@
<table class="table"> <table class="table">
<thead class=""> <thead class="">
<tr> <tr>
<th>username</th> <th>usernames</th>
<th>ID</th> <th>ID</th>
<th>First Seen</th> <th>First Seen</th>
<th>Last Seen</th> <th>Last Seen</th>
@ -28,7 +28,17 @@
</thead> </thead>
<tbody style="font-size: 15px;"> <tbody style="font-size: 15px;">
<tr> <tr>
<td>{{ meta['username']['id'] }}</td> <td>
{% if 'usernames' in meta %}
<ul>
{% for username in meta['usernames'] %}
<li>{{ username['id'] }}</li>
{% endfor %}
</ul>
{% else %}
{{ meta['username']['id'] }}
{% endif %}
</td>
<td>{{ meta['id'] }}</td> <td>{{ meta['id'] }}</td>
<td> <td>
{% if meta['first_seen'] %} {% if meta['first_seen'] %}

View file

@ -20,6 +20,7 @@
<script src="{{ url_for('static', filename='js/d3.v7.min.js') }}"></script> <script src="{{ url_for('static', filename='js/d3.v7.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/d3/heatmap_week_hour.js')}}"></script> <script src="{{ url_for('static', filename='js/d3/heatmap_week_hour.js')}}"></script>
<script src="{{ url_for('static', filename='js/d3/chord_directed_diagram.js')}}"></script> <script src="{{ url_for('static', filename='js/d3/chord_directed_diagram.js')}}"></script>
<script src="{{ url_for('static', filename='js/d3/timeline_basic.js')}}"></script>
</head> </head>
@ -47,6 +48,9 @@
<h4 class="mx-5 mt-2 text-secondary">User All Messages:</h4> <h4 class="mx-5 mt-2 text-secondary">User All Messages:</h4>
<div id="heatmapweekhourall"></div> <div id="heatmapweekhourall"></div>
<h4>Usernames:</h4>
<div id="timeline_user_usernames" style="max-width: 900px"></div>
<h4>Numbers of Messages Posted by Chat:</h4> <h4>Numbers of Messages Posted by Chat:</h4>
<div id="chord_user_chats" style="max-width: 900px"></div> <div id="chord_user_chats" style="max-width: 900px"></div>
@ -85,6 +89,13 @@
{# {% endif %}#} {# {% endif %}#}
}); });
let url_t = "{{ url_for('chats_explorer.objects_user_account_usernames_timeline_json') }}?subtype={{ meta["subtype"] }}&id={{ meta["id"] }}"
d3.json(url_t)
.then(function(data) {
create_timeline_basic('#timeline_user_usernames', data);
});
d3.json("{{ url_for('chats_explorer.user_account_messages_stats_week_all') }}?subtype={{ meta['subtype'] }}&id={{ meta['id'] }}") d3.json("{{ url_for('chats_explorer.user_account_messages_stats_week_all') }}?subtype={{ meta['subtype'] }}&id={{ meta['id'] }}")
.then(function(data) { .then(function(data) {
create_heatmap_week_hour('#heatmapweekhourall', data); create_heatmap_week_hour('#heatmapweekhourall', data);