From 328b44e729ed02dae38e5c4d552716134239e4fb Mon Sep 17 00:00:00 2001 From: realaravinth Date: Fri, 17 Jun 2022 18:12:02 +0530 Subject: [PATCH] feat: support app Summary Support app shows links to user to create new issue on the Hostea meta repository(configurable via settings.py) and view open issues. (auto)Redirection via dashboard to new issue page on Gitea and issue tracker repository is setup. In future, a form will be exposed within the dashboard itself to streamline support related workflows. --- dashboard/settings.py | 6 ++ dashboard/urls.py | 1 + support/__init__.py | 0 support/admin.py | 3 + support/apps.py | 6 ++ support/migrations/__init__.py | 0 support/models.py | 3 + support/templates/support/index.html | 6 ++ support/templates/support/list.html | 9 ++ support/templates/support/new.html | 9 ++ support/templates/support/redirect.html | 38 +++++++ support/tests.py | 138 ++++++++++++++++++++++++ support/urls.py | 24 +++++ support/utils.py | 48 +++++++++ support/views.py | 67 ++++++++++++ 15 files changed, 358 insertions(+) create mode 100644 support/__init__.py create mode 100644 support/admin.py create mode 100644 support/apps.py create mode 100644 support/migrations/__init__.py create mode 100644 support/models.py create mode 100644 support/templates/support/index.html create mode 100644 support/templates/support/list.html create mode 100644 support/templates/support/new.html create mode 100644 support/templates/support/redirect.html create mode 100644 support/tests.py create mode 100644 support/urls.py create mode 100644 support/utils.py create mode 100644 support/views.py 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)