#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;
+}
};
$('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");
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();
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;
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 = $('<div>');
+ data.people.forEach((p) => {
+ let item = $('<img>');
+ 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();
+ }
+ }
}
});
}
@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']
--- /dev/null
+# 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,
+ ),
+ ),
+ ]
+from datetime import timedelta
from django.conf import settings
from django.db import models
from django.utils.timezone import now
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
<a class='nav-item nav-link' href="{{ REDMINE_URL }}projects/wl-publikacje/wiki/Pomoc" target="_blank">
{% trans "Help" %}</a>
+ <div id="people" class="nav-item nav-link" ></div>
<div id="user-area">
{% include "registration/head_login.html" %}
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)
@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