django-content-studio 1.0.0b13__py3-none-any.whl → 1.0.0b14__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. content_studio/__init__.py +1 -1
  2. content_studio/contrib/__init__.py +0 -0
  3. content_studio/contrib/password_reset/__init__.py +1 -0
  4. content_studio/contrib/password_reset/apps.py +6 -0
  5. content_studio/contrib/password_reset/migrations/0001_initial.py +52 -0
  6. content_studio/contrib/password_reset/migrations/__init__.py +0 -0
  7. content_studio/contrib/password_reset/models.py +39 -0
  8. content_studio/contrib/password_reset/serializers.py +15 -0
  9. content_studio/contrib/password_reset/urls.py +30 -0
  10. content_studio/contrib/password_reset/views.py +118 -0
  11. content_studio/locale/nl/LC_MESSAGES/django.mo +0 -0
  12. content_studio/locale/nl/LC_MESSAGES/django.po +23 -4
  13. content_studio/login_backends/username_password.py +0 -16
  14. content_studio/settings.py +1 -0
  15. content_studio/static/content_studio/assets/index.css +1 -1
  16. content_studio/static/content_studio/assets/index.js +92 -73
  17. content_studio/static/content_studio/locales/en/translation.json +15 -0
  18. content_studio/static/content_studio/locales/nl/translation.json +15 -0
  19. content_studio/templates/content_studio/index.html +4 -4
  20. content_studio/views.py +5 -0
  21. {django_content_studio-1.0.0b13.dist-info → django_content_studio-1.0.0b14.dist-info}/METADATA +1 -1
  22. {django_content_studio-1.0.0b13.dist-info → django_content_studio-1.0.0b14.dist-info}/RECORD +24 -15
  23. {django_content_studio-1.0.0b13.dist-info → django_content_studio-1.0.0b14.dist-info}/LICENSE +0 -0
  24. {django_content_studio-1.0.0b13.dist-info → django_content_studio-1.0.0b14.dist-info}/WHEEL +0 -0
@@ -1,5 +1,5 @@
1
1
  __title__ = "Django Content Studio"
2
- __version__ = "1.0.0-beta.13"
2
+ __version__ = "1.0.0-beta.14"
3
3
  __author__ = "Leon van der Grient"
4
4
  __license__ = "MIT"
5
5
 
File without changes
@@ -0,0 +1 @@
1
+ default_app_config = "content_studio.contrib.apps.Config"
@@ -0,0 +1,6 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class Config(AppConfig):
5
+ name = "content_studio.contrib.password_reset"
6
+ verbose_name = "DCS Password Reset"
@@ -0,0 +1,52 @@
1
+ # Generated by Django 6.0.1 on 2026-02-04 09:42
2
+
3
+ import content_studio.contrib.password_reset.models
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ initial = True
10
+
11
+ dependencies = []
12
+
13
+ operations = [
14
+ migrations.CreateModel(
15
+ name="PasswordResetCode",
16
+ fields=[
17
+ (
18
+ "id",
19
+ models.BigAutoField(
20
+ auto_created=True,
21
+ primary_key=True,
22
+ serialize=False,
23
+ verbose_name="ID",
24
+ ),
25
+ ),
26
+ (
27
+ "email",
28
+ models.EmailField(
29
+ editable=False, max_length=255, verbose_name="Email address"
30
+ ),
31
+ ),
32
+ (
33
+ "code",
34
+ models.CharField(
35
+ default=content_studio.contrib.password_reset.models.generate_code,
36
+ editable=False,
37
+ max_length=6,
38
+ verbose_name="Code",
39
+ ),
40
+ ),
41
+ (
42
+ "created_at",
43
+ models.DateTimeField(auto_now_add=True, verbose_name="Created at"),
44
+ ),
45
+ ],
46
+ options={
47
+ "verbose_name": "Password reset code",
48
+ "verbose_name_plural": "Password reset codes",
49
+ "db_table": "dcs_password_reset_code",
50
+ },
51
+ ),
52
+ ]
@@ -0,0 +1,39 @@
1
+ import datetime
2
+ import random
3
+
4
+ from django.db import models
5
+ from django.utils import timezone
6
+
7
+ from content_studio.settings import cs_settings
8
+
9
+
10
+ def generate_code():
11
+ return "".join(str(random.randint(0, 9)) for _ in range(6))
12
+
13
+
14
+ class PasswordResetCode(models.Model):
15
+ class Meta:
16
+ db_table = "dcs_password_reset_code"
17
+ verbose_name = "Password reset code"
18
+ verbose_name_plural = "Password reset codes"
19
+
20
+ email = models.EmailField(
21
+ max_length=255, verbose_name="Email address", editable=False
22
+ )
23
+
24
+ code = models.CharField(
25
+ max_length=6, verbose_name="Code", default=generate_code, editable=False
26
+ )
27
+
28
+ created_at = models.DateTimeField(auto_now_add=True, verbose_name="Created at")
29
+
30
+ @property
31
+ def expired(self):
32
+ return (
33
+ self.created_at
34
+ + datetime.timedelta(minutes=cs_settings.PASSWORD_RESET_EXPIRATION_TIME)
35
+ < timezone.now()
36
+ )
37
+
38
+ def __str__(self):
39
+ return self.email
@@ -0,0 +1,15 @@
1
+ from rest_framework import serializers
2
+
3
+
4
+ class PasswordResetRequestSerializer(serializers.Serializer):
5
+ email = serializers.EmailField()
6
+
7
+
8
+ class CodeValidationSerializer(serializers.Serializer):
9
+ code = serializers.CharField(max_length=6)
10
+
11
+
12
+ class PasswordResetSubmissionSerializer(serializers.Serializer):
13
+ code = serializers.CharField(max_length=6)
14
+ email = serializers.EmailField()
15
+ password = serializers.CharField(min_length=8)
@@ -0,0 +1,30 @@
1
+ """
2
+ URL configuration for cms project.
3
+
4
+ The `urlpatterns` list routes URLs to views. For more information please see:
5
+ https://docs.djangoproject.com/en/4.2/topics/http/urls/
6
+ Examples:
7
+ Function views
8
+ 1. Add an import: from my_app import views
9
+ 2. Add a URL to urlpatterns: path('', views.home, name='home')
10
+ Class-based views
11
+ 1. Add an import: from other_app.views import Home
12
+ 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
13
+ Including another URLconf
14
+ 1. Import the include() function: from django.urls import include, path
15
+ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
16
+ """
17
+
18
+ from django.urls import path
19
+
20
+ from .views import (
21
+ PasswordResetRequestView,
22
+ PasswordResetSubmissionView,
23
+ CodeValidationView,
24
+ )
25
+
26
+ urlpatterns = [
27
+ path("api/password-reset/request", PasswordResetRequestView.as_view()),
28
+ path("api/password-reset/submit", PasswordResetSubmissionView.as_view()),
29
+ path("api/password-reset/code", CodeValidationView.as_view()),
30
+ ]
@@ -0,0 +1,118 @@
1
+ from django.contrib.auth import get_user_model
2
+ from django.core.mail import send_mail
3
+ from django.utils.translation import gettext_lazy as _
4
+ from rest_framework import status
5
+ from rest_framework.exceptions import ValidationError
6
+ from rest_framework.permissions import AllowAny
7
+ from rest_framework.response import Response
8
+ from rest_framework.views import APIView
9
+
10
+ from .models import PasswordResetCode
11
+ from .serializers import (
12
+ PasswordResetRequestSerializer,
13
+ CodeValidationSerializer,
14
+ PasswordResetSubmissionSerializer,
15
+ )
16
+
17
+
18
+ class PasswordResetRequestView(APIView):
19
+ permission_classes = [AllowAny]
20
+
21
+ def post(self, request):
22
+ serializer = PasswordResetRequestSerializer(data=request.data)
23
+
24
+ serializer.is_valid(raise_exception=True)
25
+
26
+ email = serializer.validated_data["email"]
27
+
28
+ # Delete any existing codes for this email.
29
+ PasswordResetCode.objects.filter(email=email).delete()
30
+
31
+ reset = PasswordResetCode.objects.create(email=email)
32
+
33
+ try:
34
+ self.send_email(reset)
35
+ except Exception as e:
36
+ reset.delete()
37
+ print(e)
38
+
39
+ return Response(status=status.HTTP_202_ACCEPTED)
40
+
41
+ def send_email(self, reset: PasswordResetCode):
42
+ send_mail(
43
+ from_email=None,
44
+ subject=_("Your password reset code"),
45
+ message=_("Your password reset code is: ") + reset.code,
46
+ recipient_list=[reset.email],
47
+ )
48
+
49
+
50
+ class CodeValidationView(APIView):
51
+ permission_classes = [AllowAny]
52
+
53
+ def post(self, request):
54
+ serializer = CodeValidationSerializer(data=request.data)
55
+
56
+ serializer.is_valid(raise_exception=True)
57
+
58
+ code = serializer.validated_data["code"]
59
+
60
+ existing = PasswordResetCode.objects.filter(code=code).first()
61
+
62
+ if not existing:
63
+ raise ValidationError("Invalid code.")
64
+
65
+ if existing.expired:
66
+ existing.delete()
67
+ raise ValidationError("Expired code.")
68
+
69
+ return Response(status=status.HTTP_204_NO_CONTENT)
70
+
71
+
72
+ class PasswordResetSubmissionView(APIView):
73
+ permission_classes = [AllowAny]
74
+
75
+ def post(self, request):
76
+ serializer = PasswordResetSubmissionSerializer(data=request.data)
77
+
78
+ serializer.is_valid(raise_exception=True)
79
+
80
+ code = serializer.validated_data["code"]
81
+ email = serializer.validated_data["email"]
82
+ password = serializer.validated_data["password"]
83
+
84
+ existing = PasswordResetCode.objects.filter(code=code, email=email).first()
85
+
86
+ if not existing:
87
+ raise ValidationError("Invalid code.")
88
+
89
+ if existing.expired:
90
+ existing.delete()
91
+ raise ValidationError("Expired code.")
92
+
93
+ user_model = get_user_model()
94
+ user = user_model.objects.filter(email=email).first()
95
+
96
+ if not user:
97
+ raise ValidationError("Invalid email.")
98
+
99
+ user.set_password(password)
100
+
101
+ existing.delete()
102
+
103
+ try:
104
+ self.send_email(email)
105
+ except Exception as e:
106
+ print(e)
107
+
108
+ return Response(status=status.HTTP_204_NO_CONTENT)
109
+
110
+ def send_email(self, email):
111
+ send_mail(
112
+ from_email=None,
113
+ subject=_("Your password has been reset"),
114
+ message=_(
115
+ "Your password has been reset. If you did not do this, please contact your administrator."
116
+ ),
117
+ recipient_list=[email],
118
+ )
@@ -8,7 +8,7 @@ msgid ""
8
8
  msgstr ""
9
9
  "Project-Id-Version: PACKAGE VERSION\n"
10
10
  "Report-Msgid-Bugs-To: \n"
11
- "POT-Creation-Date: 2026-02-03 20:22+0100\n"
11
+ "POT-Creation-Date: 2026-02-04 11:15+0100\n"
12
12
  "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13
13
  "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14
14
  "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,14 +17,33 @@ msgstr ""
17
17
  "Content-Type: text/plain; charset=UTF-8\n"
18
18
  "Content-Transfer-Encoding: 8bit\n"
19
19
  "Plural-Forms: nplurals=2; plural=(n != 1);\n"
20
- #: content_studio/dashboard/scheduled_tasks.py:23
20
+
21
+ #: contrib/password_reset/views.py:44
22
+ msgid "Your password reset code"
23
+ msgstr "Je wachtwoord-herstelcode"
24
+
25
+ #: contrib/password_reset/views.py:45
26
+ msgid "Your password reset code is: "
27
+ msgstr "Je wachtwoord-herstelcode is: "
28
+
29
+ #: contrib/password_reset/views.py:100
30
+ msgid "Your password has been reset"
31
+ msgstr "Je wachtwoord is gewijzigd"
32
+
33
+ #: contrib/password_reset/views.py:102
34
+ msgid ""
35
+ "Your password has been reset. If you did not do this, please contact your "
36
+ "administrator."
37
+ msgstr "Je wachtwoord is gewijzigd. Neem contact op met je administrator als jij dit niet hebt gedaan."
38
+
39
+ #: dashboard/scheduled_tasks.py:23
21
40
  msgid "Scheduled tasks"
22
41
  msgstr "Geplande taken"
23
42
 
24
- #: content_studio/dashboard/scheduled_tasks.py:26
43
+ #: dashboard/scheduled_tasks.py:26
25
44
  msgid "Monitor and manage automated tasks"
26
45
  msgstr "Monitor en beheer geautomatiseerde taken"
27
46
 
28
- #: content_studio/views.py:183
47
+ #: views.py:188
29
48
  msgid "Content"
30
49
  msgstr "Content"
@@ -64,19 +64,3 @@ class UsernamePasswordBackend:
64
64
  Returns the user if successful, None otherwise.
65
65
  """
66
66
  return authenticate(username=username, password=password)
67
-
68
- def request_password_reset(self, username):
69
- """
70
- Sends a password reset email.
71
- """
72
- raise NotImplemented(
73
- "You need to implement a method for sending a password reset token."
74
- )
75
-
76
- def complete_password_reset(self, reset_token, new_password):
77
- """
78
- Sets the new password based on the reset token.
79
- """
80
- raise NotImplemented(
81
- "You need to implement a method for validating a reset token and setting a new password."
82
- )
@@ -32,6 +32,7 @@ DEFAULTS = {
32
32
  "MEDIA_LIBRARY_MODEL": None,
33
33
  "MEDIA_LIBRARY_FOLDER_MODEL": None,
34
34
  "TENANT_MODEL": None,
35
+ "PASSWORD_RESET_EXPIRATION_TIME": 10,
35
36
  }
36
37
 
37
38