Compare commits
12 Commits
feat/add-m
...
feat/add-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
a2ad8e7ac9
|
|||
|
a0b4697c61
|
|||
|
983384f62b
|
|||
|
668ddccea5
|
|||
|
6dd826c3bd
|
|||
|
e74c1fb28d
|
|||
|
cb19bc6262
|
|||
|
a918ee73c4
|
|||
|
5913e847bc
|
|||
|
18b21b0892
|
|||
|
efb3799e12
|
|||
|
306d39bd22
|
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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -66,6 +66,11 @@ class CoursePage(Page):
|
|||||||
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.",
|
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
|
||||||
@@ -105,11 +110,11 @@ class CoursePage(Page):
|
|||||||
return created
|
return created
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
group_name = f"course_{self.id}_access"
|
if self.id is not None:
|
||||||
group, created = Group.objects.get_or_create(name=group_name)
|
group_name = f"course_{self.id}_access"
|
||||||
if state := not self.allowed_groups.filter(id=group.id).exists():
|
group, created = Group.objects.get_or_create(name=group_name)
|
||||||
print(state)
|
if not self.allowed_groups.filter(id=group.id).exists():
|
||||||
self.allowed_groups.add(group)
|
self.allowed_groups.add(group)
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
@@ -124,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 %}
|
||||||
|
|||||||
@@ -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,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)
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -241,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"
|
||||||
|
|||||||
Reference in New Issue
Block a user