Skip to content

Commit

Permalink
Adds user suggestions in comment section (#4094)
Browse files Browse the repository at this point in the history
* Adds user suggestions in comment section
* Uses codemirror and show-hint addon to show autocomplete users
* Use markdown mode with show hint over multiple different textarea
* Adds API and selenium tests for user mentions
* Modify some logics for showing user suggestions
* Using async method for showing hint

Fixes #2964

Co-authored-by: Michal Čihař <michal@cihar.com>
  • Loading branch information
SaptakS and nijel committed Jul 8, 2020
1 parent c9d3bcc commit 898beec
Show file tree
Hide file tree
Showing 17 changed files with 11,685 additions and 14 deletions.
7 changes: 7 additions & 0 deletions scripts/yarn-update
Expand Up @@ -27,6 +27,13 @@ cp node_modules/js-cookie/src/js.cookie.js ../../weblate/static/vendor/
# jQuery
cp node_modules/jquery/dist/jquery.js ../../weblate/static/vendor/

# Codemirror
cp node_modules/codemirror/lib/codemirror.css ../../weblate/static/vendor/codemirror/lib/
cp node_modules/codemirror/lib/codemirror.js ../../weblate/static/vendor/codemirror/lib/
cp node_modules/codemirror/addon/hint/show-hint.css ../../weblate/static/vendor/codemirror/addon/hint/
cp node_modules/codemirror/addon/hint/show-hint.js ../../weblate/static/vendor/codemirror/addon/hint/
cp node_modules/codemirror/mode/markdown/markdown.js ../../weblate/static/vendor/codemirror/mode/markdown/

# Clipboard
cp node_modules/clipboard/dist/clipboard.js ../../weblate/static/vendor/

Expand Down
7 changes: 4 additions & 3 deletions scripts/yarn/package.json
Expand Up @@ -8,20 +8,21 @@
"build": "npm run build:modernizr"
},
"dependencies": {
"@sentry/browser": "5.19.1",
"autosize": "^4.0.0",
"bootstrap-datepicker": "^1.9.0",
"bootstrap-rtl": "^3.3.4",
"clipboard": "^2.0.6",
"codemirror": "^5.55.0",
"dejavu-fonts-ttf": "2.37.3",
"jquery": "^3.5.1",
"js-cookie": "^2.2.1",
"modernizr": "^3.11.3",
"mousetrap": "^1.6.5",
"mousetrap-global-bind": "^1.1.0",
"multi.js": "^0.5.0",
"slugify": "^1.4.4",
"dejavu-fonts-ttf": "2.37.3",
"source-sans-pro": "3.6.0",
"source-code-pro": "2.30.2",
"@sentry/browser": "5.19.1"
"source-sans-pro": "3.6.0"
}
}
5 changes: 5 additions & 0 deletions scripts/yarn/yarn.lock
Expand Up @@ -126,6 +126,11 @@ cliui@^6.0.0:
strip-ansi "^6.0.0"
wrap-ansi "^6.2.0"

codemirror@^5.55.0:
version "5.55.0"
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.55.0.tgz#23731f641288f202a6858fdc878f3149e0e04363"
integrity sha512-TumikSANlwiGkdF/Blnu/rqovZ0Y3Jh8yy9TqrPbSM0xxSucq3RgnpVDQ+mD9q6JERJEIT2FMuF/fBGfkhIR/g==

color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
Expand Down
11 changes: 10 additions & 1 deletion weblate/api/serializers.py
Expand Up @@ -161,7 +161,7 @@ def create(self, validated_data):
return language


class UserSerializer(serializers.ModelSerializer):
class FullUserSerializer(serializers.ModelSerializer):
groups = serializers.HyperlinkedIdentityField(
view_name="api:group-detail", lookup_field="id", many=True, read_only=True,
)
Expand Down Expand Up @@ -189,6 +189,15 @@ class Meta:
}


class BasicUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = (
"full_name",
"username",
)


class PermissionSerializer(serializers.RelatedField):
class Meta:
model = Permission
Expand Down
8 changes: 7 additions & 1 deletion weblate/api/tests.py
Expand Up @@ -123,10 +123,12 @@ def do_request(
class UserAPITest(APIBaseTest):
def test_list(self):
response = self.client.get(reverse("api:user-list"))
self.assertEqual(response.data["count"], 1)
self.assertEqual(response.data["count"], 2)
self.assertFalse("email" in response.data["results"][0])
self.authenticate(True)
response = self.client.get(reverse("api:user-list"))
self.assertEqual(response.data["count"], 2)
self.assertIsNotNone(response.data["results"][0]["email"])

def test_get(self):
response = self.do_request(
Expand All @@ -138,6 +140,10 @@ def test_get(self):
)
self.assertEqual(response.data["username"], "apitest")

def test_filter(self):
response = self.client.get(reverse("api:user-list"), {"username": "api"})
self.assertEqual(response.data["count"], 1)

def test_create(self):
self.do_request(
"api:user-list", method="post", code=403,
Expand Down
25 changes: 19 additions & 6 deletions weblate/api/views.py
Expand Up @@ -44,9 +44,11 @@
from weblate.accounts.models import Subscription
from weblate.accounts.utils import remove_user
from weblate.api.serializers import (
BasicUserSerializer,
ChangeSerializer,
ComponentListSerializer,
ComponentSerializer,
FullUserSerializer,
GroupSerializer,
LanguageSerializer,
LockRequestSerializer,
Expand All @@ -62,7 +64,6 @@
TranslationSerializer,
UnitSerializer,
UploadRequestSerializer,
UserSerializer,
)
from weblate.auth.models import Group, Role, User
from weblate.checks.models import Check
Expand Down Expand Up @@ -264,17 +265,29 @@ def repository(self, request, **kwargs):
return Response(data)


class UserFilter(filters.FilterSet):
username = filters.CharFilter(field_name="username", lookup_expr="startswith")

class Meta:
model = User
fields = ["username"]


class UserViewSet(viewsets.ModelViewSet):
"""Users API."""

queryset = User.objects.none()
serializer_class = UserSerializer
lookup_field = "username"
filter_backends = (filters.DjangoFilterBackend,)
filterset_class = UserFilter

def get_queryset(self):
def get_serializer_class(self):
if self.request.user.has_perm("user.edit"):
return User.objects.order_by("id")
return User.objects.filter(pk=self.request.user.pk).order_by("id")
return FullUserSerializer
return BasicUserSerializer

def get_queryset(self):
return User.objects.order_by("id")

def perm_check(self, request):
if not request.user.has_perm("user.edit"):
Expand Down Expand Up @@ -313,7 +326,7 @@ def groups(self, request, **kwargs):
)

obj.groups.add(group)
serializer = self.serializer_class(obj, context={"request": request})
serializer = self.get_serializer_class()(obj, context={"request": request})

return Response(serializer.data, status=status.HTTP_200_OK)

Expand Down
86 changes: 86 additions & 0 deletions weblate/static/loader-codemirror.js
@@ -0,0 +1,86 @@
(function (CodeMirror) {
var currentRequest = null;

function getUserList(usernameSearch, from, to, callback) {
currentRequest = $.ajax({
type: 'GET',
url: `/api/users/?username=${usernameSearch}`,
dataType: 'json',
beforeSend : function() {
if (currentRequest !== null) {
currentRequest.abort();
}
},
success: function (data) {
var userMentionList = data.results.map(function(user) {
return {
text: user.username,
displayText: `${user.full_name} (${user.username})`
}
});
callback({
list: userMentionList,
from: from,
to: to
});
},
error: function (jqXHR, textStatus, errorThrown) {
console.error(errorThrown);
}
});
}

CodeMirror.registerHelper('hint', 'userSuggestions', function (editor, callback) {
var cur = editor.getCursor();
var curLine = editor.getLine(cur.line);

var end = cur.ch;
var start = curLine.lastIndexOf('@') + 1;
// Extract the current word from the current line using 'start' / 'end' value pair
var curWord = start !== end && curLine.slice(start, end);

if(curWord && curWord.length > 2) {
// If there is current word set, We can filter out users from the
// main list and display them
getUserList(
curWord,
CodeMirror.Pos(cur.line, start),
CodeMirror.Pos(cur.line, end),
callback
);
}
});

CodeMirror.hint.userSuggestions.async = true;


$('textarea.codemirror-markdown').each(function(idx) {

var codemirror = CodeMirror.fromTextArea(
this,
{
mode: 'text/x-markdown',
theme: 'weblate',
lineNumbers: false,
}
);

codemirror.on('change', function (cm, event) {
cm.save()
});

codemirror.on('keydown', function (cm, event) {
if(event.key === '@') {
CodeMirror.showHint(cm, CodeMirror.hint.userSuggestions, {
completeSingle: false
});
}
});
});

// Add weblate bootstrap styling on the textarea
$('.CodeMirror').addClass('form-control');
$('.CodeMirror textarea').addClass('form-control');


}) (CodeMirror);
5 changes: 5 additions & 0 deletions weblate/static/style-bootstrap.css
Expand Up @@ -223,6 +223,11 @@ textarea#id_comment {
#id_check_flags {
height: 4em;
}
div.CodeMirror.cm-s-weblate {
height: 5em;
color: #555555;
font-family: inherit;
}

.hlcheck {
border: 1px dotted #aeb7cf;
Expand Down
36 changes: 36 additions & 0 deletions weblate/static/vendor/codemirror/addon/hint/show-hint.css
@@ -0,0 +1,36 @@
.CodeMirror-hints {
position: absolute;
z-index: 10;
overflow: hidden;
list-style: none;

margin: 0;
padding: 2px;

-webkit-box-shadow: 2px 3px 5px rgba(0,0,0,.2);
-moz-box-shadow: 2px 3px 5px rgba(0,0,0,.2);
box-shadow: 2px 3px 5px rgba(0,0,0,.2);
border-radius: 3px;
border: 1px solid silver;

background: white;
font-size: 90%;
font-family: monospace;

max-height: 20em;
overflow-y: auto;
}

.CodeMirror-hint {
margin: 0;
padding: 0 4px;
border-radius: 2px;
white-space: pre;
color: black;
cursor: pointer;
}

li.CodeMirror-hint-active {
background: #08f;
color: white;
}

0 comments on commit 898beec

Please sign in to comment.