Compare commits

...

4 commits

Author SHA1 Message Date
a65b1574db Add admin user information REST API 2023-04-16 13:21:57 +02:00
9731d4e793 Add Django REST framework dependencies 2023-04-16 12:24:32 +02:00
f9ea88cd24 Add new app 'help' for user support
This commit adds a new model that enhances the user profile with an
offline support code, a postal address and an email address to allow
users to reset their profile.

The commit adds to Django admin commands 'populate' and
'reset_offline_code' to maintain the help user profiles from the Django
command line.
2023-04-16 12:20:37 +02:00
4b7e311c62 Remove tests for removed models 2023-04-16 11:14:26 +02:00
19 changed files with 295 additions and 93 deletions

View file

@ -1,6 +1,9 @@
Changelog
=========
* :feature:`-` add REST API to retrieve and set user information as admin
* :feature:`-` add support model for offline account reset codes in new help
app
* :support:`-` remove unused PowerDNS support tables from domains app
* :feature:`-` add impersonation support for superusers
* :support:`-` remove django-braces dependency

View file

@ -7,19 +7,9 @@ from unittest.mock import patch
from django.test import TestCase
from django.contrib.auth import get_user_model
from domains.models import (
DNSComment,
DNSCryptoKey,
DNSDomain,
DNSDomainMetadata,
DNSRecord,
DNSSupermaster,
DNSTSIGKey,
HostingDomain,
MailDomain,
)
from hostingpackages.models import CustomerHostingPackage, HostingPackageTemplate
from domains.models import HostingDomain, MailDomain
from hostingpackages.models import CustomerHostingPackage, HostingPackageTemplate
User = get_user_model()
@ -77,49 +67,3 @@ class HostingDomainTest(TestCase):
def test___str__(self):
hostingdomain = HostingDomain(domain="test")
self.assertEqual(str(hostingdomain), "test")
class DNSDomainTest(TestCase):
def test___str__(self):
dnsdomain = DNSDomain(domain="test")
self.assertEqual(str(dnsdomain), "test")
class DNSRecordTest(TestCase):
def test___str__(self):
dnsrecord = DNSRecord(name="localhost", recordtype="A", content="127.0.0.1")
self.assertEqual(str(dnsrecord), "localhost IN A 127.0.0.1")
class DNSSupermasterTest(TestCase):
def test___str__(self):
dnssupermaster = DNSSupermaster(ip="127.0.0.1", nameserver="dns.example.org")
self.assertEqual(str(dnssupermaster), "127.0.0.1 dns.example.org")
class DNSCommentTest(TestCase):
def test___str__(self):
dnscomment = DNSComment(name="localhost", commenttype="A", comment="good stuff")
self.assertEqual(str(dnscomment), "localhost IN A: good stuff")
class DNSDomainMetadataTest(TestCase):
def test___str__(self):
dnsdomain = DNSDomain(domain="test")
dnsdomainmetadata = DNSDomainMetadata(
domain=dnsdomain, kind="SOA-EDIT", content="INCEPTION"
)
self.assertEqual(str(dnsdomainmetadata), "test SOA-EDIT INCEPTION")
class DNSCryptoKeyTest(TestCase):
def test___str__(self):
dnsdomain = DNSDomain(domain="test")
dnscryptokey = DNSCryptoKey(domain=dnsdomain, content="testvalue")
self.assertEqual(str(dnscryptokey), "test testvalue")
class DNSTSIGKeyTest(TestCase):
def test___str__(self):
dnstsigkey = DNSTSIGKey(name="testkey", algorithm="hmac-md5", secret="dummykey")
self.assertEqual(str(dnstsigkey), "testkey hmac-md5 XXXX")

View file

@ -207,6 +207,8 @@ DJANGO_APPS = (
"django.contrib.flatpages",
"crispy_forms",
"impersonate",
"rest_framework",
"rest_framework.authtoken",
)
ALLAUTH_APPS = (
@ -232,6 +234,7 @@ LOCAL_APPS = (
"userdbs",
"hostingpackages",
"websites",
"help",
"contact_form",
)
@ -263,6 +266,17 @@ CRISPY_TEMPLATE_PACK = "bootstrap3"
# ######### END CRISPY_FORMS CONFIGURATION
# ######### REST FRAMEWORK CONFIGURATION
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.BasicAuthentication",
"rest_framework.authentication.SessionAuthentication",
"rest_framework.authentication.TokenAuthentication",
]
}
# ######### END REST FRAMEWORK CONFIGURATION
# ######### LOGGING CONFIGURATION
# See: https://docs.djangoproject.com/en/dev/ref/settings/#logging
# A sample logging configuration. The only tangible logging
@ -404,23 +418,8 @@ if GVA_ENVIRONMENT == "local":
dict(
[
(key, {"handlers": ["console"], "level": "DEBUG", "propagate": True})
for key in [
"dashboard",
"domains",
"fileservertasks",
"gvacommon",
"gvawebcore",
"hostingpackages",
"ldaptasks",
"managemails",
"mysqltasks",
"osusers",
"pgsqltasks",
"taskresults",
"userdbs",
"websites",
]
]
for key in LOCAL_APPS
],
)
)
elif GVA_ENVIRONMENT == "test":
@ -439,22 +438,7 @@ elif GVA_ENVIRONMENT == "test":
dict(
[
(key, {"handlers": ["console"], "level": "ERROR", "propagate": True})
for key in [
"dashboard",
"domains",
"fileservertasks",
"gvacommon",
"gvawebcore",
"hostingpackages",
"ldaptasks",
"managemails",
"mysqltasks",
"osusers",
"pgsqltasks",
"taskresults",
"userdbs",
"websites",
]
for key in LOCAL_APPS
]
)
)

View file

@ -6,11 +6,20 @@ from django.contrib import admin
from django.contrib.flatpages import views
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.urls import path, re_path
from rest_framework import routers
from help import views as help_views
admin.autodiscover()
router = routers.DefaultRouter()
router.register(r"users", help_views.UserViewSet)
router.register(r"help-users", help_views.HelpUserViewSet)
urlpatterns = [
re_path(r"", include("dashboard.urls")),
path("api/", include(router.urls)),
path("api-auth/", include("rest_framework.urls", namespace="rest_framework")),
re_path(r"^admin/", admin.site.urls),
re_path(r"^impersonate/", include("impersonate.urls")),
re_path(r"^accounts/", include("allauth.urls")),

View file

View file

@ -0,0 +1,22 @@
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.utils.translation import ugettext_lazy as _
from help.models import HelpUser
User = get_user_model()
class HelpUserInline(admin.StackedInline):
model = HelpUser
can_delete = False
readonly_fields = ("offline_account_code",)
class UserAdmin(BaseUserAdmin):
inlines = (HelpUserInline,)
admin.site.unregister(User)
admin.site.register(User, UserAdmin)

View file

@ -0,0 +1,8 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class HelpConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "help"
verbose_name = _("User self help")

View file

@ -0,0 +1,17 @@
from django.contrib.auth import get_user_model
from django.core.management import BaseCommand
from help.models import HelpUser
User = get_user_model()
class Command(BaseCommand):
help = "Populate help user information for existing users"
def handle(self, *args, **options):
for user in User.objects.filter(helpuser=None):
help_user = HelpUser.objects.create(user_id=user.id, email_address=user.email)
help_user.generate_offline_account_code()
help_user.save()
self.stdout.write(f"created offline account code {help_user.offline_account_code} for {user}.")

View file

@ -0,0 +1,29 @@
from django.contrib.auth import get_user_model
from django.core.management import BaseCommand, CommandError
from help.models import HelpUser
User = get_user_model()
class Command(BaseCommand):
help = "Reset offline account reset code for existing users"
def add_arguments(self, parser):
parser.add_argument("users", nargs='+', type=str)
def handle(self, *args, **options):
for name in options["users"]:
try:
user = User.objects.get(username=name)
except User.DoesNotExist:
raise CommandError(f'User {name} does not exist')
help_user = user.helpuser
if help_user is None:
help_user = HelpUser.objects.create(email_address=user.email)
help_user.generate_offline_account_code()
help_user.save()
self.stdout.write(f"generated new offline account reset code {help_user.offline_account_code} for {name}")

View file

@ -0,0 +1,55 @@
# Generated by Django 3.2.18 on 2023-04-16 09:32
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="HelpUser",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"email_address",
models.EmailField(
help_text="Contact email address", max_length=254
),
),
(
"postal_address",
models.TextField(blank=True, help_text="Contact postal address"),
),
(
"offline_account_code",
models.CharField(
default="",
help_text="Offline account reset code",
max_length=36,
),
),
(
"user",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
),
]

View file

@ -0,0 +1,17 @@
import uuid
from django.conf import settings
from django.db import models
from django.utils.translation import ugettext_lazy as _
class HelpUser(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
email_address = models.EmailField(help_text=_("Contact email address"))
postal_address = models.TextField(help_text=_("Contact postal address"), blank=True)
offline_account_code = models.CharField(
help_text=_("Offline account reset code"), max_length=36, default=""
)
def generate_offline_account_code(self):
self.offline_account_code = str(uuid.uuid4())

View file

@ -0,0 +1,30 @@
"""
Serializers for the REST API
"""
from django.contrib.auth import get_user_model
from rest_framework import serializers
from help.models import HelpUser
User = get_user_model()
class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ["url", "username", "helpuser"]
read_only_fields = ["username", "helpuser"]
class HelpUserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = HelpUser
fields = [
"url",
"user",
"email_address",
"postal_address",
"offline_account_code",
]
read_only_fields = ["user", "offline_account_code"]

View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View file

@ -0,0 +1,29 @@
from django.contrib.auth import get_user_model
from rest_framework import permissions, viewsets
from help.models import HelpUser
from help.serializers import HelpUserSerializer, UserSerializer
User = get_user_model()
class UserViewSet(viewsets.ReadOnlyModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
queryset = User.objects.all().order_by("-username")
serializer_class = UserSerializer
permission_classes = [permissions.IsAdminUser]
class HelpUserViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows user help profile to be viewed or edited.
"""
queryset = HelpUser.objects.all().order_by("-user__username")
serializer_class = HelpUserSerializer
permission_classes = [permissions.IsAdminUser]

51
poetry.lock generated
View file

@ -666,6 +666,21 @@ files = [
django = ">=3.2.4"
sqlparse = ">=0.2"
[[package]]
name = "django-filter"
version = "23.1"
description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "django-filter-23.1.tar.gz", hash = "sha256:dee5dcf2cea4d7f767e271b6d01f767fce7500676d5e5dc58dac8154000b87df"},
{file = "django_filter-23.1-py3-none-any.whl", hash = "sha256:e3c52ad83c32fb5882125105efb5fea2a1d6a85e7dc64b04ef52edbf14451b6c"},
]
[package.dependencies]
Django = ">=3.2"
[[package]]
name = "django-impersonate"
version = "1.9.1"
@ -692,6 +707,22 @@ files = [
[package.dependencies]
Django = ">=3.2"
[[package]]
name = "djangorestframework"
version = "3.14.0"
description = "Web APIs for Django, made easy."
category = "main"
optional = false
python-versions = ">=3.6"
files = [
{file = "djangorestframework-3.14.0-py3-none-any.whl", hash = "sha256:eb63f58c9f218e1a7d064d17a70751f528ed4e1d35547fdade9aaf4cd103fd08"},
{file = "djangorestframework-3.14.0.tar.gz", hash = "sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8"},
]
[package.dependencies]
django = ">=3.0"
pytz = "*"
[[package]]
name = "docutils"
version = "0.19"
@ -850,6 +881,24 @@ sqs = ["boto3 (>=1.9.12)", "pycurl (>=7.44.1,<7.45.0)", "urllib3 (>=1.26.7)"]
yaml = ["PyYAML (>=3.10)"]
zookeeper = ["kazoo (>=1.3.1)"]
[[package]]
name = "markdown"
version = "3.4.3"
description = "Python implementation of John Gruber's Markdown."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "Markdown-3.4.3-py3-none-any.whl", hash = "sha256:065fd4df22da73a625f14890dd77eb8040edcbd68794bcd35943be14490608b2"},
{file = "Markdown-3.4.3.tar.gz", hash = "sha256:8bf101198e004dc93e84a12a7395e31aac6a9c9942848ae1d99b9d72cf9b3520"},
]
[package.dependencies]
importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""}
[package.extras]
testing = ["coverage", "pyyaml"]
[[package]]
name = "markupsafe"
version = "2.1.2"
@ -1753,4 +1802,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
[metadata]
lock-version = "2.0"
python-versions = "^3.7"
content-hash = "dd56e0233689448f08dfcae943871bf9d72c05ad7bfd326c69f9ecb33ea8a461"
content-hash = "12e95bb19c0dc9d4b1388423e1007628e37e5e13a217de01b27bb34b20d5ac34"

View file

@ -20,6 +20,9 @@ passlib = "^1.7.4"
redis = "^4.5.1"
requests-oauthlib = "^1.3.1"
django-impersonate = "^1.9.1"
djangorestframework = "^3.14.0"
markdown = "^3.4.3"
django-filter = "^23.1"
[tool.poetry.group.dev.dependencies]