We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Static pages images URL ?
J'ai ajouté une cover_image à une page statique sur le serveur de DEMO sur cette page : https://geotrekdemo.ecrins-parcnational.fr/admin/flatpages/flatpage/3/change/ (admin / admin)

L'URL de l'image indiquée (dans l'Adminsite et dans l'API V2) est https://geotrekdemo.ecrins-parcnational.fr/media/paperclip/flatpages_flatpage/3/biodivecrins.jpg, mais elle n'est pas accessible.

L'image est bien sur mon serveur dans /opt/geotrek-admin/var/media/paperclip/flatpages_flatpage/3/

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `mapentity/views/base.py`
Content:
```
1 from __future__ import unicode_literals
2 
3 from io import BytesIO
4 import json
5 import logging
6 import mimetypes
7 import os
8 from datetime import datetime
9 import re
10 from urllib.parse import quote
11 
12 from django.conf import settings
13 from django.contrib.auth.decorators import login_required
14 from django.contrib.gis.db.models import GeometryField
15 from django.core.exceptions import PermissionDenied
16 from django.urls import reverse
17 from django.http import (HttpResponse, HttpResponseBadRequest, Http404)
18 from django.shortcuts import get_object_or_404
19 from django.views import static
20 from django.views.decorators.csrf import csrf_exempt
21 from django.views.decorators.http import require_http_methods
22 from django.views.generic.base import TemplateView
23 from paperclip.settings import get_attachment_permission, get_attachment_model
24 
25 from mapentity import models as mapentity_models
26 from .mixins import JSONResponseMixin, FilterListMixin, ModelViewMixin
27 from ..decorators import view_permission_required
28 from ..helpers import capture_image
29 from ..settings import app_settings
30 
31 logger = logging.getLogger(__name__)
32 
33 
34 def serve_attachment(request, path):
35     """
36     Serve media/ for authorized users only, since it can contain sensitive
37     information (uploaded documents)
38     """
39     original_path = re.sub(r'\.\d+x\d+_q\d+(_crop)?\.(jpg|png|jpeg)$', '', path, count=1, flags=re.IGNORECASE)
40     attachment = get_object_or_404(get_attachment_model(), attachment_file=original_path)
41     obj = attachment.content_object
42     if not issubclass(obj._meta.model, mapentity_models.MapEntityMixin):
43         raise Http404
44     if not obj.is_public():
45         if not request.user.is_authenticated:
46             raise PermissionDenied
47         if not request.user.has_perm(get_attachment_permission('read_attachment')):
48             raise PermissionDenied
49         if not request.user.has_perm('{}.read_{}'.format(obj._meta.app_label, obj._meta.model_name)):
50             raise PermissionDenied
51 
52     content_type, encoding = mimetypes.guess_type(path)
53 
54     if settings.DEBUG:
55         response = static.serve(request, path, settings.MEDIA_ROOT)
56     else:
57         response = HttpResponse()
58         response[app_settings['SENDFILE_HTTP_HEADER']] = os.path.join(settings.MEDIA_URL_SECURE, path)
59     response["Content-Type"] = content_type or 'application/octet-stream'
60     if encoding:
61         response["Content-Encoding"] = encoding
62     if app_settings['SERVE_MEDIA_AS_ATTACHMENT']:
63         response['Content-Disposition'] = "attachment; filename={0}".format(
64             os.path.basename(path))
65     return response
66 
67 
68 class JSSettings(JSONResponseMixin, TemplateView):
69     """
70     Javascript settings, in JSON format.
71     Likely to be overriden. Contains only necessary stuff
72     for mapentity.
73     """
74 
75     def get_context_data(self):
76         dictsettings = {}
77         dictsettings['debug'] = settings.DEBUG
78         dictsettings['map'] = dict(
79             extent=getattr(settings, 'LEAFLET_CONFIG', {}).get('SPATIAL_EXTENT'),
80             styles=app_settings['MAP_STYLES'],
81         )
82 
83         # URLs
84         root_url = app_settings['ROOT_URL']
85         root_url = root_url if root_url.endswith('/') else '{}/'.format(root_url)
86         dictsettings['urls'] = {}
87         dictsettings['urls']['root'] = root_url
88 
89         from django.db import models
90         from mapentity.registry import MapEntityOptions
91 
92         class ModelName(mapentity_models.MapEntityMixin, models.Model):
93             pass
94 
95         options = MapEntityOptions(ModelName)
96 
97         dictsettings['urls']['static'] = settings.STATIC_URL
98         dictsettings['urls']['layer'] = '{}{}'.format(root_url, options._url_path(mapentity_models.ENTITY_LAYER)[1:-1])
99         dictsettings['urls']['detail'] = '{}modelname/0/'.format(root_url)
100         dictsettings['urls']['format_list'] = '{}{}'.format(
101             root_url, options._url_path(mapentity_models.ENTITY_FORMAT_LIST)[1:-1]
102         )
103         dictsettings['urls']['screenshot'] = reverse("mapentity:map_screenshot")
104 
105         # Useful for JS calendars
106         date_format = settings.DATE_INPUT_FORMATS[0].replace('%Y', 'yyyy').replace('%m', 'mm').replace('%d', 'dd')
107         dictsettings['date_format'] = date_format
108         # Languages
109         dictsettings['languages'] = dict(available=dict(app_settings['TRANSLATED_LANGUAGES']),
110                                          default=app_settings['LANGUAGE_CODE'])
111         return dictsettings
112 
113 
114 class BaseListView(FilterListMixin, ModelViewMixin):
115     columns = None
116 
117     def __init__(self, *args, **kwargs):
118         super(BaseListView, self).__init__(*args, **kwargs)
119 
120         if self.columns is None:
121             # All model fields except geometries
122             self.columns = [field.name for field in self.get_model()._meta.fields
123                             if not isinstance(field, GeometryField)]
124             # Id column should be the first one
125             self.columns.remove('id')
126             self.columns.insert(0, 'id')
127 
128     @view_permission_required()
129     def dispatch(self, *args, **kwargs):
130         return super(BaseListView, self).dispatch(*args, **kwargs)
131 
132 
133 @csrf_exempt
134 @login_required
135 def map_screenshot(request):
136     """
137     This view allows to take screenshots, via a django-screamshot service, of
138     the map **currently viewed by the user**.
139 
140     - A context full of information is built on client-side and posted here.
141     - We reproduce this context, via headless browser, and take a capture
142     - We return the resulting image as attachment.
143 
144     This seems overkill ? Please look around and find a better way.
145     """
146     try:
147         printcontext = request.POST['printcontext']
148         assert len(printcontext) < 2048, "Print context is way too big."
149 
150         # Prepare context, extract and add infos
151         context = json.loads(printcontext)
152         selector = context.pop('selector')
153         map_url = context.pop('url')
154         map_url = request.build_absolute_uri(map_url)
155         context['print'] = True
156         printcontext = json.dumps(context)
157         contextencoded = quote(printcontext)
158         map_url += '?context=%s' % contextencoded
159         logger.debug("Capture %s" % map_url)
160 
161         # Capture image and return it
162         width = context.get('viewport', {}).get('width')
163         height = context.get('viewport', {}).get('height')
164 
165         stream = BytesIO()
166         capture_image(map_url, stream, width=width, height=height, selector=selector)
167         response = HttpResponse(stream.getvalue(), content_type='image/png')
168         response['Content-Disposition'] = 'attachment; filename=%s.png' % datetime.now().strftime('%Y%m%d-%H%M%S')
169         return response
170 
171     except Exception as exc:
172         logger.exception(exc)
173         return HttpResponseBadRequest(exc)
174 
175 
176 @require_http_methods(["POST"])
177 @csrf_exempt
178 @login_required
179 def history_delete(request, path=None):
180     path = request.POST.get('path', path)
181     if path:
182         history = request.session.get('history')
183         if history:
184             history = [h for h in history if h['path'] != path]
185             request.session['history'] = history
186     return HttpResponse()
187
```

Path: `geotrek/flatpages/models.py`
Content:
```
1 import mimetypes
2 
3 from django.contrib.contenttypes.fields import GenericRelation
4 from django.db import models
5 from django.utils.translation import gettext_lazy as _
6 from django.template.defaultfilters import slugify
7 from django.conf import settings
8 from django.urls import reverse
9 
10 from bs4 import BeautifulSoup
11 from extended_choices import Choices
12 
13 from mapentity.serializers import plain_text
14 from geotrek.common.mixins import TimeStampedModelMixin, BasePublishableMixin
15 
16 
17 FLATPAGES_TARGETS = Choices(
18     ('ALL', 'all', _('All')),
19     ('MOBILE', 'mobile', _('Mobile')),
20     ('HIDDEN', 'hidden', _('Hidden')),
21     ('WEB', 'web', _('Web')),
22 )
23 
24 
25 class FlatPage(BasePublishableMixin, TimeStampedModelMixin):
26     """
27     Manage *Geotrek-rando* static pages from Geotrek admin.
28 
29     Historically, we started static pages as static HTML files within
30     *Geotrek-rando* folders.
31     """
32     title = models.CharField(verbose_name=_('Title'), max_length=200)
33     external_url = models.URLField(verbose_name=_('External URL'), blank=True, default='',
34                                    help_text=_('Link to external website instead of HTML content'))
35     content = models.TextField(verbose_name=_('Content'), null=True, blank=True,
36                                help_text=_('HTML content'))
37     target = models.CharField(verbose_name=_('Target'), max_length=12, choices=FLATPAGES_TARGETS,
38                               default=FLATPAGES_TARGETS.ALL)
39     source = models.ManyToManyField('common.RecordSource',
40                                     blank=True, related_name='flatpages',
41                                     verbose_name=_("Source"))
42     portal = models.ManyToManyField('common.TargetPortal',
43                                     blank=True, related_name='flatpages',
44                                     verbose_name=_("Portal"))
45     order = models.IntegerField(default=None, null=True, blank=True,
46                                 help_text=_("ID order if blank", ),
47                                 verbose_name=_("Order"))
48     attachments = GenericRelation(settings.PAPERCLIP_ATTACHMENT_MODEL)
49 
50     @property
51     def slug(self):
52         return slugify(self.title)
53 
54     class Meta:
55         verbose_name = _('Flat page')
56         verbose_name_plural = _('Flat pages')
57         ordering = ['order', 'id']
58         permissions = (
59             ("read_flatpage", "Can read FlatPage"),
60         )
61 
62     def __str__(self):
63         return self.title
64 
65     def get_permission_codename(self, *args):
66         return
67 
68     def clean(self):
69         html_content = ''
70         for language in settings.MAPENTITY_CONFIG['TRANSLATED_LANGUAGES']:
71             html_content += getattr(self, 'content_%s' % language[0], None) or ''
72 
73     def parse_media(self):
74         soup = BeautifulSoup(self.content or '', 'lxml')
75         images = soup.findAll('img')
76         results = []
77         for image in images:
78             url = image.get('src')
79             if url is None:
80                 continue
81 
82             mt = mimetypes.guess_type(url, strict=True)[0]
83             if mt is None:
84                 mt = 'application/octet-stream'
85 
86             results.append({
87                 'url': url,
88                 'title': image.get('title', ''),
89                 'alt': image.get('alt', ''),
90                 'mimetype': mt.split('/'),
91             })
92 
93         return results
94 
95     def get_add_url(self):
96         return reverse('admin:flatpages_flatpage_add')
97 
98     def get_update_url(self):
99         return reverse('admin:flatpages_flatpage_change', args=[self.pk])
100 
101     def get_delete_url(self):
102         return reverse('admin:flatpages_flatpage_delete', args=[self.pk])
103 
104     @property
105     def rando_url(self):
106         return 'informations/{}/'.format(self.slug)
107 
108     @property
109     def meta_description(self):
110         return plain_text(self.content)[:500]
111
```
--- 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/geotrek/flatpages/models.py b/geotrek/flatpages/models.py
--- a/geotrek/flatpages/models.py
+++ b/geotrek/flatpages/models.py
@@ -108,3 +108,6 @@
     @property
     def meta_description(self):
         return plain_text(self.content)[:500]
+
+    def is_public(self):
+        return self.any_published
diff --git a/mapentity/views/base.py b/mapentity/views/base.py
--- a/mapentity/views/base.py
+++ b/mapentity/views/base.py
@@ -39,7 +39,7 @@
     original_path = re.sub(r'\.\d+x\d+_q\d+(_crop)?\.(jpg|png|jpeg)$', '', path, count=1, flags=re.IGNORECASE)
     attachment = get_object_or_404(get_attachment_model(), attachment_file=original_path)
     obj = attachment.content_object
-    if not issubclass(obj._meta.model, mapentity_models.MapEntityMixin):
+    if not hasattr(obj._meta.model, 'attachments'):
         raise Http404
     if not obj.is_public():
         if not request.user.is_authenticated:

{"golden_diff": "diff --git a/geotrek/flatpages/models.py b/geotrek/flatpages/models.py\n--- a/geotrek/flatpages/models.py\n+++ b/geotrek/flatpages/models.py\n@@ -108,3 +108,6 @@\n     @property\n     def meta_description(self):\n         return plain_text(self.content)[:500]\n+\n+    def is_public(self):\n+        return self.any_published\ndiff --git a/mapentity/views/base.py b/mapentity/views/base.py\n--- a/mapentity/views/base.py\n+++ b/mapentity/views/base.py\n@@ -39,7 +39,7 @@\n     original_path = re.sub(r'\\.\\d+x\\d+_q\\d+(_crop)?\\.(jpg|png|jpeg)$', '', path, count=1, flags=re.IGNORECASE)\n     attachment = get_object_or_404(get_attachment_model(), attachment_file=original_path)\n     obj = attachment.content_object\n-    if not issubclass(obj._meta.model, mapentity_models.MapEntityMixin):\n+    if not hasattr(obj._meta.model, 'attachments'):\n         raise Http404\n     if not obj.is_public():\n         if not request.user.is_authenticated:\n", "issue": "Static pages images URL ?\nJ'ai ajout\u00e9 une cover_image \u00e0 une page statique sur le serveur de DEMO sur cette page : https://geotrekdemo.ecrins-parcnational.fr/admin/flatpages/flatpage/3/change/ (admin / admin)\r\n\r\nL'URL de l'image indiqu\u00e9e (dans l'Adminsite et dans l'API V2) est https://geotrekdemo.ecrins-parcnational.fr/media/paperclip/flatpages_flatpage/3/biodivecrins.jpg, mais elle n'est pas accessible.\r\n\r\nL'image est bien sur mon serveur dans /opt/geotrek-admin/var/media/paperclip/flatpages_flatpage/3/\n", "before_files": [{"content": "from __future__ import unicode_literals\n\nfrom io import BytesIO\nimport json\nimport logging\nimport mimetypes\nimport os\nfrom datetime import datetime\nimport re\nfrom urllib.parse import quote\n\nfrom django.conf import settings\nfrom django.contrib.auth.decorators import login_required\nfrom django.contrib.gis.db.models import GeometryField\nfrom django.core.exceptions import PermissionDenied\nfrom django.urls import reverse\nfrom django.http import (HttpResponse, HttpResponseBadRequest, Http404)\nfrom django.shortcuts import get_object_or_404\nfrom django.views import static\nfrom django.views.decorators.csrf import csrf_exempt\nfrom django.views.decorators.http import require_http_methods\nfrom django.views.generic.base import TemplateView\nfrom paperclip.settings import get_attachment_permission, get_attachment_model\n\nfrom mapentity import models as mapentity_models\nfrom .mixins import JSONResponseMixin, FilterListMixin, ModelViewMixin\nfrom ..decorators import view_permission_required\nfrom ..helpers import capture_image\nfrom ..settings import app_settings\n\nlogger = logging.getLogger(__name__)\n\n\ndef serve_attachment(request, path):\n    \"\"\"\n    Serve media/ for authorized users only, since it can contain sensitive\n    information (uploaded documents)\n    \"\"\"\n    original_path = re.sub(r'\\.\\d+x\\d+_q\\d+(_crop)?\\.(jpg|png|jpeg)$', '', path, count=1, flags=re.IGNORECASE)\n    attachment = get_object_or_404(get_attachment_model(), attachment_file=original_path)\n    obj = attachment.content_object\n    if not issubclass(obj._meta.model, mapentity_models.MapEntityMixin):\n        raise Http404\n    if not obj.is_public():\n        if not request.user.is_authenticated:\n            raise PermissionDenied\n        if not request.user.has_perm(get_attachment_permission('read_attachment')):\n            raise PermissionDenied\n        if not request.user.has_perm('{}.read_{}'.format(obj._meta.app_label, obj._meta.model_name)):\n            raise PermissionDenied\n\n    content_type, encoding = mimetypes.guess_type(path)\n\n    if settings.DEBUG:\n        response = static.serve(request, path, settings.MEDIA_ROOT)\n    else:\n        response = HttpResponse()\n        response[app_settings['SENDFILE_HTTP_HEADER']] = os.path.join(settings.MEDIA_URL_SECURE, path)\n    response[\"Content-Type\"] = content_type or 'application/octet-stream'\n    if encoding:\n        response[\"Content-Encoding\"] = encoding\n    if app_settings['SERVE_MEDIA_AS_ATTACHMENT']:\n        response['Content-Disposition'] = \"attachment; filename={0}\".format(\n            os.path.basename(path))\n    return response\n\n\nclass JSSettings(JSONResponseMixin, TemplateView):\n    \"\"\"\n    Javascript settings, in JSON format.\n    Likely to be overriden. Contains only necessary stuff\n    for mapentity.\n    \"\"\"\n\n    def get_context_data(self):\n        dictsettings = {}\n        dictsettings['debug'] = settings.DEBUG\n        dictsettings['map'] = dict(\n            extent=getattr(settings, 'LEAFLET_CONFIG', {}).get('SPATIAL_EXTENT'),\n            styles=app_settings['MAP_STYLES'],\n        )\n\n        # URLs\n        root_url = app_settings['ROOT_URL']\n        root_url = root_url if root_url.endswith('/') else '{}/'.format(root_url)\n        dictsettings['urls'] = {}\n        dictsettings['urls']['root'] = root_url\n\n        from django.db import models\n        from mapentity.registry import MapEntityOptions\n\n        class ModelName(mapentity_models.MapEntityMixin, models.Model):\n            pass\n\n        options = MapEntityOptions(ModelName)\n\n        dictsettings['urls']['static'] = settings.STATIC_URL\n        dictsettings['urls']['layer'] = '{}{}'.format(root_url, options._url_path(mapentity_models.ENTITY_LAYER)[1:-1])\n        dictsettings['urls']['detail'] = '{}modelname/0/'.format(root_url)\n        dictsettings['urls']['format_list'] = '{}{}'.format(\n            root_url, options._url_path(mapentity_models.ENTITY_FORMAT_LIST)[1:-1]\n        )\n        dictsettings['urls']['screenshot'] = reverse(\"mapentity:map_screenshot\")\n\n        # Useful for JS calendars\n        date_format = settings.DATE_INPUT_FORMATS[0].replace('%Y', 'yyyy').replace('%m', 'mm').replace('%d', 'dd')\n        dictsettings['date_format'] = date_format\n        # Languages\n        dictsettings['languages'] = dict(available=dict(app_settings['TRANSLATED_LANGUAGES']),\n                                         default=app_settings['LANGUAGE_CODE'])\n        return dictsettings\n\n\nclass BaseListView(FilterListMixin, ModelViewMixin):\n    columns = None\n\n    def __init__(self, *args, **kwargs):\n        super(BaseListView, self).__init__(*args, **kwargs)\n\n        if self.columns is None:\n            # All model fields except geometries\n            self.columns = [field.name for field in self.get_model()._meta.fields\n                            if not isinstance(field, GeometryField)]\n            # Id column should be the first one\n            self.columns.remove('id')\n            self.columns.insert(0, 'id')\n\n    @view_permission_required()\n    def dispatch(self, *args, **kwargs):\n        return super(BaseListView, self).dispatch(*args, **kwargs)\n\n\n@csrf_exempt\n@login_required\ndef map_screenshot(request):\n    \"\"\"\n    This view allows to take screenshots, via a django-screamshot service, of\n    the map **currently viewed by the user**.\n\n    - A context full of information is built on client-side and posted here.\n    - We reproduce this context, via headless browser, and take a capture\n    - We return the resulting image as attachment.\n\n    This seems overkill ? Please look around and find a better way.\n    \"\"\"\n    try:\n        printcontext = request.POST['printcontext']\n        assert len(printcontext) < 2048, \"Print context is way too big.\"\n\n        # Prepare context, extract and add infos\n        context = json.loads(printcontext)\n        selector = context.pop('selector')\n        map_url = context.pop('url')\n        map_url = request.build_absolute_uri(map_url)\n        context['print'] = True\n        printcontext = json.dumps(context)\n        contextencoded = quote(printcontext)\n        map_url += '?context=%s' % contextencoded\n        logger.debug(\"Capture %s\" % map_url)\n\n        # Capture image and return it\n        width = context.get('viewport', {}).get('width')\n        height = context.get('viewport', {}).get('height')\n\n        stream = BytesIO()\n        capture_image(map_url, stream, width=width, height=height, selector=selector)\n        response = HttpResponse(stream.getvalue(), content_type='image/png')\n        response['Content-Disposition'] = 'attachment; filename=%s.png' % datetime.now().strftime('%Y%m%d-%H%M%S')\n        return response\n\n    except Exception as exc:\n        logger.exception(exc)\n        return HttpResponseBadRequest(exc)\n\n\n@require_http_methods([\"POST\"])\n@csrf_exempt\n@login_required\ndef history_delete(request, path=None):\n    path = request.POST.get('path', path)\n    if path:\n        history = request.session.get('history')\n        if history:\n            history = [h for h in history if h['path'] != path]\n            request.session['history'] = history\n    return HttpResponse()\n", "path": "mapentity/views/base.py"}, {"content": "import mimetypes\n\nfrom django.contrib.contenttypes.fields import GenericRelation\nfrom django.db import models\nfrom django.utils.translation import gettext_lazy as _\nfrom django.template.defaultfilters import slugify\nfrom django.conf import settings\nfrom django.urls import reverse\n\nfrom bs4 import BeautifulSoup\nfrom extended_choices import Choices\n\nfrom mapentity.serializers import plain_text\nfrom geotrek.common.mixins import TimeStampedModelMixin, BasePublishableMixin\n\n\nFLATPAGES_TARGETS = Choices(\n    ('ALL', 'all', _('All')),\n    ('MOBILE', 'mobile', _('Mobile')),\n    ('HIDDEN', 'hidden', _('Hidden')),\n    ('WEB', 'web', _('Web')),\n)\n\n\nclass FlatPage(BasePublishableMixin, TimeStampedModelMixin):\n    \"\"\"\n    Manage *Geotrek-rando* static pages from Geotrek admin.\n\n    Historically, we started static pages as static HTML files within\n    *Geotrek-rando* folders.\n    \"\"\"\n    title = models.CharField(verbose_name=_('Title'), max_length=200)\n    external_url = models.URLField(verbose_name=_('External URL'), blank=True, default='',\n                                   help_text=_('Link to external website instead of HTML content'))\n    content = models.TextField(verbose_name=_('Content'), null=True, blank=True,\n                               help_text=_('HTML content'))\n    target = models.CharField(verbose_name=_('Target'), max_length=12, choices=FLATPAGES_TARGETS,\n                              default=FLATPAGES_TARGETS.ALL)\n    source = models.ManyToManyField('common.RecordSource',\n                                    blank=True, related_name='flatpages',\n                                    verbose_name=_(\"Source\"))\n    portal = models.ManyToManyField('common.TargetPortal',\n                                    blank=True, related_name='flatpages',\n                                    verbose_name=_(\"Portal\"))\n    order = models.IntegerField(default=None, null=True, blank=True,\n                                help_text=_(\"ID order if blank\", ),\n                                verbose_name=_(\"Order\"))\n    attachments = GenericRelation(settings.PAPERCLIP_ATTACHMENT_MODEL)\n\n    @property\n    def slug(self):\n        return slugify(self.title)\n\n    class Meta:\n        verbose_name = _('Flat page')\n        verbose_name_plural = _('Flat pages')\n        ordering = ['order', 'id']\n        permissions = (\n            (\"read_flatpage\", \"Can read FlatPage\"),\n        )\n\n    def __str__(self):\n        return self.title\n\n    def get_permission_codename(self, *args):\n        return\n\n    def clean(self):\n        html_content = ''\n        for language in settings.MAPENTITY_CONFIG['TRANSLATED_LANGUAGES']:\n            html_content += getattr(self, 'content_%s' % language[0], None) or ''\n\n    def parse_media(self):\n        soup = BeautifulSoup(self.content or '', 'lxml')\n        images = soup.findAll('img')\n        results = []\n        for image in images:\n            url = image.get('src')\n            if url is None:\n                continue\n\n            mt = mimetypes.guess_type(url, strict=True)[0]\n            if mt is None:\n                mt = 'application/octet-stream'\n\n            results.append({\n                'url': url,\n                'title': image.get('title', ''),\n                'alt': image.get('alt', ''),\n                'mimetype': mt.split('/'),\n            })\n\n        return results\n\n    def get_add_url(self):\n        return reverse('admin:flatpages_flatpage_add')\n\n    def get_update_url(self):\n        return reverse('admin:flatpages_flatpage_change', args=[self.pk])\n\n    def get_delete_url(self):\n        return reverse('admin:flatpages_flatpage_delete', args=[self.pk])\n\n    @property\n    def rando_url(self):\n        return 'informations/{}/'.format(self.slug)\n\n    @property\n    def meta_description(self):\n        return plain_text(self.content)[:500]\n", "path": "geotrek/flatpages/models.py"}], "after_files": [{"content": "from __future__ import unicode_literals\n\nfrom io import BytesIO\nimport json\nimport logging\nimport mimetypes\nimport os\nfrom datetime import datetime\nimport re\nfrom urllib.parse import quote\n\nfrom django.conf import settings\nfrom django.contrib.auth.decorators import login_required\nfrom django.contrib.gis.db.models import GeometryField\nfrom django.core.exceptions import PermissionDenied\nfrom django.urls import reverse\nfrom django.http import (HttpResponse, HttpResponseBadRequest, Http404)\nfrom django.shortcuts import get_object_or_404\nfrom django.views import static\nfrom django.views.decorators.csrf import csrf_exempt\nfrom django.views.decorators.http import require_http_methods\nfrom django.views.generic.base import TemplateView\nfrom paperclip.settings import get_attachment_permission, get_attachment_model\n\nfrom mapentity import models as mapentity_models\nfrom .mixins import JSONResponseMixin, FilterListMixin, ModelViewMixin\nfrom ..decorators import view_permission_required\nfrom ..helpers import capture_image\nfrom ..settings import app_settings\n\nlogger = logging.getLogger(__name__)\n\n\ndef serve_attachment(request, path):\n    \"\"\"\n    Serve media/ for authorized users only, since it can contain sensitive\n    information (uploaded documents)\n    \"\"\"\n    original_path = re.sub(r'\\.\\d+x\\d+_q\\d+(_crop)?\\.(jpg|png|jpeg)$', '', path, count=1, flags=re.IGNORECASE)\n    attachment = get_object_or_404(get_attachment_model(), attachment_file=original_path)\n    obj = attachment.content_object\n    if not hasattr(obj._meta.model, 'attachments'):\n        raise Http404\n    if not obj.is_public():\n        if not request.user.is_authenticated:\n            raise PermissionDenied\n        if not request.user.has_perm(get_attachment_permission('read_attachment')):\n            raise PermissionDenied\n        if not request.user.has_perm('{}.read_{}'.format(obj._meta.app_label, obj._meta.model_name)):\n            raise PermissionDenied\n\n    content_type, encoding = mimetypes.guess_type(path)\n\n    if settings.DEBUG:\n        response = static.serve(request, path, settings.MEDIA_ROOT)\n    else:\n        response = HttpResponse()\n        response[app_settings['SENDFILE_HTTP_HEADER']] = os.path.join(settings.MEDIA_URL_SECURE, path)\n    response[\"Content-Type\"] = content_type or 'application/octet-stream'\n    if encoding:\n        response[\"Content-Encoding\"] = encoding\n    if app_settings['SERVE_MEDIA_AS_ATTACHMENT']:\n        response['Content-Disposition'] = \"attachment; filename={0}\".format(\n            os.path.basename(path))\n    return response\n\n\nclass JSSettings(JSONResponseMixin, TemplateView):\n    \"\"\"\n    Javascript settings, in JSON format.\n    Likely to be overriden. Contains only necessary stuff\n    for mapentity.\n    \"\"\"\n\n    def get_context_data(self):\n        dictsettings = {}\n        dictsettings['debug'] = settings.DEBUG\n        dictsettings['map'] = dict(\n            extent=getattr(settings, 'LEAFLET_CONFIG', {}).get('SPATIAL_EXTENT'),\n            styles=app_settings['MAP_STYLES'],\n        )\n\n        # URLs\n        root_url = app_settings['ROOT_URL']\n        root_url = root_url if root_url.endswith('/') else '{}/'.format(root_url)\n        dictsettings['urls'] = {}\n        dictsettings['urls']['root'] = root_url\n\n        from django.db import models\n        from mapentity.registry import MapEntityOptions\n\n        class ModelName(mapentity_models.MapEntityMixin, models.Model):\n            pass\n\n        options = MapEntityOptions(ModelName)\n\n        dictsettings['urls']['static'] = settings.STATIC_URL\n        dictsettings['urls']['layer'] = '{}{}'.format(root_url, options._url_path(mapentity_models.ENTITY_LAYER)[1:-1])\n        dictsettings['urls']['detail'] = '{}modelname/0/'.format(root_url)\n        dictsettings['urls']['format_list'] = '{}{}'.format(\n            root_url, options._url_path(mapentity_models.ENTITY_FORMAT_LIST)[1:-1]\n        )\n        dictsettings['urls']['screenshot'] = reverse(\"mapentity:map_screenshot\")\n\n        # Useful for JS calendars\n        date_format = settings.DATE_INPUT_FORMATS[0].replace('%Y', 'yyyy').replace('%m', 'mm').replace('%d', 'dd')\n        dictsettings['date_format'] = date_format\n        # Languages\n        dictsettings['languages'] = dict(available=dict(app_settings['TRANSLATED_LANGUAGES']),\n                                         default=app_settings['LANGUAGE_CODE'])\n        return dictsettings\n\n\nclass BaseListView(FilterListMixin, ModelViewMixin):\n    columns = None\n\n    def __init__(self, *args, **kwargs):\n        super(BaseListView, self).__init__(*args, **kwargs)\n\n        if self.columns is None:\n            # All model fields except geometries\n            self.columns = [field.name for field in self.get_model()._meta.fields\n                            if not isinstance(field, GeometryField)]\n            # Id column should be the first one\n            self.columns.remove('id')\n            self.columns.insert(0, 'id')\n\n    @view_permission_required()\n    def dispatch(self, *args, **kwargs):\n        return super(BaseListView, self).dispatch(*args, **kwargs)\n\n\n@csrf_exempt\n@login_required\ndef map_screenshot(request):\n    \"\"\"\n    This view allows to take screenshots, via a django-screamshot service, of\n    the map **currently viewed by the user**.\n\n    - A context full of information is built on client-side and posted here.\n    - We reproduce this context, via headless browser, and take a capture\n    - We return the resulting image as attachment.\n\n    This seems overkill ? Please look around and find a better way.\n    \"\"\"\n    try:\n        printcontext = request.POST['printcontext']\n        assert len(printcontext) < 2048, \"Print context is way too big.\"\n\n        # Prepare context, extract and add infos\n        context = json.loads(printcontext)\n        selector = context.pop('selector')\n        map_url = context.pop('url')\n        map_url = request.build_absolute_uri(map_url)\n        context['print'] = True\n        printcontext = json.dumps(context)\n        contextencoded = quote(printcontext)\n        map_url += '?context=%s' % contextencoded\n        logger.debug(\"Capture %s\" % map_url)\n\n        # Capture image and return it\n        width = context.get('viewport', {}).get('width')\n        height = context.get('viewport', {}).get('height')\n\n        stream = BytesIO()\n        capture_image(map_url, stream, width=width, height=height, selector=selector)\n        response = HttpResponse(stream.getvalue(), content_type='image/png')\n        response['Content-Disposition'] = 'attachment; filename=%s.png' % datetime.now().strftime('%Y%m%d-%H%M%S')\n        return response\n\n    except Exception as exc:\n        logger.exception(exc)\n        return HttpResponseBadRequest(exc)\n\n\n@require_http_methods([\"POST\"])\n@csrf_exempt\n@login_required\ndef history_delete(request, path=None):\n    path = request.POST.get('path', path)\n    if path:\n        history = request.session.get('history')\n        if history:\n            history = [h for h in history if h['path'] != path]\n            request.session['history'] = history\n    return HttpResponse()\n", "path": "mapentity/views/base.py"}, {"content": "import mimetypes\n\nfrom django.contrib.contenttypes.fields import GenericRelation\nfrom django.db import models\nfrom django.utils.translation import gettext_lazy as _\nfrom django.template.defaultfilters import slugify\nfrom django.conf import settings\nfrom django.urls import reverse\n\nfrom bs4 import BeautifulSoup\nfrom extended_choices import Choices\n\nfrom mapentity.serializers import plain_text\nfrom geotrek.common.mixins import TimeStampedModelMixin, BasePublishableMixin\n\n\nFLATPAGES_TARGETS = Choices(\n    ('ALL', 'all', _('All')),\n    ('MOBILE', 'mobile', _('Mobile')),\n    ('HIDDEN', 'hidden', _('Hidden')),\n    ('WEB', 'web', _('Web')),\n)\n\n\nclass FlatPage(BasePublishableMixin, TimeStampedModelMixin):\n    \"\"\"\n    Manage *Geotrek-rando* static pages from Geotrek admin.\n\n    Historically, we started static pages as static HTML files within\n    *Geotrek-rando* folders.\n    \"\"\"\n    title = models.CharField(verbose_name=_('Title'), max_length=200)\n    external_url = models.URLField(verbose_name=_('External URL'), blank=True, default='',\n                                   help_text=_('Link to external website instead of HTML content'))\n    content = models.TextField(verbose_name=_('Content'), null=True, blank=True,\n                               help_text=_('HTML content'))\n    target = models.CharField(verbose_name=_('Target'), max_length=12, choices=FLATPAGES_TARGETS,\n                              default=FLATPAGES_TARGETS.ALL)\n    source = models.ManyToManyField('common.RecordSource',\n                                    blank=True, related_name='flatpages',\n                                    verbose_name=_(\"Source\"))\n    portal = models.ManyToManyField('common.TargetPortal',\n                                    blank=True, related_name='flatpages',\n                                    verbose_name=_(\"Portal\"))\n    order = models.IntegerField(default=None, null=True, blank=True,\n                                help_text=_(\"ID order if blank\", ),\n                                verbose_name=_(\"Order\"))\n    attachments = GenericRelation(settings.PAPERCLIP_ATTACHMENT_MODEL)\n\n    @property\n    def slug(self):\n        return slugify(self.title)\n\n    class Meta:\n        verbose_name = _('Flat page')\n        verbose_name_plural = _('Flat pages')\n        ordering = ['order', 'id']\n        permissions = (\n            (\"read_flatpage\", \"Can read FlatPage\"),\n        )\n\n    def __str__(self):\n        return self.title\n\n    def get_permission_codename(self, *args):\n        return\n\n    def clean(self):\n        html_content = ''\n        for language in settings.MAPENTITY_CONFIG['TRANSLATED_LANGUAGES']:\n            html_content += getattr(self, 'content_%s' % language[0], None) or ''\n\n    def parse_media(self):\n        soup = BeautifulSoup(self.content or '', 'lxml')\n        images = soup.findAll('img')\n        results = []\n        for image in images:\n            url = image.get('src')\n            if url is None:\n                continue\n\n            mt = mimetypes.guess_type(url, strict=True)[0]\n            if mt is None:\n                mt = 'application/octet-stream'\n\n            results.append({\n                'url': url,\n                'title': image.get('title', ''),\n                'alt': image.get('alt', ''),\n                'mimetype': mt.split('/'),\n            })\n\n        return results\n\n    def get_add_url(self):\n        return reverse('admin:flatpages_flatpage_add')\n\n    def get_update_url(self):\n        return reverse('admin:flatpages_flatpage_change', args=[self.pk])\n\n    def get_delete_url(self):\n        return reverse('admin:flatpages_flatpage_delete', args=[self.pk])\n\n    @property\n    def rando_url(self):\n        return 'informations/{}/'.format(self.slug)\n\n    @property\n    def meta_description(self):\n        return plain_text(self.content)[:500]\n\n    def is_public(self):\n        return self.any_published\n", "path": "geotrek/flatpages/models.py"}]}