Projet

Général

Profil

0001-WIP-support-avatar-picture-in-user-profile-26022.patch

Paul Marillonnet, 12 septembre 2018 17:28

Télécharger (15,8 ko)

Voir les différences:

Subject: [PATCH] WIP support avatar picture in user profile (#26022)

 src/authentic2/api_views.py                   |  5 +-
 src/authentic2/attribute_kinds.py             | 26 ++++++
 src/authentic2/custom_user/apps.py            | 20 ++--
 src/authentic2/custom_user/models.py          |  4 +-
 src/authentic2/forms/widgets.py               | 21 ++++-
 src/authentic2/models.py                      | 21 ++++-
 src/authentic2/settings.py                    |  2 +
 .../templates/authentic2/accounts.html        | 22 +++--
 .../templates/authentic2/accounts_edit.html   |  7 +-
 src/authentic2/urls.py                        |  4 +
 src/authentic2/utils.py                       | 91 +++++++++++++++++++
 11 files changed, 200 insertions(+), 23 deletions(-)
src/authentic2/api_views.py
298 298
@cache_control(private=True, max_age=60)
299 299
@decorators.json
300 300
def user(request):
301
    if request.user.is_anonymous():
301
    u = request.user
302
    if u.is_anonymous():
302 303
        return {}
303
    return request.user.to_json()
304
    return u.to_json(request)
304 305

  
305 306

  
306 307
def attributes_hash(attributes):
src/authentic2/attribute_kinds.py
17 17
from .plugins import collect_from_plugins
18 18
from . import app_settings
19 19
from .forms import widgets
20
from .utils import image_serialize
20 21

  
21 22
capfirst = allow_lazy(capfirst, unicode)
22 23

  
......
91 92
    default_validators = [validate_fr_postcode]
92 93

  
93 94

  
95
class A2ImageDRFField(serializers.ImageField):
96
    def to_representation(self, value, *args, **kwargs):
97
        return self.context['request'].build_absolute_uri(value)
98

  
99

  
94 100
DEFAULT_ALLOW_BLANK = True
95 101
DEFAULT_MAX_LENGTH = 256
96 102

  
......
151 157
        'field_class': PhoneNumberField,
152 158
        'rest_framework_field_class': PhoneNumberDRFField,
153 159
    },
160
    {
161
        'label': _('image'),
162
        'name': 'image',
163
        'field_class': forms.ImageField,
164
        'kwargs': {
165
            'widget': widgets.AttributeImageInput,
166
        },
167
        'serialize': image_serialize,
168
        'serialize_eval_kwargs' : {
169
            'owner_uuid': 'owner.uuid',
170
            'owner_pk': 'owner.pk',
171
            'attr_label': 'self.label',
172
        },
173
        'deserialize': lambda x: x,
174
        'rest_framework_field_class': A2ImageDRFField,
175
        'rest_framework_field_kwargs': {
176
            'read_only': True,
177
            },
178
        'value_is_relative_uri': True,
179
    },
154 180
]
155 181

  
156 182

  
src/authentic2/custom_user/apps.py
10 10
        from django.db.models.signals import post_migrate
11 11

  
12 12
        post_migrate.connect(
13
            self.create_first_name_last_name_attributes,
13
            self.create_custom_attributes,
14 14
            sender=self)
15 15

  
16
    def create_first_name_last_name_attributes(self, app_config, verbosity=2, interactive=True,
16
    def create_custom_attributes(self, app_config, verbosity=2, interactive=True,
17 17
                                               using=DEFAULT_DB_ALIAS, **kwargs):
18 18
        from django.utils import translation
19 19
        from django.utils.translation import ugettext_lazy as _
......
34 34
        content_type = ContentType.objects.get_for_model(User)
35 35

  
36 36
        attrs = {}
37
        attrs['avatar_picture'], created = Attribute.objects.get_or_create(
38
            name='avatar_picture',
39
            defaults={'kind': 'image',
40
                      'label': _('Avatar picture'),
41
                      'required': False,
42
                      'asked_on_registration': False,
43
                      'user_editable': True,
44
                      'user_visible': True})
37 45
        attrs['first_name'], created = Attribute.objects.get_or_create(
38 46
            name='first_name',
39 47
            defaults={'kind': 'string',
......
51 59
                      'user_editable': True,
52 60
                      'user_visible': True})
53 61

  
54
        serialize = get_kind('string').get('serialize')
55 62
        for user in User.objects.all():
56
            for attr_name in attrs:
63
            for at in attrs:
64
                serialize = get_kind(at.kind).get('serialize')
57 65
                av, created = AttributeValue.objects.get_or_create(
58 66
                    content_type=content_type,
59 67
                    object_id=user.id,
60
                    attribute=attrs[attr_name],
68
                    attribute=attrs[at],
61 69
                    defaults={
62 70
                        'multiple': False,
63 71
                        'verified': False,
64
                        'content': serialize(getattr(user, attr_name, None))
72
                        'content': serialize(getattr(user, at, None))
65 73
                    })
src/authentic2/custom_user/models.py
219 219
    def has_verified_attributes(self):
220 220
        return AttributeValue.objects.with_owner(self).filter(verified=True).exists()
221 221

  
222
    def to_json(self):
222
    def to_json(self, request):
223 223
        d = {}
224 224
        for av in AttributeValue.objects.with_owner(self):
225
            d[str(av.attribute.name)] = av.to_python()
225
            d[str(av.attribute.name)] = av.to_python(request)
226 226
        d.update({
227 227
            'uuid': self.uuid,
228 228
            'username': self.username,
src/authentic2/forms/widgets.py
11 11
import re
12 12
import uuid
13 13

  
14
from django.forms.widgets import DateTimeInput, DateInput, TimeInput
14
from django.forms.widgets import DateTimeInput, DateInput, TimeInput, \
15
        ClearableFileInput
15 16
from django.forms.widgets import PasswordInput as BasePasswordInput
16 17
from django.utils.formats import get_language, get_format
17 18
from django.utils.safestring import mark_safe
......
246 247
                    json.dumps(_id),
247 248
                )
248 249
        return output
250

  
251

  
252
class AttributeImageInput(ClearableFileInput):
253
    # template_name = "authentic2/accounts_image.html" # Django 1.11 only todo
254
    template_with_initial = (
255
        '%(initial_text)s: <br /> <img src="%(initial)s"/> <br />'
256
        '%(clear_template)s<br />%(input_text)s: %(input)s'
257
    )
258

  
259
    def is_initial(self, value):
260
        return bool(value)
261

  
262
    def get_template_substitution_values(self, value):
263
        subs_values = dict()
264
        subs_values.update({
265
            'initial': value,
266
        })
267
        return subs_values
src/authentic2/models.py
225 225
                    av.verified = verified
226 226
                    av.save()
227 227
        else:
228
            content = serialize(value)
228
            serialize_eval_kwargs = self.get_kind().get('serialize_eval_kwargs', {})
229
            if serialize_eval_kwargs:
230
                kwargs = dict()
231
                for key, flat_value in serialize_eval_kwargs.items():
232
                    evalue = eval(flat_value)
233
                    kwargs.update({key: evalue})
234
                content = serialize(value, **kwargs)
235
            else:
236
                content = serialize(value)
237

  
229 238
            av, created = AttributeValue.objects.get_or_create(
230 239
                    content_type=ContentType.objects.get_for_model(owner),
231 240
                    object_id=owner.pk,
......
275 284

  
276 285
    objects = managers.AttributeValueManager()
277 286

  
278
    def to_python(self):
279
        deserialize = self.attribute.get_kind()['deserialize']
280
        return deserialize(self.content)
287
    def to_python(self, request=None):
288
        kind = self.attribute.get_kind()
289
        deserialize = kind['deserialize']
290
        content = self.content
291
        if request and kind.get('value_is_relative_uri'):
292
            content = request.build_absolute_uri(content)
293
        return deserialize(content)
281 294

  
282 295
    def natural_key(self):
283 296
        if not hasattr(self.owner, 'natural_key'):
src/authentic2/settings.py
22 22
DEBUG = False
23 23
DEBUG_DB = False
24 24
MEDIA = 'media'
25
MEDIA_ROOT = 'media'
26
MEDIA_URL = '/media/'
25 27

  
26 28
# See https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
27 29
ALLOWED_HOSTS = []
src/authentic2/templates/authentic2/accounts.html
18 18
          {% for attribute in attributes %}
19 19
            <dt>{{ attribute.attribute.label|capfirst }}&nbsp;:</dt>
20 20
            <dd>
21
              {% if attribute.values|length == 1 %}
22
                {{ attribute.values.0 }}
21
              {% if attribute.attribute.kind == 'image' %}
22
                {% if attribute.attribute.name == 'avatar_picture' %}
23
                    <img class="avatar_picture" src="{{ attribute.values.0 }}">
24
                {% else %}
25
                    <img src="{{ attribute.values.0 }}">
26
                {% endif %}
23 27
              {% else %}
24
                <ul>
25
                  {% for value in attribute.values %}
26
                    <li>{{ value }}</li>
27
                  {% endfor %}
28
                </ul>
28
                {% if attribute.values|length == 1 %}
29
                  {{ attribute.values.0 }}
30
                {% else %}
31
                  <ul>
32
                    {% for value in attribute.values %}
33
                      <li>{{ value }}</li>
34
                    {% endfor %}
35
                  </ul>
36
                {% endif %}
29 37
              {% endif %}
30 38
            </dd>
31 39
          {% endfor %}
src/authentic2/templates/authentic2/accounts_edit.html
12 12
{% endblock %}
13 13

  
14 14
{% block content %}
15
  <form method="post">
15
    {% if form.is_multipart %}
16
      <form enctype="multipart/form-data" method="post">
17
    {% else %}
18
      <form method="post">
19
    {% endif %}
20

  
16 21
    {% csrf_token %}
17 22
    {{ form.as_p }}
18 23
    {% if form.instance and form.instance.id %}
src/authentic2/urls.py
44 44
    urlpatterns += [
45 45
        url(r'^static/(?P<path>.*)$', serve)
46 46
    ]
47
    urlpatterns += [
48
        url(r'^media/(?P<path>.*)$', 'django.views.static.serve', {
49
        'document_root': settings.MEDIA_ROOT})
50
    ]
47 51

  
48 52
if settings.DEBUG and 'debug_toolbar' in settings.INSTALLED_APPS:
49 53
    import debug_toolbar
src/authentic2/utils.py
8 8
import uuid
9 9
import datetime
10 10
import copy
11
import fcntl
12
import os
13
import tempfile
11 14

  
12 15
from functools import wraps
13 16
from itertools import islice, chain, count
14 17

  
15 18
from importlib import import_module
19
from hashlib import md5
16 20

  
17 21
from django.conf import settings
18 22
from django.http import HttpResponseRedirect, HttpResponse
......
30 34
from django.template.loader import render_to_string, TemplateDoesNotExist
31 35
from django.core.mail import send_mail
32 36
from django.core import signing
37
from django.core.files.storage import default_storage
33 38
from django.core.urlresolvers import reverse, NoReverseMatch
34 39
from django.utils.formats import localize
35 40
from django.contrib import messages
......
1073 1078
        if ou_value is not None:
1074 1079
            return ou_value
1075 1080
    return default
1081

  
1082

  
1083
def _store_image(in_memory_image, owner_uuid):
1084
    logger = logging.getLogger(__name__)
1085

  
1086
    digest = md5(in_memory_image.read())
1087

  
1088
    img_id = digest.hexdigest()
1089
    img_ext = in_memory_image.image.format
1090

  
1091
    img_media_dir = 'avatars/{oid}/'.format(oid=owner_uuid)
1092
    img_media_path = '{imdir}/{iid}.{ext}'.format(
1093
            imdir=img_media_dir, iid=img_id, ext=img_ext)
1094

  
1095
    img_abs_path = default_storage.path(img_media_path)
1096
    img_abs_dir = default_storage.path(img_media_dir)
1097

  
1098
    try:
1099
        if not os.path.exists(img_abs_dir):
1100
            os.makedirs(img_abs_dir)
1101

  
1102
        with open(img_abs_path, 'wb') as f:
1103
            try:
1104
                fcntl.lockf(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
1105
                with tempfile.NamedTemporaryFile(mode='wb', dir=img_abs_dir, delete=False) as temp:
1106
                    try:
1107
                        in_memory_image.seek(0)
1108
                        temp.write(in_memory_image.read())
1109
                        temp.flush()
1110
                        os.rename(temp.name, img_abs_path)
1111
                    except:
1112
                        logger.error("Could'nt store image to {}".format(img_abs_path))
1113
                        os.unlink(temp.name)
1114
            except:
1115
                logger.error("Couldn't hold exclusive lock for file {}".format(img_abs_path))
1116
            finally:
1117
                fcntl.lockf(f, fcntl.LOCK_UN)
1118
    except IOError:
1119
        return
1120

  
1121
    return img_media_path
1122

  
1123
def _delete_images_from_user(owner_pk, attr_label):
1124
    from .models import Attribute, AttributeValue
1125

  
1126
    logger = logging.getLogger(__name__)
1127
    User = get_user_model()
1128

  
1129
    try:
1130
        owner = User.objects.get(pk=owner_pk)
1131
    except User.DoesNotExist:
1132
        logger.error("Primary key {} doesn't match with any user.".format(owner_pk))
1133
        return
1134

  
1135
    try:
1136
        attr = Attribute.objects.get(label=attr_label)
1137
        all_values =  AttributeValue.objects.with_owner(owner)
1138
        values = all_values.filter(attribute=attr)
1139
    except:
1140
        logger.error("Couldn't retrieve values for Attribute {}.".format(attr_label))
1141

  
1142
    for value in values:
1143
        # Direct URI <-> file location correspondence
1144
        local_file = value.content.split(default_storage.base_url)[-1]
1145
        if not local_file:
1146
            continue
1147
        media_file = default_storage.path(local_file)
1148

  
1149
        try:
1150
            os.remove(media_file)
1151
            value.delete()
1152
        except:
1153
            logger.error("Could'nt delete image {}".format(media_file))
1154

  
1155

  
1156
def image_serialize(image, owner_uuid, owner_pk, attr_label):
1157
    uri = ''
1158
    if isinstance(image, basestring):
1159
        uri = image
1160
    else:
1161
        # Discard previous user avatars
1162
        _delete_images_from_user(owner_pk, attr_label)
1163
        if image:
1164
            img_media_path = _store_image(image, owner_uuid)
1165
            uri = os.path.join(settings.MEDIA_URL, img_media_path)
1166
    return uri
1076
-