• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

wger-project / wger / 24964072477

26 Apr 2026 12:25PM UTC coverage: 86.106% (+0.4%) from 85.665%
24964072477

push

github

rolandgeider
Restrict trainer login access

17 of 18 new or added lines in 2 files covered. (94.44%)

91 existing lines in 9 files now uncovered.

17495 of 20318 relevant lines covered (86.11%)

0.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.17
/wger/core/api/serializers.py
1
# This file is part of wger Workout Manager.
2
#
3
# wger Workout Manager is free software: you can redistribute it and/or modify
4
# it under the terms of the GNU Affero General Public License as published by
5
# the Free Software Foundation, either version 3 of the License, or
6
# (at your option) any later version.
7
#
8
# wger Workout Manager is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU General Public License for more details.
12
#
13
# You should have received a copy of the GNU Affero General Public License
14
# along with Workout Manager.  If not, see <http://www.gnu.org/licenses/>.
15

16
# Standard Library
17
import logging
1✔
18

19
# Django
20
from django.contrib.auth import authenticate
1✔
21
from django.contrib.auth.models import User
1✔
22
from django.contrib.auth.password_validation import validate_password
1✔
23
from django.http import HttpRequest
1✔
24

25
# Third Party
26
from allauth.account.models import EmailAddress
1✔
27
from allauth.account.utils import (
1✔
28
    filter_users_by_email,
29
    user_email,
30
)
31
from lingua import LanguageDetectorBuilder
1✔
32
from rest_framework import serializers
1✔
33
from rest_framework.fields import empty
1✔
34
from rest_framework.validators import UniqueValidator
1✔
35

36
# wger
37
from wger.core.models import (
1✔
38
    Language,
39
    License,
40
    RepetitionUnit,
41
    UserProfile,
42
    WeightUnit,
43
)
44

45

46
logger = logging.getLogger(__name__)
1✔
47

48

49
class UserprofileSerializer(serializers.ModelSerializer):
1✔
50
    """
51
    Workout session serializer
52
    """
53

54
    email = serializers.EmailField(source='user.email', required=False)
1✔
55
    username = serializers.CharField(source='user.username', read_only=True)
1✔
56
    date_joined = serializers.DateTimeField(source='user.date_joined', read_only=True)
1✔
57
    email_verified = serializers.BooleanField(source='is_verified', read_only=True)
1✔
58

59
    class Meta:
1✔
60
        model = UserProfile
1✔
61
        fields = (
1✔
62
            'username',
63
            'email',
64
            'email_verified',
65
            'is_trustworthy',
66
            'date_joined',
67
            'gym',
68
            'weight_rounding',
69
            'repetitions_rounding',
70
            'is_temporary',
71
            'show_comments',
72
            'show_english_ingredients',
73
            'workout_reminder_active',
74
            'workout_reminder',
75
            'workout_duration',
76
            'last_workout_notification',
77
            'notification_language',
78
            'age',
79
            'birthdate',
80
            'height',
81
            'gender',
82
            'sleep_hours',
83
            'work_hours',
84
            'work_intensity',
85
            'sport_hours',
86
            'sport_intensity',
87
            'freetime_hours',
88
            'freetime_intensity',
89
            'calories',
90
            'weight_unit',
91
            'ro_access',
92
            'num_days_weight_reminder',
93
        )
94

95
    def validate_email(self, value):
1✔
96
        if not value:
1✔
UNCOV
97
            return value
×
98
        user = self.context['request'].user
1✔
99

100
        # Re-submitting one's own email is fine
101
        if value.lower() == (user.email or '').lower():
1✔
102
            return value
1✔
103

104
        # Uniqueness across `User.email` and allauth's `EmailAddress` table
105
        if any(u.pk != user.pk for u in filter_users_by_email(value)):
1✔
106
            raise serializers.ValidationError('A user with this email already exists.')
1✔
107
        return value
1✔
108

109
    def update(self, instance, validated_data):
1✔
110
        new_email = validated_data.pop('user', {}).get('email')
1✔
111
        instance = super().update(instance, validated_data)
1✔
112

113
        user = instance.user
1✔
114
        if new_email and new_email.lower() != (user.email or '').lower():
1✔
115
            user_email(user, new_email)
1✔
116
            user.save()
1✔
117
            EmailAddress.objects.add_email(
1✔
118
                self.context['request'],
119
                user,
120
                new_email,
121
                confirm=True,
122
            )
123

124
        return instance
1✔
125

126

127
class UserLoginSerializer(serializers.ModelSerializer):
1✔
128
    """Serializer to map to User model in relation to api user"""
129

130
    email = serializers.EmailField(required=False)
1✔
131
    username = serializers.CharField(required=False)
1✔
132
    password = serializers.CharField(required=True, min_length=8)
1✔
133

134
    request: HttpRequest
1✔
135

136
    class Meta:
1✔
137
        model = User
1✔
138
        fields = ('username', 'password', 'email')
1✔
139

140
    def __init__(self, request: HttpRequest = None, instance=None, data=empty, **kwargs):
1✔
141
        self.request = request
1✔
142
        super().__init__(instance, data, **kwargs)
1✔
143

144
    def validate(self, data):
1✔
145
        email = data.get('email', None)
1✔
146
        username = data.get('username', None)
1✔
147
        password = data.get('password', None)
1✔
148

149
        if email is None and username is None:
1✔
150
            raise serializers.ValidationError('Please provide an "email" or a "username"')
1✔
151

152
        user_username = authenticate(request=self.request, username=username, password=password)
1✔
153
        user_email = authenticate(request=self.request, username=email, password=password)
1✔
154
        user = user_username or user_email
1✔
155

156
        if user is None:
1✔
157
            logger.info(f"Tried logging via API with unknown user: '{username}'")
1✔
158
            raise serializers.ValidationError('Username or password unknown')
1✔
159

160
        return data
1✔
161

162

163
class UserRegistrationSerializer(serializers.ModelSerializer):
1✔
164
    email = serializers.EmailField(required=False)
1✔
165
    username = serializers.CharField(
1✔
166
        required=True,
167
        validators=[UniqueValidator(queryset=User.objects.all())],
168
    )
169
    password = serializers.CharField(write_only=True, required=True, validators=[validate_password])
1✔
170

171
    class Meta:
1✔
172
        model = User
1✔
173
        fields = ('username', 'email', 'password')
1✔
174

175
    def validate_email(self, value):
1✔
176
        # Case-insensitive uniqueness across both User.email and the allauth
177
        # EmailAddress table (which holds secondary addresses too)
178
        if value and filter_users_by_email(value):
1✔
179
            raise serializers.ValidationError('A user with this email already exists.')
1✔
180
        return value
1✔
181

182
    def create(self, validated_data):
1✔
183
        user = User.objects.create(username=validated_data['username'])
1✔
184
        user.set_password(validated_data['password'])
1✔
185
        if validated_data.get('email'):
1✔
186
            user.email = validated_data['email']
1✔
187
        user.save()
1✔
188

189
        return user
1✔
190

191

192
class LanguageSerializer(serializers.ModelSerializer):
1✔
193
    """
194
    Language serializer
195
    """
196

197
    class Meta:
1✔
198
        model = Language
1✔
199
        fields = (
1✔
200
            'id',
201
            'short_name',
202
            'full_name',
203
            'full_name_en',
204
        )
205

206

207
class LicenseSerializer(serializers.ModelSerializer):
1✔
208
    """
209
    License serializer
210
    """
211

212
    class Meta:
1✔
213
        model = License
1✔
214
        fields = (
1✔
215
            'id',
216
            'full_name',
217
            'short_name',
218
            'url',
219
        )
220

221

222
class RepetitionUnitSerializer(serializers.ModelSerializer):
1✔
223
    """
224
    Repetition unit serializer
225
    """
226

227
    class Meta:
1✔
228
        model = RepetitionUnit
1✔
229
        fields = ('id', 'name', 'unit_type', 'multiplier')
1✔
230

231

232
class RoutineWeightUnitSerializer(serializers.ModelSerializer):
1✔
233
    """
234
    Weight unit serializer
235
    """
236

237
    class Meta:
1✔
238
        model = WeightUnit
1✔
239
        fields = ('id', 'name')
1✔
240

241

242
class LanguageCheckSerializer(serializers.Serializer):
1✔
243
    """
244
    Serializer for language check
245
    """
246

247
    language = serializers.PrimaryKeyRelatedField(queryset=Language.objects.all(), required=False)
1✔
248
    language_code = serializers.CharField(required=False, min_length=2, max_length=2)
1✔
249
    input = serializers.CharField(min_length=10)
1✔
250

251
    def validate(self, data):
1✔
252
        """
253
        Check that the detected language of the description corresponds with the
254
        provided language.
255
        """
UNCOV
256
        language = data.get('language')
×
UNCOV
257
        language_code = data.get('language_code')
×
258

UNCOV
259
        if not language and not language_code:
×
UNCOV
260
            raise serializers.ValidationError(
×
261
                {'language': 'Either a language ID or a language code must be provided.'}
262
            )
263

UNCOV
264
        if not language:
×
UNCOV
265
            try:
×
UNCOV
266
                language = Language.objects.get(short_name=language_code)
×
UNCOV
267
            except Language.DoesNotExist:
×
268
                raise serializers.ValidationError(
×
269
                    {'language': f'Language with code "{language_code}" does not exist.'}
270
                )
271

272
        # Try to detect the language
UNCOV
273
        detector = (
×
274
            LanguageDetectorBuilder.from_all_languages()
275
            .with_low_accuracy_mode()
276
            .with_preloaded_language_models()
277
            .build()
278
        )
UNCOV
279
        input_str = data.get('input')
×
280

UNCOV
281
        detected_language = detector.detect_language_of(input_str)
×
UNCOV
282
        detected_language_code = detected_language.iso_code_639_1.name.lower()
×
UNCOV
283
        confidence_values = detector.compute_language_confidence_values(input_str)
×
UNCOV
284
        logger.debug(
×
285
            f'Detected language: {detected_language_code}, '
286
            f'confidence values: {confidence_values}, '
287
            f'input: {input_str}'
288
        )
289

UNCOV
290
        if detected_language_code != language.short_name.lower():
×
UNCOV
291
            raise serializers.ValidationError(
×
292
                {
293
                    'check': {
294
                        'result': False,
295
                        'detected_language': detected_language_code,
296
                        'message': f'The detected language is "{detected_language.name.capitalize()}" ({detected_language_code}), '
297
                        f'which does not match your selected language "{language.full_name.capitalize()}" '
298
                        f'({language.short_name}). If you believe this is incorrect, try adding more content '
299
                        f'or rephrasing your text, as language detection works better with longer or more '
300
                        f'complete sentences.',
301
                    }
302
                }
303
            )
304

UNCOV
305
        return super().validate(data)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc