A lot of new views

Color picker fields
Renamed pixel.png in order not to upset uBlock...
This commit is contained in:
Kumi 2020-06-02 17:58:20 +02:00
parent 65f34b32eb
commit 2e8bbdd8a7
56 changed files with 1657 additions and 241 deletions

View file

@ -1,5 +1,6 @@
from core.classes.cron import Cronjob
from core.helpers.auth import clear_login_log, clear_ratelimits
from core.helpers.cron import clear_cron_log
CRONDEFINITIONS = []
CRONFUNCTIONS = {}
@ -26,4 +27,11 @@ CRONDEFINITIONS.append(loginlog_cron)
ratelimit_cron = Cronjob("core.clear_ratelimits", "* * * * *")
CRONFUNCTIONS["core.clear_ratelimits"] = clear_ratelimits
CRONDEFINITIONS.append(ratelimit_cron)
CRONDEFINITIONS.append(ratelimit_cron)
### Remove old entries from the cron execution log
cronlog_cron = Cronjob("core.clear_cron_log", "* * * * *")
CRONFUNCTIONS["core.clear_cron_log"] = clear_cron_log
CRONDEFINITIONS.append(cronlog_cron)

View file

@ -0,0 +1,2 @@
from core.fields.base import LongCharField
from core.fields.color import ColorField

7
core/fields/color.py Normal file
View file

@ -0,0 +1,7 @@
from core.fields.base import LongCharField
from core.widgets.color import ColorPickerWidget
class ColorField(LongCharField):
def formfield(self, **kwargs):
kwargs['widget'] = ColorPickerWidget
return super().formfield(**kwargs)

View file

@ -1,9 +1,14 @@
from django.forms import ModelForm, CharField, BooleanField, ImageField
from django.forms import ModelForm, CharField, BooleanField, ImageField, ModelChoiceField, ModelMultipleChoiceField, BooleanField
from django.contrib.auth import get_user_model
from django.utils.translation import gettext_lazy as _
from django.contrib.auth import get_user_model
from phonenumber_field.formfields import PhoneNumberField
from django_countries.fields import CountryField
from internationalflavor.vat_number.forms import VATNumberFormField
from core.models.local import Currency
from core.models.brands import Brand
class AdminEditForm(ModelForm):
display_name = CharField(required=False, label=_('Internal Display Name'))
@ -24,4 +29,24 @@ class AdminCreateForm(ModelForm):
class Meta:
model = get_user_model()
fields = ('first_name', 'last_name', "display_name", "email", 'mobile', "role", "image")
fields = ('first_name', 'last_name', "display_name", "email", 'mobile', "role", "image")
class ClientForm(ModelForm):
company = CharField(required=False, label=_('Company Name'))
mobile = PhoneNumberField(required=False, label=_('Mobile Number'))
address1 = CharField(label=_('Address'))
address2 = CharField(label=_('Address'))
zip = CharField(label=_('ZIP'))
city = CharField(label=_('City'))
state = CharField(label=_('State'))
country = CountryField()
vat_id = VATNumberFormField(label=_('VAT Number'))
company_id = CharField(label=_('Company Registration Number'))
default_currency = ModelChoiceField(Currency.objects.all(), label=_("Default Currency"))
brands = ModelMultipleChoiceField(Brand.objects.all(), label=_("Associated Brands"))
marketing_opt_in = BooleanField(label=_("Opted in to marketing messages"))
pgp_key = CharField(label=_("GPG encryption key"))
class Meta:
model = get_user_model()
fields = ('first_name', 'last_name', "company", "email", 'mobile')

View file

@ -1,13 +1,25 @@
from django_celery_beat.models import PeriodicTask, IntervalSchedule
from django_celery_results.models import TaskResult
def setup_cron():
from django.utils import timezone
from dbsettings.functions import getValue
def setup_cron(frequency=int(getValue("core.cron.frequency", 5))):
schedule, created = IntervalSchedule.objects.get_or_create(
every=5,
every=frequency,
period=IntervalSchedule.SECONDS,
)
PeriodicTask.objects.get_or_create(
interval=schedule,
cron = PeriodicTask.objects.get_or_create(
name='Expephacron',
task='cron',
)
)[0]
cron.interval = schedule
cron.save()
def clear_cron_log(maxage=int(getValue("core.cron.log.retention", 86400))):
timestamp = timezone.now() - timezone.timedelta(seconds=maxage)
TaskResult.objects.filter(date_done__lt=timestamp).delete()

View file

@ -5,4 +5,5 @@ from core.models.local import *
from core.models.cron import *
from core.models.products import *
from core.models.billable import *
from core.models.services import *
from core.models.services import *
from core.models.invoices import *

View file

@ -2,12 +2,14 @@ from django.db.models import Model, ImageField
from core.fields.base import LongCharField
from core.helpers.files import generate_storage_filename
from core.fields.color import ColorField
from internationalflavor.vat_number.models import VATNumberField
from django_countries.fields import CountryField
class Brand(Model):
name = LongCharField(null=True, blank=True)
color = ColorField()
logo = ImageField(null=True, blank=True, upload_to=generate_storage_filename)
address1 = LongCharField()
address2 = LongCharField(null=True, blank=True)
@ -16,4 +18,7 @@ class Brand(Model):
state = LongCharField(null=True, blank=True)
country = CountryField()
vat_id = VATNumberField(null=True, blank=True)
company_id = LongCharField(null=True, blank=True)
company_id = LongCharField(null=True, blank=True)
def __str__(self):
return self.name

View file

@ -12,19 +12,22 @@ from core.fields.base import LongCharField
class BaseFile(PolymorphicModel):
filename = LongCharField()
def __str__(self):
return self.filename
class ImageFile(BaseFile):
rawfile = ImageField(upload_to=generate_storage_filename)
@property
def get_file(self):
return self.image
return self.rawfile
class File(BaseFile):
rawfile = FileField(upload_to=generate_storage_filename)
@property
def get_file(self):
return self.file
return self.rawfile
class FileAssociation(Model):
file = ForeignKey(BaseFile, CASCADE)

24
core/models/invoices.py Normal file
View file

@ -0,0 +1,24 @@
from django.db.models import Model, ForeignKey, CASCADE, PositiveIntegerField, TextField, DecimalField, BooleanField, DateField, SET_NULL, PROTECT
from core.fields.base import LongCharField
from core.models.services import Service
from core.models.profiles import ClientProfile
from core.models.local import Currency
class Invoice(Model):
client = ForeignKey(ClientProfile, on_delete=CASCADE)
number = LongCharField()
created = DateField()
due = DateField()
payment_method = LongCharField()
currency = ForeignKey(Currency, on_delete=PROTECT)
class InvoiceItem(Model):
invoice = ForeignKey(Invoice, on_delete=CASCADE)
sort = PositiveIntegerField()
name = LongCharField()
description = TextField(blank=True, null=True)
price = DecimalField(max_digits=32, decimal_places=2)
discount = DecimalField(max_digits=32, decimal_places=2)
taxable = BooleanField()
service = ForeignKey(Service, on_delete=SET_NULL, null=True)

View file

@ -19,6 +19,9 @@ class Currency(Model):
def get_base(cls):
return cls.objects.get(base=True)
def __str__(self):
return f"{name} ({code})"
class TaxPolicy(Model):
name = LongCharField(blank=True)
default_rate = DecimalField(default=0, max_digits=10, decimal_places=5)
@ -28,9 +31,15 @@ class TaxPolicy(Model):
if reverse_charge:
return rule.tax_rate if not rule.reverse_charge else 0
return rule.tax_rate
def __str__(self):
return self.name
class TaxRule(Model):
policy = ForeignKey(TaxPolicy, on_delete=CASCADE)
destination_country = CountryField()
tax_rate = DecimalField(max_digits=10, decimal_places=5)
reverse_charge = BooleanField(default=False)
def __str__(self):
return str(self.destination_country)

View file

@ -1,5 +1,6 @@
from core.models.billable import CycleChoices
from core.fields.base import LongCharField
from core.fields.color import ColorField
from django.db.models import Model, IntegerChoices, PositiveIntegerField, DecimalField, ForeignKey, CASCADE, CharField, TextField, ManyToManyField
@ -11,6 +12,7 @@ logger = logging.getLogger(__name__)
class ProductGroup(Model):
name = LongCharField()
color = ColorField()
class Product(Model):
name = LongCharField()

View file

@ -10,9 +10,11 @@ from core.helpers.files import generate_storage_filename
from core.models.local import Currency
from core.models.brands import Brand
from core.fields.base import LongCharField
from core.fields.color import ColorField
class ClientGroup(Model):
name = LongCharField()
color = ColorField()
class Profile(PolymorphicModel):
user = OneToOneField(get_user_model(), CASCADE)

View file

@ -3,81 +3,8 @@ import importlib
from django.conf import settings
from django.urls import path
from core.views import (
DashboardView,
LoginView,
OTPSelectorView,
LogoutView,
OTPValidatorView,
PWResetView,
PWRequestView,
RateLimitedView,
BackendNotImplementedView,
AdminListView,
AdminDeleteView,
AdminEditView,
AdminCreateView,
DBSettingsListView,
DBSettingsEditView,
DBSettingsDeleteView,
DBSettingsCreateView,
BrandCreateView,
BrandDeleteView,
BrandEditView,
BrandListView,
RateLimitCreateView,
RateLimitDeleteView,
RateLimitEditView,
RateLimitListView,
)
URLPATTERNS = []
# Auth URLs
URLPATTERNS.append(path('login/', LoginView.as_view(), name="login"))
URLPATTERNS.append(path('login/otp/select/', OTPSelectorView.as_view(), name="otpselector"))
URLPATTERNS.append(path('login/otp/validate/', OTPValidatorView.as_view(), name="otpvalidator"))
URLPATTERNS.append(path('logout/', LogoutView.as_view(), name="logout"))
URLPATTERNS.append(path('login/reset/', PWRequestView.as_view(), name="pwrequest"))
URLPATTERNS.append(path('login/reset/<pk>/', PWResetView.as_view(), name="pwreset"))
URLPATTERNS.append(path('login/ratelimit/', RateLimitedView.as_view(), name="ratelimited"))
# Base Backend URLs
URLPATTERNS.append(path('admin/', DashboardView.as_view(), name="dashboard"))
URLPATTERNS.append(path('admin/oops/', BackendNotImplementedView.as_view(), name="backendni"))
# Backend Database Settings URLs
URLPATTERNS.append(path("admin/dbsettings/", DBSettingsListView.as_view(), name="dbsettings"))
URLPATTERNS.append(path("admin/dbsettings/<pk>/delete/", DBSettingsDeleteView.as_view(), name="dbsettings_delete"))
URLPATTERNS.append(path("admin/dbsettings/<pk>/edit/", DBSettingsEditView.as_view(), name="dbsettings_edit"))
URLPATTERNS.append(path("admin/dbsettings/create/", DBSettingsCreateView.as_view(), name="dbsettings_create"))
# Backend User Administration URLs
URLPATTERNS.append(path('admin/profiles/', AdminListView.as_view(), name="admins"))
URLPATTERNS.append(path("admin/profiles/<pk>/delete/", AdminDeleteView.as_view(), name="admins_delete"))
URLPATTERNS.append(path("admin/profiles/<pk>/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/<pk>/delete/", BrandDeleteView.as_view(), name="brands_delete"))
URLPATTERNS.append(path("admin/brands/<pk>/edit/", BrandEditView.as_view(), name="brands_edit"))
URLPATTERNS.append(path("admin/brands/create/", BrandCreateView.as_view(), name="brands_create"))
# Rate Limit Administration URLs
URLPATTERNS.append(path('admin/firewall/', RateLimitListView.as_view(), name="ratelimits"))
URLPATTERNS.append(path("admin/firewall/<pk>/delete/", RateLimitDeleteView.as_view(), name="ratelimits_delete"))
URLPATTERNS.append(path("admin/firewall/<pk>/edit/", RateLimitEditView.as_view(), name="ratelimits_edit"))
URLPATTERNS.append(path("admin/firewall/create/", RateLimitCreateView.as_view(), name="ratelimits_create"))
# External Module URLs
for module in settings.EXPEPHALON_MODULES:
try:
mou = importlib.import_module(f"{module}.urls")

View file

@ -17,9 +17,9 @@ navigations["backend_main"].add_section(dashboard_section)
clients_section = NavSection("Clients", "")
client_list_item = NavItem("List Clients", "fa-user-tag", "backendni")
client_add_item = NavItem("Add Client", "fa-user-edit", "backendni")
client_groups_item = NavItem("Client Groups", "fa-users", "backendni")
client_list_item = NavItem("List Clients", "fa-user-tag", "clients")
client_add_item = NavItem("Add Client", "fa-user-edit", "clients_create")
client_groups_item = NavItem("Client Groups", "fa-users", "clientgroups")
client_leads_item = NavItem("Leads", "fa-blender-phone", "backendni")
clients_section.add_item(client_list_item)
@ -45,8 +45,8 @@ navigations["backend_main"].add_section(quotes_section)
billing_section = NavSection("Billing", "")
invoice_list_item = NavItem("List Invoices", "fa-file-invoice-dollar", "backendni")
invoice_create_item = NavItem("Create Invoice", "fa-plus-square", "backendni")
invoice_list_item = NavItem("List Invoices", "fa-file-invoice-dollar", "invoices")
invoice_create_item = NavItem("Create Invoice", "fa-plus-square", "invoices_create")
billable_list_item = NavItem("List Billable Items", "fa-hand-holding-usd", "backendni")
billable_create_item = NavItem("Create Billable Item", "fa-plus-square", "backendni")
list_transaction_item = NavItem("Transaction List", "fa-funnel-dollar", "backendni")

View file

@ -1 +0,0 @@
from core.modules.urls import URLPATTERNS as urlpatterns

9
core/urls/__init__.py Normal file
View file

@ -0,0 +1,9 @@
from core.modules.urls import URLPATTERNS as modulepatterns
from core.urls.auth import urlpatterns as authpatterns
from core.urls.backend import urlpatterns as backendpatterns
from django.urls import path
corepatterns = authpatterns + backendpatterns
urlpatterns = corepatterns + modulepatterns

13
core/urls/auth.py Normal file
View file

@ -0,0 +1,13 @@
from django.urls import path
from core.views.auth import LoginView, OTPSelectorView, LogoutView, OTPValidatorView, PWResetView, PWRequestView, RateLimitedView
urlpatterns = []
urlpatterns.append(path('login/', LoginView.as_view(), name="login"))
urlpatterns.append(path('login/otp/select/', OTPSelectorView.as_view(), name="otpselector"))
urlpatterns.append(path('login/otp/validate/', OTPValidatorView.as_view(), name="otpvalidator"))
urlpatterns.append(path('logout/', LogoutView.as_view(), name="logout"))
urlpatterns.append(path('login/reset/', PWRequestView.as_view(), name="pwrequest"))
urlpatterns.append(path('login/reset/<pk>/', PWResetView.as_view(), name="pwreset"))
urlpatterns.append(path('login/ratelimit/', RateLimitedView.as_view(), name="ratelimited"))

View file

@ -0,0 +1,15 @@
from django.urls import path
from core.views.backend import DashboardView, BackendNotImplementedView
from core.urls.backend.admins import urlpatterns as adminpatterns
from core.urls.backend.clients import urlpatterns as clientpatterns
from core.urls.backend.dbsettings import urlpatterns as dbsettingspatterns
from core.urls.backend.brands import urlpatterns as brandpatterns
from core.urls.backend.firewall import urlpatterns as firewallpatterns
from core.urls.backend.invoices import urlpatterns as invoicepatterns
from core.urls.backend.clientgroups import urlpatterns as clientgrouppatterns
urlpatterns = adminpatterns + clientpatterns + dbsettingspatterns + brandpatterns + firewallpatterns + invoicepatterns + clientgrouppatterns
urlpatterns.append(path('admin/', DashboardView.as_view(), name="dashboard"))
urlpatterns.append(path('admin/oops/', BackendNotImplementedView.as_view(), name="backendni"))

View file

@ -0,0 +1,10 @@
from django.urls import path
from core.views.backend.profiles import AdminListView, AdminDeleteView, AdminEditView, AdminCreateView
urlpatterns = []
urlpatterns.append(path('admin/profiles/', AdminListView.as_view(), name="admins"))
urlpatterns.append(path("admin/profiles/<pk>/delete/", AdminDeleteView.as_view(), name="admins_delete"))
urlpatterns.append(path("admin/profiles/<pk>/edit/", AdminEditView.as_view(), name="admins_edit"))
urlpatterns.append(path("admin/profiles/create/", AdminCreateView.as_view(), name="admins_create"))

View file

@ -0,0 +1,10 @@
from django.urls import path
from core.views.backend.brands import BrandCreateView, BrandDeleteView, BrandEditView, BrandListView
urlpatterns = []
urlpatterns.append(path('admin/brands/', BrandListView.as_view(), name="brands"))
urlpatterns.append(path("admin/brands/<pk>/delete/", BrandDeleteView.as_view(), name="brands_delete"))
urlpatterns.append(path("admin/brands/<pk>/edit/", BrandEditView.as_view(), name="brands_edit"))
urlpatterns.append(path("admin/brands/create/", BrandCreateView.as_view(), name="brands_create"))

View file

@ -0,0 +1,10 @@
from django.urls import path
from core.views.backend.profiles import ClientGroupListView, ClientGroupDeleteView, ClientGroupEditView, ClientGroupCreateView
urlpatterns = []
urlpatterns.append(path('admin/clientgroups/', ClientGroupListView.as_view(), name="clientgroups"))
urlpatterns.append(path("admin/clientgroups/<pk>/delete/", ClientGroupDeleteView.as_view(), name="clientgroups_delete"))
urlpatterns.append(path("admin/clientgroups/<pk>/edit/", ClientGroupEditView.as_view(), name="clientgroups_edit"))
urlpatterns.append(path("admin/clientgroups/create/", ClientGroupCreateView.as_view(), name="clientgroups_create"))

View file

@ -0,0 +1,10 @@
from django.urls import path
from core.views.backend.profiles import ClientListView, ClientDeleteView, ClientEditView, ClientCreateView
urlpatterns = []
urlpatterns.append(path('admin/clients/', ClientListView.as_view(), name="clients"))
urlpatterns.append(path("admin/clients/<pk>/delete/", ClientDeleteView.as_view(), name="clients_delete"))
urlpatterns.append(path("admin/clients/<pk>/edit/", ClientEditView.as_view(), name="clients_edit"))
urlpatterns.append(path("admin/clients/create/", ClientCreateView.as_view(), name="clients_create"))

View file

@ -0,0 +1,10 @@
from django.urls import path
from core.views.backend.dbsettings import DBSettingsListView, DBSettingsEditView, DBSettingsDeleteView, DBSettingsCreateView
urlpatterns = []
urlpatterns.append(path("admin/dbsettings/", DBSettingsListView.as_view(), name="dbsettings"))
urlpatterns.append(path("admin/dbsettings/<pk>/delete/", DBSettingsDeleteView.as_view(), name="dbsettings_delete"))
urlpatterns.append(path("admin/dbsettings/<pk>/edit/", DBSettingsEditView.as_view(), name="dbsettings_edit"))
urlpatterns.append(path("admin/dbsettings/create/", DBSettingsCreateView.as_view(), name="dbsettings_create"))

View file

@ -0,0 +1,10 @@
from django.urls import path
from core.views.backend.firewall import RateLimitCreateView, RateLimitDeleteView, RateLimitEditView, RateLimitListView
urlpatterns = []
urlpatterns.append(path('admin/firewall/', RateLimitListView.as_view(), name="ratelimits"))
urlpatterns.append(path("admin/firewall/<pk>/delete/", RateLimitDeleteView.as_view(), name="ratelimits_delete"))
urlpatterns.append(path("admin/firewall/<pk>/edit/", RateLimitEditView.as_view(), name="ratelimits_edit"))
urlpatterns.append(path("admin/firewall/create/", RateLimitCreateView.as_view(), name="ratelimits_create"))

View file

@ -0,0 +1,10 @@
from django.urls import path
from core.views.backend.invoices import InvoiceCreateView, InvoiceDeleteView, InvoiceEditView, InvoiceListView
urlpatterns = []
urlpatterns.append(path('admin/invoices/', InvoiceListView.as_view(), name="invoices"))
urlpatterns.append(path("admin/invoices/<pk>/delete/", InvoiceDeleteView.as_view(), name="invoices_delete"))
urlpatterns.append(path("admin/invoices/<pk>/edit/", InvoiceEditView.as_view(), name="invoices_edit"))
urlpatterns.append(path("admin/invoices/create/", InvoiceCreateView.as_view(), name="invoices_create"))

View file

@ -1,16 +1,4 @@
from django.shortcuts import render
from django.views.generic import TemplateView
from django.conf import settings
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.views.firewall import *
from core.mixins.auth import AdminMixin
# Create your views here.
from core.views.backend import *
class IndexView(TemplateView):
template_name = f"{settings.EXPEPHALON_FRONTEND}/index.html"
@ -18,20 +6,4 @@ class IndexView(TemplateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Home"
return context
class DashboardView(BackendTemplateView):
template_name = f"{settings.EXPEPHALON_BACKEND}/index.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Dashboard"
return context
class BackendNotImplementedView(BackendTemplateView):
template_name = f"{settings.EXPEPHALON_BACKEND}/notimplemented.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Oops!"
return context
return context

View file

@ -24,7 +24,7 @@ class RateLimitedView(TemplateView):
def dispatch(self, request, *args, **kwargs):
try:
limit = IPLimit.objects.filter(ip=get_client_ip(request), end__gte=timezone.now()).first()
messages.error(request, f"Sorry, your IP has been blocked, so you cannot login at the moment.{f' Reason: {limit.reason}' if limit.reason else ''} Please try again after {str(iplimit.end)}, or contact support if you need help getting into your account.")
messages.error(request, f"Sorry, your IP has been blocked, so you cannot login at the moment.{f' Reason: {limit.reason}' if limit.reason else ''} Please try again after {str(limit.end)}, or contact support if you need help getting into your account.")
return super().dispatch(request, *args, **kwargs)
except:
return redirect("login")
@ -38,7 +38,8 @@ class AuthView(FormView):
period = timezone.now() - timezone.timedelta(seconds=int(getValue("core.auth.ratelimit.period", 600)))
failures = LoginLog.objects.filter(ip=get_client_ip(request), success=False, timestamp__gte=period)
if len(failures) >= int(getValue("core.auth.ratelimit.attempts", 5)):
IPLimit.objects.create(ip=get_client_ip(request), end=timezone.now() + timezone.timedelta(seconds=getValue("core.auth.ratelimit.block", 3600)), reason="Too many failed login attempts.")
IPLimit.objects.create(ip=get_client_ip(request), end=timezone.now() + timezone.timedelta(seconds=int(getValue("core.auth.ratelimit.block", 3600))), reason="Too many failed login attempts.")
failures.delete()
return redirect("ratelimited")
return super().dispatch(request, *args, **kwargs)

View file

@ -0,0 +1,27 @@
from django.shortcuts import render
from django.views.generic import TemplateView
from django.conf import settings
from core.views.backend.dbsettings import *
from core.views.auth import *
from core.views.backend.profiles import *
from core.views.generic import *
from core.views.backend.brands import *
from core.views.backend.firewall import *
from core.views.backend.invoices import *
class DashboardView(BackendTemplateView):
template_name = f"{settings.EXPEPHALON_BACKEND}/index.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Dashboard"
return context
class BackendNotImplementedView(BackendTemplateView):
template_name = f"{settings.EXPEPHALON_BACKEND}/notimplemented.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Oops!"
return context

View file

@ -38,7 +38,7 @@ class RateLimitCreateView(BackendCreateView):
template_name = f"{settings.EXPEPHALON_BACKEND}/firewall/create.html"
model = IPLimit
success_url = reverse_lazy("ratelimits")
fields = ["name", "logo", "address1", "address2", "zip", "city", "state", "country", "vat_id", "company_id"]
fields = ["ip", "end", "reason"]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)

View file

@ -0,0 +1,46 @@
from django.conf import settings
from django.urls import reverse_lazy
from core.models.invoices import Invoice, InvoiceItem
from core.views.generic import BackendListView, BackendUpdateView, BackendDeleteView, BackendCreateView
class InvoiceListView(BackendListView):
template_name = f"{settings.EXPEPHALON_BACKEND}/invoices/index.html"
model = Invoice
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Brand Settings"
return context
class InvoiceEditView(BackendUpdateView):
template_name = f"{settings.EXPEPHALON_BACKEND}/invoices/update.html"
model = Invoice
success_url = reverse_lazy("brands")
fields = ["name", "logo", "address1", "address2", "zip", "city", "state", "country", "vat_id", "company_id"]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Edit Brand"
return context
class InvoiceDeleteView(BackendDeleteView):
template_name = f"{settings.EXPEPHALON_BACKEND}/invoices/delete.html"
model = Invoice
success_url = reverse_lazy("brands")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Delete Brand"
return context
class InvoiceCreateView(BackendCreateView):
template_name = f"{settings.EXPEPHALON_BACKEND}/invoices/create.html"
model = Invoice
success_url = reverse_lazy("brands")
fields = ["name", "logo", "address1", "address2", "zip", "city", "state", "country", "vat_id", "company_id"]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Create Brand"
return context

View file

@ -0,0 +1,238 @@
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, ClientProfile, ClientGroup
from core.forms.profiles import AdminEditForm, AdminCreateForm, ClientForm
from core.views.generic import BackendFormView, BackendListView, BackendDeleteView, BackendUpdateView, BackendCreateView
from core.helpers.auth import request_password
### AdminProfiles
class AdminListView(BackendListView):
template_name = f"{settings.EXPEPHALON_BACKEND}/profiles/index.html"
model = AdminProfile
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Administrator Users"
return context
class AdminEditView(BackendFormView):
template_name = f"{settings.EXPEPHALON_BACKEND}/profiles/update.html"
form_class = AdminEditForm
success_url = reverse_lazy("admins")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Edit Administrator"
return context
def get_initial(self):
initial = super().get_initial()
admin = get_user_model().objects.get(id=self.kwargs["pk"])
assert type(admin.profile) == AdminProfile
initial["first_name"] = admin.first_name
initial["last_name"] = admin.last_name
initial["email"] = admin.username
initial["mobile"] = admin.profile.mobile
initial["role"] = admin.profile.role
initial["display_name"] = admin.profile.display_name
return initial
def form_valid(self, form):
admin = get_user_model().objects.get(id=self.kwargs["pk"])
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"]
admin.profile.mobile = form.cleaned_data["mobile"]
admin.profile.role = form.cleaned_data["role"]
admin.profile.display_name = form.cleaned_data["display_name"]
if form.cleaned_data["image"] or form.cleaned_data["remove_image"]:
admin.profile.image = form.cleaned_data["image"]
admin.profile.save()
admin.save()
return super().form_valid(form)
class AdminDeleteView(BackendDeleteView):
template_name = f"{settings.EXPEPHALON_BACKEND}/profiles/delete.html"
model = get_user_model()
success_url = reverse_lazy("admins")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Delete Administrator"
return context
def get_object(self, queryset=None):
admin = super().get_object(queryset=queryset)
assert type(admin.profile) == AdminProfile
return admin
class AdminCreateView(BackendFormView):
template_name = f"{settings.EXPEPHALON_BACKEND}/profiles/create.html"
form_class = AdminCreateForm
success_url = reverse_lazy("admins")
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(self.request, f"User {admin.get_full_name} was successfully created. They should receive an email to set their password shortly.")
return super().form_valid(form)
### ClientProfiles
class ClientListView(BackendListView):
template_name = f"{settings.EXPEPHALON_BACKEND}/clients/index.html"
model = ClientProfile
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Clients"
return context
class ClientEditView(BackendFormView):
template_name = f"{settings.EXPEPHALON_BACKEND}/clients/update.html"
form_class = ClientForm
success_url = reverse_lazy("clients")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Edit Client"
return context
def get_initial(self):
initial = super().get_initial()
client = get_user_model().objects.get(id=self.kwargs["pk"])
assert type(client.profile) == ClientProfile
initial["first_name"] = client.first_name
initial["last_name"] = client.last_name
initial["email"] = client.username
initial["mobile"] = client.profile.mobile
return initial
def form_valid(self, form):
client = get_user_model().objects.get(id=self.kwargs["pk"])
client.first_name = form.cleaned_data["first_name"]
client.last_name = form.cleaned_data["last_name"]
client.username = form.cleaned_data["email"]
client.email = form.cleaned_data["email"]
client.profile.mobile = form.cleaned_data["mobile"]
client.profile.save()
client.save()
return super().form_valid(form)
class ClientDeleteView(BackendDeleteView):
template_name = f"{settings.EXPEPHALON_BACKEND}/clients/delete.html"
model = get_user_model()
success_url = reverse_lazy("clients")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Delete Client"
return context
def get_object(self, queryset=None):
admin = super().get_object(queryset=queryset)
assert type(admin.profile) == ClientProfile
return admin
class ClientCreateView(BackendFormView):
template_name = f"{settings.EXPEPHALON_BACKEND}/clients/create.html"
form_class = ClientForm
success_url = reverse_lazy("admins")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Create Client"
return context
def form_valid(self, form):
client = get_user_model()()
client.first_name = form.cleaned_data["first_name"]
client.last_name = form.cleaned_data["last_name"]
client.username = form.cleaned_data["email"]
client.email = form.cleaned_data["email"]
profile = ClientProfile()
profile.user = client
profile.mobile = form.cleaned_data["mobile"]
client.save()
profile.save()
request_password(client)
messages.success(self.request, f"Client {client.get_full_name} was successfully created. They should receive an email to set their password shortly.")
return super().form_valid(form)
### ClientGroups
class ClientGroupListView(BackendListView):
template_name = f"{settings.EXPEPHALON_BACKEND}/clientgroups/index.html"
model = ClientGroup
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Client Group Settings"
return context
class ClientGroupEditView(BackendUpdateView):
template_name = f"{settings.EXPEPHALON_BACKEND}/clientgroups/update.html"
model = ClientGroup
success_url = reverse_lazy("clientgroups")
fields = ["name", "color"]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Edit Client Group"
return context
class ClientGroupDeleteView(BackendDeleteView):
template_name = f"{settings.EXPEPHALON_BACKEND}/clientgroups/delete.html"
model = ClientGroup
success_url = reverse_lazy("clientgroups")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Delete Client Group"
return context
class ClientGroupCreateView(BackendCreateView):
template_name = f"{settings.EXPEPHALON_BACKEND}/clientgroups/create.html"
model = ClientGroup
success_url = reverse_lazy("clientgroups")
fields = ["name", "color"]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Create Client Group"
return context

View file

@ -1,105 +0,0 @@
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.profiles import AdminEditForm, AdminCreateForm
from core.views.generic import BackendFormView, BackendListView, BackendDeleteView
from core.helpers.auth import request_password
class AdminListView(BackendListView):
template_name = f"{settings.EXPEPHALON_BACKEND}/profiles/index.html"
model = AdminProfile
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Administrator Users"
return context
class AdminEditView(BackendFormView):
template_name = f"{settings.EXPEPHALON_BACKEND}/profiles/update.html"
form_class = AdminEditForm
success_url = reverse_lazy("admins")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Edit Administrator"
return context
def get_initial(self):
initial = super().get_initial()
admin = get_user_model().objects.get(id=self.kwargs["pk"])
assert type(admin.profile) == AdminProfile
initial["first_name"] = admin.first_name
initial["last_name"] = admin.last_name
initial["email"] = admin.username
initial["mobile"] = admin.profile.mobile
initial["role"] = admin.profile.role
initial["display_name"] = admin.profile.display_name
return initial
def form_valid(self, form):
admin = get_user_model().objects.get(id=self.kwargs["pk"])
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"]
admin.profile.mobile = form.cleaned_data["mobile"]
admin.profile.role = form.cleaned_data["role"]
admin.profile.display_name = form.cleaned_data["display_name"]
if form.cleaned_data["image"] or form.cleaned_data["remove_image"]:
admin.profile.image = form.cleaned_data["image"]
admin.profile.save()
admin.save()
return super().form_valid(form)
class AdminDeleteView(BackendDeleteView):
template_name = f"{settings.EXPEPHALON_BACKEND}/profiles/delete.html"
model = get_user_model()
success_url = reverse_lazy("admins")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Delete Administrator"
return context
def get_object(self, queryset=None):
admin = super().get_object(queryset=queryset)
assert type(admin.profile) == AdminProfile
return admin
class AdminCreateView(BackendFormView):
template_name = f"{settings.EXPEPHALON_BACKEND}/profiles/create.html"
form_class = AdminCreateForm
success_url = reverse_lazy("admins")
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(self.request, f"User {admin.get_full_name} was successfully created. They should receive an email to set their password shortly.")
return super().form_valid(form)

0
core/widgets/__init__.py Normal file
View file

19
core/widgets/color.py Normal file
View file

@ -0,0 +1,19 @@
from django.forms import TextInput
from django.conf import settings
from django.templatetags.static import static
from django.utils.safestring import mark_safe
class ColorPickerWidget(TextInput):
class Media:
css = {
"all": (
static("backend/css/colorPicker.css")
)
}
js = (
static("backend/scripts/jquery.colorPicker.js")
)
def render(self, name, value, attrs=None, renderer=None):
rendered = super().render(name, value, attrs=attrs, renderer=renderer)
return rendered + mark_safe(f'<script type="text/javascript">$("#id_{name}").colorPicker();</script>')

View file

@ -0,0 +1,34 @@
div.colorPicker-picker {
height: 16px;
width: 16px;
padding: 0 !important;
border: 1px solid #ccc;
background: url(../arrow.gif) no-repeat top right;
cursor: pointer;
line-height: 16px;
font-size:0.75em;
font-weight:bold;
text-align: center;
}
div.colorPicker-palette {
width: 110px;
position: absolute;
border: 1px solid #598FEF;
background-color: #EFEFEF;
padding: 2px;
z-index: 9999;
}
div.colorPicker_hexWrap {width: 100%; float:left }
div.colorPicker_hexWrap label {font-size: 95%; color: #2F2F2F; margin: 5px 2px; width: 25%}
div.colorPicker_hexWrap input {margin: 5px 2px; padding: 0; font-size: 95%; border: 1px solid #000; width: 65%; }
div.colorPicker-swatch {
height: 12px;
width: 12px;
border: 1px solid #000;
margin: 2px;
float: left;
cursor: pointer;
line-height: 12px;
}

View file

Before

Width:  |  Height:  |  Size: 146 B

After

Width:  |  Height:  |  Size: 146 B

View file

@ -0,0 +1,352 @@
/**
* Really Simple Color Picker in jQuery
*
* Licensed under the MIT (MIT-LICENSE.txt) licenses.
*
* Copyright (c) 2008-2012
* Lakshan Perera (www.laktek.com) & Daniel Lacy (daniellacy.com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
(function ($) {
/**
* Create a couple private variables.
**/
var selectorOwner,
activePalette,
cItterate = 0,
templates = {
control : $('<div class="colorPicker-picker">&nbsp;</div>'),
palette : $('<div id="colorPicker_palette" class="colorPicker-palette" />'),
swatch : $('<div class="colorPicker-swatch">&nbsp;</div>'),
hexLabel: $('<label for="colorPicker_hex">Hex</label>'),
hexField: $('<input type="text" id="colorPicker_hex" />')
},
transparent = "transparent",
lastColor;
/**
* Create our colorPicker function
**/
$.fn.colorPicker = function (options) {
return this.each(function () {
// Setup time. Clone new elements from our templates, set some IDs, make shortcuts, jazzercise.
var element = $(this),
opts = $.extend({}, $.fn.colorPicker.defaults, options),
defaultColor = $.fn.colorPicker.toHex(
(element.val().length > 0) ? element.val() : opts.pickerDefault
),
newControl = templates.control.clone(),
newPalette = templates.palette.clone().attr('id', 'colorPicker_palette-' + cItterate),
newHexLabel = templates.hexLabel.clone(),
newHexField = templates.hexField.clone(),
paletteId = newPalette[0].id,
swatch, controlText;
/**
* Build a color palette.
**/
$.each(opts.colors, function (i) {
swatch = templates.swatch.clone();
if (opts.colors[i] === transparent) {
swatch.addClass(transparent).text('X');
$.fn.colorPicker.bindPalette(newHexField, swatch, transparent);
} else {
swatch.css("background-color", "#" + this);
$.fn.colorPicker.bindPalette(newHexField, swatch);
}
swatch.appendTo(newPalette);
});
newHexLabel.attr('for', 'colorPicker_hex-' + cItterate);
newHexField.attr({
'id' : 'colorPicker_hex-' + cItterate,
'value' : defaultColor
});
newHexField.bind("keydown", function (event) {
if (event.keyCode === 13) {
var hexColor = $.fn.colorPicker.toHex($(this).val());
$.fn.colorPicker.changeColor(hexColor ? hexColor : element.val());
}
if (event.keyCode === 27) {
$.fn.colorPicker.hidePalette();
}
});
newHexField.bind("keyup", function (event) {
var hexColor = $.fn.colorPicker.toHex($(event.target).val());
$.fn.colorPicker.previewColor(hexColor ? hexColor : element.val());
});
$('<div class="colorPicker_hexWrap" />').append(newHexLabel).appendTo(newPalette);
newPalette.find('.colorPicker_hexWrap').append(newHexField);
if (opts.showHexField === false) {
newHexField.hide();
newHexLabel.hide();
}
$("body").append(newPalette);
newPalette.hide();
/**
* Build replacement interface for original color input.
**/
newControl.css("background-color", defaultColor);
newControl.bind("click", function () {
if( element.is( ':not(:disabled)' ) ) {
$.fn.colorPicker.togglePalette($('#' + paletteId), $(this));
}
});
if( options && options.onColorChange ) {
newControl.data('onColorChange', options.onColorChange);
} else {
newControl.data('onColorChange', function() {} );
}
if (controlText = element.data('text'))
newControl.html(controlText)
element.after(newControl);
element.bind("change", function () {
element.next(".colorPicker-picker").css(
"background-color", $.fn.colorPicker.toHex($(this).val())
);
});
element.val(defaultColor);
// Hide the original input.
if (element[0].tagName.toLowerCase() === 'input') {
try {
element.attr('type', 'hidden')
} catch(err) { // oldIE doesn't allow changing of input.type
element.css('visibility', 'hidden').css('position', 'absolute')
}
} else {
element.hide();
}
cItterate++;
});
};
/**
* Extend colorPicker with... all our functionality.
**/
$.extend(true, $.fn.colorPicker, {
/**
* Return a Hex color, convert an RGB value and return Hex, or return false.
*
* Inspired by http://code.google.com/p/jquery-color-utils
**/
toHex : function (color) {
// If we have a standard or shorthand Hex color, return that value.
if (color.match(/[0-9A-F]{6}|[0-9A-F]{3}$/i)) {
return (color.charAt(0) === "#") ? color : ("#" + color);
// Alternatively, check for RGB color, then convert and return it as Hex.
} else if (color.match(/^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/)) {
var c = ([parseInt(RegExp.$1, 10), parseInt(RegExp.$2, 10), parseInt(RegExp.$3, 10)]),
pad = function (str) {
if (str.length < 2) {
for (var i = 0, len = 2 - str.length; i < len; i++) {
str = '0' + str;
}
}
return str;
};
if (c.length === 3) {
var r = pad(c[0].toString(16)),
g = pad(c[1].toString(16)),
b = pad(c[2].toString(16));
return '#' + r + g + b;
}
// Otherwise we wont do anything.
} else {
return false;
}
},
/**
* Check whether user clicked on the selector or owner.
**/
checkMouse : function (event, paletteId) {
var selector = activePalette,
selectorParent = $(event.target).parents("#" + selector.attr('id')).length;
if (event.target === $(selector)[0] || event.target === selectorOwner[0] || selectorParent > 0) {
return;
}
$.fn.colorPicker.hidePalette();
},
/**
* Hide the color palette modal.
**/
hidePalette : function () {
$(document).unbind("mousedown", $.fn.colorPicker.checkMouse);
$('.colorPicker-palette').hide();
},
/**
* Show the color palette modal.
**/
showPalette : function (palette) {
var hexColor = selectorOwner.prev("input").val();
palette.css({
top: selectorOwner.offset().top + (selectorOwner.outerHeight()),
left: selectorOwner.offset().left
});
$("#color_value").val(hexColor);
palette.show();
$(document).bind("mousedown", $.fn.colorPicker.checkMouse);
},
/**
* Toggle visibility of the colorPicker palette.
**/
togglePalette : function (palette, origin) {
// selectorOwner is the clicked .colorPicker-picker.
if (origin) {
selectorOwner = origin;
}
activePalette = palette;
if (activePalette.is(':visible')) {
$.fn.colorPicker.hidePalette();
} else {
$.fn.colorPicker.showPalette(palette);
}
},
/**
* Update the input with a newly selected color.
**/
changeColor : function (value) {
selectorOwner.css("background-color", value);
selectorOwner.prev("input").val(value).change();
$.fn.colorPicker.hidePalette();
selectorOwner.data('onColorChange').call(selectorOwner, $(selectorOwner).prev("input").attr("id"), value);
},
/**
* Preview the input with a newly selected color.
**/
previewColor : function (value) {
selectorOwner.css("background-color", value);
},
/**
* Bind events to the color palette swatches.
*/
bindPalette : function (paletteInput, element, color) {
color = color ? color : $.fn.colorPicker.toHex(element.css("background-color"));
element.bind({
click : function (ev) {
lastColor = color;
$.fn.colorPicker.changeColor(color);
},
mouseover : function (ev) {
lastColor = paletteInput.val();
$(this).css("border-color", "#598FEF");
paletteInput.val(color);
$.fn.colorPicker.previewColor(color);
},
mouseout : function (ev) {
$(this).css("border-color", "#000");
paletteInput.val(selectorOwner.css("background-color"));
paletteInput.val(lastColor);
$.fn.colorPicker.previewColor(lastColor);
}
});
}
});
/**
* Default colorPicker options.
*
* These are publibly available for global modification using a setting such as:
*
* $.fn.colorPicker.defaults.colors = ['151337', '111111']
*
* They can also be applied on a per-bound element basis like so:
*
* $('#element1').colorPicker({pickerDefault: 'efefef', transparency: true});
* $('#element2').colorPicker({pickerDefault: '333333', colors: ['333333', '111111']});
*
**/
$.fn.colorPicker.defaults = {
// colorPicker default selected color.
pickerDefault : "FFFFFF",
// Default color set.
colors : [
'000000', '993300', '333300', '000080', '333399', '333333', '800000', 'FF6600',
'808000', '008000', '008080', '0000FF', '666699', '808080', 'FF0000', 'FF9900',
'99CC00', '339966', '33CCCC', '3366FF', '800080', '999999', 'FF00FF', 'FFCC00',
'FFFF00', '00FF00', '00FFFF', '00CCFF', '993366', 'C0C0C0', 'FF99CC', 'FFCC99',
'FFFF99', 'CCFFFF', '99CCFF', 'FFFFFF'
],
// If we want to simply add more colors to the default set, use addColors.
addColors : [],
// Show hex field
showHexField: true
};
})(jQuery);

View file

@ -207,7 +207,7 @@
<div class="widget-content p-0">
<div class="widget-content-wrapper">
<div class="widget-content-left mr-3">
<img width="42" class="rounded-circle" src="{% static "backend/images/pixel.png" %}" alt="">
<img width="42" class="rounded-circle" src="{% static "backend/images/blank.png" %}" alt="">
</div>
<div class="widget-content-left">
<div class="widget-heading">Ella-Rose Henry</div>
@ -229,7 +229,7 @@
<div class="widget-content p-0">
<div class="widget-content-wrapper">
<div class="widget-content-left mr-3">
<img width="42" class="rounded-circle" src="{% static "backend/images/pixel.png" %}" alt="">
<img width="42" class="rounded-circle" src="{% static "backend/images/blank.png" %}" alt="">
</div>
<div class="widget-content-left">
<div class="widget-heading">Ruben Tillman</div>
@ -251,7 +251,7 @@
<div class="widget-content p-0">
<div class="widget-content-wrapper">
<div class="widget-content-left mr-3">
<img width="42" class="rounded-circle" src="{% static "backend/images/pixel.png" %}" alt="">
<img width="42" class="rounded-circle" src="{% static "backend/images/blank.png" %}" alt="">
</div>
<div class="widget-content-left">
<div class="widget-heading">Vinnie Wagstaff</div>
@ -273,7 +273,7 @@
<div class="widget-content p-0">
<div class="widget-content-wrapper">
<div class="widget-content-left mr-3">
<img width="42" class="rounded-circle" src="{% static "backend/images/pixel.png" %}" alt="">
<img width="42" class="rounded-circle" src="{% static "backend/images/blank.png" %}" alt="">
</div>
<div class="widget-content-left">
<div class="widget-heading">Ella-Rose Henry</div>
@ -295,7 +295,7 @@
<div class="widget-content p-0">
<div class="widget-content-wrapper">
<div class="widget-content-left mr-3">
<img width="42" class="rounded-circle" src="{% static "backend/images/pixel.png" %}" alt="">
<img width="42" class="rounded-circle" src="{% static "backend/images/blank.png" %}" alt="">
</div>
<div class="widget-content-left">
<div class="widget-heading">Ruben Tillman</div>

View file

@ -40,7 +40,7 @@
<button type="submit" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-check"></i> Save
</button>
<a href="{% url "admins" %}" class="btn-shadow mr-3 btn btn-danger">
<a href="{% url "brands" %}" class="btn-shadow mr-3 btn btn-danger">
<i class="fa fa-times"></i> Cancel
</a>
{% endbuttons %}

View file

@ -40,7 +40,7 @@
<button type="submit" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-check"></i> Save
</button>
<a href="{% url "admins" %}" class="btn-shadow mr-3 btn btn-danger">
<a href="{% url "brands" %}" class="btn-shadow mr-3 btn btn-danger">
<i class="fa fa-times"></i> Cancel
</a>
{% endbuttons %}

View file

@ -40,7 +40,7 @@
<button type="submit" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-check"></i> Save
</button>
<a href="{% url "admins" %}" class="btn-shadow mr-3 btn btn-danger">
<a href="{% url "brands" %}" class="btn-shadow mr-3 btn btn-danger">
<i class="fa fa-times"></i> Cancel
</a>
{% endbuttons %}
@ -51,7 +51,4 @@
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,57 @@
{% extends "backend/base.html" %}
{% load bootstrap4 %}
{% block content %}
<div class="app-page-title">
<div class="page-title-wrapper">
<div class="page-title-heading">
<div class="page-title-icon">
<i class="fa fa-users-cog">
</i>
</div>
<div>Client Groups - Create Client Group
<div class="page-title-subheading">Create a new client group
</div>
</div>
</div>
<div class="page-title-actions">
<button type="button" data-toggle="tooltip" title="New Client Group" data-placement="bottom" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-plus"></i> New Client Group
</button>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 col-lg-0">
<div class="mb-3 card">
<div class="card-header-tab card-header-tab-animation card-header">
<div class="card-header-title">
<i class="header-icon lnr-apartment icon-gradient bg-love-kiss"> </i>
Create Client Group
</div>
</div>
<div class="card-body">
<div class="tab-content">
<div class="tab-pane fade show active" id="tabs-eg-77">
<form method="POST" enctype="multipart/form-data" >
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button type="submit" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-check"></i> Save
</button>
<a href="{% url "clientgroups" %}" class="btn-shadow mr-3 btn btn-danger">
<i class="fa fa-times"></i> Cancel
</a>
{% endbuttons %}
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,57 @@
{% extends "backend/base.html" %}
{% load bootstrap4 %}
{% block content %}
<div class="app-page-title">
<div class="page-title-wrapper">
<div class="page-title-heading">
<div class="page-title-icon">
<i class="fa fa-database">
</i>
</div>
<div>Client Groups - Delete Client Group
<div class="page-title-subheading">Delete a client group from the system
</div>
</div>
</div>
<div class="page-title-actions">
<button type="button" data-toggle="tooltip" title="New Brand" data-placement="bottom" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-plus"></i> New Client Group
</button>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 col-lg-0">
<div class="mb-3 card">
<div class="card-header-tab card-header-tab-animation card-header">
<div class="card-header-title">
<i class="header-icon lnr-apartment icon-gradient bg-love-kiss"> </i>
Deleting {{ object.name }}
</div>
</div>
<div class="card-body">
<div class="tab-content">
<div class="tab-pane fade show active" id="tabs-eg-77">
<form method="POST">
{% csrf_token %}
Are you sure you wish to delete {{ object.name }}?
{% buttons %}
<button type="submit" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-check"></i> Save
</button>
<a href="{% url "clientgroups" %}" class="btn-shadow mr-3 btn btn-danger">
<i class="fa fa-times"></i> Cancel
</a>
{% endbuttons %}
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,59 @@
{% extends "backend/base.html" %}
{% block content %}
<div class="app-page-title">
<div class="page-title-wrapper">
<div class="page-title-heading">
<div class="page-title-icon">
<i class="fa fa-users-cog">
</i>
</div>
<div>Client Groups
<div class="page-title-subheading">Create, edit and delete client groups
</div>
</div>
</div>
<div class="page-title-actions">
<a href="{% url "clientgroups_create" %}" type="button" data-toggle="tooltip" title="New Client Group" data-placement="bottom" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-plus"></i> New Client Group
</a>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 col-lg-0">
<div class="mb-3 card">
<div class="card-header-tab card-header-tab-animation card-header">
<div class="card-header-title">
<i class="header-icon lnr-apartment icon-gradient bg-love-kiss"> </i>
Active Client Groups
</div>
</div>
<div class="card-body">
<div class="tab-content">
<div class="tab-pane fade show active" id="tabs-eg-77">
<div class="card mb-3 widget-chart widget-chart2 text-left w-100">
<table class="mb-0 table table-hover">
<thead>
<tr>
<th>Name</th>
<th>Options</th>
</tr>
</thead>
<tbody>
{% for group in object_list %}
<tr>
<td>{{ group.name }}</td>
<td><a href="{% url "clientgroups_edit" group.id %}"><i class="fas fa-edit" title="Edit Client Group"></i></a> <a href="{% url "clientgroups_delete" group.id %}"><i style="color: darkred;" class="fas fa-trash-alt" title="Delete Client Group"></i></a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,57 @@
{% extends "backend/base.html" %}
{% load bootstrap4 %}
{% block content %}
<div class="app-page-title">
<div class="page-title-wrapper">
<div class="page-title-heading">
<div class="page-title-icon">
<i class="fa fa-users-cog">
</i>
</div>
<div>Client Groups - Edit Client Group
<div class="page-title-subheading">Edit client group name
</div>
</div>
</div>
<div class="page-title-actions">
<button type="button" data-toggle="tooltip" title="New Client Group" data-placement="bottom" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-plus"></i> New Client Group
</button>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 col-lg-0">
<div class="mb-3 card">
<div class="card-header-tab card-header-tab-animation card-header">
<div class="card-header-title">
<i class="header-icon lnr-apartment icon-gradient bg-love-kiss"> </i>
Editing {{ object.name }}
</div>
</div>
<div class="card-body">
<div class="tab-content">
<div class="tab-pane fade show active" id="tabs-eg-77">
<form method="POST" enctype="multipart/form-data" >
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button type="submit" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-check"></i> Save
</button>
<a href="{% url "clientgroups" %}" class="btn-shadow mr-3 btn btn-danger">
<i class="fa fa-times"></i> Cancel
</a>
{% endbuttons %}
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,57 @@
{% extends "backend/base.html" %}
{% load bootstrap4 %}
{% block content %}
<div class="app-page-title">
<div class="page-title-wrapper">
<div class="page-title-heading">
<div class="page-title-icon">
<i class="fa fa-users-cog">
</i>
</div>
<div>Clients - Create Client
<div class="page-title-subheading">Create a new client
</div>
</div>
</div>
<div class="page-title-actions">
<button type="button" data-toggle="tooltip" title="New Client" data-placement="bottom" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-plus"></i> New Client
</button>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 col-lg-0">
<div class="mb-3 card">
<div class="card-header-tab card-header-tab-animation card-header">
<div class="card-header-title">
<i class="header-icon lnr-apartment icon-gradient bg-love-kiss"> </i>
Create Client
</div>
</div>
<div class="card-body">
<div class="tab-content">
<div class="tab-pane fade show active" id="tabs-eg-77">
<form method="POST" enctype="multipart/form-data" >
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button type="submit" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-check"></i> Save
</button>
<a href="{% url "clients" %}" class="btn-shadow mr-3 btn btn-danger">
<i class="fa fa-times"></i> Cancel
</a>
{% endbuttons %}
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,57 @@
{% extends "backend/base.html" %}
{% load bootstrap4 %}
{% block content %}
<div class="app-page-title">
<div class="page-title-wrapper">
<div class="page-title-heading">
<div class="page-title-icon">
<i class="fa fa-database">
</i>
</div>
<div>Clients - Delete Client
<div class="page-title-subheading">Delete a client from the system
</div>
</div>
</div>
<div class="page-title-actions">
<button type="button" data-toggle="tooltip" title="New Brand" data-placement="bottom" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-plus"></i> New Client
</button>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 col-lg-0">
<div class="mb-3 card">
<div class="card-header-tab card-header-tab-animation card-header">
<div class="card-header-title">
<i class="header-icon lnr-apartment icon-gradient bg-love-kiss"> </i>
Deleting {{ object.get_full_name }}
</div>
</div>
<div class="card-body">
<div class="tab-content">
<div class="tab-pane fade show active" id="tabs-eg-77">
<form method="POST">
{% csrf_token %}
Are you sure you wish to delete {{ object.get_full_name }}? This will irrevocably delete the client and any associated information. You can also disable the client without deleting their data!
{% buttons %}
<button type="submit" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-check"></i> Save
</button>
<a href="{% url "clients" %}" class="btn-shadow mr-3 btn btn-danger">
<i class="fa fa-times"></i> Cancel
</a>
{% endbuttons %}
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,61 @@
{% extends "backend/base.html" %}
{% block content %}
<div class="app-page-title">
<div class="page-title-wrapper">
<div class="page-title-heading">
<div class="page-title-icon">
<i class="fa fa-users-cog">
</i>
</div>
<div>Clients
<div class="page-title-subheading">Create, edit and delete clients
</div>
</div>
</div>
<div class="page-title-actions">
<a href="{% url "clients_create" %}" type="button" data-toggle="tooltip" title="New Client" data-placement="bottom" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-plus"></i> New Client
</a>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 col-lg-0">
<div class="mb-3 card">
<div class="card-header-tab card-header-tab-animation card-header">
<div class="card-header-title">
<i class="header-icon lnr-apartment icon-gradient bg-love-kiss"> </i>
Active Clients
</div>
</div>
<div class="card-body">
<div class="tab-content">
<div class="tab-pane fade show active" id="tabs-eg-77">
<div class="card mb-3 widget-chart widget-chart2 text-left w-100">
<table class="mb-0 table table-hover">
<thead>
<tr>
<th>Name</th>
<th>Country</th>
<th>Options</th>
</tr>
</thead>
<tbody>
{% for client in object_list %}
<tr>
<td>{% if client.profile.company %}{{ client.profile.company }}{% else %}{{ client.get_full_name }}{% endif %}</td>
<td>{{ client.profile.country }}</td>
<td><a href="{% url "clients_edit" client.id %}"><i class="fas fa-edit" title="Edit Client"></i></a> <a href="{% url "clients_delete" client.id %}"><i style="color: darkred;" class="fas fa-trash-alt" title="Delete Client"></i></a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,57 @@
{% extends "backend/base.html" %}
{% load bootstrap4 %}
{% block content %}
<div class="app-page-title">
<div class="page-title-wrapper">
<div class="page-title-heading">
<div class="page-title-icon">
<i class="fa fa-users-cog">
</i>
</div>
<div>Clients - Edit Client
<div class="page-title-subheading">Edit client data
</div>
</div>
</div>
<div class="page-title-actions">
<button type="button" data-toggle="tooltip" title="New Client" data-placement="bottom" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-plus"></i> New Client
</button>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 col-lg-0">
<div class="mb-3 card">
<div class="card-header-tab card-header-tab-animation card-header">
<div class="card-header-title">
<i class="header-icon lnr-apartment icon-gradient bg-love-kiss"> </i>
Editing {{ object.get_full_name }}
</div>
</div>
<div class="card-body">
<div class="tab-content">
<div class="tab-pane fade show active" id="tabs-eg-77">
<form method="POST" enctype="multipart/form-data" >
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button type="submit" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-check"></i> Save
</button>
<a href="{% url "clients" %}" class="btn-shadow mr-3 btn btn-danger">
<i class="fa fa-times"></i> Cancel
</a>
{% endbuttons %}
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -14,7 +14,7 @@
</div>
</div>
<div class="page-title-actions">
<button type="button" data-toggle="tooltip" title="New Brand" data-placement="bottom" class="btn-shadow mr-3 btn btn-success">
<button type="button" data-toggle="tooltip" title="New Rule" data-placement="bottom" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-plus"></i> New Rule
</button>

View file

@ -0,0 +1,57 @@
{% extends "backend/base.html" %}
{% load bootstrap4 %}
{% block content %}
<div class="app-page-title">
<div class="page-title-wrapper">
<div class="page-title-heading">
<div class="page-title-icon">
<i class="fa fa-users-cog">
</i>
</div>
<div>Invoices - Create Invoice
<div class="page-title-subheading">Create a new invoice
</div>
</div>
</div>
<div class="page-title-actions">
<button type="button" data-toggle="tooltip" title="New Invoice" data-placement="bottom" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-plus"></i> New Invoice
</button>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 col-lg-0">
<div class="mb-3 card">
<div class="card-header-tab card-header-tab-animation card-header">
<div class="card-header-title">
<i class="header-icon lnr-apartment icon-gradient bg-love-kiss"> </i>
Create Invoice
</div>
</div>
<div class="card-body">
<div class="tab-content">
<div class="tab-pane fade show active" id="tabs-eg-77">
<form method="POST" enctype="multipart/form-data" >
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button type="submit" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-check"></i> Save
</button>
<a href="{% url "admins" %}" class="btn-shadow mr-3 btn btn-danger">
<i class="fa fa-times"></i> Cancel
</a>
{% endbuttons %}
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,57 @@
{% extends "backend/base.html" %}
{% load bootstrap4 %}
{% block content %}
<div class="app-page-title">
<div class="page-title-wrapper">
<div class="page-title-heading">
<div class="page-title-icon">
<i class="fa fa-database">
</i>
</div>
<div>Invoices - Delete Invoice
<div class="page-title-subheading">Delete an invoice from the system
</div>
</div>
</div>
<div class="page-title-actions">
<button type="button" data-toggle="tooltip" title="New Rule" data-placement="bottom" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-plus"></i> New Invoice
</button>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 col-lg-0">
<div class="mb-3 card">
<div class="card-header-tab card-header-tab-animation card-header">
<div class="card-header-title">
<i class="header-icon lnr-apartment icon-gradient bg-love-kiss"> </i>
Deleting invoice {{ object.number or object.id }}
</div>
</div>
<div class="card-body">
<div class="tab-content">
<div class="tab-pane fade show active" id="tabs-eg-77">
<form method="POST">
{% csrf_token %}
Are you sure you wish to delete invoice {{ object.number or object.id }}?
{% buttons %}
<button type="submit" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-check"></i> Save
</button>
<a href="{% url "admins" %}" class="btn-shadow mr-3 btn btn-danger">
<i class="fa fa-times"></i> Cancel
</a>
{% endbuttons %}
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,69 @@
{% extends "backend/base.html" %}
{% block content %}
<div class="app-page-title">
<div class="page-title-wrapper">
<div class="page-title-heading">
<div class="page-title-icon">
<i class="fa fa-shield-alt">
</i>
</div>
<div>Invoices
<div class="page-title-subheading">Create, edit and delete invoices
</div>
</div>
</div>
<div class="page-title-actions">
<a href="{% url "ratelimits_create" %}" type="button" data-toggle="tooltip" title="New Invoice" data-placement="bottom" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-plus"></i> New Invoice
</a>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 col-lg-0">
<div class="mb-3 card">
<div class="card-header-tab card-header-tab-animation card-header">
<div class="card-header-title">
<i class="header-icon lnr-apartment icon-gradient bg-love-kiss"> </i>
Active Invoices
</div>
</div>
<div class="card-body">
<div class="tab-content">
<div class="tab-pane fade show active" id="tabs-eg-77">
<div class="card mb-3 widget-chart widget-chart2 text-left w-100">
<table class="mb-0 table table-hover">
<thead>
<tr>
<th>ID</th>
<th>Invoice Number</th>
<th>Recipient</th>
<th>Total (net)</th>
<th>Total (gross)</th>
<th>Status</th>
<th>Options</th>
</tr>
</thead>
<tbody>
{% for invoice in object_list %}
<tr>
<td>{{ invoice.id }}</td>
<td>{{ invoice.number }}</td>
<td>{{ invoice.client.user.get_full_name }}</td>
<td>{{ invoice.total_net }}</td>
<td>{{ invoice.total_gross }}</td>
<td>{{ invoice.status }}</td>
<td><a href="{% url "invoices_edit" invoice.id %}"><i class="fas fa-edit" title="Edit Invoice"></i></a> <a href="{% url "invoices_delete" invoice.id %}"><i style="color: darkred;" class="fas fa-trash-alt" title="Delete Invoice"></i></a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,57 @@
{% extends "backend/base.html" %}
{% load bootstrap4 %}
{% block content %}
<div class="app-page-title">
<div class="page-title-wrapper">
<div class="page-title-heading">
<div class="page-title-icon">
<i class="fa fa-users-cog">
</i>
</div>
<div>Invoice - Edit Invoice
<div class="page-title-subheading">Edit invoice
</div>
</div>
</div>
<div class="page-title-actions">
<button type="button" data-toggle="tooltip" title="New Invoice" data-placement="bottom" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-plus"></i> New Invoice
</button>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 col-lg-0">
<div class="mb-3 card">
<div class="card-header-tab card-header-tab-animation card-header">
<div class="card-header-title">
<i class="header-icon lnr-apartment icon-gradient bg-love-kiss"> </i>
Editing {{ object.number or object.id }}
</div>
</div>
<div class="card-body">
<div class="tab-content">
<div class="tab-pane fade show active" id="tabs-eg-77">
<form method="POST" enctype="multipart/form-data" >
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button type="submit" class="btn-shadow mr-3 btn btn-success">
<i class="fa fa-check"></i> Save
</button>
<a href="{% url "admins" %}" class="btn-shadow mr-3 btn btn-danger">
<i class="fa fa-times"></i> Cancel
</a>
{% endbuttons %}
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}