Compare commits
27 Commits
feat/add-c
...
feat/add-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
a2ad8e7ac9
|
|||
|
a0b4697c61
|
|||
|
983384f62b
|
|||
|
668ddccea5
|
|||
|
6dd826c3bd
|
|||
|
e74c1fb28d
|
|||
|
cb19bc6262
|
|||
|
a918ee73c4
|
|||
|
5913e847bc
|
|||
|
18b21b0892
|
|||
|
efb3799e12
|
|||
|
306d39bd22
|
|||
|
e503d69235
|
|||
|
57ec3162d0
|
|||
|
c4e9ec5484
|
|||
|
c8732a05cb
|
|||
|
6810e540e5
|
|||
|
21500e0f10
|
|||
|
be42d71bb8
|
|||
|
b5e9e1ec66
|
|||
|
d575c836e9
|
|||
|
84a6c4cf5e
|
|||
|
e46f034d9e
|
|||
|
dc7e34f5b6
|
|||
|
f002651e2a
|
|||
|
c789eeb4ff
|
|||
|
acb6ea58ce
|
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-03-17 11:46+0000\n"
|
"POT-Creation-Date: 2026-03-20 12:18+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -18,6 +18,79 @@ msgstr ""
|
|||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
|
#: home/templates/chat/admin/admin_chat.html:5
|
||||||
|
msgid "Chat with"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: home/templates/chat/admin/admin_chat.html:10
|
||||||
|
msgid "Admin Chat View"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: home/templates/chat/admin/admin_chat.html:11
|
||||||
|
msgid ""
|
||||||
|
"This is the admin view of the chat. Here you can manage conversations and "
|
||||||
|
"monitor user interactions."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: home/templates/chat/admin/admin_chat.html:19
|
||||||
|
#: home/templates/chat/user_chat.html:18
|
||||||
|
msgid "No messages found."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: home/templates/chat/admin/admin_chat.html:24
|
||||||
|
#: home/templates/chat/user_chat.html:23
|
||||||
|
msgid "Type your message here..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: home/templates/chat/admin/admin_chat.html:25
|
||||||
|
#: home/templates/chat/user_chat.html:24
|
||||||
|
msgid "Send"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: home/templates/chat/admin/admin_chat_dashboard.html:5
|
||||||
|
#: home/templates/chat/user_chat.html:5
|
||||||
|
msgid "Chat"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: home/templates/chat/admin/admin_chat_dashboard.html:10
|
||||||
|
msgid "Admin Chat Dashboard"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: home/templates/chat/admin/admin_chat_dashboard.html:19
|
||||||
|
msgid "No active chats found."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: home/templates/chat/user_chat.html:9
|
||||||
|
msgid "Chat with Support"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: home/templates/chat/user_chat.html:10
|
||||||
|
msgid ""
|
||||||
|
"This is the user chat interface. Here you can communicate with our support "
|
||||||
|
"team for assistance."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: home/templates/home/course_index_page.html:4
|
||||||
|
#: home/templates/home/course_index_page.html:10
|
||||||
|
msgid "Courses"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: home/templates/home/course_index_page.html:12
|
||||||
|
msgid "Purchased Courses"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: home/templates/home/course_index_page.html:22
|
||||||
|
msgid "Purchased"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: home/templates/home/course_index_page.html:32
|
||||||
|
msgid "Available Courses"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: home/templates/home/course_index_page.html:42
|
||||||
|
msgid "Not Purchased"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: home/templates/home/course_module_page.html:21
|
#: home/templates/home/course_module_page.html:21
|
||||||
msgid "Lessons"
|
msgid "Lessons"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -26,36 +99,40 @@ msgstr ""
|
|||||||
msgid "No lessons yet."
|
msgid "No lessons yet."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: home/templates/home/course_page.html:26
|
#: home/templates/home/course_page.html:31
|
||||||
msgid "Modules"
|
msgid "Refund Purchase"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: home/templates/home/course_page.html:33
|
#: home/templates/home/course_page.html:33
|
||||||
|
msgid "Modules"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: home/templates/home/course_page.html:40
|
||||||
msgid "No modules yet."
|
msgid "No modules yet."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: home/templates/home/course_page.html:39
|
#: home/templates/home/course_page.html:46
|
||||||
msgid ""
|
msgid ""
|
||||||
"You need to be logged in to access this course. Please log in or sign up to "
|
"You need to be logged in to access this course. Please log in or sign up to "
|
||||||
"view the modules."
|
"view the modules."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: home/templates/home/course_page.html:40
|
#: home/templates/home/course_page.html:47
|
||||||
#: home/templates/home/event_page.html:40
|
#: home/templates/home/event_page.html:40
|
||||||
msgid "Login"
|
msgid "Login"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: home/templates/home/course_page.html:41
|
#: home/templates/home/course_page.html:48
|
||||||
#: home/templates/home/event_page.html:43
|
#: home/templates/home/event_page.html:43
|
||||||
msgid "Sign Up"
|
msgid "Sign Up"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: home/templates/home/course_page.html:46
|
#: home/templates/home/course_page.html:53
|
||||||
msgid ""
|
msgid ""
|
||||||
"You don't have access to this course. Please purchase it to view the modules."
|
"You don't have access to this course. Please purchase it to view the modules."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: home/templates/home/course_page.html:47
|
#: home/templates/home/course_page.html:54
|
||||||
msgid "Purchase Course"
|
msgid "Purchase Course"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -91,46 +168,3 @@ msgstr ""
|
|||||||
#: home/templates/home/event_page.html:54
|
#: home/templates/home/event_page.html:54
|
||||||
msgid "Sign Up for Event"
|
msgid "Sign Up for Event"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: home/templates/home/welcome_page.html:6
|
|
||||||
msgid "Visit the Wagtail website"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: home/templates/home/welcome_page.html:15
|
|
||||||
msgid "View the release notes"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: home/templates/home/welcome_page.html:27
|
|
||||||
msgid "Welcome to your new Wagtail site!"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: home/templates/home/welcome_page.html:28
|
|
||||||
msgid ""
|
|
||||||
"Please feel free to <a href=\"https://github.com/wagtail/wagtail/wiki/"
|
|
||||||
"Slack\">join our community on Slack</a>, or get started with one of the "
|
|
||||||
"links below."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: home/templates/home/welcome_page.html:35
|
|
||||||
msgid "Wagtail Documentation"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: home/templates/home/welcome_page.html:36
|
|
||||||
msgid "Topics, references, & how-tos"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: home/templates/home/welcome_page.html:42
|
|
||||||
msgid "Tutorial"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: home/templates/home/welcome_page.html:43
|
|
||||||
msgid "Build your first Wagtail site"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: home/templates/home/welcome_page.html:49
|
|
||||||
msgid "Admin Interface"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: home/templates/home/welcome_page.html:50
|
|
||||||
msgid "Create your superuser first!"
|
|
||||||
msgstr ""
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-03-17 11:46+0000\n"
|
"POT-Creation-Date: 2026-03-20 12:18+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -20,25 +20,102 @@ msgstr ""
|
|||||||
"(n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && "
|
"(n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && "
|
||||||
"n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
|
"n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
|
||||||
|
|
||||||
|
#: home/templates/chat/admin/admin_chat.html:5
|
||||||
|
msgid "Chat with"
|
||||||
|
msgstr "Czat z"
|
||||||
|
|
||||||
|
#: home/templates/chat/admin/admin_chat.html:10
|
||||||
|
msgid "Admin Chat View"
|
||||||
|
msgstr "Widok administratora czatu"
|
||||||
|
|
||||||
|
#: home/templates/chat/admin/admin_chat.html:11
|
||||||
|
msgid ""
|
||||||
|
"This is the admin view of the chat. Here you can manage conversations and "
|
||||||
|
"monitor user interactions."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: home/templates/chat/admin/admin_chat.html:19
|
||||||
|
#: home/templates/chat/user_chat.html:18
|
||||||
|
msgid "No messages found."
|
||||||
|
msgstr "Brak wiadomości."
|
||||||
|
|
||||||
|
#: home/templates/chat/admin/admin_chat.html:24
|
||||||
|
#: home/templates/chat/user_chat.html:23
|
||||||
|
msgid "Type your message here..."
|
||||||
|
msgstr "Wiadomość..."
|
||||||
|
|
||||||
|
#: home/templates/chat/admin/admin_chat.html:25
|
||||||
|
#: home/templates/chat/user_chat.html:24
|
||||||
|
msgid "Send"
|
||||||
|
msgstr "Wyślij"
|
||||||
|
|
||||||
|
#: home/templates/chat/admin/admin_chat_dashboard.html:5
|
||||||
|
#: home/templates/chat/user_chat.html:5
|
||||||
|
msgid "Chat"
|
||||||
|
msgstr "Czat"
|
||||||
|
|
||||||
|
#: home/templates/chat/admin/admin_chat_dashboard.html:10
|
||||||
|
msgid "Admin Chat Dashboard"
|
||||||
|
msgstr "Panel administratora czatu"
|
||||||
|
|
||||||
|
#: home/templates/chat/admin/admin_chat_dashboard.html:19
|
||||||
|
msgid "No active chats found."
|
||||||
|
msgstr "Brak aktywnych czatów."
|
||||||
|
|
||||||
|
#: home/templates/chat/user_chat.html:9
|
||||||
|
msgid "Chat with Support"
|
||||||
|
msgstr "Czat z administracją"
|
||||||
|
|
||||||
|
#: home/templates/chat/user_chat.html:10
|
||||||
|
msgid ""
|
||||||
|
"This is the user chat interface. Here you can communicate with our support "
|
||||||
|
"team for assistance."
|
||||||
|
msgstr ""
|
||||||
|
"To jest interfejs czatu dla użytkowników. Tutaj możesz komunikować się z "
|
||||||
|
"naszym zespołem wsparcia w celu uzyskania pomocy."
|
||||||
|
|
||||||
|
#: home/templates/home/course_index_page.html:4
|
||||||
|
#: home/templates/home/course_index_page.html:10
|
||||||
|
msgid "Courses"
|
||||||
|
msgstr "Kursy"
|
||||||
|
|
||||||
|
#: home/templates/home/course_index_page.html:12
|
||||||
|
msgid "Purchased Courses"
|
||||||
|
msgstr "Zakupione kursy"
|
||||||
|
|
||||||
|
#: home/templates/home/course_index_page.html:22
|
||||||
|
msgid "Purchased"
|
||||||
|
msgstr "Zakupiony"
|
||||||
|
|
||||||
|
#: home/templates/home/course_index_page.html:32
|
||||||
|
msgid "Available Courses"
|
||||||
|
msgstr "Dostępne kursy"
|
||||||
|
|
||||||
|
#: home/templates/home/course_index_page.html:42
|
||||||
|
msgid "Not Purchased"
|
||||||
|
msgstr "Niezakupiony"
|
||||||
|
|
||||||
#: home/templates/home/course_module_page.html:21
|
#: home/templates/home/course_module_page.html:21
|
||||||
msgid "Lessons"
|
msgid "Lessons"
|
||||||
msgstr "Lekcje"
|
msgstr "Lekcje"
|
||||||
|
|
||||||
#: home/templates/home/course_module_page.html:28
|
#: home/templates/home/course_module_page.html:28
|
||||||
#, fuzzy
|
|
||||||
#| msgid "No modules yet."
|
|
||||||
msgid "No lessons yet."
|
msgid "No lessons yet."
|
||||||
msgstr "Brak lekcji."
|
msgstr "Brak lekcji."
|
||||||
|
|
||||||
#: home/templates/home/course_page.html:26
|
#: home/templates/home/course_page.html:31
|
||||||
|
msgid "Refund Purchase"
|
||||||
|
msgstr "Zwróć zakup"
|
||||||
|
|
||||||
|
#: home/templates/home/course_page.html:33
|
||||||
msgid "Modules"
|
msgid "Modules"
|
||||||
msgstr "Moduły"
|
msgstr "Moduły"
|
||||||
|
|
||||||
#: home/templates/home/course_page.html:33
|
#: home/templates/home/course_page.html:40
|
||||||
msgid "No modules yet."
|
msgid "No modules yet."
|
||||||
msgstr "Brak modułów."
|
msgstr "Brak modułów."
|
||||||
|
|
||||||
#: home/templates/home/course_page.html:39
|
#: home/templates/home/course_page.html:46
|
||||||
msgid ""
|
msgid ""
|
||||||
"You need to be logged in to access this course. Please log in or sign up to "
|
"You need to be logged in to access this course. Please log in or sign up to "
|
||||||
"view the modules."
|
"view the modules."
|
||||||
@@ -46,22 +123,22 @@ msgstr ""
|
|||||||
"Musisz być zalogowany, aby uzyskać dostęp do tego kursu. Zaloguj się lub "
|
"Musisz być zalogowany, aby uzyskać dostęp do tego kursu. Zaloguj się lub "
|
||||||
"zarejestruj, aby zobaczyć moduły."
|
"zarejestruj, aby zobaczyć moduły."
|
||||||
|
|
||||||
#: home/templates/home/course_page.html:40
|
#: home/templates/home/course_page.html:47
|
||||||
#: home/templates/home/event_page.html:40
|
#: home/templates/home/event_page.html:40
|
||||||
msgid "Login"
|
msgid "Login"
|
||||||
msgstr "Zaloguj się"
|
msgstr "Zaloguj się"
|
||||||
|
|
||||||
#: home/templates/home/course_page.html:41
|
#: home/templates/home/course_page.html:48
|
||||||
#: home/templates/home/event_page.html:43
|
#: home/templates/home/event_page.html:43
|
||||||
msgid "Sign Up"
|
msgid "Sign Up"
|
||||||
msgstr "Zarejestruj się"
|
msgstr "Zarejestruj się"
|
||||||
|
|
||||||
#: home/templates/home/course_page.html:46
|
#: home/templates/home/course_page.html:53
|
||||||
msgid ""
|
msgid ""
|
||||||
"You don't have access to this course. Please purchase it to view the modules."
|
"You don't have access to this course. Please purchase it to view the modules."
|
||||||
msgstr "Nie masz dostępu do tego kursu. Zakup go, aby zobaczyć moduły."
|
msgstr "Nie masz dostępu do tego kursu. Zakup go, aby zobaczyć moduły."
|
||||||
|
|
||||||
#: home/templates/home/course_page.html:47
|
#: home/templates/home/course_page.html:54
|
||||||
msgid "Purchase Course"
|
msgid "Purchase Course"
|
||||||
msgstr "Kup kurs"
|
msgstr "Kup kurs"
|
||||||
|
|
||||||
@@ -105,46 +182,3 @@ msgstr ""
|
|||||||
#: home/templates/home/event_page.html:54
|
#: home/templates/home/event_page.html:54
|
||||||
msgid "Sign Up for Event"
|
msgid "Sign Up for Event"
|
||||||
msgstr "Zapisz się"
|
msgstr "Zapisz się"
|
||||||
|
|
||||||
#: home/templates/home/welcome_page.html:6
|
|
||||||
msgid "Visit the Wagtail website"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: home/templates/home/welcome_page.html:15
|
|
||||||
msgid "View the release notes"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: home/templates/home/welcome_page.html:27
|
|
||||||
msgid "Welcome to your new Wagtail site!"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: home/templates/home/welcome_page.html:28
|
|
||||||
msgid ""
|
|
||||||
"Please feel free to <a href=\"https://github.com/wagtail/wagtail/wiki/"
|
|
||||||
"Slack\">join our community on Slack</a>, or get started with one of the "
|
|
||||||
"links below."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: home/templates/home/welcome_page.html:35
|
|
||||||
msgid "Wagtail Documentation"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: home/templates/home/welcome_page.html:36
|
|
||||||
msgid "Topics, references, & how-tos"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: home/templates/home/welcome_page.html:42
|
|
||||||
msgid "Tutorial"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: home/templates/home/welcome_page.html:43
|
|
||||||
msgid "Build your first Wagtail site"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: home/templates/home/welcome_page.html:49
|
|
||||||
msgid "Admin Interface"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: home/templates/home/welcome_page.html:50
|
|
||||||
msgid "Create your superuser first!"
|
|
||||||
msgstr ""
|
|
||||||
|
|||||||
25
home/migrations/0020_coursepage_repository_url_and_more.py
Normal file
25
home/migrations/0020_coursepage_repository_url_and_more.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-03-23 11:59
|
||||||
|
|
||||||
|
import modelcluster.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('auth', '0012_alter_user_first_name_max_length'),
|
||||||
|
('home', '0019_coursepage_description'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='coursepage',
|
||||||
|
name='repository_url',
|
||||||
|
field=models.URLField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='coursepage',
|
||||||
|
name='allowed_groups',
|
||||||
|
field=modelcluster.fields.ParentalManyToManyField(help_text="Additional groups that should have access to this course, e.g. Editors. NOTE: Users who purchase the course will be automatically added to a dedicated access group for this course, so you don't need to add that group here.", related_name='course_pages', to='auth.group'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth.models import Group, User
|
||||||
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.forms import CheckboxSelectMultiple
|
from django.forms import CheckboxSelectMultiple
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@@ -14,6 +15,8 @@ from wagtail.models.copying import ParentalManyToManyField
|
|||||||
from wagtail_color_panel.edit_handlers import NativeColorPanel
|
from wagtail_color_panel.edit_handlers import NativeColorPanel
|
||||||
from wagtail_color_panel.fields import ColorField
|
from wagtail_color_panel.fields import ColorField
|
||||||
|
|
||||||
|
from purchase.models import CoursePurchase
|
||||||
|
|
||||||
|
|
||||||
class EmptyPage(Page):
|
class EmptyPage(Page):
|
||||||
pass
|
pass
|
||||||
@@ -60,18 +63,65 @@ class CoursePage(Page):
|
|||||||
allowed_groups = ParentalManyToManyField(
|
allowed_groups = ParentalManyToManyField(
|
||||||
Group,
|
Group,
|
||||||
related_name="course_pages",
|
related_name="course_pages",
|
||||||
help_text="Select a group to restrict access to this course. Non-members will be prompted to purchase the course to view modules.",
|
help_text="Additional groups that should have access to this course, e.g. Editors. NOTE: Users who purchase the course will be automatically added to a dedicated access group for this course, so you don't need to add that group here.",
|
||||||
|
)
|
||||||
|
|
||||||
|
repository_url = models.URLField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _user_has_access(self, user):
|
def _user_has_access(self, user):
|
||||||
if not user.is_authenticated:
|
if not user.is_authenticated:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
user_group_ids = user.groups.values_list("id", flat=True)
|
user_group_ids = user.groups.values_list("id", flat=True)
|
||||||
return self.allowed_groups.filter(id__in=user_group_ids).exists() # pyright: ignore[reportAttributeAccessIssue]
|
if self.allowed_groups.filter(id__in=user_group_ids).exists():
|
||||||
|
return True
|
||||||
|
|
||||||
|
return CoursePurchase.objects.filter(
|
||||||
|
user=user, course=self, refunded=False
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
def _user_purchase_id(self, user):
|
||||||
|
if not user.is_authenticated:
|
||||||
|
return None
|
||||||
|
|
||||||
|
purchase = CoursePurchase.objects.filter(
|
||||||
|
user=user, course=self, refunded=False
|
||||||
|
).first()
|
||||||
|
print(f"User {user} purchase for course {self}: {purchase}")
|
||||||
|
return purchase.id if purchase else None
|
||||||
|
|
||||||
|
def mock_purchase(self, user):
|
||||||
|
"""Mock method to simulate purchasing this course for a user."""
|
||||||
|
if not user.is_authenticated:
|
||||||
|
return False
|
||||||
|
obj, created = CoursePurchase.objects.get_or_create(
|
||||||
|
user=user, course=self, refunded=False
|
||||||
|
)
|
||||||
|
# Add user to dedicated access group for this course
|
||||||
|
group_name = f"course_{self.id}_access"
|
||||||
|
group, _ = Group.objects.get_or_create(name=group_name)
|
||||||
|
user.groups.add(group)
|
||||||
|
# Ensure allowed_groups only includes this access group
|
||||||
|
if not self.allowed_groups.filter(id=group.id).exists():
|
||||||
|
self.allowed_groups.add(group)
|
||||||
|
return created
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.id is not None:
|
||||||
|
group_name = f"course_{self.id}_access"
|
||||||
|
group, created = Group.objects.get_or_create(name=group_name)
|
||||||
|
if not self.allowed_groups.filter(id=group.id).exists():
|
||||||
|
self.allowed_groups.add(group)
|
||||||
|
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def get_context(self, request):
|
def get_context(self, request):
|
||||||
context = super().get_context(request)
|
context = super().get_context(request)
|
||||||
context["user_has_access"] = self._user_has_access(request.user)
|
context["user_has_access"] = self._user_has_access(request.user)
|
||||||
|
context["user_purchase_id"] = self._user_purchase_id(request.user)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
content_panels = Page.content_panels + [
|
content_panels = Page.content_panels + [
|
||||||
@@ -79,6 +129,11 @@ class CoursePage(Page):
|
|||||||
FieldPanel("description"),
|
FieldPanel("description"),
|
||||||
FieldPanel("body"),
|
FieldPanel("body"),
|
||||||
FieldPanel("allowed_groups", widget=CheckboxSelectMultiple),
|
FieldPanel("allowed_groups", widget=CheckboxSelectMultiple),
|
||||||
|
FieldPanel(
|
||||||
|
"repository_url",
|
||||||
|
read_only=True,
|
||||||
|
heading="Repository URL (auto-generated)",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
parent_page_types = ["home.CourseIndexPage"]
|
parent_page_types = ["home.CourseIndexPage"]
|
||||||
subpage_types = ["home.CourseModulePage"]
|
subpage_types = ["home.CourseModulePage"]
|
||||||
|
|||||||
164
home/signals.py
164
home/signals.py
@@ -1,39 +1,141 @@
|
|||||||
|
import logging as lg
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
from home.models.pages import CoursePage
|
||||||
|
|
||||||
@receiver(post_save, sender=User)
|
GITEA_ORG_NAME = "Studio77"
|
||||||
def notify_external_service_on_signup(sender, instance, created, **kwargs):
|
|
||||||
pass
|
logger = lg.getLogger(__name__)
|
||||||
# if created and not instance.is_staff:
|
|
||||||
# payload = {
|
|
||||||
# "user_id": instance.id,
|
@receiver(post_save, sender=CoursePage)
|
||||||
# "username": f"KURSY-{instance.id}",
|
def create_gitea_team_repo_on_course_creation(sender, instance, created, **kwargs):
|
||||||
# "email": instance.email,
|
if not instance.live:
|
||||||
# "full_name": f"{instance.first_name} {instance.last_name}".strip(),
|
logger.debug(
|
||||||
# # "must_change_password": True,
|
f"Course {instance.title} is not live, skipping Gitea team creation"
|
||||||
# # "password": instance.password,
|
)
|
||||||
# "visibility": "private",
|
return
|
||||||
# }
|
|
||||||
# api_url = getattr(settings, "GITEA_URL", None)
|
course = instance
|
||||||
# if api_url:
|
team_name = f"course-{course.id}"
|
||||||
# url = f"{api_url}/admin/users"
|
api_url = getattr(settings, "GITEA_URL", None)
|
||||||
# try:
|
|
||||||
# response = requests.post(
|
if not api_url:
|
||||||
# url,
|
logger.debug("GITEA_URL is not set, skipping Gitea team creation")
|
||||||
# json=payload,
|
return
|
||||||
# timeout=5,
|
|
||||||
# headers={"Authorization": f"token {os.getenv('GITEA_API_TOKEN')}"},
|
def team():
|
||||||
# )
|
# check if team already exists
|
||||||
# response.raise_for_status()
|
try:
|
||||||
# print(f"Successfully created Gitea account for {instance.email}")
|
response = requests.get(
|
||||||
# except Exception as e:
|
f"{api_url}/orgs/{GITEA_ORG_NAME}/teams",
|
||||||
# print(
|
timeout=5,
|
||||||
# f"Failed to create Gitea account for user {instance.email}: {e}\n{response.text}"
|
headers={"Authorization": f"token {os.getenv('GITEA_API_TOKEN')}"},
|
||||||
# )
|
)
|
||||||
# raise e
|
response.raise_for_status()
|
||||||
|
teams = response.json()
|
||||||
|
if any(team["name"] == team_name for team in teams):
|
||||||
|
logger.info(f"Gitea team {team_name} already exists, skipping creation")
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(
|
||||||
|
f"Failed to check existing Gitea teams: {e}\n{response.text}",
|
||||||
|
e,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
url = f"{api_url}/orgs/{GITEA_ORG_NAME}/teams"
|
||||||
|
payload = {
|
||||||
|
"can_create_org_repo": False,
|
||||||
|
"description": f"Team for course {course.title}",
|
||||||
|
"includes_all_repositories": False,
|
||||||
|
"name": team_name,
|
||||||
|
"permission": "read",
|
||||||
|
"units": [
|
||||||
|
# "repo.actions",
|
||||||
|
"repo.code",
|
||||||
|
# "repo.issues",
|
||||||
|
# "repo.ext_issues",
|
||||||
|
# "repo.wiki",
|
||||||
|
# "repo.ext_wiki",
|
||||||
|
# "repo.pulls",
|
||||||
|
# "repo.releases",
|
||||||
|
# "repo.projects",
|
||||||
|
# "repo.ext_wiki",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
url,
|
||||||
|
json=payload,
|
||||||
|
timeout=5,
|
||||||
|
headers={"Authorization": f"token {os.getenv('GITEA_API_TOKEN')}"},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
logger.info(f"Successfully created Gitea team for course {course.title}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(
|
||||||
|
f"Failed to create Gitea team for course {course.title}: {e}\n{response.text}",
|
||||||
|
e,
|
||||||
|
)
|
||||||
|
|
||||||
|
def repo():
|
||||||
|
# check if repository already exists
|
||||||
|
try:
|
||||||
|
response = requests.get(
|
||||||
|
f"{api_url}/orgs/{GITEA_ORG_NAME}/repos",
|
||||||
|
timeout=5,
|
||||||
|
headers={"Authorization": f"token {os.getenv('GITEA_API_TOKEN')}"},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
repos = response.json()
|
||||||
|
if any(repo["name"] == team_name for repo in repos):
|
||||||
|
logger.debug(
|
||||||
|
f"Gitea repository {team_name} already exists, skipping creation"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(
|
||||||
|
f"Failed to check existing Gitea repositories: {e}\n{response.text}",
|
||||||
|
e,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# create course repository
|
||||||
|
repo_name = f"course-{course.id}"
|
||||||
|
url = f"{api_url}/orgs/{GITEA_ORG_NAME}/repos"
|
||||||
|
payload = {
|
||||||
|
"auto_init": True,
|
||||||
|
"default_branch": "main",
|
||||||
|
"description": f"{course.title}",
|
||||||
|
"name": repo_name,
|
||||||
|
"private": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
url,
|
||||||
|
json=payload,
|
||||||
|
timeout=5,
|
||||||
|
headers={"Authorization": f"token {os.getenv('GITEA_API_TOKEN')}"},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
repo_url = response.json().get("url", None)
|
||||||
|
course.repository_url = repo_url
|
||||||
|
course.save(update_fields=["repository_url"])
|
||||||
|
logger.info(
|
||||||
|
f"Successfully created Gitea repository for course {course.title}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(
|
||||||
|
f"Failed to create Gitea repository for course {course.title}: {e}\n{response.text}",
|
||||||
|
e,
|
||||||
|
)
|
||||||
|
|
||||||
|
team()
|
||||||
|
repo()
|
||||||
|
|||||||
@@ -13,7 +13,9 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2 class="not-prose text-xl mb-4 text-gray-700">
|
<h2 class="not-prose text-xl mb-4 text-gray-700">
|
||||||
<a href="{{ page.course.url }}" class="font-bold">{{ page.course.title }}</a> » {{ page.title }}
|
<a href="{% slugurl 'courses' %}" class="font-bold hover:underline">{% trans "Courses" %}</a>
|
||||||
|
» <a href="{{ page.course.url }}" class="font-bold hover:underline">{{ page.course.title }}</a>
|
||||||
|
» {{ page.title }}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{{ page.body|richtext }}
|
{{ page.body|richtext }}
|
||||||
|
|||||||
@@ -12,8 +12,9 @@
|
|||||||
{% block content_class %}prose{% endblock content_class %}
|
{% block content_class %}prose{% endblock content_class %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 class="not-prose text-3xl mb-4 text-gray-700 font-bold">
|
<h1 class="not-prose text-3xl mb-4 text-gray-700">
|
||||||
{{ page.title }}
|
<a href="{% slugurl 'courses' %}" class="font-bold hover:underline">{% trans "Courses" %}</a>
|
||||||
|
» {{ page.title }}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{% if page.course_image %}
|
{% if page.course_image %}
|
||||||
@@ -27,6 +28,9 @@
|
|||||||
{{ page.body|richtext }}
|
{{ page.body|richtext }}
|
||||||
|
|
||||||
{% if user_has_access %}
|
{% if user_has_access %}
|
||||||
|
{% if user_purchase_id %}
|
||||||
|
<a href="{% url 'mock_refund_purchase' purchase_id=user_purchase_id %}" class="mt-4 inline-block bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition">{% trans "Refund Purchase" %}</a>
|
||||||
|
{% endif %}
|
||||||
<h2 class="not-prose text-2xl mt-8 mb-4 text-gray-700 font-semibold">{% trans "Modules" %}</h2>
|
<h2 class="not-prose text-2xl mt-8 mb-4 text-gray-700 font-semibold">{% trans "Modules" %}</h2>
|
||||||
<ul class="list-disc list-inside">
|
<ul class="list-disc list-inside">
|
||||||
{% for module in page.get_children.specific.live %}
|
{% for module in page.get_children.specific.live %}
|
||||||
@@ -48,7 +52,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<div class="not-prose mt-8 p-4 bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700">
|
<div class="not-prose mt-8 p-4 bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700">
|
||||||
<p>{% trans "You don't have access to this course. Please purchase it to view the modules." %}</p>
|
<p>{% trans "You don't have access to this course. Please purchase it to view the modules." %}</p>
|
||||||
<a href="" class="mt-4 inline-block bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition">{% trans "Purchase Course" %}</a>
|
<a href="{% url 'mock_purchase_course' course_id=page.id %}" class="mt-4 inline-block bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition">{% trans "Purchase Course" %}</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|||||||
@@ -13,11 +13,10 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2 class="not-prose text-xl mb-4 text-gray-700">
|
<h2 class="not-prose text-xl mb-4 text-gray-700">
|
||||||
<a href="{{ page.module.course.url }}" class="font-bold">{{ page.module.course.title }}</a>
|
<a href="{% slugurl 'courses' %}" class="font-bold hover:underline">{% trans "Courses" %}</a>
|
||||||
»
|
» <a href="{{ page.module.course.url }}" class="font-bold hover:underline">{{ page.module.course.title }}</a>
|
||||||
<a href="{{ page.module.url }}" class="font-bold">{{ page.module.title }}</a>
|
» <a href="{{ page.module.url }}" class="font-bold hover:underline">{{ page.module.title }}</a>
|
||||||
»
|
» <span class="text-gray-500">{{ page.title }}</span>
|
||||||
<span class="text-gray-500">{{ page.title }}</span>
|
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{{ page.body|richtext }}
|
{{ page.body|richtext }}
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
{% load i18n wagtailcore_tags %}
|
|
||||||
|
|
||||||
<header class="header">
|
|
||||||
<div class="logo">
|
|
||||||
<a href="https://wagtail.org/">
|
|
||||||
<svg class="figure-logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 342.5 126.2"><title>{% trans "Visit the Wagtail website" %}</title><path fill="#FFF" d="M84 1.9v5.7s-10.2-3.8-16.8 3.1c-4.8 5-5.2 10.6-3 18.1 21.6 0 25 12.1 25 12.1L87 27l6.8-8.3c0-9.8-8.1-16.3-9.8-16.8z"/><circle cx="85.9" cy="15.9" r="2.6"/><path d="M89.2 40.9s-3.3-16.6-24.9-12.1c-2.2-7.5-1.8-13 3-18.1C73.8 3.8 84 7.6 84 7.6V1.9C80.4.3 77 0 73.2 0 59.3 0 51.6 10.4 48.3 17.4L9.2 89.3l11-2.1-20.2 39 14.1-2.5L24.9 93c30.6 0 69.8-11 64.3-52.1z"/><path d="M102.4 27l-8.6-8.3L87 27z"/><path fill="#FFF" d="M30 84.1s1-.2 2.8-.6c1.8-.4 4.3-1 7.3-1.8 1.5-.4 3.1-.9 4.8-1.5 1.7-.6 3.5-1.2 5.2-2 1.8-.7 3.6-1.6 5.4-2.6 1.8-1 3.5-2.1 5.1-3.4.4-.3.8-.6 1.2-1l1.2-1c.7-.7 1.5-1.4 2.2-2.2.7-.7 1.3-1.5 1.9-2.3l.9-1.2.4-.6.4-.6c.2-.4.5-.8.7-1.2.2-.4.4-.8.7-1.2l.3-.6.3-.6c.2-.4.4-.8.5-1.2l.9-2.4c.2-.8.5-1.6.7-2.3.2-.7.3-1.5.5-2.1.1-.7.2-1.3.3-2 .1-.6.2-1.2.2-1.7.1-.5.1-1 .2-1.5.1-1.8.1-2.8.1-2.8l1.6.1s-.1 1.1-.2 2.9c-.1.5-.1 1-.2 1.5-.1.6-.1 1.2-.3 1.8-.1.6-.3 1.3-.4 2-.2.7-.4 1.4-.6 2.2-.2.8-.5 1.5-.8 2.4-.3.8-.6 1.6-1 2.5l-.6 1.2-.3.6-.3.6c-.2.4-.5.8-.7 1.3-.3.4-.5.8-.8 1.2-.1.2-.3.4-.4.6l-.4.6-.9 1.2c-.7.8-1.3 1.6-2.1 2.3-.7.8-1.5 1.4-2.3 2.2l-1.2 1c-.4.3-.8.6-1.3.9-1.7 1.2-3.5 2.3-5.3 3.3-1.8.9-3.7 1.8-5.5 2.5-1.8.7-3.6 1.3-5.3 1.8-1.7.5-3.3 1-4.9 1.3-3 .7-5.6 1.3-7.4 1.6-1.6.6-2.6.8-2.6.8z"/><g fill="#231F20"><path d="M127 83.9h-8.8l-12.6-36.4h7.9l9 27.5 9-27.5h7.9l9 27.5 9-27.5h7.9L153 83.9h-8.8L135.6 59 127 83.9zM200.1 83.9h-7V79c-3 3.6-7 5.4-12.1 5.4-3.8 0-6.9-1.1-9.4-3.2s-3.7-5-3.7-8.6c0-3.6 1.3-6.3 4-8 2.6-1.8 6.2-2.7 10.7-2.7h9.9v-1.4c0-4.8-2.7-7.3-8.1-7.3-3.4 0-6.9 1.2-10.5 3.7l-3.4-4.8c4.4-3.5 9.4-5.3 15.1-5.3 4.3 0 7.8 1.1 10.5 3.2 2.7 2.2 4.1 5.6 4.1 10.2v23.7zm-7.7-13.6v-3.1h-8.6c-5.5 0-8.3 1.7-8.3 5.2 0 1.8.7 3.1 2.1 4.1 1.4.9 3.3 1.4 5.7 1.4 2.4 0 4.6-.7 6.4-2.1 1.8-1.3 2.7-3.1 2.7-5.5zM241.7 47.5v31.7c0 6.4-1.7 11.3-5.2 14.5-3.5 3.2-8 4.8-13.4 4.8-5.5 0-10.4-1.7-14.8-5.1l3.6-5.8c3.6 2.7 7.1 4 10.8 4 3.6 0 6.5-.9 8.6-2.8 2.1-1.9 3.2-4.9 3.2-9v-4.7c-1.1 2.1-2.8 3.9-4.9 5.1-2.1 1.3-4.5 1.9-7.1 1.9-4.8 0-8.8-1.7-11.9-5.1-3.1-3.4-4.7-7.6-4.7-12.6s1.6-9.2 4.7-12.6c3.1-3.4 7.1-5.1 11.9-5.1 4.8 0 8.7 2 11.7 6v-5.4h7.5zm-28.4 16.8c0 3 .9 5.6 2.8 7.7 1.8 2.2 4.3 3.2 7.5 3.2 3.1 0 5.7-1 7.6-3.1 1.9-2.1 2.9-4.7 2.9-7.8 0-3.1-1-5.8-2.9-7.9-2-2.2-4.5-3.2-7.6-3.2-3.1 0-5.6 1.1-7.4 3.4-2 2.1-2.9 4.7-2.9 7.7zM260.9 53.6v18.5c0 1.7.5 3.1 1.4 4.1.9 1 2.2 1.5 3.8 1.5 1.6 0 3.2-.8 4.7-2.4l3.1 5.4c-2.7 2.4-5.7 3.6-8.9 3.6-3.3 0-6-1.1-8.3-3.4-2.3-2.3-3.5-5.3-3.5-9.1V53.6h-4.6v-6.2h4.6V36.1h7.7v11.4h9.6v6.2h-9.6zM309.5 83.9h-7V79c-3 3.6-7 5.4-12.1 5.4-3.8 0-6.9-1.1-9.4-3.2s-3.7-5-3.7-8.6c0-3.6 1.3-6.3 4-8 2.6-1.8 6.2-2.7 10.7-2.7h9.9v-1.4c0-4.8-2.7-7.3-8.1-7.3-3.4 0-6.9 1.2-10.5 3.7l-3.4-4.8c4.4-3.5 9.4-5.3 15.1-5.3 4.3 0 7.8 1.1 10.5 3.2 2.7 2.2 4.1 5.6 4.1 10.2v23.7zm-7.7-13.6v-3.1h-8.6c-5.5 0-8.3 1.7-8.3 5.2 0 1.8.7 3.1 2.1 4.1 1.4.9 3.3 1.4 5.7 1.4 2.4 0 4.6-.7 6.4-2.1 1.8-1.3 2.7-3.1 2.7-5.5zM319.3 40.2c-1-1-1.4-2.1-1.4-3.4 0-1.3.5-2.5 1.4-3.4 1-1 2.1-1.4 3.4-1.4 1.3 0 2.5.5 3.4 1.4 1 1 1.4 2.1 1.4 3.4 0 1.3-.5 2.5-1.4 3.4s-2.1 1.4-3.4 1.4c-1.3.1-2.4-.4-3.4-1.4zm7.2 43.7h-7.7V47.5h7.7v36.4zM342.5 83.9h-7.7V33.1h7.7v50.8z"/></g></svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="header-link">
|
|
||||||
{% comment %}
|
|
||||||
This works for all cases but prerelease versions:
|
|
||||||
{% endcomment %}
|
|
||||||
<a href="{% wagtail_documentation_path %}/releases/{% wagtail_release_notes_path %}">
|
|
||||||
{% trans "View the release notes" %}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main class="main">
|
|
||||||
<div class="figure">
|
|
||||||
<svg class="figure-space" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300" aria-hidden="true">
|
|
||||||
<path class="egg" fill="currentColor" d="M150 250c-42.741 0-75-32.693-75-90s42.913-110 75-110c32.088 0 75 52.693 75 110s-32.258 90-75 90z"/>
|
|
||||||
<ellipse fill="#ddd" cx="150" cy="270" rx="40" ry="7"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="main-text">
|
|
||||||
<h1>{% trans "Welcome to your new Wagtail site!" %}</h1>
|
|
||||||
<p>{% trans 'Please feel free to <a href="https://github.com/wagtail/wagtail/wiki/Slack">join our community on Slack</a>, or get started with one of the links below.' %}</p>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<footer class="footer" role="contentinfo">
|
|
||||||
<a class="option option-one" href="{% wagtail_documentation_path %}/">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M9 21c0 .5.4 1 1 1h4c.6 0 1-.5 1-1v-1H9v1zm3-19C8.1 2 5 5.1 5 9c0 2.4 1.2 4.5 3 5.7V17c0 .5.4 1 1 1h6c.6 0 1-.5 1-1v-2.3c1.8-1.3 3-3.4 3-5.7 0-3.9-3.1-7-7-7zm2.9 11.1l-.9.6V16h-4v-2.3l-.9-.6C7.8 12.2 7 10.6 7 9c0-2.8 2.2-5 5-5s5 2.2 5 5c0 1.6-.8 3.2-2.1 4.1z"/></svg>
|
|
||||||
<div>
|
|
||||||
<h2>{% trans "Wagtail Documentation" %}</h2>
|
|
||||||
<p>{% trans "Topics, references, & how-tos" %}</p>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a class="option option-two" href="{% wagtail_documentation_path %}/getting_started/tutorial.html">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/></svg>
|
|
||||||
<div>
|
|
||||||
<h2>{% trans "Tutorial" %}</h2>
|
|
||||||
<p>{% trans "Build your first Wagtail site" %}</p>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a class="option option-three" href="{% url 'wagtailadmin_home' %}">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M0 0h24v24H0z" fill="none"/><path d="M16.5 13c-1.2 0-3.07.34-4.5 1-1.43-.67-3.3-1-4.5-1C5.33 13 1 14.08 1 16.25V19h22v-2.75c0-2.17-4.33-3.25-6.5-3.25zm-4 4.5h-10v-1.25c0-.54 2.56-1.75 5-1.75s5 1.21 5 1.75v1.25zm9 0H14v-1.25c0-.46-.2-.86-.52-1.22.88-.3 1.96-.53 3.02-.53 2.44 0 5 1.21 5 1.75v1.25zM7.5 12c1.93 0 3.5-1.57 3.5-3.5S9.43 5 7.5 5 4 6.57 4 8.5 5.57 12 7.5 12zm0-5.5c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2zm9 5.5c1.93 0 3.5-1.57 3.5-3.5S18.43 5 16.5 5 13 6.57 13 8.5s1.57 3.5 3.5 3.5zm0-5.5c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2z"/></svg>
|
|
||||||
<div>
|
|
||||||
<h2>{% trans "Admin Interface" %}</h2>
|
|
||||||
<p>{% trans "Create your superuser first!" %}</p>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</footer>
|
|
||||||
@@ -1,13 +1,49 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
import requests
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.handlers.wsgi import WSGIRequest
|
||||||
|
|
||||||
|
|
||||||
|
def create_gitea_account(user, password):
|
||||||
|
payload = {
|
||||||
|
"user_id": user.id,
|
||||||
|
"username": f"studio77-{user.id}",
|
||||||
|
"email": user.email,
|
||||||
|
"full_name": f"{user.first_name} {user.last_name}".strip(),
|
||||||
|
"password": password,
|
||||||
|
"must_change_password": False,
|
||||||
|
"visibility": "private",
|
||||||
|
}
|
||||||
|
api_url = getattr(settings, "GITEA_URL", None)
|
||||||
|
if api_url:
|
||||||
|
url = f"{api_url}/admin/users"
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
url,
|
||||||
|
json=payload,
|
||||||
|
timeout=5,
|
||||||
|
headers={"Authorization": f"token {os.getenv('GITEA_API_TOKEN')}"},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
print(f"Successfully created Gitea account for {user.email}")
|
||||||
|
except Exception as e:
|
||||||
|
print(
|
||||||
|
f"Failed to create Gitea account for user {user.email}: {e}\n{response.text}"
|
||||||
|
)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
class SignUpForm(forms.Form):
|
class SignUpForm(forms.Form):
|
||||||
first_name = forms.CharField(max_length=60, required=True, label="First Name")
|
first_name = forms.CharField(max_length=60, required=True, label="First Name")
|
||||||
last_name = forms.CharField(max_length=60, required=True, label="Last Name")
|
last_name = forms.CharField(max_length=60, required=True, label="Last Name")
|
||||||
|
|
||||||
def signup(self, request, user):
|
def signup(self, request: WSGIRequest, user):
|
||||||
user.first_name = self.cleaned_data["first_name"]
|
user.first_name = self.cleaned_data["first_name"].strip().title()
|
||||||
user.last_name = self.cleaned_data["last_name"]
|
user.last_name = self.cleaned_data["last_name"].strip().title()
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
return user
|
# gitea account creation
|
||||||
|
password = request.POST.get("password1")
|
||||||
|
create_gitea_account(user, password)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-03-17 11:46+0000\n"
|
"POT-Creation-Date: 2026-03-20 12:18+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -26,7 +26,7 @@ msgstr ""
|
|||||||
msgid "Sorry, you don't have permission to access this page."
|
msgid "Sorry, you don't have permission to access this page."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: kursy/templates/calendar.html:4 kursy/templates/header.html:8
|
#: kursy/templates/calendar.html:4
|
||||||
msgid "Course Calendar"
|
msgid "Course Calendar"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -34,19 +34,27 @@ msgstr ""
|
|||||||
msgid "Loading..."
|
msgid "Loading..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: kursy/templates/header.html:10
|
#: kursy/templates/header.html:7
|
||||||
|
msgid "Courses"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: kursy/templates/header.html:8
|
||||||
|
msgid "Calendar"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: kursy/templates/header.html:13
|
||||||
msgid "Logout"
|
msgid "Logout"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: kursy/templates/header.html:12 kursy/templates/occurrence_detail.html:39
|
#: kursy/templates/header.html:15 kursy/templates/occurrence_detail.html:39
|
||||||
msgid "Login"
|
msgid "Login"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: kursy/templates/header.html:13 kursy/templates/occurrence_detail.html:42
|
#: kursy/templates/header.html:16 kursy/templates/occurrence_detail.html:42
|
||||||
msgid "Sign Up"
|
msgid "Sign Up"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: kursy/templates/header.html:32
|
#: kursy/templates/header.html:35
|
||||||
msgid "Search courses..."
|
msgid "Search courses..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-03-17 11:46+0000\n"
|
"POT-Creation-Date: 2026-03-20 12:18+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -28,7 +28,7 @@ msgstr "Odmowa dostępu"
|
|||||||
msgid "Sorry, you don't have permission to access this page."
|
msgid "Sorry, you don't have permission to access this page."
|
||||||
msgstr "Przepraszamy, ale nie masz uprawnień do dostępu do tej strony."
|
msgstr "Przepraszamy, ale nie masz uprawnień do dostępu do tej strony."
|
||||||
|
|
||||||
#: kursy/templates/calendar.html:4 kursy/templates/header.html:8
|
#: kursy/templates/calendar.html:4
|
||||||
msgid "Course Calendar"
|
msgid "Course Calendar"
|
||||||
msgstr "Kalendarz kursów"
|
msgstr "Kalendarz kursów"
|
||||||
|
|
||||||
@@ -36,19 +36,27 @@ msgstr "Kalendarz kursów"
|
|||||||
msgid "Loading..."
|
msgid "Loading..."
|
||||||
msgstr "Ładowanie..."
|
msgstr "Ładowanie..."
|
||||||
|
|
||||||
#: kursy/templates/header.html:10
|
#: kursy/templates/header.html:7
|
||||||
|
msgid "Courses"
|
||||||
|
msgstr "Kursy"
|
||||||
|
|
||||||
|
#: kursy/templates/header.html:8
|
||||||
|
msgid "Calendar"
|
||||||
|
msgstr "Kalendarz"
|
||||||
|
|
||||||
|
#: kursy/templates/header.html:13
|
||||||
msgid "Logout"
|
msgid "Logout"
|
||||||
msgstr "Wyloguj się"
|
msgstr "Wyloguj się"
|
||||||
|
|
||||||
#: kursy/templates/header.html:12 kursy/templates/occurrence_detail.html:39
|
#: kursy/templates/header.html:15 kursy/templates/occurrence_detail.html:39
|
||||||
msgid "Login"
|
msgid "Login"
|
||||||
msgstr "Zaloguj się"
|
msgstr "Zaloguj się"
|
||||||
|
|
||||||
#: kursy/templates/header.html:13 kursy/templates/occurrence_detail.html:42
|
#: kursy/templates/header.html:16 kursy/templates/occurrence_detail.html:42
|
||||||
msgid "Sign Up"
|
msgid "Sign Up"
|
||||||
msgstr "Zarejestruj się"
|
msgstr "Zarejestruj się"
|
||||||
|
|
||||||
#: kursy/templates/header.html:32
|
#: kursy/templates/header.html:35
|
||||||
msgid "Search courses..."
|
msgid "Search courses..."
|
||||||
msgstr "Szukaj kursów..."
|
msgstr "Szukaj kursów..."
|
||||||
|
|
||||||
@@ -62,8 +70,6 @@ msgid "You are signed up for this event. We look forward to seeing you there!"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: kursy/templates/occurrence_detail.html:31
|
#: kursy/templates/occurrence_detail.html:31
|
||||||
#, fuzzy
|
|
||||||
#| msgid "Sign Up"
|
|
||||||
msgid "Cancel Sign Up"
|
msgid "Cancel Sign Up"
|
||||||
msgstr "Zrezygnuj"
|
msgstr "Zrezygnuj"
|
||||||
|
|
||||||
@@ -71,17 +77,23 @@ msgstr "Zrezygnuj"
|
|||||||
msgid ""
|
msgid ""
|
||||||
"You need to be logged in to sign up for this event. Please log in or sign up "
|
"You need to be logged in to sign up for this event. Please log in or sign up "
|
||||||
"to reserve your spot."
|
"to reserve your spot."
|
||||||
msgstr "Musisz być zalogowany, aby zapisać się na to wydarzenie. Zaloguj się lub zarejestruj, aby zarezerwować swoje miejsce."
|
msgstr ""
|
||||||
|
"Musisz być zalogowany, aby zapisać się na to wydarzenie. Zaloguj się lub "
|
||||||
|
"zarejestruj, aby zarezerwować swoje miejsce."
|
||||||
|
|
||||||
#: kursy/templates/occurrence_detail.html:47
|
#: kursy/templates/occurrence_detail.html:47
|
||||||
msgid ""
|
msgid ""
|
||||||
"This event is fully booked. Please check back later for any cancellations."
|
"This event is fully booked. Please check back later for any cancellations."
|
||||||
msgstr "To wydarzenie jest w pełni zarezerwowane. Sprawdź ponownie później w przypadku zwolnienia miejsc."
|
msgstr ""
|
||||||
|
"To wydarzenie jest w pełni zarezerwowane. Sprawdź ponownie później w "
|
||||||
|
"przypadku zwolnienia miejsc."
|
||||||
|
|
||||||
#: kursy/templates/occurrence_detail.html:51
|
#: kursy/templates/occurrence_detail.html:51
|
||||||
msgid ""
|
msgid ""
|
||||||
"You are not signed up for this event. Please sign up to reserve your spot."
|
"You are not signed up for this event. Please sign up to reserve your spot."
|
||||||
msgstr "Nie jesteś zapisany na to wydarzenie. Zapisz się, aby zarezerwować swoje miejsce."
|
msgstr ""
|
||||||
|
"Nie jesteś zapisany na to wydarzenie. Zapisz się, aby zarezerwować swoje "
|
||||||
|
"miejsce."
|
||||||
|
|
||||||
#: kursy/templates/occurrence_detail.html:53
|
#: kursy/templates/occurrence_detail.html:53
|
||||||
msgid "Sign Up for Event"
|
msgid "Sign Up for Event"
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ class CustomOAuth2Validator(OAuth2Validator):
|
|||||||
print("get_additional_claims", request.user)
|
print("get_additional_claims", request.user)
|
||||||
return {
|
return {
|
||||||
"name": " ".join([request.user.first_name, request.user.last_name]),
|
"name": " ".join([request.user.first_name, request.user.last_name]),
|
||||||
"preferred_username": f"studio77-{request.user.username}",
|
"preferred_username": f"studio77-{request.user.id}",
|
||||||
"email": request.user.email,
|
"email": request.user.email,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ dotenv.load_dotenv(BASE_DIR / ".env")
|
|||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
"home",
|
"home",
|
||||||
"search",
|
"search",
|
||||||
|
"purchase",
|
||||||
"wagtail.contrib.forms",
|
"wagtail.contrib.forms",
|
||||||
"wagtail.contrib.redirects",
|
"wagtail.contrib.redirects",
|
||||||
"wagtail.embeds",
|
"wagtail.embeds",
|
||||||
@@ -240,6 +241,43 @@ STORAGES = {
|
|||||||
DATA_UPLOAD_MAX_NUMBER_FIELDS = 10_000
|
DATA_UPLOAD_MAX_NUMBER_FIELDS = 10_000
|
||||||
|
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
LOGGING = {
|
||||||
|
"version": 1,
|
||||||
|
"disable_existing_loggers": False,
|
||||||
|
"formatters": {
|
||||||
|
"verbose": {
|
||||||
|
"format": "{asctime} : {levelname} : {filename}:{lineno} : {name} :: {message}",
|
||||||
|
"style": "{",
|
||||||
|
},
|
||||||
|
"simple": {"format": "{asctime} : {levelname} :: {message}", "style": "{"},
|
||||||
|
},
|
||||||
|
"handlers": {
|
||||||
|
"console": {
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
"formatter": "verbose",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"loggers": {
|
||||||
|
"django": {
|
||||||
|
"handlers": ["console"],
|
||||||
|
"level": "INFO",
|
||||||
|
"propagate": True,
|
||||||
|
},
|
||||||
|
"django.request": {
|
||||||
|
"handlers": ["console"],
|
||||||
|
"level": "INFO",
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"handlers": ["console"],
|
||||||
|
"level": "DEBUG",
|
||||||
|
"propagate": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Wagtail settings
|
# Wagtail settings
|
||||||
|
|
||||||
WAGTAIL_SITE_NAME = "kursy"
|
WAGTAIL_SITE_NAME = "kursy"
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{% load i18n wagtailcore_tags %}
|
{% load i18n wagtailcore_tags %}
|
||||||
<header class="bg-blue-900 text-white shadow-md relative">
|
<header class="bg-blue-900 text-white shadow-md lg:sticky top-0 z-40">
|
||||||
<div class="container mx-auto flex items-center justify-between py-4 px-6">
|
<div class="container mx-auto flex items-center justify-between py-4 px-6">
|
||||||
{% wagtail_site as current_site %}
|
{% wagtail_site as current_site %}
|
||||||
<div class="flex items-center gap-4">
|
<nav class="flex items-center gap-4">
|
||||||
<a class="text-xl font-bold" href="/">{{ current_site.site_name }}</a>
|
<a class="text-xl font-bold" href="/">{{ current_site.site_name }}</a>
|
||||||
<a href="{% slugurl 'courses' %}" class="hover:underline">{% trans "Courses" %}</a>
|
<a href="{% slugurl 'courses' %}" class="hover:underline">{% trans "Courses" %}</a>
|
||||||
<a href="{% url 'calendar' %}" class="hover:underline">{% trans "Course Calendar" %}</a>
|
<a href="{% url 'calendar' %}" class="hover:underline">{% trans "Calendar" %}</a>
|
||||||
</div>
|
</nav>
|
||||||
|
|
||||||
<nav class="flex items-center gap-4">
|
<nav class="flex items-center gap-4">
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
@@ -31,8 +31,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container mx-auto px-6 mb-2 md:mb-0">
|
<div class="container mx-auto px-6 mb-2 md:mb-0">
|
||||||
<form action="{% url 'search' %}" method="get" class="flex items-center bg-blue-950 rounded-md md:w-auto md:absolute md:left-1/2 md:top-1/2 md:transform md:-translate-x-1/2 md:-translate-y-1/2 md:mt-0">
|
<form action="{% url 'search' %}" method="get" class="flex items-center bg-blue-950 rounded-md mb-2 lg:w-auto lg:absolute lg:left-1/2 lg:top-1/2 lg:transform lg:-translate-x-1/2 lg:-translate-y-1/2 lg:mt-0">
|
||||||
<input type="text" name="query" placeholder="{% trans 'Search courses...' %}" class="rounded-md px-3 py-2 w-full md:w-auto focus:outline-none">
|
<input type="text" name="query" placeholder="{% trans 'Search courses...' %}" class="rounded-lg px-3 py-2 w-full lg:w-auto focus:outline-none">
|
||||||
<button type="submit" class="bg-white text-blue-900 rounded-md px-3 py-2 hover:bg-gray-200 transition"><i class="fi fi-br-search"></i></button>
|
<button type="submit" class="bg-white text-blue-900 rounded-md px-3 py-2 hover:bg-gray-200 transition"><i class="fi fi-br-search"></i></button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ urlpatterns = [
|
|||||||
path("i18n/", include("django.conf.urls.i18n")),
|
path("i18n/", include("django.conf.urls.i18n")),
|
||||||
path("oauth2/", include("oauth2_provider.urls", namespace="oauth2_provider")),
|
path("oauth2/", include("oauth2_provider.urls", namespace="oauth2_provider")),
|
||||||
path("", include("home.urls")),
|
path("", include("home.urls")),
|
||||||
|
path("", include("purchase.urls")),
|
||||||
path("calendar/", views.calendar, name="calendar"),
|
path("calendar/", views.calendar, name="calendar"),
|
||||||
# TODO: move occurrence related urls to home app
|
# TODO: move occurrence related urls to home app
|
||||||
path(
|
path(
|
||||||
|
|||||||
0
purchase/__init__.py
Normal file
0
purchase/__init__.py
Normal file
3
purchase/admin.py
Normal file
3
purchase/admin.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
5
purchase/apps.py
Normal file
5
purchase/apps.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PurchaseConfig(AppConfig):
|
||||||
|
name = 'purchase'
|
||||||
28
purchase/migrations/0001_initial.py
Normal file
28
purchase/migrations/0001_initial.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-03-19 17:36
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('home', '0019_coursepage_description'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CoursePurchase',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('purchased_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('refunded', models.BooleanField(default=False)),
|
||||||
|
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='home.coursepage')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
0
purchase/migrations/__init__.py
Normal file
0
purchase/migrations/__init__.py
Normal file
23
purchase/models.py
Normal file
23
purchase/models.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class CoursePurchase(models.Model):
|
||||||
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||||
|
course = models.ForeignKey("home.CoursePage", on_delete=models.CASCADE)
|
||||||
|
purchased_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
refunded = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
def mock_refund(self):
|
||||||
|
self.refunded = True
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
group_name = f"course_{self.course.id}_access"
|
||||||
|
|
||||||
|
group, _ = Group.objects.get_or_create(name=group_name)
|
||||||
|
if self.refunded:
|
||||||
|
print(f"Removing user {self.user} from group {group_name} due to refund")
|
||||||
|
self.user.groups.remove(group)
|
||||||
3
purchase/tests.py
Normal file
3
purchase/tests.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
16
purchase/urls.py
Normal file
16
purchase/urls.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"mock-purchase/<int:course_id>/",
|
||||||
|
views.mock_purchase_course,
|
||||||
|
name="mock_purchase_course",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"mock-refund/<int:purchase_id>/",
|
||||||
|
views.mock_refund_purchase,
|
||||||
|
name="mock_refund_purchase",
|
||||||
|
),
|
||||||
|
]
|
||||||
21
purchase/views.py
Normal file
21
purchase/views.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from django.shortcuts import redirect, render
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from home.models import CoursePage
|
||||||
|
from purchase.models import CoursePurchase
|
||||||
|
|
||||||
|
|
||||||
|
def mock_purchase_course(request, course_id):
|
||||||
|
course = CoursePage.objects.get(id=course_id)
|
||||||
|
|
||||||
|
course.mock_purchase(request.user)
|
||||||
|
|
||||||
|
return redirect(course.url)
|
||||||
|
|
||||||
|
|
||||||
|
def mock_refund_purchase(request, purchase_id):
|
||||||
|
purchase = CoursePurchase.objects.get(id=purchase_id)
|
||||||
|
|
||||||
|
purchase.mock_refund()
|
||||||
|
|
||||||
|
return redirect(purchase.course.url)
|
||||||
Reference in New Issue
Block a user