We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Improvement: show location for texts in screenshot management
### Steps to reproduce

1. Add a new screenshot
2. Search for a text string
3. See multiple identical results

### Actual behaviour

Sometimes you have the exact same text in multiple places in your project, but the translations can differ depending on the context. This is currently not supported very well in screenshot management, resulting in impossible to make choices like this:

![image](https://user-images.githubusercontent.com/199657/43308724-c35488ca-9182-11e8-8753-f0c887038937.png)

### Expected behaviour

There should be some way to see differentiate the otherwise identical results in the source string list. Showing the context (where possible; I'm not sure any format other than gettext supports it?) and comments would help. A tooltip or something to show locations would also be very useful.

### Server configuration and status

Currently running the `weblate/weblate:2.20-1` docker image:

```
Postgres is up
 * Weblate 2.20
 * Python 2.7.13
 * Django 1.11.12
 * six 1.10.0
 * social-auth-core 1.7.0
 * social-auth-app-django 2.1.0
 * django-appconf 1.0.2
 * Translate Toolkit 2.3.0
 * Whoosh 2.7.4
 * defusedxml 0.5.0
 * Git 2.11.0
 * Pillow (PIL) 1.1.7
 * dateutil 2.5.3
 * lxml 3.7.1
 * django-crispy-forms 1.7.2
 * compressor 2.2
 * djangorestframework 3.8.1
 * user-agents 1.1.0
 * pytz 2018.3
 * pyuca N/A
 * pyLibravatar N/A
 * PyYAML 3.12
 * tesserocr 2.2.2
 * Mercurial 4.0
 * git-svn 2.11.0
 * Database backends: django.db.backends.postgresql_psycopg2
```




<bountysource-plugin>

---
Want to back this issue? **[Post a bounty on it!](https://www.bountysource.com/issues/61401631-improvement-show-location-for-texts-in-screenshot-management?utm_campaign=plugin&utm_content=tracker%2F253393&utm_medium=issues&utm_source=github)** We accept bounties via [Bountysource](https://www.bountysource.com/?utm_campaign=plugin&utm_content=tracker%2F253393&utm_medium=issues&utm_source=github).
</bountysource-plugin>

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `weblate/screenshots/views.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 #
3 # Copyright © 2012 - 2018 Michal Čihař <michal@cihar.com>
4 #
5 # This file is part of Weblate <https://weblate.org/>
6 #
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
19 #
20 
21 import difflib
22 
23 from django.contrib.auth.decorators import login_required
24 from django.core.exceptions import PermissionDenied
25 from django.http import JsonResponse
26 from django.utils.translation import ugettext as _
27 from django.views.decorators.http import require_POST
28 from django.views.generic import ListView, DetailView
29 from django.shortcuts import get_object_or_404, redirect, render
30 
31 from PIL import Image
32 
33 try:
34     from tesserocr import PyTessBaseAPI, RIL
35     HAS_OCR = True
36 except ImportError:
37     HAS_OCR = False
38 
39 from weblate.screenshots.forms import ScreenshotForm
40 from weblate.screenshots.models import Screenshot
41 from weblate.trans.models import Source
42 from weblate.utils import messages
43 from weblate.utils.views import ComponentViewMixin
44 
45 
46 def try_add_source(request, obj):
47     if 'source' not in request.POST or not request.POST['source'].isdigit():
48         return False
49 
50     try:
51         source = Source.objects.get(
52             pk=request.POST['source'],
53             component=obj.component
54         )
55         obj.sources.add(source)
56         return True
57     except Source.DoesNotExist:
58         return False
59 
60 
61 class ScreenshotList(ListView, ComponentViewMixin):
62     paginate_by = 25
63     model = Screenshot
64     _add_form = None
65 
66     def get_queryset(self):
67         self.kwargs['component'] = self.get_component()
68         return Screenshot.objects.filter(component=self.kwargs['component'])
69 
70     def get_context_data(self):
71         result = super(ScreenshotList, self).get_context_data()
72         component = self.kwargs['component']
73         result['object'] = component
74         if self.request.user.has_perm('screenshot.add', component):
75             if self._add_form is not None:
76                 result['add_form'] = self._add_form
77             else:
78                 result['add_form'] = ScreenshotForm()
79         return result
80 
81     def post(self, request, **kwargs):
82         component = self.get_component()
83         if not request.user.has_perm('screenshot.add', component):
84             raise PermissionDenied()
85         self._add_form = ScreenshotForm(request.POST, request.FILES)
86         if self._add_form.is_valid():
87             obj = Screenshot.objects.create(
88                 component=component,
89                 user=request.user,
90                 **self._add_form.cleaned_data
91             )
92             request.user.profile.uploaded += 1
93             request.user.profile.save(update_fields=['uploaded'])
94 
95             try_add_source(request, obj)
96             messages.success(
97                 request,
98                 _(
99                     'Screenshot has been uploaded, '
100                     'you can now assign it to source strings.'
101                 )
102             )
103             return redirect(obj)
104         messages.error(
105             request,
106             _('Failed to upload screenshot, please fix errors below.')
107         )
108         return self.get(request, **kwargs)
109 
110 
111 class ScreenshotDetail(DetailView):
112     model = Screenshot
113     _edit_form = None
114 
115     def get_object(self, *args, **kwargs):
116         obj = super(ScreenshotDetail, self).get_object(*args, **kwargs)
117         self.request.user.check_access(obj.component.project)
118         return obj
119 
120     def get_context_data(self, **kwargs):
121         result = super(ScreenshotDetail, self).get_context_data(**kwargs)
122         component = result['object'].component
123         if self.request.user.has_perm('screenshot.edit', component):
124             if self._edit_form is not None:
125                 result['edit_form'] = self._edit_form
126             else:
127                 result['edit_form'] = ScreenshotForm(instance=result['object'])
128         return result
129 
130     def post(self, request, **kwargs):
131         obj = self.get_object()
132         if request.user.has_perm('screenshot.edit', obj.component):
133             self._edit_form = ScreenshotForm(
134                 request.POST, request.FILES, instance=obj
135             )
136             if self._edit_form.is_valid():
137                 if request.FILES:
138                     obj.user = request.user
139                     request.user.profile.uploaded += 1
140                     request.user.profile.save(update_fields=['uploaded'])
141                 self._edit_form.save()
142             else:
143                 return self.get(request, **kwargs)
144         return redirect(obj)
145 
146 
147 @require_POST
148 @login_required
149 def delete_screenshot(request, pk):
150     obj = get_object_or_404(Screenshot, pk=pk)
151     request.user.check_access(obj.component.project)
152     if not request.user.has_perm('screenshot.delete', obj.component):
153         raise PermissionDenied()
154 
155     kwargs = {
156         'project': obj.component.project.slug,
157         'component': obj.component.slug,
158     }
159 
160     obj.delete()
161 
162     messages.success(request, _('Screenshot %s has been deleted.') % obj.name)
163 
164     return redirect('screenshots', **kwargs)
165 
166 
167 def get_screenshot(request, pk):
168     obj = get_object_or_404(Screenshot, pk=pk)
169     request.user.check_access(obj.component.project)
170     if not request.user.has_perm('screenshot.edit', obj.component):
171         raise PermissionDenied()
172     return obj
173 
174 
175 @require_POST
176 @login_required
177 def remove_source(request, pk):
178     obj = get_screenshot(request, pk)
179 
180     obj.sources.remove(request.POST['source'])
181 
182     messages.success(request, _('Source has been removed.'))
183 
184     return redirect(obj)
185 
186 
187 def search_results(code, obj, units=None):
188     if units is None:
189         units = []
190     else:
191         units = units.exclude(
192             id_hash__in=obj.sources.values_list('id_hash', flat=True)
193         )
194 
195     results = [
196         {'text': unit.get_source_plurals()[0], 'pk': unit.source_info.pk}
197         for unit in units
198     ]
199 
200     return JsonResponse(
201         data={'responseCode': code, 'results': results}
202     )
203 
204 
205 @login_required
206 @require_POST
207 def search_source(request, pk):
208     obj = get_screenshot(request, pk)
209     try:
210         translation = obj.component.translation_set.all()[0]
211     except IndexError:
212         return search_results(500, obj)
213 
214     units = translation.unit_set.search(
215         {
216             'search': 'substring',
217             'q': request.POST.get('q', ''),
218             'type': 'all',
219             'source': True,
220             'context': True,
221         },
222         translation=translation,
223     )
224 
225     return search_results(200, obj, units)
226 
227 
228 def ocr_extract(api, image, strings):
229     """Extract closes matches from an image"""
230     api.SetImage(image)
231     for item in api.GetComponentImages(RIL.TEXTLINE, True):
232         api.SetRectangle(
233             item[1]['x'], item[1]['y'], item[1]['w'], item[1]['h']
234         )
235         ocr_result = api.GetUTF8Text()
236         parts = [ocr_result] + ocr_result.split('|') + ocr_result.split()
237         for part in parts:
238             for match in difflib.get_close_matches(part, strings, cutoff=0.9):
239                 yield match
240 
241 
242 @login_required
243 @require_POST
244 def ocr_search(request, pk):
245     obj = get_screenshot(request, pk)
246     if not HAS_OCR:
247         return search_results(500, obj)
248     try:
249         translation = obj.component.translation_set.all()[0]
250     except IndexError:
251         return search_results(500, obj)
252 
253     # Load image
254     original_image = Image.open(obj.image.path)
255     # Convert to greyscale
256     original_image = original_image.convert("L")
257     # Resize image (tesseract works best around 300dpi)
258     scaled_image = original_image.copy().resize(
259         [size * 4 for size in original_image.size],
260         Image.BICUBIC
261     )
262 
263     # Find all our strings
264     sources = dict(translation.unit_set.values_list('source', 'pk'))
265     strings = tuple(sources.keys())
266 
267     results = set()
268 
269     # Extract and match strings
270     with PyTessBaseAPI() as api:
271         for image in (original_image, scaled_image):
272             for match in ocr_extract(api, image, strings):
273                 results.add(sources[match])
274 
275     return search_results(
276         200,
277         obj,
278         translation.unit_set.filter(pk__in=results)
279     )
280 
281 
282 @login_required
283 @require_POST
284 def add_source(request, pk):
285     obj = get_screenshot(request, pk)
286     result = try_add_source(request, obj)
287     return JsonResponse(
288         data={'responseCode': 200, 'status': result}
289     )
290 
291 
292 @login_required
293 def get_sources(request, pk):
294     obj = get_screenshot(request, pk)
295     return render(
296         request, 'screenshots/screenshot_sources_body.html',
297         {'sources': obj.sources.all(), 'object': obj}
298     )
299
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/weblate/screenshots/views.py b/weblate/screenshots/views.py
--- a/weblate/screenshots/views.py
+++ b/weblate/screenshots/views.py
@@ -193,7 +193,11 @@
         )
 
     results = [
-        {'text': unit.get_source_plurals()[0], 'pk': unit.source_info.pk}
+        {'text': unit.get_source_plurals()[0],
+         'pk': unit.source_info.pk,
+         'context': unit.context,
+         'location': unit.location}
+
         for unit in units
     ]

{"golden_diff": "diff --git a/weblate/screenshots/views.py b/weblate/screenshots/views.py\n--- a/weblate/screenshots/views.py\n+++ b/weblate/screenshots/views.py\n@@ -193,7 +193,11 @@\n         )\n \n     results = [\n-        {'text': unit.get_source_plurals()[0], 'pk': unit.source_info.pk}\n+        {'text': unit.get_source_plurals()[0],\n+         'pk': unit.source_info.pk,\n+         'context': unit.context,\n+         'location': unit.location}\n+\n         for unit in units\n     ]\n", "issue": "Improvement: show location for texts in screenshot management\n### Steps to reproduce\r\n\r\n1. Add a new screenshot\r\n2. Search for a text string\r\n3. See multiple identical results\r\n\r\n### Actual behaviour\r\n\r\nSometimes you have the exact same text in multiple places in your project, but the translations can differ depending on the context. This is currently not supported very well in screenshot management, resulting in impossible to make choices like this:\r\n\r\n![image](https://user-images.githubusercontent.com/199657/43308724-c35488ca-9182-11e8-8753-f0c887038937.png)\r\n\r\n### Expected behaviour\r\n\r\nThere should be some way to see differentiate the otherwise identical results in the source string list. Showing the context (where possible; I'm not sure any format other than gettext supports it?) and comments would help. A tooltip or something to show locations would also be very useful.\r\n\r\n### Server configuration and status\r\n\r\nCurrently running the `weblate/weblate:2.20-1` docker image:\r\n\r\n```\r\nPostgres is up\r\n * Weblate 2.20\r\n * Python 2.7.13\r\n * Django 1.11.12\r\n * six 1.10.0\r\n * social-auth-core 1.7.0\r\n * social-auth-app-django 2.1.0\r\n * django-appconf 1.0.2\r\n * Translate Toolkit 2.3.0\r\n * Whoosh 2.7.4\r\n * defusedxml 0.5.0\r\n * Git 2.11.0\r\n * Pillow (PIL) 1.1.7\r\n * dateutil 2.5.3\r\n * lxml 3.7.1\r\n * django-crispy-forms 1.7.2\r\n * compressor 2.2\r\n * djangorestframework 3.8.1\r\n * user-agents 1.1.0\r\n * pytz 2018.3\r\n * pyuca N/A\r\n * pyLibravatar N/A\r\n * PyYAML 3.12\r\n * tesserocr 2.2.2\r\n * Mercurial 4.0\r\n * git-svn 2.11.0\r\n * Database backends: django.db.backends.postgresql_psycopg2\r\n```\r\n\r\n\r\n\r\n\r\n<bountysource-plugin>\r\n\r\n---\r\nWant to back this issue? **[Post a bounty on it!](https://www.bountysource.com/issues/61401631-improvement-show-location-for-texts-in-screenshot-management?utm_campaign=plugin&utm_content=tracker%2F253393&utm_medium=issues&utm_source=github)** We accept bounties via [Bountysource](https://www.bountysource.com/?utm_campaign=plugin&utm_content=tracker%2F253393&utm_medium=issues&utm_source=github).\r\n</bountysource-plugin>\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\n#\n# Copyright \u00a9 2012 - 2018 Michal \u010ciha\u0159 <michal@cihar.com>\n#\n# This file is part of Weblate <https://weblate.org/>\n#\n# This program is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License as published by\n# the Free Software Foundation, either version 3 of the License, or\n# (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program.  If not, see <https://www.gnu.org/licenses/>.\n#\n\nimport difflib\n\nfrom django.contrib.auth.decorators import login_required\nfrom django.core.exceptions import PermissionDenied\nfrom django.http import JsonResponse\nfrom django.utils.translation import ugettext as _\nfrom django.views.decorators.http import require_POST\nfrom django.views.generic import ListView, DetailView\nfrom django.shortcuts import get_object_or_404, redirect, render\n\nfrom PIL import Image\n\ntry:\n    from tesserocr import PyTessBaseAPI, RIL\n    HAS_OCR = True\nexcept ImportError:\n    HAS_OCR = False\n\nfrom weblate.screenshots.forms import ScreenshotForm\nfrom weblate.screenshots.models import Screenshot\nfrom weblate.trans.models import Source\nfrom weblate.utils import messages\nfrom weblate.utils.views import ComponentViewMixin\n\n\ndef try_add_source(request, obj):\n    if 'source' not in request.POST or not request.POST['source'].isdigit():\n        return False\n\n    try:\n        source = Source.objects.get(\n            pk=request.POST['source'],\n            component=obj.component\n        )\n        obj.sources.add(source)\n        return True\n    except Source.DoesNotExist:\n        return False\n\n\nclass ScreenshotList(ListView, ComponentViewMixin):\n    paginate_by = 25\n    model = Screenshot\n    _add_form = None\n\n    def get_queryset(self):\n        self.kwargs['component'] = self.get_component()\n        return Screenshot.objects.filter(component=self.kwargs['component'])\n\n    def get_context_data(self):\n        result = super(ScreenshotList, self).get_context_data()\n        component = self.kwargs['component']\n        result['object'] = component\n        if self.request.user.has_perm('screenshot.add', component):\n            if self._add_form is not None:\n                result['add_form'] = self._add_form\n            else:\n                result['add_form'] = ScreenshotForm()\n        return result\n\n    def post(self, request, **kwargs):\n        component = self.get_component()\n        if not request.user.has_perm('screenshot.add', component):\n            raise PermissionDenied()\n        self._add_form = ScreenshotForm(request.POST, request.FILES)\n        if self._add_form.is_valid():\n            obj = Screenshot.objects.create(\n                component=component,\n                user=request.user,\n                **self._add_form.cleaned_data\n            )\n            request.user.profile.uploaded += 1\n            request.user.profile.save(update_fields=['uploaded'])\n\n            try_add_source(request, obj)\n            messages.success(\n                request,\n                _(\n                    'Screenshot has been uploaded, '\n                    'you can now assign it to source strings.'\n                )\n            )\n            return redirect(obj)\n        messages.error(\n            request,\n            _('Failed to upload screenshot, please fix errors below.')\n        )\n        return self.get(request, **kwargs)\n\n\nclass ScreenshotDetail(DetailView):\n    model = Screenshot\n    _edit_form = None\n\n    def get_object(self, *args, **kwargs):\n        obj = super(ScreenshotDetail, self).get_object(*args, **kwargs)\n        self.request.user.check_access(obj.component.project)\n        return obj\n\n    def get_context_data(self, **kwargs):\n        result = super(ScreenshotDetail, self).get_context_data(**kwargs)\n        component = result['object'].component\n        if self.request.user.has_perm('screenshot.edit', component):\n            if self._edit_form is not None:\n                result['edit_form'] = self._edit_form\n            else:\n                result['edit_form'] = ScreenshotForm(instance=result['object'])\n        return result\n\n    def post(self, request, **kwargs):\n        obj = self.get_object()\n        if request.user.has_perm('screenshot.edit', obj.component):\n            self._edit_form = ScreenshotForm(\n                request.POST, request.FILES, instance=obj\n            )\n            if self._edit_form.is_valid():\n                if request.FILES:\n                    obj.user = request.user\n                    request.user.profile.uploaded += 1\n                    request.user.profile.save(update_fields=['uploaded'])\n                self._edit_form.save()\n            else:\n                return self.get(request, **kwargs)\n        return redirect(obj)\n\n\n@require_POST\n@login_required\ndef delete_screenshot(request, pk):\n    obj = get_object_or_404(Screenshot, pk=pk)\n    request.user.check_access(obj.component.project)\n    if not request.user.has_perm('screenshot.delete', obj.component):\n        raise PermissionDenied()\n\n    kwargs = {\n        'project': obj.component.project.slug,\n        'component': obj.component.slug,\n    }\n\n    obj.delete()\n\n    messages.success(request, _('Screenshot %s has been deleted.') % obj.name)\n\n    return redirect('screenshots', **kwargs)\n\n\ndef get_screenshot(request, pk):\n    obj = get_object_or_404(Screenshot, pk=pk)\n    request.user.check_access(obj.component.project)\n    if not request.user.has_perm('screenshot.edit', obj.component):\n        raise PermissionDenied()\n    return obj\n\n\n@require_POST\n@login_required\ndef remove_source(request, pk):\n    obj = get_screenshot(request, pk)\n\n    obj.sources.remove(request.POST['source'])\n\n    messages.success(request, _('Source has been removed.'))\n\n    return redirect(obj)\n\n\ndef search_results(code, obj, units=None):\n    if units is None:\n        units = []\n    else:\n        units = units.exclude(\n            id_hash__in=obj.sources.values_list('id_hash', flat=True)\n        )\n\n    results = [\n        {'text': unit.get_source_plurals()[0], 'pk': unit.source_info.pk}\n        for unit in units\n    ]\n\n    return JsonResponse(\n        data={'responseCode': code, 'results': results}\n    )\n\n\n@login_required\n@require_POST\ndef search_source(request, pk):\n    obj = get_screenshot(request, pk)\n    try:\n        translation = obj.component.translation_set.all()[0]\n    except IndexError:\n        return search_results(500, obj)\n\n    units = translation.unit_set.search(\n        {\n            'search': 'substring',\n            'q': request.POST.get('q', ''),\n            'type': 'all',\n            'source': True,\n            'context': True,\n        },\n        translation=translation,\n    )\n\n    return search_results(200, obj, units)\n\n\ndef ocr_extract(api, image, strings):\n    \"\"\"Extract closes matches from an image\"\"\"\n    api.SetImage(image)\n    for item in api.GetComponentImages(RIL.TEXTLINE, True):\n        api.SetRectangle(\n            item[1]['x'], item[1]['y'], item[1]['w'], item[1]['h']\n        )\n        ocr_result = api.GetUTF8Text()\n        parts = [ocr_result] + ocr_result.split('|') + ocr_result.split()\n        for part in parts:\n            for match in difflib.get_close_matches(part, strings, cutoff=0.9):\n                yield match\n\n\n@login_required\n@require_POST\ndef ocr_search(request, pk):\n    obj = get_screenshot(request, pk)\n    if not HAS_OCR:\n        return search_results(500, obj)\n    try:\n        translation = obj.component.translation_set.all()[0]\n    except IndexError:\n        return search_results(500, obj)\n\n    # Load image\n    original_image = Image.open(obj.image.path)\n    # Convert to greyscale\n    original_image = original_image.convert(\"L\")\n    # Resize image (tesseract works best around 300dpi)\n    scaled_image = original_image.copy().resize(\n        [size * 4 for size in original_image.size],\n        Image.BICUBIC\n    )\n\n    # Find all our strings\n    sources = dict(translation.unit_set.values_list('source', 'pk'))\n    strings = tuple(sources.keys())\n\n    results = set()\n\n    # Extract and match strings\n    with PyTessBaseAPI() as api:\n        for image in (original_image, scaled_image):\n            for match in ocr_extract(api, image, strings):\n                results.add(sources[match])\n\n    return search_results(\n        200,\n        obj,\n        translation.unit_set.filter(pk__in=results)\n    )\n\n\n@login_required\n@require_POST\ndef add_source(request, pk):\n    obj = get_screenshot(request, pk)\n    result = try_add_source(request, obj)\n    return JsonResponse(\n        data={'responseCode': 200, 'status': result}\n    )\n\n\n@login_required\ndef get_sources(request, pk):\n    obj = get_screenshot(request, pk)\n    return render(\n        request, 'screenshots/screenshot_sources_body.html',\n        {'sources': obj.sources.all(), 'object': obj}\n    )\n", "path": "weblate/screenshots/views.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\n#\n# Copyright \u00a9 2012 - 2018 Michal \u010ciha\u0159 <michal@cihar.com>\n#\n# This file is part of Weblate <https://weblate.org/>\n#\n# This program is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License as published by\n# the Free Software Foundation, either version 3 of the License, or\n# (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program.  If not, see <https://www.gnu.org/licenses/>.\n#\n\nimport difflib\n\nfrom django.contrib.auth.decorators import login_required\nfrom django.core.exceptions import PermissionDenied\nfrom django.http import JsonResponse\nfrom django.utils.translation import ugettext as _\nfrom django.views.decorators.http import require_POST\nfrom django.views.generic import ListView, DetailView\nfrom django.shortcuts import get_object_or_404, redirect, render\n\nfrom PIL import Image\n\ntry:\n    from tesserocr import PyTessBaseAPI, RIL\n    HAS_OCR = True\nexcept ImportError:\n    HAS_OCR = False\n\nfrom weblate.screenshots.forms import ScreenshotForm\nfrom weblate.screenshots.models import Screenshot\nfrom weblate.trans.models import Source\nfrom weblate.utils import messages\nfrom weblate.utils.views import ComponentViewMixin\n\n\ndef try_add_source(request, obj):\n    if 'source' not in request.POST or not request.POST['source'].isdigit():\n        return False\n\n    try:\n        source = Source.objects.get(\n            pk=request.POST['source'],\n            component=obj.component\n        )\n        obj.sources.add(source)\n        return True\n    except Source.DoesNotExist:\n        return False\n\n\nclass ScreenshotList(ListView, ComponentViewMixin):\n    paginate_by = 25\n    model = Screenshot\n    _add_form = None\n\n    def get_queryset(self):\n        self.kwargs['component'] = self.get_component()\n        return Screenshot.objects.filter(component=self.kwargs['component'])\n\n    def get_context_data(self):\n        result = super(ScreenshotList, self).get_context_data()\n        component = self.kwargs['component']\n        result['object'] = component\n        if self.request.user.has_perm('screenshot.add', component):\n            if self._add_form is not None:\n                result['add_form'] = self._add_form\n            else:\n                result['add_form'] = ScreenshotForm()\n        return result\n\n    def post(self, request, **kwargs):\n        component = self.get_component()\n        if not request.user.has_perm('screenshot.add', component):\n            raise PermissionDenied()\n        self._add_form = ScreenshotForm(request.POST, request.FILES)\n        if self._add_form.is_valid():\n            obj = Screenshot.objects.create(\n                component=component,\n                user=request.user,\n                **self._add_form.cleaned_data\n            )\n            request.user.profile.uploaded += 1\n            request.user.profile.save(update_fields=['uploaded'])\n\n            try_add_source(request, obj)\n            messages.success(\n                request,\n                _(\n                    'Screenshot has been uploaded, '\n                    'you can now assign it to source strings.'\n                )\n            )\n            return redirect(obj)\n        messages.error(\n            request,\n            _('Failed to upload screenshot, please fix errors below.')\n        )\n        return self.get(request, **kwargs)\n\n\nclass ScreenshotDetail(DetailView):\n    model = Screenshot\n    _edit_form = None\n\n    def get_object(self, *args, **kwargs):\n        obj = super(ScreenshotDetail, self).get_object(*args, **kwargs)\n        self.request.user.check_access(obj.component.project)\n        return obj\n\n    def get_context_data(self, **kwargs):\n        result = super(ScreenshotDetail, self).get_context_data(**kwargs)\n        component = result['object'].component\n        if self.request.user.has_perm('screenshot.edit', component):\n            if self._edit_form is not None:\n                result['edit_form'] = self._edit_form\n            else:\n                result['edit_form'] = ScreenshotForm(instance=result['object'])\n        return result\n\n    def post(self, request, **kwargs):\n        obj = self.get_object()\n        if request.user.has_perm('screenshot.edit', obj.component):\n            self._edit_form = ScreenshotForm(\n                request.POST, request.FILES, instance=obj\n            )\n            if self._edit_form.is_valid():\n                if request.FILES:\n                    obj.user = request.user\n                    request.user.profile.uploaded += 1\n                    request.user.profile.save(update_fields=['uploaded'])\n                self._edit_form.save()\n            else:\n                return self.get(request, **kwargs)\n        return redirect(obj)\n\n\n@require_POST\n@login_required\ndef delete_screenshot(request, pk):\n    obj = get_object_or_404(Screenshot, pk=pk)\n    request.user.check_access(obj.component.project)\n    if not request.user.has_perm('screenshot.delete', obj.component):\n        raise PermissionDenied()\n\n    kwargs = {\n        'project': obj.component.project.slug,\n        'component': obj.component.slug,\n    }\n\n    obj.delete()\n\n    messages.success(request, _('Screenshot %s has been deleted.') % obj.name)\n\n    return redirect('screenshots', **kwargs)\n\n\ndef get_screenshot(request, pk):\n    obj = get_object_or_404(Screenshot, pk=pk)\n    request.user.check_access(obj.component.project)\n    if not request.user.has_perm('screenshot.edit', obj.component):\n        raise PermissionDenied()\n    return obj\n\n\n@require_POST\n@login_required\ndef remove_source(request, pk):\n    obj = get_screenshot(request, pk)\n\n    obj.sources.remove(request.POST['source'])\n\n    messages.success(request, _('Source has been removed.'))\n\n    return redirect(obj)\n\n\ndef search_results(code, obj, units=None):\n    if units is None:\n        units = []\n    else:\n        units = units.exclude(\n            id_hash__in=obj.sources.values_list('id_hash', flat=True)\n        )\n\n    results = [\n        {'text': unit.get_source_plurals()[0],\n         'pk': unit.source_info.pk,\n         'context': unit.context,\n         'location': unit.location}\n\n        for unit in units\n    ]\n\n    return JsonResponse(\n        data={'responseCode': code, 'results': results}\n    )\n\n\n@login_required\n@require_POST\ndef search_source(request, pk):\n    obj = get_screenshot(request, pk)\n    try:\n        translation = obj.component.translation_set.all()[0]\n    except IndexError:\n        return search_results(500, obj)\n\n    units = translation.unit_set.search(\n        {\n            'search': 'substring',\n            'q': request.POST.get('q', ''),\n            'type': 'all',\n            'source': True,\n            'context': True,\n        },\n        translation=translation,\n    )\n\n    return search_results(200, obj, units)\n\n\ndef ocr_extract(api, image, strings):\n    \"\"\"Extract closes matches from an image\"\"\"\n    api.SetImage(image)\n    for item in api.GetComponentImages(RIL.TEXTLINE, True):\n        api.SetRectangle(\n            item[1]['x'], item[1]['y'], item[1]['w'], item[1]['h']\n        )\n        ocr_result = api.GetUTF8Text()\n        parts = [ocr_result] + ocr_result.split('|') + ocr_result.split()\n        for part in parts:\n            for match in difflib.get_close_matches(part, strings, cutoff=0.9):\n                yield match\n\n\n@login_required\n@require_POST\ndef ocr_search(request, pk):\n    obj = get_screenshot(request, pk)\n    if not HAS_OCR:\n        return search_results(500, obj)\n    try:\n        translation = obj.component.translation_set.all()[0]\n    except IndexError:\n        return search_results(500, obj)\n\n    # Load image\n    original_image = Image.open(obj.image.path)\n    # Convert to greyscale\n    original_image = original_image.convert(\"L\")\n    # Resize image (tesseract works best around 300dpi)\n    scaled_image = original_image.copy().resize(\n        [size * 4 for size in original_image.size],\n        Image.BICUBIC\n    )\n\n    # Find all our strings\n    sources = dict(translation.unit_set.values_list('source', 'pk'))\n    strings = tuple(sources.keys())\n\n    results = set()\n\n    # Extract and match strings\n    with PyTessBaseAPI() as api:\n        for image in (original_image, scaled_image):\n            for match in ocr_extract(api, image, strings):\n                results.add(sources[match])\n\n    return search_results(\n        200,\n        obj,\n        translation.unit_set.filter(pk__in=results)\n    )\n\n\n@login_required\n@require_POST\ndef add_source(request, pk):\n    obj = get_screenshot(request, pk)\n    result = try_add_source(request, obj)\n    return JsonResponse(\n        data={'responseCode': 200, 'status': result}\n    )\n\n\n@login_required\ndef get_sources(request, pk):\n    obj = get_screenshot(request, pk)\n    return render(\n        request, 'screenshots/screenshot_sources_body.html',\n        {'sources': obj.sources.all(), 'object': obj}\n    )\n", "path": "weblate/screenshots/views.py"}]}