mirror of https://github.com/zulip/zulip.git
hotspots: Add backend support for tutorial hotspots.
This commit adds the backend support for a new style of tutorial which allows for highlighting of multiple areas of the page with hotspots that disappear when clicked by the user.
This commit is contained in:
parent
33c130a603
commit
6f061beb46
|
@ -119,7 +119,8 @@
|
|||
"emoji_codes": false,
|
||||
"drafts": false,
|
||||
"katex": false,
|
||||
"Clipboard": false
|
||||
"Clipboard": false,
|
||||
"hotspots": false
|
||||
},
|
||||
"rules": {
|
||||
"no-restricted-syntax": 0,
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
var hotspots = (function () {
|
||||
|
||||
var exports = {};
|
||||
|
||||
exports.show = function (hotspot_list) {
|
||||
$('.hotspot').hide();
|
||||
for (var i = 0; i < hotspot_list.length; i += 1) {
|
||||
$("#hotspot_".concat(hotspot_list[i])).show();
|
||||
}
|
||||
};
|
||||
|
||||
exports.initialize = function () {
|
||||
exports.show(page_params.hotspots);
|
||||
};
|
||||
|
||||
function mark_hotspot_as_read(hotspot) {
|
||||
channel.post({
|
||||
url: '/json/users/me/hotspots',
|
||||
data: {hotspot: JSON.stringify(hotspot)},
|
||||
});
|
||||
}
|
||||
|
||||
$(function () {
|
||||
$("#hotspot_welcome").on('click', function (e) {
|
||||
mark_hotspot_as_read("welcome");
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
$("#hotspot_streams").on('click', function (e) {
|
||||
mark_hotspot_as_read("streams");
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
$("#hotspot_topics").on('click', function (e) {
|
||||
mark_hotspot_as_read("topics");
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
$("#hotspot_narrowing").on('click', function (e) {
|
||||
mark_hotspot_as_read("narrowing");
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
$("#hotspot_replying").on('click', function (e) {
|
||||
mark_hotspot_as_read("replying");
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
$("#hotspot_get_started").on('click', function (e) {
|
||||
mark_hotspot_as_read("get_started");
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
});
|
||||
|
||||
return exports;
|
||||
}());
|
||||
if (typeof module !== 'undefined') {
|
||||
module.exports = hotspots;
|
||||
}
|
|
@ -23,6 +23,11 @@ function dispatch_normal_event(event) {
|
|||
admin.update_default_streams_table();
|
||||
break;
|
||||
|
||||
case 'hotspots':
|
||||
hotspots.show(event.hotspots);
|
||||
page_params.hotspots = event.hotspots;
|
||||
break;
|
||||
|
||||
case 'muted_topics':
|
||||
muting_ui.handle_updates(event.muted_topics);
|
||||
break;
|
||||
|
|
|
@ -256,6 +256,7 @@ $(function () {
|
|||
unread_ui.initialize();
|
||||
activity.initialize();
|
||||
emoji.initialize();
|
||||
hotspots.initialize();
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ from zerver.lib.cache import (
|
|||
to_dict_cache_key_id,
|
||||
)
|
||||
from zerver.lib.context_managers import lockfile
|
||||
from zerver.lib.hotspots import get_next_hotspots
|
||||
from zerver.lib.message import (
|
||||
access_message,
|
||||
MessageDict,
|
||||
|
@ -28,7 +29,7 @@ from zerver.lib.message import (
|
|||
)
|
||||
from zerver.lib.realm_icon import realm_icon_url
|
||||
from zerver.models import Realm, RealmEmoji, Stream, UserProfile, UserActivity, RealmAlias, \
|
||||
Subscription, Recipient, Message, Attachment, UserMessage, RealmAuditLog, \
|
||||
Subscription, Recipient, Message, Attachment, UserMessage, RealmAuditLog, UserHotspot, \
|
||||
Client, DefaultStream, UserPresence, Referral, PushDeviceToken, MAX_SUBJECT_LENGTH, \
|
||||
MAX_MESSAGE_LENGTH, get_client, get_stream, get_recipient, get_huddle, \
|
||||
get_user_profile_by_id, PreregistrationUser, get_display_recipient, \
|
||||
|
@ -3174,6 +3175,12 @@ def do_update_muted_topic(user_profile, stream, topic, op):
|
|||
event = dict(type="muted_topics", muted_topics=muted_topics)
|
||||
send_event(event, [user_profile.id])
|
||||
|
||||
def do_mark_hotspot_as_read(user, hotspot):
|
||||
# type: (UserProfile, str) -> None
|
||||
UserHotspot.objects.get_or_create(user=user, hotspot=hotspot)
|
||||
event = dict(type="hotspots", hotspots=get_next_hotspots(user))
|
||||
send_event(event, [user.id])
|
||||
|
||||
def notify_realm_filters(realm):
|
||||
# type: (Realm) -> None
|
||||
realm_filters = realm_filters_for_realm(realm.id)
|
||||
|
|
|
@ -20,6 +20,7 @@ session_engine = import_module(settings.SESSION_ENGINE)
|
|||
from zerver.lib.alert_words import user_alert_words
|
||||
from zerver.lib.attachments import user_attachments
|
||||
from zerver.lib.avatar import get_avatar_url
|
||||
from zerver.lib.hotspots import get_next_hotspots
|
||||
from zerver.lib.narrow import check_supported_events_narrow_filter
|
||||
from zerver.lib.realm_icon import realm_icon_url
|
||||
from zerver.lib.request import JsonableError
|
||||
|
@ -72,6 +73,9 @@ def fetch_initial_state_data(user_profile, event_types, queue_id,
|
|||
if want('attachments'):
|
||||
state['attachments'] = user_attachments(user_profile)
|
||||
|
||||
if want('hotspots'):
|
||||
state['hotspots'] = get_next_hotspots(user_profile)
|
||||
|
||||
if want('message'):
|
||||
# The client should use get_messages() to fetch messages
|
||||
# starting with the max_message_id. They will get messages
|
||||
|
@ -181,6 +185,8 @@ def apply_event(state, event, user_profile, include_subscribers):
|
|||
# type: (Dict[str, Any], Dict[str, Any], UserProfile, bool) -> None
|
||||
if event['type'] == "message":
|
||||
state['max_message_id'] = max(state['max_message_id'], event['message']['id'])
|
||||
elif event['type'] == "hotspots":
|
||||
state['hotspots'] = event['hotspots']
|
||||
elif event['type'] == "pointer":
|
||||
state['pointer'] = max(state['pointer'], event['pointer'])
|
||||
elif event['type'] == "realm_user":
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
from zerver.models import UserProfile, UserHotspot
|
||||
|
||||
from typing import List, Text
|
||||
|
||||
ALL_HOTSPOTS = ['welcome', 'streams', 'topics', 'narrowing', 'replying', 'get_started']
|
||||
def get_next_hotspots(user):
|
||||
# type: (UserProfile) -> List[Text]
|
||||
seen_hotspots = frozenset(UserHotspot.objects.filter(user=user).values_list('hotspot', flat=True))
|
||||
for hotspot in ALL_HOTSPOTS:
|
||||
if hotspot not in seen_hotspots:
|
||||
return [hotspot]
|
||||
return []
|
|
@ -0,0 +1,31 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.5 on 2017-03-28 00:22
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('zerver', '0069_realmauditlog_extra_data'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UserHotspot',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('hotspot', models.CharField(max_length=30)),
|
||||
('timestamp', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='userhotspot',
|
||||
unique_together=set([('user', 'hotspot')]),
|
||||
),
|
||||
]
|
|
@ -1553,3 +1553,11 @@ class RealmAuditLog(models.Model):
|
|||
event_time = models.DateTimeField() # type: datetime.datetime
|
||||
backfilled = models.BooleanField(default=False) # type: bool
|
||||
extra_data = models.TextField(null=True) # type: Text
|
||||
|
||||
class UserHotspot(models.Model):
|
||||
user = models.ForeignKey(UserProfile) # type: UserProfile
|
||||
hotspot = models.CharField(max_length=30) # type: Text
|
||||
timestamp = models.DateTimeField(default=timezone.now) # type: datetime.datetime
|
||||
|
||||
class Meta(object):
|
||||
unique_together = ("user", "hotspot")
|
||||
|
|
|
@ -38,6 +38,7 @@ from zerver.lib.actions import (
|
|||
do_create_user,
|
||||
do_deactivate_stream,
|
||||
do_deactivate_user,
|
||||
do_mark_hotspot_as_read,
|
||||
do_reactivate_user,
|
||||
do_refer_friend,
|
||||
do_regenerate_api_key,
|
||||
|
@ -1451,6 +1452,16 @@ class EventsRegisterTest(ZulipTestCase):
|
|||
error = bot_reactivate_checker('events[1]', events[1])
|
||||
self.assert_on_error(error)
|
||||
|
||||
def test_do_mark_hotspot_as_read(self):
|
||||
# type: () -> None
|
||||
schema_checker = check_dict([
|
||||
('type', equals('hotspots')),
|
||||
('hotspots', check_list(check_string)),
|
||||
])
|
||||
events = self.do_test(lambda: do_mark_hotspot_as_read(self.user_profile, 'welcome'))
|
||||
error = schema_checker('events[0]', events[0])
|
||||
self.assert_on_error(error)
|
||||
|
||||
def test_rename_stream(self):
|
||||
# type: () -> None
|
||||
stream = self.make_stream('old_name')
|
||||
|
|
|
@ -68,6 +68,7 @@ class HomeTest(ZulipTestCase):
|
|||
"furthest_read_time",
|
||||
"has_mobile_devices",
|
||||
"have_initial_messages",
|
||||
"hotspots",
|
||||
"initial_pointer",
|
||||
"initial_presences",
|
||||
"initial_servertime",
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import
|
||||
|
||||
from zerver.lib.actions import do_mark_hotspot_as_read
|
||||
from zerver.lib.hotspots import ALL_HOTSPOTS, get_next_hotspots
|
||||
from zerver.lib.test_classes import ZulipTestCase
|
||||
from zerver.models import UserProfile, UserHotspot
|
||||
from zerver.views.hotspots import mark_hotspot_as_read
|
||||
|
||||
from typing import Any, Dict
|
||||
import ujson
|
||||
|
||||
# Splitting this out, since I imagine this will eventually have most of the
|
||||
# complicated hotspots logic.
|
||||
class TestGetNextHotspots(ZulipTestCase):
|
||||
def test_first_hotspot(self):
|
||||
# type: () -> None
|
||||
user = UserProfile.objects.get(email='hamlet@zulip.com')
|
||||
self.assertEqual(get_next_hotspots(user), ['welcome'])
|
||||
|
||||
def test_some_done_some_not(self):
|
||||
# type: () -> None
|
||||
user = UserProfile.objects.get(email='hamlet@zulip.com')
|
||||
do_mark_hotspot_as_read(user, 'welcome')
|
||||
do_mark_hotspot_as_read(user, 'topics')
|
||||
self.assertEqual(get_next_hotspots(user), ['streams'])
|
||||
|
||||
def test_all_done(self):
|
||||
# type: () -> None
|
||||
user = UserProfile.objects.get(email='hamlet@zulip.com')
|
||||
for hotspot in ALL_HOTSPOTS:
|
||||
do_mark_hotspot_as_read(user, hotspot)
|
||||
self.assertEqual(get_next_hotspots(user), [])
|
||||
|
||||
class TestHotspots(ZulipTestCase):
|
||||
def test_do_mark_hotspot_as_read(self):
|
||||
# type: () -> None
|
||||
user = UserProfile.objects.get(email='hamlet@zulip.com')
|
||||
do_mark_hotspot_as_read(user, 'streams')
|
||||
self.assertEqual(list(UserHotspot.objects.filter(user=user)
|
||||
.values_list('hotspot', flat=True)), ['streams'])
|
||||
|
||||
def test_hotspots_url_endpoint(self):
|
||||
# type: () -> None
|
||||
email = 'hamlet@zulip.com'
|
||||
user = UserProfile.objects.get(email=email)
|
||||
self.login(email)
|
||||
result = self.client_post('/json/users/me/hotspots',
|
||||
{'hotspot': ujson.dumps('welcome')})
|
||||
self.assert_json_success(result)
|
||||
self.assertEqual(list(UserHotspot.objects.filter(user=user)
|
||||
.values_list('hotspot', flat=True)), ['welcome'])
|
|
@ -283,6 +283,7 @@ def home_real(request):
|
|||
'attachments',
|
||||
'default_language',
|
||||
'emoji_alt_code',
|
||||
'hotspots',
|
||||
'last_event_id',
|
||||
'left_side_userlist',
|
||||
'max_icon_file_size',
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from zerver.decorator import has_request_variables, REQ
|
||||
from zerver.lib.actions import do_mark_hotspot_as_read
|
||||
from zerver.lib.hotspots import ALL_HOTSPOTS
|
||||
from zerver.lib.response import json_error, json_success
|
||||
from zerver.lib.validator import check_string
|
||||
from zerver.models import UserProfile
|
||||
|
||||
@has_request_variables
|
||||
def mark_hotspot_as_read(request, user, hotspot=REQ(validator=check_string)):
|
||||
# type: (HttpRequest, UserProfile, str) -> HttpResponse
|
||||
if hotspot not in ALL_HOTSPOTS:
|
||||
return json_error(_('Unknown hotspot: %s') % (hotspot,))
|
||||
do_mark_hotspot_as_read(user, hotspot)
|
||||
return json_success()
|
|
@ -895,6 +895,7 @@ JS_SPECS = {
|
|||
'js/colorspace.js',
|
||||
'js/timerender.js',
|
||||
'js/tutorial.js',
|
||||
'js/hotspots.js',
|
||||
'js/templates.js',
|
||||
'js/upload_widget.js',
|
||||
'js/avatar.js',
|
||||
|
|
|
@ -298,6 +298,10 @@ v1_api_and_json_patterns = [
|
|||
{'PUT': 'zerver.views.user_settings.set_avatar_backend',
|
||||
'DELETE': 'zerver.views.user_settings.delete_avatar_backend'}),
|
||||
|
||||
# users/me/hotspots -> zerver.views.hotspots
|
||||
url(r'^users/me/hotspots$', rest_dispatch,
|
||||
{'POST': 'zerver.views.hotspots.mark_hotspot_as_read'}),
|
||||
|
||||
# settings -> zerver.views.user_settings
|
||||
url(r'^settings/display$', rest_dispatch,
|
||||
{'PATCH': 'zerver.views.user_settings.update_display_settings_backend'}),
|
||||
|
|
Loading…
Reference in New Issue