diff --git a/dashboard/settings.py b/dashboard/settings.py index de34d7a..229089b 100644 --- a/dashboard/settings.py +++ b/dashboard/settings.py @@ -43,6 +43,7 @@ INSTALLED_APPS = [ "django.contrib.staticfiles", "accounts", "dash", + "support", ] MIDDLEWARE = [ @@ -136,6 +137,11 @@ HOSTEA = { "RESTRICT_NEW_INTEGRATION_INSTALLATION": True, "INSTANCE_MAINTAINER_CONTACT": "contact@hostea.example.org", "ACCOUNTS": {"MAX_VERIFICATION_TOLERANCE_PERIOD": 60 * 60 * 24}, # in seconds + "META": { + "GITEA_INSTANCE": "https://gitea.hostea.org", + "GITEA_ORG_NAME": "Hostea", + "SUPPORT_REPOSITORY": "support", + }, } EMAIL_CONFIG = env.email("EMAIL_URL", default="smtp://admin:password@localhost:10025") diff --git a/dashboard/urls.py b/dashboard/urls.py index 2394c0a..dc9d8d1 100644 --- a/dashboard/urls.py +++ b/dashboard/urls.py @@ -19,5 +19,6 @@ from django.urls import path, include urlpatterns = [ path("admin/", admin.site.urls), path("dash/", include("dash.urls")), + path("support/", include("support.urls")), path("", include("accounts.urls")), ] diff --git a/support/__init__.py b/support/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/support/admin.py b/support/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/support/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/support/apps.py b/support/apps.py new file mode 100644 index 0000000..dd1d778 --- /dev/null +++ b/support/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class SupportConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "support" diff --git a/support/migrations/__init__.py b/support/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/support/models.py b/support/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/support/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/support/templates/support/index.html b/support/templates/support/index.html new file mode 100644 index 0000000..ce6446f --- /dev/null +++ b/support/templates/support/index.html @@ -0,0 +1,6 @@ +{% extends 'dash/common/base.html' %} +{% block dash %} +{% include "common/components/error.html" %} +

{{ title }}

+ {% include "common/components/error.html" %} +{% endblock %} diff --git a/support/templates/support/list.html b/support/templates/support/list.html new file mode 100644 index 0000000..430fcca --- /dev/null +++ b/support/templates/support/list.html @@ -0,0 +1,9 @@ +{% extends 'dash/common/base.html' %} {% block dash %} +

{{ title }}

+ +

+ You will be redirected to Hostea's issue tracker + momentarily. If not, please click + here. +

+{% include "support/redirect.html" %} {% endblock %} diff --git a/support/templates/support/new.html b/support/templates/support/new.html new file mode 100644 index 0000000..2521236 --- /dev/null +++ b/support/templates/support/new.html @@ -0,0 +1,9 @@ +{% extends 'dash/common/base.html' %} {% block dash %} +

{{ title }}

+ +

+ You will be redirected to Hostea's issue tracker + momentarily. If not, please click + here. +

+{% include "support/redirect.html" %} {% endblock %} diff --git a/support/templates/support/redirect.html b/support/templates/support/redirect.html new file mode 100644 index 0000000..10c265a --- /dev/null +++ b/support/templates/support/redirect.html @@ -0,0 +1,38 @@ + diff --git a/support/tests.py b/support/tests.py new file mode 100644 index 0000000..f04e8cf --- /dev/null +++ b/support/tests.py @@ -0,0 +1,138 @@ +# Copyright © 2022 Aravinth Manivannan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from django.contrib.auth import get_user_model +from django.utils.http import urlencode +from django.urls import reverse +from django.test import TestCase, Client, override_settings +from django.contrib.auth import authenticate +from django.conf import settings + +from .utils import IssueTracker + + +hostea_issue_tracker_settings = settings.HOSTEA +hostea_issue_tracker_settings["META"] = { + "GITEA_INSTANCE": "https://gitea.hostea.org", + "GITEA_ORG_NAME": "Hostea", + "SUPPORT_REPOSITORY": "support", +} + + +@override_settings(HOSTEA=hostea_issue_tracker_settings) +class IssueTrackerTests(TestCase): + """ + Test IssueTracker utility + """ + + def test_defaults(self): + """ + Verify default credentials; all further tests are based on defaults set + """ + it = IssueTracker() + self.assertEqual(it.config["GITEA_INSTANCE"], "https://gitea.hostea.org") + self.assertEqual(it.config["GITEA_ORG_NAME"], "Hostea") + self.assertEqual(it.config["SUPPORT_REPOSITORY"], "support") + + def test_uri_builders(self): + """ + Verify default credentials; all further tests are based on defaults set + """ + it = IssueTracker() + self.assertEqual( + it.get_issue_tracker(), "https://gitea.hostea.org/Hostea/support/issues" + ) + self.assertEqual( + it.open_issue(), "https://gitea.hostea.org/Hostea/support/issues/new" + ) + + +class SupportWorks(TestCase): + """ + Tests create new app view + """ + + def setUp(self): + self.password = "password121231" + self.username = "suport_user" + self.email = f"{self.username}@example.org" + self.user = get_user_model().objects.create( + username=self.username, + email=self.email, + ) + self.user.set_password(self.password) + self.user.save() + + def test_dash_is_protected(self): + """ + Tests if support templates render + """ + # default LOGIN redirect URI that is used by @login_required decorator is + # /accounts/login. There's a redirection endpoint at /accounts/login/ that + # will redirect user to /login. Hence the /accounts prefix + def redirect_login_uri(uri: str) -> str: + return f"/accounts{reverse('accounts.login')}?next={uri}" + + urls = [ + reverse("support.home"), + reverse("support.new"), + reverse("support.view"), + ] + for i in urls: + print(f"[*] Testing URI: {i}") + resp = self.client.get(i) + self.assertEqual(resp.status_code, 302) + expected = redirect_login_uri(i) + self.assertEqual(resp.headers["location"], expected) + + def test_dash_home_renders(self): + """ + Tests if login template renders + """ + c = Client() + + # username login works + payload = { + "login": self.username, + "password": self.password, + } + resp = c.post(reverse("accounts.login"), payload) + self.assertEqual(resp.status_code, 302) + self.assertEqual(resp.headers["location"], reverse("accounts.home")) + + urls = [ + reverse("support.home"), + reverse("support.new"), + reverse("support.view"), + ] + for i in urls: + print(f"[*] Testing URI: {i}") + resp = c.get(i) + self.assertEqual(resp.status_code, 200) + self.assertEqual(b"Billing" in resp.content, True) + self.assertEqual(b"Support" in resp.content, True) + self.assertEqual(b"Logout" in resp.content, True) + + # new issue view + resp = c.get(reverse("support.new")) + self.assertEqual(resp.status_code, 200) + it = IssueTracker() + new_issue = str.encode(it.open_issue()) + self.assertEqual(new_issue in resp.content, True) + + # list issues view + resp = c.get(reverse("support.new")) + self.assertEqual(resp.status_code, 200) + issue_tracker = str.encode(it.get_issue_tracker()) + self.assertEqual(issue_tracker in resp.content, True) diff --git a/support/urls.py b/support/urls.py new file mode 100644 index 0000000..3892e0c --- /dev/null +++ b/support/urls.py @@ -0,0 +1,24 @@ +# Copyright © 2022 Aravinth Manivannan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from django.contrib import admin +from django.urls import path, include + +from .views import home, new_ticket, view_tickets + +urlpatterns = [ + path("new/", new_ticket, name="support.new"), + path("view/", view_tickets, name="support.view"), + path("", home, name="support.home"), +] diff --git a/support/utils.py b/support/utils.py new file mode 100644 index 0000000..ada7525 --- /dev/null +++ b/support/utils.py @@ -0,0 +1,48 @@ +# Copyright © 2022 Aravinth Manivannan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from urllib.parse import urlparse, urlunparse + +from django.conf import settings + + +class IssueTracker: + """ + Hostea support repository Issue tracker URL generation stuff + """ + + def __init__(self): + self.config = settings.HOSTEA["META"] + self.instance = urlparse(self.config["GITEA_INSTANCE"]) + self.repo = ( + f"{self.config['GITEA_ORG_NAME']}/{self.config['SUPPORT_REPOSITORY']}" + ) + self.issues = f"{self.repo}/issues" + + def __path(self, path=str): + i = self.instance + return urlunparse((i.scheme, i.netloc, path, "", "", "")) + + def get_issue_tracker(self): + """ + Get issue tracker URL + """ + return self.__path(path=self.issues) + + def open_issue(self): + """ + Get open new issue URL + """ + path = f"{self.issues}/new" + return self.__path(path=path) diff --git a/support/views.py b/support/views.py new file mode 100644 index 0000000..0011745 --- /dev/null +++ b/support/views.py @@ -0,0 +1,67 @@ +# Copyright © 2022 Aravinth Manivannan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from django.shortcuts import render, redirect +from django.contrib.auth.decorators import login_required + +from .utils import IssueTracker + + +def default_ctx(title: str, username: str): + """ + Default context for all dashboard pages + """ + it = IssueTracker() + return { + "title": title, + "username": username, + "open_support": "open", + "support": {"list": it.get_issue_tracker(), "new": it.open_issue()}, + } + + +@login_required +def home(request): + """ + Support page view + """ + PAGE_TITLE = "Support" + username = request.user + ctx = default_ctx(title=PAGE_TITLE, username=username.username) + return render(request, "support/index.html", context=ctx) + + +@login_required +def new_ticket(request): + """ + Support page view + """ + PAGE_TITLE = "New Ticket" + username = request.user + it = IssueTracker() + ctx = default_ctx(title=PAGE_TITLE, username=username.username) + return render(request, "support/new.html", context=ctx) + + +@login_required +def view_tickets(request): + """ + Support page view + """ + PAGE_TITLE = "Opened Tickets" + username = request.user + it = IssueTracker() + ctx = default_ctx(title=PAGE_TITLE, username=username.username) + ctx["support"] = {"list": it.get_issue_tracker(), "new": it.open_issue()} + return render(request, "support/list.html", context=ctx)