Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
9fae21b
Add content elements from lernanta
dirkcuys Jul 25, 2025
439975a
Add content to learning circles
dirkcuys Nov 27, 2025
e94f518
Add link to course content if a learning circle has content associated
dirkcuys Dec 8, 2025
6a29c61
Comments and other small changes
dirkcuys Dec 12, 2025
3d04538
Remove HTML from text email
dirkcuys Dec 12, 2025
ede420a
Comments and small changes hanging around in my local workdir
dirkcuys Dec 12, 2025
407427b
Add signup limit field, update API and add input in learning circle form
dirkcuys Dec 12, 2025
899d81b
Check for learning circle capacity on signup view
dirkcuys Dec 15, 2025
f1df874
Prevent facilitator from adding signups that would exceed the signup …
dirkcuys Dec 18, 2025
5648635
use property defined on model to check if a learning circle has reach…
dirkcuys Dec 19, 2025
4088bd2
Add test, that should be failing
dirkcuys Dec 19, 2025
22e66f0
Make test pass
dirkcuys Dec 19, 2025
a6ec7b1
Fix bug where signup_limit could be None leading to comparison betwee…
dirkcuys Dec 19, 2025
46bb66a
Remove comment and update user message
dirkcuys Jan 5, 2026
e1d1fcc
Make sure signup limit is serialized as an int
dirkcuys Jan 5, 2026
1a140a3
Add link to docs
dirkcuys Jan 6, 2026
f9f9b3a
Merge pull request #1195 from p2pu/2025-signup-limit
dirkcuys Jan 6, 2026
ffebacf
Merge branch 'master' into 2025-content
dirkcuys Jan 6, 2026
1aa70a6
Add view for uploading images for courses. Remove unused URLs and views
dirkcuys Jan 9, 2026
816dd53
Add script used to convert google doc to course content
dirkcuys Jan 9, 2026
f688024
Update ignore files
dirkcuys Jan 12, 2026
bc7ada1
Disable sticky top on content pages
dirkcuys Jan 12, 2026
0c3a170
Update content navigation for learning circle with content
dirkcuys Jan 12, 2026
c115bfc
Add missing migrations for courses
dirkcuys Jan 12, 2026
a1927e2
Fix display of icons for buttons to re-order content
dirkcuys Jan 15, 2026
d22f637
Add course content association to courses and use when creating a lea…
dirkcuys Jan 15, 2026
3e92de2
Removed unused code and templates for courses app
dirkcuys Jan 15, 2026
6835b02
Update tests for courses
dirkcuys Jan 15, 2026
3236e10
Remove removed imports
dirkcuys Jan 15, 2026
01fc5c8
Remove next_url functionality
dirkcuys Jan 15, 2026
2e0c175
Fix tests for content
dirkcuys Jan 15, 2026
e690656
Merge branch 'master' into 2025-content
dirkcuys Jan 15, 2026
7f469cb
Add link to content on facilitator dashboard
dirkcuys Jan 16, 2026
18da9d5
Fix file mode and small updates
dirkcuys Jan 16, 2026
8995843
Remove unused code
dirkcuys Jan 16, 2026
c402481
Remove unused code
dirkcuys Jan 16, 2026
105ce3e
Remove unused template
dirkcuys Jan 16, 2026
9e594a1
Remove removed functionality from course sidebar
dirkcuys Jan 16, 2026
4d3fde5
Update foreign key on learning circles to set content to null if a co…
dirkcuys Jan 16, 2026
eb12a21
Update foreign key on studygroups.Course to avoid CASCADE deletion
dirkcuys Jan 16, 2026
7e35f20
Merge pull request #1194 from p2pu/2025-content
dirkcuys Jan 16, 2026
7ce6d72
First take at capturing granular attendance for learning circles
dirkcuys Jan 16, 2026
58a25c6
Implement UX switch between summarized and granular attendance data. …
dirkcuys Jan 19, 2026
05137ee
Merge pull request #1198 from p2pu/2026-granular-attendance
dirkcuys Jan 19, 2026
f8d1f5f
Add template view as placeholder for program agreement form
dirkcuys Jan 19, 2026
7b12de2
Pass URL for device agreement to frontend
dirkcuys Jan 19, 2026
a3a2399
Add django form and save data
dirkcuys Jan 22, 2026
610b590
Update frontend UX for device agreement to be collapsed and show diff…
dirkcuys Jan 22, 2026
8cafaf4
Add previously completed data to device form when editing/updating info
dirkcuys Jan 22, 2026
4e2b352
Update form to use Django form definition and crispyforms for rendering
dirkcuys Jan 22, 2026
5063f48
Use phonenumber field on Django form to validate phone numbers
dirkcuys Jan 23, 2026
e2c004f
Only show device agreement forms to learning circles that qualify
dirkcuys Jan 29, 2026
2d52118
Consistently use device agreement terminology
dirkcuys Jan 29, 2026
6241b89
Check conditions on device agreement form view
dirkcuys Jan 29, 2026
d4a1126
Merge pull request #1199 from p2pu/2026-dd-device-form
dirkcuys Jan 29, 2026
80b2f7a
Display task for facilitators about device agreement forms
dirkcuys Jan 29, 2026
29f42d6
Update copy and tweak markup for facilitator device agreement prompt
dirkcuys Jan 30, 2026
4139f3b
Remove unused CSS since only displaying learners with outstanding dev…
dirkcuys Jan 30, 2026
ce88613
change list to generator
dirkcuys Jan 30, 2026
3c0ad3c
Merge pull request #1202 from p2pu/2026-device-agreement-facilitator-…
dirkcuys Jan 30, 2026
7549103
Basic model and logic for managing device allocations
dirkcuys Jan 30, 2026
a77c651
Add checks on API endpoints for signup limits
dirkcuys Feb 2, 2026
60f796d
Fix error using team where it is not defined
dirkcuys Feb 2, 2026
3f3cbe7
Add signup_limit field to list with fields for tabs so that error get…
dirkcuys Feb 2, 2026
e460ecb
Remove initial value of 0 for signup_limit. 0 now means zero and unde…
dirkcuys Feb 2, 2026
561bb5d
Update UX for signup limit. Use seperate swich input UI to change bet…
dirkcuys Feb 2, 2026
ec1850b
Simplify calculation of available signup slots for learning circles w…
dirkcuys Feb 3, 2026
d9324b8
Calcuate correct maximum signup limit for exising learning circle
dirkcuys Feb 3, 2026
9332c61
Add tests for device allocation
dirkcuys Feb 3, 2026
af94fff
Merge pull request #1204 from p2pu/2026-device-allocations
dirkcuys Feb 3, 2026
41f5635
Add permission checks for course content
dirkcuys Feb 3, 2026
1ea9e07
Merge branch '2026-device-allocations'
dirkcuys Feb 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ Dockerfile
compose/
places/data/
docker.env
gdoc/
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ tmp/*.png
*.mo
scripts/*
docker.env
compose/
compose/
token.pickle
credentials.json
Empty file added content/__init__.py
Empty file.
17 changes: 17 additions & 0 deletions content/db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from django.db import models


class Content(models.Model):

latest = models.ForeignKey('content.ContentVersion', models.CASCADE, related_name='+', null=True, blank=True)
based_on = models.ForeignKey('content.Content', models.SET_NULL, related_name='derived_content', null=True, blank=True) #on_delete=SET_NULL might cause a problem here


class ContentVersion(models.Model):

container = models.ForeignKey(Content, models.CASCADE, related_name='versions')
title = models.CharField(max_length = 100)
content = models.TextField()
date = models.DateTimeField(auto_now_add=True)
comment = models.CharField(max_length = 100)
author_uri = models.CharField(max_length=256)
9 changes: 9 additions & 0 deletions content/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django import forms
from content import utils

class ContentForm(forms.Form):
title = forms.CharField(max_length=80)
content = forms.CharField(widget=forms.Textarea, required=False)

def clean_content(self):
return utils.clean_user_content(self.cleaned_data['content'])
39 changes: 39 additions & 0 deletions content/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Generated by Django 4.2.11 on 2025-07-24 07:26

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

initial = True

dependencies = [
]

operations = [
migrations.CreateModel(
name='Content',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('based_on', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='derived_content', to='content.content')),
],
),
migrations.CreateModel(
name='ContentVersion',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=100)),
('content', models.TextField()),
('date', models.DateTimeField(auto_now_add=True)),
('comment', models.CharField(max_length=100)),
('author_uri', models.CharField(max_length=256)),
('container', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='versions', to='content.content')),
],
),
migrations.AddField(
model_name='content',
name='latest',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='content.contentversion'),
),
]
Empty file added content/migrations/__init__.py
Empty file.
106 changes: 106 additions & 0 deletions content/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import json

from content import db
from django.utils.text import slugify
from django.utils.translation import gettext as _

import logging
log = logging.getLogger(__name__)


def content_uri2id(content_uri):
return content_uri.strip('/').split('/')[-1]


def get_content(content_uri, fields=[]):
content_id = content_uri2id(content_uri)
try:
wrapper_db = db.Content.objects.get(id=content_id)
latest_db = wrapper_db.latest
except Exception as e:
#TODO
log.debug(e)
return None

content = {
"id": wrapper_db.id,
"uri": "/uri/content/{0}".format(wrapper_db.id),
"title": latest_db.title,
"content": latest_db.content,
}
if "history" in fields:
content["history"] = []
for version in wrapper_db.versions.sort_by("date"):
content['history'] += {
"date": version.date,
"author_uri": version.author_uri,
"title": version.title,
"comment": version.comment,
}

return content


def create_content(title, content, author_uri):
#TODO check all required properties
container_db = db.Content()
container_db.save()
content_db = db.ContentVersion(
container=container_db,
title=title,
content=content,
author_uri = author_uri,
)
#TODO if "comment" in content:
# content_db.comment = content["comment"]
content_db.save()
container_db.latest = content_db
container_db.save()
return get_content("/uri/content/{0}".format(container_db.id))


def update_content( uri, title, content, author_uri ):
content_id = content_uri2id(uri)
try:
wrapper_db = db.Content.objects.get(id=content_id)
except Exception as e:
#TODO
log.debug(e)
return None

content_db = db.ContentVersion(
container=wrapper_db,
title=title,
content=content,
author_uri = author_uri,
)
#TODO if "comment" in content:
# content_db.comment = content["comment"]
content_db.save()
wrapper_db.latest = content_db
wrapper_db.save()
return get_content("/uri/content/{0}".format(wrapper_db.id))


def clone_content(uri):
content_id = content_uri2id(uri)
try:
original_db = db.Content.objects.get(id=content_id)
except Exception as e:
log.debug(e)
raise

container_db = db.Content(based_on=original_db)
container_db.save()

content_db = db.ContentVersion(
container=container_db,
title=original_db.latest.title,
content=original_db.latest.content,
author_uri=original_db.latest.author_uri,
)
content_db.save()
container_db.latest = content_db
container_db.save()

return get_content("/uri/content/{0}".format(container_db.id))
Empty file.
28 changes: 28 additions & 0 deletions content/templatetags/content_tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from django import template
from django.template.defaultfilters import stringfilter
from django.utils.safestring import mark_safe

import markdown
import bleach
#from html5lib.tokenizer import HTMLTokenizer
import re

register = template.Library()

def add_target_blank(attrs, new=False):
attrs[(None, 'target')] = '_blank'
return attrs

@register.filter(is_safe=True)
@stringfilter
def convert_content( markdown_text ):
html = markdown.markdown(markdown_text)#TODO , ['tables'])
html = bleach.linkify(html,
callbacks=bleach.linkifier.DEFAULT_CALLBACKS + [add_target_blank]
) #TODO, tokenizer=HTMLTokenizer)
regex = re.compile(r'<pre\>.*?</pre\>', re.IGNORECASE|re.DOTALL)
f = lambda m: re.sub(r'&amp;', '&', m.group(0))
html = regex.sub(f, html)
return mark_safe(html)

convert_content.is_safe = True
55 changes: 55 additions & 0 deletions content/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from django.test import Client
from django.test import TestCase

from custom_registration.models import create_user
from content import models as content_model


class CourseTests(TestCase):

test_username = 'testuser'
test_email = 'test@mail.org'
test_password = 'testpass'

test_username2 = 'bob'
test_email2 = 'bob@mail.org'
test_password2 = '13245'

def setUp(self):
self.client = Client()
self.locale = 'en'

self.user = create_user(
self.test_email,
self.test_username,
'lastname',
self.test_password
)

self.user2 = create_user(
self.test_email2,
self.test_username2,
'lastname2',
self.test_password2
)


def test_content_creation(self):
content = content_model.create_content('title', 'content', '/uri/users/bob')
content = content_model.get_content(content['uri'])
self.assertEquals(content, content)


def test_content_history(self):
content = content_model.create_content('title', 'content', '/uri/users/bob')
content_model.update_content(
content['uri'], 'New title', 'New content', '/uri/users/testuser'
)
# TODO this doesn't test anything?


def test_clone_content(self):
content = content_model.create_content('title', 'content', '/uri/users/bob')
clone = content_model.clone_content(content['uri'])
for key in ['title', 'content']:
self.assertTrue(content[key], clone[key])
11 changes: 11 additions & 0 deletions content/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import bleach

def clean_user_content(content):
config = {
'tags': ['table', 'tr', 'th', 'td', 'tbody', 'a', 'iframe'],
'attributes': {'a': ['href'], 'iframe': ['src', 'width', 'height'] },
'strip': True
}
content = bleach.clean(content, **config)
#TODO ?? no idea why?
return content
Empty file added courses/__init__.py
Empty file.
6 changes: 6 additions & 0 deletions courses/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.contrib import admin

from .db import Course

admin.site.register(Course)

28 changes: 28 additions & 0 deletions courses/db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from django.db import models

class Course(models.Model):
title = models.CharField(max_length=255)
short_title = models.CharField(max_length=20)
description = models.CharField(max_length=1000)
language = models.CharField(max_length=32)
image_uri = models.CharField(max_length=256)
creation_date = models.DateTimeField(auto_now_add=True)
draft = models.BooleanField(default=True)
archived = models.BooleanField(default=False)
deleted = models.BooleanField(default=False)
creator_uri = models.CharField(max_length=256)
based_on = models.ForeignKey('courses.Course', models.SET_NULL, related_name='derived_courses', blank=True, null=True)

def __str__(self):
return self.title


class CourseContent(models.Model):
course = models.ForeignKey(Course, models.CASCADE, related_name='content')
content_uri = models.CharField(max_length=256)
index = models.PositiveIntegerField()


class CourseTags(models.Model):
course = models.ForeignKey(Course, models.CASCADE, related_name='tags')
tag = models.CharField(max_length=64)
17 changes: 17 additions & 0 deletions courses/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from django import http
from django.utils.translation import gettext as _

from courses import models as course_model

def require_organizer( function ):
def check_organizer( *args, **kwargs ):
request = args[0]
course_id = kwargs.get('course_id')
course_uri = course_model.course_id2uri(course_id)
user_uri = "/uri/user/{0}".format(request.user.username)
organizer = course_model.is_organizer(course_uri, user_uri)
organizer |= request.user.is_superuser
if not organizer:
return http.HttpResponseForbidden(_("You need to be a course organizer."))
return function(*args, **kwargs)
return check_organizer
46 changes: 46 additions & 0 deletions courses/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from django import forms
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from django.forms.widgets import RadioSelect

import re

class HashtagField(forms.CharField):
def clean(self, value):
if not value:
return super(HashtagField, self).clean(value)
if not re.match('^[#]*[a-z,A-Z][a-z,A-Z,0-9,_,-]*$', value):
raise forms.ValidationError(_("The hashtag must start with letter and contain only letters, digits, _ and -"))
return super(HashtagField, self).clean(value.strip('#'))


class CourseCreationForm(forms.Form):
title = forms.CharField()
hashtag = HashtagField(max_length=20)
description = forms.CharField(widget=forms.Textarea)
language = forms.ChoiceField(choices=settings.LANGUAGES)


class CourseUpdateForm(forms.Form):
title = forms.CharField(required=False)
hashtag = HashtagField(required=False, max_length=20)
description = forms.CharField(widget=forms.Textarea, required=False)
language = forms.ChoiceField(choices=settings.LANGUAGES, required=False)


class CourseImageForm(forms.Form):
image = forms.ImageField()


class CourseTagsForm(forms.Form):
tags = forms.CharField(max_length=256)


class CourseStatusForm(forms.Form):
STATUS_CHOICES = [
('draft', _('Draft'), ),
('published', _('Published'), ),
('archived', _('Archived'), ),
]

status = forms.ChoiceField(choices=STATUS_CHOICES, widget=RadioSelect)
Loading