From 32ee10aaf4c035a199a96c06bd2506befed5e17f Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Wed, 9 Oct 2024 11:50:47 +0200 Subject: [PATCH] Show active users. --- src/redakcja/static/css/master.css | 10 ++++ src/redakcja/static/js/wiki/loader.js | 12 +++-- src/redakcja/static/js/wiki/wikiapi.js | 31 +++++++++--- src/team/admin.py | 5 +- .../0003_alter_profile_options_and_more.py | 47 +++++++++++++++++++ src/team/models.py | 47 +++++++++++++++---- .../templates/wiki/document_details_base.html | 1 + src/wiki/views.py | 30 ++++++++++-- 8 files changed, 158 insertions(+), 25 deletions(-) create mode 100644 src/team/migrations/0003_alter_profile_options_and_more.py diff --git a/src/redakcja/static/css/master.css b/src/redakcja/static/css/master.css index 9d3744ba..6bdf1905 100644 --- a/src/redakcja/static/css/master.css +++ b/src/redakcja/static/css/master.css @@ -228,3 +228,13 @@ img.tabclose { #source-editor .CodeMirror-wrap pre.CodeMirror-line-like { word-wrap: anywhere; } + + +#people img { + margin-left: -10px; + border-radius: 100%; + opacity: .5; +} +#people img.active { + opacity: 1; +} diff --git a/src/redakcja/static/js/wiki/loader.js b/src/redakcja/static/js/wiki/loader.js index 7ea9b407..e9cf2b1a 100644 --- a/src/redakcja/static/js/wiki/loader.js +++ b/src/redakcja/static/js/wiki/loader.js @@ -113,10 +113,10 @@ $(function() { }; $('body').mousemove(function(e) { - CurrentDocument.active = true; + CurrentDocument.active = new Date(); }); $('body').keydown(function(e) { - CurrentDocument.active = true; + CurrentDocument.active = new Date(); }); console.log("Fetching document's text"); @@ -141,13 +141,15 @@ $(function() { console.log("Initial tab is:", active_tab) $.wiki.switchToTab(active_tab); - /* every minute check for a newer version */ + /* check for a newer version */ + CurrentDocument.checkRevision({outdated: function(){ + $('#header').addClass('out-of-date'); + }}); var revTimer = setInterval(function() { CurrentDocument.checkRevision({outdated: function(){ $('#header').addClass('out-of-date'); - clearInterval(revTimer); }}); - }, 60 * 1000); + }, 5 * 1000); }, failure: function() { $('#loading-overlay').fadeOut(); diff --git a/src/redakcja/static/js/wiki/wikiapi.js b/src/redakcja/static/js/wiki/wikiapi.js index 14b8337e..4a28d630 100644 --- a/src/redakcja/static/js/wiki/wikiapi.js +++ b/src/redakcja/static/js/wiki/wikiapi.js @@ -127,7 +127,7 @@ this.text = null; this.has_local_changes = false; - this.active = true; + this.active = new Date(); this._lock = -1; this._context_lock = -1; this._lock_count = 0; @@ -216,22 +216,41 @@ checkRevision(params) { /* this doesn't modify anything, so no locks */ var self = this; - let active = self.active; - self.active = false; + let active = new Date() - self.active < 30 * 1000; $.ajax({ method: "GET", url: reverse("ajax_document_rev", self.id), data: { 'a': active, + 'new': 1, }, - dataType: 'text', + dataType: 'json', success: function(data) { if (data == '') { if (params.error) params.error(); } - else if (data != self.revision) - params.outdated(); + else { + let people = $('
'); + data.people.forEach((p) => { + let item = $(''); + item.attr('src', p.gravatar), + item.attr( + 'title', + p.name + ' (' + + (p.active ? 'akt.' : 'nieakt.') + + ' od ' + p.since + ')') + if (p.active) { + item.addClass('active'); + } + people.append(item); + }); + $("#people").html(people); + + if (data.rev != self.revision) { + params.outdated(); + } + } } }); } diff --git a/src/team/admin.py b/src/team/admin.py index b8c448a1..6bdca87e 100644 --- a/src/team/admin.py +++ b/src/team/admin.py @@ -22,6 +22,7 @@ admin.site.register(User, CustomUserAdmin) @admin.register(models.Presence) -class ProfileAdmin(admin.ModelAdmin): - list_display = ['user', 'timestamp', 'active'] +class PresenceAdmin(admin.ModelAdmin): + list_display = ['session_key', 'chunk', 'user', 'since', 'until', 'active'] + raw_id_fields = ['chunk', 'user'] diff --git a/src/team/migrations/0003_alter_profile_options_and_more.py b/src/team/migrations/0003_alter_profile_options_and_more.py new file mode 100644 index 00000000..f4e01d31 --- /dev/null +++ b/src/team/migrations/0003_alter_profile_options_and_more.py @@ -0,0 +1,47 @@ +# Generated by Django 4.1.9 on 2024-10-09 10:25 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("team", "0002_profile_approve_by_default"), + ] + + operations = [ + migrations.AlterModelOptions( + name="profile", + options={"verbose_name": "profil", "verbose_name_plural": "profil"}, + ), + migrations.RenameField( + model_name="presence", + old_name="timestamp", + new_name="since", + ), + migrations.AddField( + model_name="presence", + name="session_key", + field=models.CharField(default="", max_length=255), + preserve_default=False, + ), + migrations.AddField( + model_name="presence", + name="until", + field=models.DateTimeField(db_index=True, default="1970-01-01"), + preserve_default=False, + ), + migrations.AlterField( + model_name="presence", + name="user", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/src/team/models.py b/src/team/models.py index 6e53dd3c..9bd4bf5d 100644 --- a/src/team/models.py +++ b/src/team/models.py @@ -1,3 +1,4 @@ +from datetime import timedelta from django.conf import settings from django.db import models from django.utils.timezone import now @@ -16,18 +17,46 @@ class Profile(models.Model): class Presence(models.Model): - user = models.ForeignKey(settings.AUTH_USER_MODEL, models.CASCADE) + GAP_THRESHOLD = 60 + + user = models.ForeignKey(settings.AUTH_USER_MODEL, models.CASCADE, null=True, blank=True) + session_key = models.CharField(max_length=255) chunk = models.ForeignKey('documents.Chunk', models.SET_NULL, blank=True, null=True) - timestamp = models.DateTimeField(auto_now_add=True, db_index=True) + since = models.DateTimeField(auto_now_add=True, db_index=True) + until = models.DateTimeField(db_index=True) active = models.BooleanField() @classmethod - def report(cls, user, chunk, active): - if user.is_anonymous or not hasattr(user, 'profile') or not user.profile.presence: - return - cls.objects.create( + def report(cls, user, session_key, chunk, active): + user = user if not user.is_anonymous else None + report = cls.objects.filter( user=user, + session_key=session_key, + chunk=chunk, + until__gt=now() - timedelta(seconds=cls.GAP_THRESHOLD) + ).order_by('-until').first() + if report is None or report.active != active: + report = cls.objects.create( + user=user, + session_key=session_key, + chunk=chunk, + active=active, + until=now(), + ) + else: + report.until = now() + report.save() + + @classmethod + def get_current(cls, session_key, chunk): + sessions = set() + presences = [] + for p in cls.objects.filter( chunk=chunk, - timestamp=now(), - active=active - ) + until__gt=now() - timedelta(seconds=cls.GAP_THRESHOLD) + ).exclude(session_key=session_key).order_by('-since'): + if p.session_key not in sessions: + sessions.add(p.session_key) + presences.append(p) + presences.reverse() + return presences diff --git a/src/wiki/templates/wiki/document_details_base.html b/src/wiki/templates/wiki/document_details_base.html index 1906f018..88f837c6 100644 --- a/src/wiki/templates/wiki/document_details_base.html +++ b/src/wiki/templates/wiki/document_details_base.html @@ -55,6 +55,7 @@ {% trans "Help" %} +
{% include "registration/head_login.html" %} diff --git a/src/wiki/views.py b/src/wiki/views.py index 47f41f1e..3e6fedbf 100644 --- a/src/wiki/views.py +++ b/src/wiki/views.py @@ -19,11 +19,13 @@ from django.utils.formats import localize from django.utils.translation import gettext as _ from django.views.decorators.http import require_POST, require_GET from django.shortcuts import get_object_or_404, render +from django_gravatar.helpers import get_gravatar_url from sorl.thumbnail import get_thumbnail from documents.models import Book, Chunk import sources.models from . import nice_diff +from team.models import Presence from wiki import forms from wiki.helpers import (JSONResponse, JSONFormInvalid, JSONServerError, ajax_require_permission) @@ -317,12 +319,34 @@ def diff(request, chunk_id): @never_cache def revision(request, chunk_id): + if not request.session.session_key: + return HttpResponseForbidden("Not authorized.") doc = get_object_or_404(Chunk, pk=chunk_id) if not doc.book.accessible(request): return HttpResponseForbidden("Not authorized.") - Presence = apps.get_model('team', 'Presence') - Presence.report(request.user, doc, request.GET.get('a') == 'true') - return http.HttpResponse(str(doc.revision())) + + Presence.report( + request.user, request.session.session_key, + doc, + request.GET.get('a') == 'true' + ) + + # Temporary compat for unreloaded clients. + if not request.GET.get('new'): + return http.HttpResponse(str(doc.revision())) + + return JSONResponse({ + 'rev': doc.revision(), + 'people': list([ + { + 'name': (p.user.first_name + ' ' + p.user.last_name) if p.user is not None else '?', + 'gravatar': get_gravatar_url(p.user.email if p.user is not None else '-', size=26), + 'since': p.since.strftime('%H:%M'), + 'active': p.active, + } + for p in Presence.get_current(request.session.session_key, doc) + ]), + }) @never_cache -- 2.20.1