diff --git a/core/forms/profiles.py b/core/forms/profiles.py index 7ec2f2f..48e9aa1 100644 --- a/core/forms/profiles.py +++ b/core/forms/profiles.py @@ -5,8 +5,6 @@ from django.contrib.auth import get_user_model from phonenumber_field.formfields import PhoneNumberField -from core.models import AdminProfile - class AdminEditForm(ModelForm): display_name = CharField(required=False, label=_('Internal Display Name')) mobile = PhoneNumberField(required=False, label=_('Mobile Number')) @@ -16,4 +14,14 @@ class AdminEditForm(ModelForm): class Meta: model = get_user_model() - fields = ('first_name', 'last_name', "display_name", "email", 'mobile', "role", "image", "remove_image") \ No newline at end of file + fields = ('first_name', 'last_name', "display_name", "email", 'mobile', "role", "image", "remove_image") + +class AdminCreateForm(ModelForm): + display_name = CharField(required=False, label=_('Internal Display Name')) + mobile = PhoneNumberField(required=False, label=_('Mobile Number')) + role = CharField(required=False, label=_("Role")) + image = ImageField(required=False, label=_("Image")) + + class Meta: + model = get_user_model() + fields = ('first_name', 'last_name', "display_name", "email", 'mobile', "role", "image") \ No newline at end of file diff --git a/core/helpers/auth.py b/core/helpers/auth.py index f44fb71..2615ec5 100644 --- a/core/helpers/auth.py +++ b/core/helpers/auth.py @@ -1,6 +1,6 @@ -from core.helpers.mail import get_template +from core.helpers.mail import get_template, simple_send_mail from core.helpers.urls import relative_to_absolute as reltoabs -from core.models.auth import LoginLog +from core.models.auth import LoginLog, PWResetToken from core.helpers.request import get_client_ip from django.urls import reverse @@ -28,4 +28,9 @@ def clear_login_log(maxage=int(getValue("core.auth.ratelimit.period", 600))): def clear_ratelimits(maxage=int(getValue("core.auth.ratelimit.block", 3600))): timestamp = timezone.now() - timezone.timedelta(seconds=maxage) - LoginLog.objects.filter(timestamp__lt=timestamp).delete() \ No newline at end of file + LoginLog.objects.filter(timestamp__lt=timestamp).delete() + +def request_password(user): + token = PWResetToken.objects.create(user=user) + mail = generate_pwreset_mail(user, token) + simple_send_mail("Password Reset", mail, user.email) \ No newline at end of file diff --git a/core/models/brands.py b/core/models/brands.py index 481ed3b..1b967e6 100644 --- a/core/models/brands.py +++ b/core/models/brands.py @@ -1,4 +1,19 @@ -from django.db.models import Model +from django.db.models import Model, ImageField + +from core.fields.base import LongCharField +from core.helpers.files import generate_storage_filename + +from internationalflavor.vat_number.models import VATNumberField +from django_countries.fields import CountryField class Brand(Model): - pass \ No newline at end of file + name = LongCharField(null=True, blank=True) + logo = ImageField(null=True, blank=True, upload_to=generate_storage_filename) + address1 = LongCharField() + address2 = LongCharField(null=True, blank=True) + zip = LongCharField() + city = LongCharField() + state = LongCharField(null=True, blank=True) + country = CountryField() + vat_id = VATNumberField(null=True, blank=True) + company_id = LongCharField(null=True, blank=True) \ No newline at end of file diff --git a/core/models/profiles.py b/core/models/profiles.py index 9170254..144ad1e 100644 --- a/core/models/profiles.py +++ b/core/models/profiles.py @@ -28,15 +28,15 @@ class AdminProfile(Profile): return self.display_name if self.display_name else self.user.get_full_name class ClientProfile(Profile): - company = LongCharField() + company = LongCharField(null=True, blank=True) address1 = LongCharField() - address2 = LongCharField() + address2 = LongCharField(null=True, blank=True) zip = LongCharField() city = LongCharField() - state = LongCharField() + state = LongCharField(null=True, blank=True) country = CountryField() - vat_id = VATNumberField() - company_id = LongCharField() + vat_id = VATNumberField(null=True, blank=True) + company_id = LongCharField(null=True, blank=True) default_currency = ForeignKey(Currency, on_delete=SET_DEFAULT, default=Currency.get_base) client_groups = ManyToManyField(ClientGroup) brands = ManyToManyField(Brand) diff --git a/core/modules/urls.py b/core/modules/urls.py index d0948e2..d661053 100644 --- a/core/modules/urls.py +++ b/core/modules/urls.py @@ -21,6 +21,10 @@ from core.views import ( DBSettingsEditView, DBSettingsDeleteView, DBSettingsCreateView, + BrandCreateView, + BrandDeleteView, + BrandEditView, + BrandListView, ) URLPATTERNS = [] @@ -54,6 +58,13 @@ URLPATTERNS.append(path("admin/profiles//delete/", AdminDeleteView.as_view() URLPATTERNS.append(path("admin/profiles//edit/", AdminEditView.as_view(), name="admins_edit")) URLPATTERNS.append(path("admin/profiles/create/", AdminCreateView.as_view(), name="admins_create")) +# Brand Administration URLs + +URLPATTERNS.append(path('admin/brands/', BrandListView.as_view(), name="brands")) +URLPATTERNS.append(path("admin/brands//delete/", BrandDeleteView.as_view(), name="brands_delete")) +URLPATTERNS.append(path("admin/brands//edit/", BrandEditView.as_view(), name="brands_edit")) +URLPATTERNS.append(path("admin/brands/create/", BrandCreateView.as_view(), name="brands_create")) + # External Module URLs for module in settings.EXPEPHALON_MODULES: diff --git a/core/navigation.py b/core/navigation.py index 81e606b..2261a18 100644 --- a/core/navigation.py +++ b/core/navigation.py @@ -89,7 +89,7 @@ navigations["backend_main"].add_section(reports_section) administration_section = NavSection("Administration", "") user_administration_item = NavItem("Administrator Users", "fa-users-cog", "admins") -brand_administration_item = NavItem("Brands", "fa-code-branch", "backendni") +brand_administration_item = NavItem("Brands", "fa-code-branch", "brands") sms_administration_item = NavItem("SMS Gateway", "fa-sms", "backendni") otp_administration_item = NavItem("Two-Factor Authentication", "fa-id-badge", "backendni") backup_administration_item = NavItem("Backups", "fa-shield-alt", "backendni") diff --git a/core/views/__init__.py b/core/views/__init__.py index 2988aaa..69dfb47 100644 --- a/core/views/__init__.py +++ b/core/views/__init__.py @@ -6,6 +6,7 @@ from core.views.dbsettings import * from core.views.auth import * from core.views.profiles import * from core.views.generic import * +from core.views.brands import * from core.mixins.auth import AdminMixin # Create your views here. diff --git a/core/views/auth.py b/core/views/auth.py index d79ef08..2ef9222 100644 --- a/core/views/auth.py +++ b/core/views/auth.py @@ -10,7 +10,7 @@ from core.forms import LoginForm, OTPSelectorForm, OTPVerificationForm, PWResetF from core.models.auth import LoginSession, PWResetToken, IPLimit, LoginLog from core.helpers.otp import get_user_otps, get_otp_choices, get_otp_by_name from core.helpers.mail import simple_send_mail -from core.helpers.auth import generate_pwreset_mail, login_fail, login_success +from core.helpers.auth import generate_pwreset_mail, login_fail, login_success, request_password from core.helpers.request import get_client_ip from dbsettings.functions import getValue @@ -205,9 +205,7 @@ class PWRequestView(AuthView): def form_valid(self, form): try: user = get_user_model().objects.get(username=form.cleaned_data["email"]) - token = PWResetToken.objects.create(user=user) - mail = generate_pwreset_mail(user, token) - simple_send_mail("Password Reset", mail, user.email) + request_password(user) finally: messages.success(self.request, "If a matching account was found, you should shortly receive an email containing password reset instructions. If you have not received this message after five minutes, please verify that you have entered the correct email address, or contact support.") return redirect("login") \ No newline at end of file diff --git a/core/views/brands.py b/core/views/brands.py new file mode 100644 index 0000000..ebd9f22 --- /dev/null +++ b/core/views/brands.py @@ -0,0 +1,46 @@ +from django.conf import settings +from django.urls import reverse_lazy + +from core.models.brands import Brand +from core.views.generic import BackendListView, BackendUpdateView, BackendDeleteView, BackendCreateView + +class BrandListView(BackendListView): + template_name = f"{settings.EXPEPHALON_BACKEND}/brands/index.html" + model = Brand + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["title"] = "Brand Settings" + return context + +class BrandEditView(BackendUpdateView): + template_name = f"{settings.EXPEPHALON_BACKEND}/brands/update.html" + model = Brand + success_url = reverse_lazy("dbsettings") + fields = ["key", "value"] + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["title"] = "Edit Brand" + return context + +class BrandDeleteView(BackendDeleteView): + template_name = f"{settings.EXPEPHALON_BACKEND}/brands/delete.html" + model = Brand + success_url = reverse_lazy("dbsettings") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["title"] = "Delete Brand" + return context + +class BrandCreateView(BackendCreateView): + template_name = f"{settings.EXPEPHALON_BACKEND}/dbsettings/create.html" + model = Brand + success_url = reverse_lazy("dbsettings") + fields = ["key", "value"] + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["title"] = "Create Brand" + return context diff --git a/core/views/dbsettings.py b/core/views/dbsettings.py index 6e1b7a8..0fd3741 100644 --- a/core/views/dbsettings.py +++ b/core/views/dbsettings.py @@ -1,10 +1,10 @@ from django.conf import settings -from django.views.generic import ListView, UpdateView, DeleteView, CreateView from django.urls import reverse_lazy from dbsettings.models import Setting +from core.views.generic import BackendListView, BackendUpdateView, BackendDeleteView, BackendCreateView -class DBSettingsListView(ListView): +class DBSettingsListView(BackendListView): template_name = f"{settings.EXPEPHALON_BACKEND}/dbsettings/index.html" model = Setting @@ -13,7 +13,7 @@ class DBSettingsListView(ListView): context["title"] = "Database Settings" return context -class DBSettingsEditView(UpdateView): +class DBSettingsEditView(BackendUpdateView): template_name = f"{settings.EXPEPHALON_BACKEND}/dbsettings/update.html" model = Setting success_url = reverse_lazy("dbsettings") @@ -24,7 +24,7 @@ class DBSettingsEditView(UpdateView): context["title"] = "Edit Setting" return context -class DBSettingsDeleteView(DeleteView): +class DBSettingsDeleteView(BackendDeleteView): template_name = f"{settings.EXPEPHALON_BACKEND}/dbsettings/delete.html" model = Setting success_url = reverse_lazy("dbsettings") @@ -34,7 +34,7 @@ class DBSettingsDeleteView(DeleteView): context["title"] = "Delete Setting" return context -class DBSettingsCreateView(CreateView): +class DBSettingsCreateView(BackendCreateView): template_name = f"{settings.EXPEPHALON_BACKEND}/dbsettings/create.html" model = Setting success_url = reverse_lazy("dbsettings") diff --git a/core/views/generic.py b/core/views/generic.py index 64c3379..e4d7766 100644 --- a/core/views/generic.py +++ b/core/views/generic.py @@ -1,4 +1,4 @@ -from django.views.generic import TemplateView, ListView, CreateView, FormView, DeleteView +from django.views.generic import TemplateView, ListView, CreateView, FormView, DeleteView, UpdateView from core.mixins.auth import AdminMixin class BackendTemplateView(AdminMixin, TemplateView): @@ -14,4 +14,7 @@ class BackendFormView(AdminMixin, FormView): pass class BackendDeleteView(AdminMixin, DeleteView): + pass + +class BackendUpdateView(AdminMixin, UpdateView): pass \ No newline at end of file diff --git a/core/views/profiles.py b/core/views/profiles.py index dfd2f0f..084424c 100644 --- a/core/views/profiles.py +++ b/core/views/profiles.py @@ -1,12 +1,14 @@ from django.conf import settings from django.urls import reverse_lazy from django.contrib.auth import get_user_model +from django.contrib import messages from core.models import AdminProfile -from core.forms import AdminEditForm -from core.views.generic import BackendFormView as FormView, BackendListView as ListView, BackendDeleteView as DeleteView +from core.forms.profiles import AdminEditForm, AdminCreateForm +from core.views.generic import BackendFormView, BackendListView, BackendDeleteView +from core.helpers.auth import request_password -class AdminListView(ListView): +class AdminListView(BackendListView): template_name = f"{settings.EXPEPHALON_BACKEND}/profiles/index.html" model = AdminProfile @@ -15,7 +17,7 @@ class AdminListView(ListView): context["title"] = "Administrator Users" return context -class AdminEditView(FormView): +class AdminEditView(BackendFormView): template_name = f"{settings.EXPEPHALON_BACKEND}/profiles/update.html" form_class = AdminEditForm success_url = reverse_lazy("admins") @@ -54,7 +56,7 @@ class AdminEditView(FormView): admin.save() return super().form_valid(form) -class AdminDeleteView(DeleteView): +class AdminDeleteView(BackendDeleteView): template_name = f"{settings.EXPEPHALON_BACKEND}/profiles/delete.html" model = get_user_model() success_url = reverse_lazy("admins") @@ -69,13 +71,35 @@ class AdminDeleteView(DeleteView): assert type(admin.profile) == AdminProfile return admin -class AdminCreateView(FormView): +class AdminCreateView(BackendFormView): template_name = f"{settings.EXPEPHALON_BACKEND}/profiles/create.html" - model = get_user_model() + form_class = AdminCreateForm success_url = reverse_lazy("admins") - fields = ["key", "value"] def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["title"] = "Create Administrator" return context + + def form_valid(self, form): + admin = get_user_model()() + admin.first_name = form.cleaned_data["first_name"] + admin.last_name = form.cleaned_data["last_name"] + admin.username = form.cleaned_data["email"] + admin.email = form.cleaned_data["email"] + + profile = AdminProfile() + profile.user = admin + profile.mobile = form.cleaned_data["mobile"] + profile.role = form.cleaned_data["role"] + profile.display_name = form.cleaned_data["display_name"] + profile.image = form.cleaned_data["image"] + + admin.save() + profile.save() + + request_password(admin) + + messages.success(f"User {admin.get_full_name} was successfully created. They should receive an email to set their password shortly.") + + return super().form_valid(form) diff --git a/static/backend/images/pixel.png b/static/backend/images/pixel.png new file mode 100644 index 0000000..4934504 Binary files /dev/null and b/static/backend/images/pixel.png differ diff --git a/templates/backend/base.html b/templates/backend/base.html index b3895bb..59e54b6 100644 --- a/templates/backend/base.html +++ b/templates/backend/base.html @@ -207,7 +207,7 @@
- +
Ella-Rose Henry
@@ -229,7 +229,7 @@
- +
Ruben Tillman
@@ -251,7 +251,7 @@
- +
Vinnie Wagstaff
@@ -273,7 +273,7 @@
- +
Ella-Rose Henry
@@ -295,7 +295,7 @@
- +
Ruben Tillman
diff --git a/templates/backend/brands/create.html b/templates/backend/brands/create.html new file mode 100644 index 0000000..f1ecaed --- /dev/null +++ b/templates/backend/brands/create.html @@ -0,0 +1,57 @@ +{% extends "backend/base.html" %} +{% load bootstrap4 %} +{% block content %} +
+
+
+
+ + +
+
Brands - Create Brand +
Create a new brand +
+
+
+
+ + +
+
+
+
+
+
+
+
+ + Create Brand +
+
+
+
+
+
+ {% csrf_token %} + {% bootstrap_form form %} + {% buttons %} + + + Cancel + + {% endbuttons %} +
+
+
+
+
+
+
+ + + +{% endblock %} \ No newline at end of file diff --git a/templates/backend/brands/delete.html b/templates/backend/brands/delete.html new file mode 100644 index 0000000..4fd5b6e --- /dev/null +++ b/templates/backend/brands/delete.html @@ -0,0 +1,57 @@ +{% extends "backend/base.html" %} +{% load bootstrap4 %} +{% block content %} +
+
+
+
+ + +
+
Brands - Delete Brand +
Delete a brand from the system +
+
+
+
+ + +
+
+
+
+
+
+
+
+ + Deleting {{ object.name }} +
+
+
+
+
+
+ {% csrf_token %} + Are you sure you wish to delete {{ object.name }}? This will irrevocably delete the brand and any associated information. You can also disable the brand without deleting its data! + {% buttons %} + + + Cancel + + {% endbuttons %} +
+
+
+
+
+
+
+ + + +{% endblock %} \ No newline at end of file diff --git a/templates/backend/brands/index.html b/templates/backend/brands/index.html new file mode 100644 index 0000000..0f8c262 --- /dev/null +++ b/templates/backend/brands/index.html @@ -0,0 +1,61 @@ +{% extends "backend/base.html" %} +{% block content %} +
+
+
+
+ + +
+
Brands +
Create, edit and delete brands +
+
+
+ +
+
+
+
+
+
+
+ + Active Brands +
+
+
+
+
+
+ + + + + + + + + + {% for brand in object_list %} + + + + + + {% endfor %} + +
NameCountryOptions
{{ brand.name }}{{ brand.country }}
+
+
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/backend/brands/update.html b/templates/backend/brands/update.html new file mode 100644 index 0000000..5ebb4b0 --- /dev/null +++ b/templates/backend/brands/update.html @@ -0,0 +1,57 @@ +{% extends "backend/base.html" %} +{% load bootstrap4 %} +{% block content %} +
+
+
+
+ + +
+
Brands - Edit Brand +
Edit brand properties +
+
+
+
+ + +
+
+
+
+
+
+
+
+ + Editing {{ object.name }} +
+
+
+
+
+
+ {% csrf_token %} + {% bootstrap_form form %} + {% buttons %} + + + Cancel + + {% endbuttons %} +
+
+
+
+
+
+
+ + + +{% endblock %} \ No newline at end of file diff --git a/templates/backend/profiles/create.html b/templates/backend/profiles/create.html index 84204f9..3e55a64 100644 --- a/templates/backend/profiles/create.html +++ b/templates/backend/profiles/create.html @@ -14,7 +14,7 @@
- @@ -40,7 +40,7 @@ - + Cancel {% endbuttons %} diff --git a/templates/backend/profiles/index.html b/templates/backend/profiles/index.html index 39e53a7..8343c12 100644 --- a/templates/backend/profiles/index.html +++ b/templates/backend/profiles/index.html @@ -13,7 +13,7 @@
- + New Administrator diff --git a/templates/backend/profiles/update.html b/templates/backend/profiles/update.html index 3519020..7f3f7ca 100644 --- a/templates/backend/profiles/update.html +++ b/templates/backend/profiles/update.html @@ -40,7 +40,7 @@ - + Cancel {% endbuttons %}