mirror of https://github.com/CIRCL/AIL-framework
chg: [user-account] chat user-accounts: show usernames list + usernames timeline
parent
ced00d14cb
commit
c7204d5bbd
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
|
@ -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'] %}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in New Issue