django-content-studio 1.0.0b12__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 (25) 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/dashboard/scheduled_tasks.py +43 -0
  12. content_studio/locale/nl/LC_MESSAGES/django.mo +0 -0
  13. content_studio/locale/nl/LC_MESSAGES/django.po +49 -0
  14. content_studio/login_backends/username_password.py +0 -16
  15. content_studio/settings.py +1 -0
  16. content_studio/static/content_studio/assets/index.css +1 -1
  17. content_studio/static/content_studio/assets/index.js +92 -73
  18. content_studio/static/content_studio/locales/en/translation.json +25 -0
  19. content_studio/static/content_studio/locales/nl/translation.json +25 -0
  20. content_studio/templates/content_studio/index.html +7 -7
  21. content_studio/views.py +8 -0
  22. {django_content_studio-1.0.0b12.dist-info → django_content_studio-1.0.0b14.dist-info}/METADATA +1 -1
  23. {django_content_studio-1.0.0b12.dist-info → django_content_studio-1.0.0b14.dist-info}/RECORD +25 -13
  24. {django_content_studio-1.0.0b12.dist-info → django_content_studio-1.0.0b14.dist-info}/LICENSE +0 -0
  25. {django_content_studio-1.0.0b12.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.12"
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
+ )
@@ -0,0 +1,43 @@
1
+ from django.utils.translation import gettext_lazy as _
2
+ from rest_framework import serializers
3
+
4
+ from ..dashboard import BaseWidget
5
+
6
+
7
+ class ScheduledTaskSerializer(serializers.Serializer):
8
+ title = serializers.CharField()
9
+ description = serializers.CharField(allow_blank=True, required=False)
10
+ last_run_at = serializers.DateTimeField(allow_null=True)
11
+ next_run_at = serializers.DateTimeField(allow_null=True)
12
+ duration = serializers.IntegerField(allow_null=True)
13
+ status = serializers.ChoiceField(
14
+ choices=["RUNNING", "SCHEDULED", "SUCCESS", "FAILURE"]
15
+ )
16
+ error_message = serializers.CharField(
17
+ allow_blank=True, allow_null=True, required=False
18
+ )
19
+
20
+
21
+ class ScheduledTasksWidgetSerializer(serializers.Serializer):
22
+ title = serializers.CharField(
23
+ default=_("Scheduled tasks"), allow_blank=True, required=False
24
+ )
25
+ description = serializers.CharField(
26
+ default=_("Monitor and manage automated tasks"),
27
+ allow_blank=True,
28
+ required=False,
29
+ )
30
+ tasks = serializers.ListField(child=ScheduledTaskSerializer())
31
+
32
+
33
+ class ScheduledTasksWidget(BaseWidget):
34
+ """
35
+ Widget for showing a list of scheduled tasks.
36
+ """
37
+
38
+ name = "ScheduledTasksWidget"
39
+
40
+ col_span = 2
41
+
42
+ def get_data(self, request):
43
+ raise NotImplementedError("You need to implement get_data for your widget.")
@@ -0,0 +1,49 @@
1
+ # SOME DESCRIPTIVE TITLE.
2
+ # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3
+ # This file is distributed under the same license as the PACKAGE package.
4
+ # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5
+ #
6
+ #, fuzzy
7
+ msgid ""
8
+ msgstr ""
9
+ "Project-Id-Version: PACKAGE VERSION\n"
10
+ "Report-Msgid-Bugs-To: \n"
11
+ "POT-Creation-Date: 2026-02-04 11:15+0100\n"
12
+ "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13
+ "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14
+ "Language-Team: LANGUAGE <LL@li.org>\n"
15
+ "Language: \n"
16
+ "MIME-Version: 1.0\n"
17
+ "Content-Type: text/plain; charset=UTF-8\n"
18
+ "Content-Transfer-Encoding: 8bit\n"
19
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
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
40
+ msgid "Scheduled tasks"
41
+ msgstr "Geplande taken"
42
+
43
+ #: dashboard/scheduled_tasks.py:26
44
+ msgid "Monitor and manage automated tasks"
45
+ msgstr "Monitor en beheer geautomatiseerde taken"
46
+
47
+ #: views.py:188
48
+ msgid "Content"
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