diff --git a/bin/lib/chats_viewer.py b/bin/lib/chats_viewer.py index 34ab6157..0654d9e9 100755 --- a/bin/lib/chats_viewer.py +++ b/bin/lib/chats_viewer.py @@ -465,6 +465,22 @@ def get_user_account_nb_all_week_messages(user_id, chats, subchannels): nb_day += 1 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): nb = {} 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) if not user_account.exists(): 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']: meta['chats'] = get_user_account_chats_meta(user_id, meta['chats'], meta['subchannels']) return meta, 200 diff --git a/bin/lib/objects/UsersAccount.py b/bin/lib/objects/UsersAccount.py index 950b2c1c..dea1b7a2 100755 --- a/bin/lib/objects/UsersAccount.py +++ b/bin/lib/objects/UsersAccount.py @@ -132,7 +132,15 @@ class UserAccount(AbstractSubtypeObject): return self._get_timeline_username().get_last_obj_id() 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): self._get_timeline_username().add_timestamp(timestamp, username_global_id) diff --git a/bin/lib/timeline_engine.py b/bin/lib/timeline_engine.py index 58c222f6..5d6ad81e 100755 --- a/bin/lib/timeline_engine.py +++ b/bin/lib/timeline_engine.py @@ -112,9 +112,29 @@ class Timeline: for block in r_meta.zrange(f'line:{self.id}:{self.name}', 0, -1): if block: if block.startswith('start:'): + print(self._get_block_obj_global_id(block[6:])) objs.add(self._get_block_obj_global_id(block[6:])) 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): # objs = {} # last_obj_id = None diff --git a/var/www/blueprints/chats_explorer.py b/var/www/blueprints/chats_explorer.py index fd2f8b34..83126b1e 100644 --- a/var/www/blueprints/chats_explorer.py +++ b/var/www/blueprints/chats_explorer.py @@ -288,6 +288,9 @@ def objects_user_account(): if target == "Don't Translate": target = None 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: return create_json_response(user_account[0], user_account[1]) 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']), 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 @login_required @login_read_only diff --git a/var/www/static/js/d3/chord_directed_diagram.js b/var/www/static/js/d3/chord_directed_diagram.js index 5be3f294..9ffa6a49 100644 --- a/var/www/static/js/d3/chord_directed_diagram.js +++ b/var/www/static/js/d3/chord_directed_diagram.js @@ -8,6 +8,10 @@ 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 if (min_value > 0) { data.data = data.data.filter(function(value) { diff --git a/var/www/static/js/d3/timeline_basic.js b/var/www/static/js/d3/timeline_basic.js new file mode 100644 index 00000000..ce3f2415 --- /dev/null +++ b/var/www/static/js/d3/timeline_basic.js @@ -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}
⏳ ${dateTimeFormat(new Date(d.start))}
⌛ ${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(); +} diff --git a/var/www/templates/chats_explorer/card_user_account.html b/var/www/templates/chats_explorer/card_user_account.html index a892c3ca..ee3fa8b4 100644 --- a/var/www/templates/chats_explorer/card_user_account.html +++ b/var/www/templates/chats_explorer/card_user_account.html @@ -19,7 +19,7 @@ - + @@ -28,7 +28,17 @@ - +
usernameusernames ID First Seen Last Seen
{{ meta['username']['id'] }} + {% if 'usernames' in meta %} +
    + {% for username in meta['usernames'] %} +
  • {{ username['id'] }}
  • + {% endfor %} +
+ {% else %} + {{ meta['username']['id'] }} + {% endif %} +
{{ meta['id'] }} {% if meta['first_seen'] %} diff --git a/var/www/templates/chats_explorer/user_account.html b/var/www/templates/chats_explorer/user_account.html index a1832af3..627c4b87 100644 --- a/var/www/templates/chats_explorer/user_account.html +++ b/var/www/templates/chats_explorer/user_account.html @@ -20,6 +20,7 @@ + @@ -47,6 +48,9 @@

User All Messages:

+

Usernames:

+
+

Numbers of Messages Posted by Chat:

@@ -85,6 +89,13 @@ {# {% 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'] }}") .then(function(data) { create_heatmap_week_hour('#heatmapweekhourall', data);