Skip to content

Commit

Permalink
Merge branch 'v2.0-beta' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
Nico-AP committed Nov 15, 2024
2 parents abf64b9 + 540afec commit 6100b58
Show file tree
Hide file tree
Showing 296 changed files with 10,729 additions and 17,681 deletions.
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
coverage_html_report/
.tox/
.nox/
.coverage
Expand Down Expand Up @@ -317,4 +318,7 @@ yarn-error.log
.idea

docs/*.html
ddm_test_env*
ddm_test_env*
test_project/static
test_project/media
test_project/test_config.json
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
# Data Donation Module (ddm)
# Data Donation Module (DDM)

![PyPI - Version](https://img.shields.io/pypi/v/django-ddm?logo=pypi&logoColor=white&label=pip%20install%20django-ddm&color=%23009c94)
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/django-ddm?logo=python&logoColor=white&label=Python&color=%233570a0)
![PyPI - Versions from Framework Classifiers](https://img.shields.io/pypi/frameworkversions/django/django-ddm?logo=django&label=Django&color=%2320aa76)
![PyPI - License](https://img.shields.io/pypi/l/django-ddm?logo=gnu&label=License&color=%2379bee8)
[![DOI](https://img.shields.io/badge/doi-https%3A%2F%2Fdoi.org%2F10.5117%2FCCR2024.2.4.PFIF-%237800bc)](https://doi.org/10.5117/CCR2024.2.4.PFIF)

**DDM** (Data Donation Module) is a Django application that helps researchers to
setup data donation projects and to collect data donations for academic research.
Expand Down
12 changes: 0 additions & 12 deletions ddm/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,3 @@ class DdmConfig(AppConfig):
default_auto_field = 'django.db.models.AutoField'
name = 'ddm'
verbose_name = 'Data Donation Module'

def ready(self):
from ddm import signals

# Add default settings for DDM if they are not defined in main application's settings.py.
from django.conf import settings
from ddm import default_settings as defaults

if not hasattr(settings, 'CKEDITOR_CONFIGS'):
setattr(settings, 'CKEDITOR_CONFIGS', {})
if 'ddm_ckeditor' not in settings.CKEDITOR_CONFIGS:
settings.CKEDITOR_CONFIGS['ddm_ckeditor'] = defaults.ddm_ckeditor
File renamed without changes.
9 changes: 9 additions & 0 deletions ddm/auth/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _


class DDMAuthConfig(AppConfig):
default_auto_field = 'django.db.models.AutoField'
name = 'ddm.auth'
label = 'ddm_auth'
verbose_name = _('DDM Authentication')
15 changes: 15 additions & 0 deletions ddm/auth/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from django import forms


class TokenCreationForm(forms.Form):
expiration_days = forms.IntegerField(
initial=30,
min_value=1,
max_value=90,
required=True
)
action = forms.CharField(
max_length=20,
initial='create',
widget=forms.HiddenInput()
)
33 changes: 33 additions & 0 deletions ddm/auth/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Generated by Django 3.2.13 on 2024-10-25 08:40

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


class Migration(migrations.Migration):

initial = True

dependencies = [
('ddm', '0049_delete_projectaccesstoken'),
]

operations = [
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.CreateModel(
name='ProjectAccessToken',
fields=[
('key', models.CharField(max_length=40, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True)),
('expiration_date', models.DateTimeField(blank=True, null=True)),
('project', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE,
related_name='donation_project', to='ddm.donationproject',
verbose_name='Donation Project')),
],
)
],
# Table already exists. See ddm/migrations/0049_delete_projectaccesstoken.py (may be moved to ddm.core)
database_operations = [],
)
]
26 changes: 26 additions & 0 deletions ddm/auth/migrations/0002_alter_projectaccesstoken_project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 3.2.13 on 2024-10-26 08:48

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


class Migration(migrations.Migration):

dependencies = [
('ddm_projects', '0001_initial'),
('ddm_auth', '0001_initial'),
]

operations = [
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.AlterField(
model_name='projectaccesstoken',
name='project',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='donation_project', to='ddm_projects.donationproject', verbose_name='Donation Project'),
),
],
# Reusing an existing table, so do nothing.
database_operations=[]
)
]
File renamed without changes.
5 changes: 3 additions & 2 deletions ddm/models/auth.py → ddm/auth/models.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import binascii
import os

from ddm.models.logs import EventLogEntry
from django.db import models
from django.utils import timezone
from rest_framework import exceptions
from rest_framework.authentication import TokenAuthentication, get_authorization_header

from ddm.logging.models import EventLogEntry


class ProjectAccessToken(models.Model):
"""
Expand All @@ -15,7 +16,7 @@ class ProjectAccessToken(models.Model):
"""
key = models.CharField(max_length=40, primary_key=True)
project = models.OneToOneField(
'DonationProject', related_name='donation_project',
'ddm_projects.DonationProject', related_name='donation_project',
on_delete=models.CASCADE, verbose_name='Donation Project'
)
created = models.DateTimeField(auto_now_add=True)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% extends 'ddm/admin/base.html' %}
{% extends 'core/base.html' %}

{% load static %}

Expand All @@ -11,7 +11,7 @@
<div class="container row justify-content-center">
<div class="col-4 text-center mt-5">
<div class="row">
<img alt="Data Donation Module" height="100" alt="" src="{% static 'ddm/img/logos/DDM_Logo_Schwarz.svg' %}">
<img alt="Data Donation Module" height="100" alt="" src="{% static 'core/img/logos/DDM_Logo_Schwarz.svg' %}">
</div>

<div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% extends 'ddm/admin/base.html' %}
{% extends 'core/base.html' %}

{% load static %}

Expand All @@ -11,7 +11,7 @@
<div class="container row justify-content-center">
<div class="col-4 text-center mt-5">
<div class="row">
<img alt="Data Donation Module" height="100" alt="" src="{% static 'ddm/img/logos/DDM_Logo_Schwarz.svg' %}">
<img alt="Data Donation Module" height="100" alt="" src="{% static 'core/img/logos/DDM_Logo_Schwarz.svg' %}">
</div>

<div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% extends 'ddm/admin/base.html' %}
{% extends 'core/base.html' %}

{% block page_title %}Permission Denied{% endblock %}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% extends 'ddm/admin/generic/page_with_form.html' %}
{% extends 'core/page_with_form.html' %}

{% block page_title %}Project Access Token{% endblock %}

Expand Down Expand Up @@ -78,15 +78,15 @@
<input class="ddm-btn" type="submit" value="Create Token">
{% block cancel %}
<p>
<a href="{% url 'project-detail' project.pk %}">&#129040; Back</a>
<a href="{% url 'ddm_projects:detail' project.pk %}">&#129040; Back</a>
</p>
{% endblock %}
</div>
</form>
{% endblock %}

{% block breadcrumbs %}
<a href="{% url 'project-list' %}">Projects</a> /
<a href="{% url 'project-detail' project.pk %}">"{{ project.name|truncatechars:15 }}" Project</a> /
<a href="{% url 'ddm_projects:list' %}">Projects</a> /
<a href="{% url 'ddm_projects:detail' project.pk %}">"{{ project.name|truncatechars:15 }}" Project</a> /
Manage Access Token
{% endblock %}
Empty file added ddm/auth/tests/__init__.py
Empty file.
79 changes: 79 additions & 0 deletions ddm/auth/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import datetime

from django.test import TestCase
from django.utils import timezone
from django.contrib.auth import get_user_model
from rest_framework import exceptions

from ddm.auth.models import ProjectTokenAuthenticator, ProjectAccessToken
from ddm.projects.models import ResearchProfile, DonationProject


User = get_user_model()


class TestCustomTokenAuthenticator(TestCase):

@classmethod
def setUpTestData(cls):
# User
base_creds = {
'username': 'base_user', 'password': '123', 'email': 'base@mail.com'
}
base_user = User.objects.create_user(**base_creds)
base_user_profile = ResearchProfile.objects.create(user=base_user)

# Project
cls.project = DonationProject.objects.create(
name='Base Project', slug='base', owner=base_user_profile
)

# Authenticator
cls.authenticator = ProjectTokenAuthenticator()
cls.token = ProjectAccessToken.objects.create(
project=cls.project, created=timezone.now(), expiration_date=None
)

def test_valid_token_without_expiration(self):
validated_token = self.authenticator.authenticate_credentials(
self.token, self.project.pk)[1]
self.assertEqual(self.token, validated_token)

def test_valid_token_with_expiration(self):
self.token.delete()
token = ProjectAccessToken.objects.create(
project=self.project,
created=timezone.now(),
expiration_date=timezone.now() + datetime.timedelta(days=2)
)
validated_token = self.authenticator.authenticate_credentials(
token, self.project.pk)[1]
self.assertEqual(token, validated_token)

def test_invalid_token(self):
token = 'rubbish'
self.assertRaises(
exceptions.AuthenticationFailed,
self.authenticator.authenticate_credentials,
token, self.project.pk
)

def test_without_token(self):
self.assertRaises(
exceptions.AuthenticationFailed,
self.authenticator.authenticate_credentials,
None, self.project.pk
)

def test_expired_token(self):
self.token.delete()
expired_token = ProjectAccessToken.objects.create(
project=self.project,
created=timezone.now(),
expiration_date=datetime.datetime(2022, 2, 2, 22, 22).replace(tzinfo=datetime.timezone.utc)
)
self.assertRaises(
exceptions.AuthenticationFailed,
self.authenticator.authenticate_credentials,
expired_token, self.project.pk
)
67 changes: 67 additions & 0 deletions ddm/auth/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from django.contrib import auth
from django.test import TestCase, override_settings, Client
from django.contrib.auth import get_user_model

from ddm.auth.utils import email_is_valid, user_has_project_access, user_is_permitted
from ddm.projects.models import ResearchProfile, DonationProject

User = get_user_model()


@override_settings(DDM_SETTINGS={'EMAIL_PERMISSION_CHECK': r'.*(\.|@)mail\.com$', })
class TestAUthUtils(TestCase):

@classmethod
def setUpTestData(cls):
cls.valid_creds = {
'username': 'owner', 'password': '123', 'email': 'owner@mail.com'
}
cls.valid_user = User.objects.create_user(**cls.valid_creds)
cls.valid_profile = ResearchProfile.objects.create(user=cls.valid_user)

cls.non_permission_creds = {
'username': 'no_per', 'password': '123', 'email': 'noperm@liam.com'
}
cls.user_wo_permission = User.objects.create_user(**cls.non_permission_creds)
cls.profile_wo_permission = ResearchProfile.objects.create(user=cls.user_wo_permission)

cls.wo_profile_creds = {
'username': 'no_prof', 'password': '123', 'email': 'noprof@mail.com'
}
cls.user_wo_profile = User.objects.create_user(**cls.wo_profile_creds)

cls.superuser = User.objects.create_superuser('user', 'some@mail.com', 'password')

def test_email_is_valid(self):
self.assertTrue(email_is_valid('some-address@mail.com'))

def test_email_is_invalid(self):
self.assertFalse(email_is_valid('some-address@mail.ch'))
self.assertFalse(email_is_valid('some-address@liam.com'))

def test_user_has_project_access(self):
project = DonationProject.objects.create(
name='test-project', slug='test', owner=self.valid_profile)
self.assertTrue(user_has_project_access(self.valid_user, project))

def test_user_has_no_project_access(self):
project = DonationProject.objects.create(
name='test-project', slug='test', owner=self.valid_profile)
self.assertFalse(user_has_project_access(self.user_wo_profile, project))
self.assertFalse(user_has_project_access(self.user_wo_permission, project))
anonymous_user = auth.get_user(self.client)
self.assertFalse(user_has_project_access(anonymous_user, project))

def test_user_is_permitted(self):
self.assertTrue(user_is_permitted(self.valid_user))
self.assertTrue(user_is_permitted(self.superuser))
self.profile_wo_permission.ignore_email_restriction = True
self.profile_wo_permission.save()
self.assertTrue(user_is_permitted(self.user_wo_permission))
self.assertTrue(user_is_permitted(self.user_wo_profile))

def test_user_is_not_permitted(self):
self.assertFalse(user_is_permitted(self.user_wo_permission))
client = Client()
anonymous_user = auth.get_user(client)
self.assertFalse(user_is_permitted(anonymous_user))
Loading

0 comments on commit 6100b58

Please sign in to comment.