diff --git a/backend/igny8_core/admin/site.py b/backend/igny8_core/admin/site.py
index 4feb7d76..7bd162c5 100644
--- a/backend/igny8_core/admin/site.py
+++ b/backend/igny8_core/admin/site.py
@@ -34,6 +34,7 @@ class Igny8AdminSite(admin.AdminSite):
'💰 Billing & Accounts': {
'models': [
('igny8_core_auth', 'Plan'),
+ ('billing', 'PlanLimitUsage'),
('igny8_core_auth', 'Account'),
('igny8_core_auth', 'Subscription'),
('billing', 'Invoice'),
diff --git a/backend/igny8_core/modules/billing/admin.py b/backend/igny8_core/modules/billing/admin.py
index 14652885..9c011c74 100644
--- a/backend/igny8_core/modules/billing/admin.py
+++ b/backend/igny8_core/modules/billing/admin.py
@@ -3,6 +3,7 @@ Billing Module Admin
"""
from django.contrib import admin
from django.utils.html import format_html
+from django.contrib import messages
from igny8_core.admin.base import AccountAdminMixin
from igny8_core.business.billing.models import (
CreditCostConfig,
@@ -12,12 +13,28 @@ from igny8_core.business.billing.models import (
PaymentMethodConfig,
)
from .models import CreditTransaction, CreditUsageLog, AccountPaymentMethod
+from import_export.admin import ExportMixin
+from import_export import resources
+from rangefilter.filters import DateRangeFilter
+
+
+from rangefilter.filters import DateRangeFilter
+
+
+class CreditTransactionResource(resources.ModelResource):
+ """Resource class for exporting Credit Transactions"""
+ class Meta:
+ model = CreditTransaction
+ fields = ('id', 'account__name', 'transaction_type', 'amount', 'balance_after',
+ 'description', 'reference_id', 'created_at')
+ export_order = fields
@admin.register(CreditTransaction)
-class CreditTransactionAdmin(AccountAdminMixin, admin.ModelAdmin):
+class CreditTransactionAdmin(ExportMixin, AccountAdminMixin, admin.ModelAdmin):
+ resource_class = CreditTransactionResource
list_display = ['id', 'account', 'transaction_type', 'amount', 'balance_after', 'description', 'created_at']
- list_filter = ['transaction_type', 'created_at', 'account']
+ list_filter = ['transaction_type', ('created_at', DateRangeFilter), 'account']
search_fields = ['description', 'account__name']
readonly_fields = ['created_at']
date_hierarchy = 'created_at'
@@ -66,8 +83,18 @@ class InvoiceAdmin(AccountAdminMixin, admin.ModelAdmin):
readonly_fields = ['created_at', 'updated_at']
+class PaymentResource(resources.ModelResource):
+ """Resource class for exporting Payments"""
+ class Meta:
+ model = Payment
+ fields = ('id', 'invoice__invoice_number', 'account__name', 'payment_method',
+ 'status', 'amount', 'currency', 'manual_reference', 'approved_by__email',
+ 'processed_at', 'created_at')
+ export_order = fields
+
+
@admin.register(Payment)
-class PaymentAdmin(AccountAdminMixin, admin.ModelAdmin):
+class PaymentAdmin(ExportMixin, AccountAdminMixin, admin.ModelAdmin):
"""
Main Payment Admin with approval workflow.
When you change status to 'succeeded', it automatically:
@@ -76,6 +103,7 @@ class PaymentAdmin(AccountAdminMixin, admin.ModelAdmin):
- Activates account
- Adds credits
"""
+ resource_class = PaymentResource
list_display = [
'id',
'invoice',
@@ -88,7 +116,7 @@ class PaymentAdmin(AccountAdminMixin, admin.ModelAdmin):
'approved_by',
'processed_at',
]
- list_filter = ['status', 'payment_method', 'currency', 'created_at', 'processed_at']
+ list_filter = ['status', 'payment_method', 'currency', ('created_at', DateRangeFilter), ('processed_at', DateRangeFilter)]
search_fields = [
'invoice__invoice_number',
'account__name',
diff --git a/backend/igny8_core/modules/writer/admin.py b/backend/igny8_core/modules/writer/admin.py
index ce17d241..d1228b86 100644
--- a/backend/igny8_core/modules/writer/admin.py
+++ b/backend/igny8_core/modules/writer/admin.py
@@ -1,7 +1,10 @@
from django.contrib import admin
+from django.contrib import messages
from igny8_core.admin.base import SiteSectorAdminMixin
from .models import Tasks, Images, Content
from igny8_core.business.content.models import ContentTaxonomy, ContentAttribute, ContentTaxonomyRelation, ContentClusterMap
+from import_export.admin import ExportMixin
+from import_export import resources
class ContentTaxonomyInline(admin.TabularInline):
@@ -13,13 +16,24 @@ class ContentTaxonomyInline(admin.TabularInline):
verbose_name_plural = 'Taxonomy Terms (Tags & Categories)'
+class TaskResource(resources.ModelResource):
+ """Resource class for exporting Tasks"""
+ class Meta:
+ model = Tasks
+ fields = ('id', 'title', 'description', 'status', 'content_type', 'content_structure',
+ 'site__name', 'sector__name', 'cluster__name', 'created_at', 'updated_at')
+ export_order = fields
+
+
@admin.register(Tasks)
-class TasksAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
+class TasksAdmin(ExportMixin, SiteSectorAdminMixin, admin.ModelAdmin):
+ resource_class = TaskResource
list_display = ['title', 'content_type', 'content_structure', 'site', 'sector', 'status', 'cluster', 'created_at']
list_filter = ['status', 'content_type', 'content_structure', 'site', 'sector', 'cluster']
search_fields = ['title', 'description']
ordering = ['-created_at']
readonly_fields = ['created_at', 'updated_at']
+ actions = ['bulk_set_status_draft', 'bulk_set_status_in_progress', 'bulk_set_status_completed']
fieldsets = (
('Basic Info', {
@@ -37,6 +51,24 @@ class TasksAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
}),
)
+ def bulk_set_status_draft(self, request, queryset):
+ """Set selected tasks to draft status"""
+ updated = queryset.update(status='draft')
+ self.message_user(request, f'{updated} task(s) set to draft.', messages.SUCCESS)
+ bulk_set_status_draft.short_description = 'Set status to Draft'
+
+ def bulk_set_status_in_progress(self, request, queryset):
+ """Set selected tasks to in-progress status"""
+ updated = queryset.update(status='in_progress')
+ self.message_user(request, f'{updated} task(s) set to in progress.', messages.SUCCESS)
+ bulk_set_status_in_progress.short_description = 'Set status to In Progress'
+
+ def bulk_set_status_completed(self, request, queryset):
+ """Set selected tasks to completed status"""
+ updated = queryset.update(status='completed')
+ self.message_user(request, f'{updated} task(s) set to completed.', messages.SUCCESS)
+ bulk_set_status_completed.short_description = 'Set status to Completed'
+
def get_site_display(self, obj):
"""Safely get site name"""
try:
@@ -93,14 +125,26 @@ class ImagesAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
return '-'
+class ContentResource(resources.ModelResource):
+ """Resource class for exporting Content"""
+ class Meta:
+ model = Content
+ fields = ('id', 'title', 'content_type', 'content_structure', 'status', 'source',
+ 'site__name', 'sector__name', 'cluster__name', 'word_count',
+ 'meta_title', 'meta_description', 'primary_keyword', 'external_url', 'created_at')
+ export_order = fields
+
+
@admin.register(Content)
-class ContentAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
+class ContentAdmin(ExportMixin, SiteSectorAdminMixin, admin.ModelAdmin):
+ resource_class = ContentResource
list_display = ['title', 'content_type', 'content_structure', 'site', 'sector', 'source', 'status', 'get_taxonomy_count', 'created_at']
list_filter = ['content_type', 'content_structure', 'source', 'status', 'site', 'sector', 'created_at']
search_fields = ['title', 'content_html', 'external_url']
ordering = ['-created_at']
readonly_fields = ['created_at', 'updated_at', 'word_count', 'get_tags_display', 'get_categories_display']
inlines = [ContentTaxonomyInline]
+ actions = ['bulk_set_status_published', 'bulk_set_status_draft']
fieldsets = (
('Basic Info', {
@@ -152,6 +196,18 @@ class ContentAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
return 'No categories'
get_categories_display.short_description = 'Categories'
+ def bulk_set_status_published(self, request, queryset):
+ """Set selected content to published status"""
+ updated = queryset.update(status='published')
+ self.message_user(request, f'{updated} content item(s) set to published.', messages.SUCCESS)
+ bulk_set_status_published.short_description = 'Set status to Published'
+
+ def bulk_set_status_draft(self, request, queryset):
+ """Set selected content to draft status"""
+ updated = queryset.update(status='draft')
+ self.message_user(request, f'{updated} content item(s) set to draft.', messages.SUCCESS)
+ bulk_set_status_draft.short_description = 'Set status to Draft'
+
def get_site_display(self, obj):
"""Safely get site name"""
try:
diff --git a/backend/igny8_core/settings.py b/backend/igny8_core/settings.py
index 52960a59..bfc88a88 100644
--- a/backend/igny8_core/settings.py
+++ b/backend/igny8_core/settings.py
@@ -36,16 +36,25 @@ ALLOWED_HOSTS = [
]
INSTALLED_APPS = [
+ # Django Admin Enhancements (must be before django.contrib.admin)
+ 'admin_interface',
+ 'colorfield',
+ # Core Django apps
'igny8_core.admin.apps.Igny8AdminConfig', # Custom admin config
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
+ # Third-party apps
'rest_framework',
'django_filters',
'corsheaders',
'drf_spectacular', # OpenAPI 3.0 schema generation
+ 'import_export',
+ 'rangefilter',
+ 'django_celery_results',
+ # IGNY8 apps
'igny8_core.auth.apps.Igny8CoreAuthConfig', # Use app config with custom label
'igny8_core.ai.apps.AIConfig', # AI Framework
'igny8_core.modules.planner.apps.PlannerConfig',
@@ -591,6 +600,16 @@ LOGGING = {
},
}
+# Admin Interface Configuration
+X_FRAME_OPTIONS = 'SAMEORIGIN' # Required for django-admin-interface
+
+# Celery Results Backend
+CELERY_RESULT_BACKEND = 'django-db'
+CELERY_CACHE_BACKEND = 'django-cache'
+
+# Import/Export Settings
+IMPORT_EXPORT_USE_TRANSACTIONS = True
+
# Billing / Payments configuration
STRIPE_PUBLIC_KEY = os.getenv('STRIPE_PUBLIC_KEY', '')
STRIPE_SECRET_KEY = os.getenv('STRIPE_SECRET_KEY', '')
diff --git a/backend/requirements.txt b/backend/requirements.txt
index 41db8f12..bc867666 100755
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -15,3 +15,9 @@ psutil>=5.9.0
docker>=7.0.0
drf-spectacular>=0.27.0
stripe>=7.10.0
+
+# Django Admin Enhancements
+django-admin-interface==0.26.0
+django-import-export==3.3.1
+django-admin-rangefilter==0.11.1
+django-celery-results==2.5.1
diff --git a/backend/staticfiles/admin/js/cancel.js b/backend/staticfiles/admin/js/cancel.js
index 3069c6f2..36c963f7 100644
--- a/backend/staticfiles/admin/js/cancel.js
+++ b/backend/staticfiles/admin/js/cancel.js
@@ -1,29 +1,20 @@
-'use strict';
-{
- // Call function fn when the DOM is loaded and ready. If it is already
- // loaded, call the function now.
- // http://youmightnotneedjquery.com/#ready
- function ready(fn) {
- if (document.readyState !== 'loading') {
- fn();
- } else {
- document.addEventListener('DOMContentLoaded', fn);
- }
- }
+/** global: django */
- ready(function() {
- function handleClick(event) {
- event.preventDefault();
- const params = new URLSearchParams(window.location.search);
- if (params.has('_popup')) {
- window.close(); // Close the popup.
- } else {
- window.history.back(); // Otherwise, go back.
- }
- }
-
- document.querySelectorAll('.cancel-link').forEach(function(el) {
- el.addEventListener('click', handleClick);
+if (typeof(django) !== 'undefined' && typeof(django.jQuery) !== 'undefined') {
+ (function($) {
+ 'use strict';
+ $(document).ready(function() {
+ $('.cancel-link').click(function(e) {
+ e.preventDefault();
+ var parentWindow = window.parent;
+ if (parentWindow && typeof(parentWindow.dismissRelatedObjectModal) === 'function' && parentWindow !== window) {
+ parentWindow.dismissRelatedObjectModal();
+ } else {
+ // fallback to default behavior
+ window.history.back();
+ }
+ return false;
+ });
});
- });
+ })(django.jQuery);
}
diff --git a/backend/staticfiles/admin/js/collapse.js b/backend/staticfiles/admin/js/collapse.js
new file mode 100644
index 00000000..efe0b1b1
--- /dev/null
+++ b/backend/staticfiles/admin/js/collapse.js
@@ -0,0 +1,49 @@
+/*global gettext*/
+/* copied from django 4.0.7 */
+'use strict';
+{
+ window.addEventListener('load', function() {
+ // Add anchor tag for Show/Hide link
+ const fieldsets = document.querySelectorAll('fieldset.collapse');
+ for (const [i, elem] of fieldsets.entries()) {
+ // Don't hide if fields in this fieldset have errors
+ if (elem.querySelectorAll('div.errors, ul.errorlist').length === 0) {
+ const h2 = elem.querySelector('h2');
+ const link = document.createElement('a');
+ link.id = 'fieldsetcollapser' + i;
+ link.className = 'collapse-toggle';
+ link.href = '#';
+ // changed: can opt into starting visible
+ if (elem.classList.contains('expanded')) {
+ link.textContent = gettext('Hide');
+ } else {
+ link.textContent = gettext('Show');
+ elem.classList.add('collapsed');
+ }
+ h2.appendChild(document.createTextNode(' ('));
+ h2.appendChild(link);
+ h2.appendChild(document.createTextNode(')'));
+ }
+ }
+ // Add toggle to hide/show anchor tag
+ const toggleFunc = function(ev) {
+ if (ev.target.matches('.collapse-toggle')) {
+ ev.preventDefault();
+ ev.stopPropagation();
+ const fieldset = ev.target.closest('fieldset');
+ if (fieldset.classList.contains('collapsed')) {
+ // Show
+ ev.target.textContent = gettext('Hide');
+ fieldset.classList.remove('collapsed');
+ } else {
+ // Hide
+ ev.target.textContent = gettext('Show');
+ fieldset.classList.add('collapsed');
+ }
+ }
+ };
+ document.querySelectorAll('fieldset.module').forEach(function(el) {
+ el.addEventListener('click', toggleFunc);
+ });
+ });
+}
diff --git a/backend/staticfiles/admin/js/popup_response.js b/backend/staticfiles/admin/js/popup_response.js
index fecf0f47..dc24ef2b 100644
--- a/backend/staticfiles/admin/js/popup_response.js
+++ b/backend/staticfiles/admin/js/popup_response.js
@@ -1,15 +1,48 @@
-'use strict';
-{
- const initData = JSON.parse(document.getElementById('django-admin-popup-response-constants').dataset.popupResponse);
- switch(initData.action) {
- case 'change':
- opener.dismissChangeRelatedObjectPopup(window, initData.value, initData.obj, initData.new_value);
- break;
- case 'delete':
- opener.dismissDeleteRelatedObjectPopup(window, initData.value);
- break;
- default:
- opener.dismissAddRelatedObjectPopup(window, initData.value, initData.obj);
- break;
+(function() {
+
+ 'use strict';
+
+ var windowRef = window;
+ var windowRefProxy;
+ var windowName, widgetName;
+ var openerRef = windowRef.opener;
+ if (!openerRef) {
+ // related modal is active
+ openerRef = windowRef.parent;
+ windowName = windowRef.name;
+ widgetName = windowName.replace(/^(change|add|delete|lookup)_/, '');
+ if (typeof(openerRef.id_to_windowname) === 'function') {
+ // django < 3.1 compatibility
+ widgetName = openerRef.id_to_windowname(widgetName);
+ }
+ windowRefProxy = {
+ name: widgetName,
+ location: windowRef.location,
+ close: function() {
+ openerRef.dismissRelatedObjectModal();
+ }
+ };
+ windowRef = windowRefProxy;
}
-}
+
+ // default django popup_response.js
+ var initData = JSON.parse(document.getElementById('django-admin-popup-response-constants').dataset.popupResponse);
+ switch (initData.action) {
+ case 'change':
+ if (typeof(openerRef.dismissChangeRelatedObjectPopup) === 'function') {
+ openerRef.dismissChangeRelatedObjectPopup(windowRef, initData.value, initData.obj, initData.new_value);
+ }
+ break;
+ case 'delete':
+ if (typeof(openerRef.dismissDeleteRelatedObjectPopup) === 'function') {
+ openerRef.dismissDeleteRelatedObjectPopup(windowRef, initData.value);
+ }
+ break;
+ default:
+ if (typeof(openerRef.dismissAddRelatedObjectPopup) === 'function') {
+ openerRef.dismissAddRelatedObjectPopup(windowRef, initData.value, initData.obj);
+ }
+ break;
+ }
+
+})();
diff --git a/backend/staticfiles/admin_interface/collapsible-inlines/collapsible-inlines.js b/backend/staticfiles/admin_interface/collapsible-inlines/collapsible-inlines.js
new file mode 100644
index 00000000..1f560f46
--- /dev/null
+++ b/backend/staticfiles/admin_interface/collapsible-inlines/collapsible-inlines.js
@@ -0,0 +1,45 @@
+/** global: django */
+
+if (typeof(django) !== 'undefined' && typeof(django.jQuery) !== 'undefined')
+{
+ (function($) {
+
+ $(document).ready(function(){
+
+ function collapsibleInline(scope, collapsed) {
+ var fieldsetCollapsed = collapsed;
+ var fieldsetEl = $(scope).find('> fieldset.module');
+ fieldsetEl.addClass('collapse');
+ var fieldsetHasErrors = (fieldsetEl.children('.errors').length > 0);
+ if (fieldsetHasErrors === true) {
+ fieldsetCollapsed = false;
+ }
+ if (fieldsetCollapsed === true) {
+ fieldsetEl.addClass('collapsed');
+ }
+ var collapseToggleText = (fieldsetCollapsed ? gettext('Show') : gettext('Hide'));
+ var collapseToggleHTML = ' (' + collapseToggleText + ')';
+ var headerEl = fieldsetEl.find('> h2,> h3');
+ headerEl.append(collapseToggleHTML);
+ }
+
+ var stackedInlinesOptionSel = '.admin-interface.collapsible-stacked-inlines';
+ var stackedInlinesSel = stackedInlinesOptionSel + ' .inline-group[data-inline-type="stacked"]';
+ var stackedInlinesCollapsed = $(stackedInlinesOptionSel).hasClass('collapsible-stacked-inlines-collapsed');
+
+ var tabularInlinesOptionSel = '.admin-interface.collapsible-tabular-inlines';
+ var tabularInlinesSel = tabularInlinesOptionSel + ' .inline-group[data-inline-type="tabular"] .inline-related.tabular';
+ var tabularInlinesCollapsed = $(stackedInlinesOptionSel).hasClass('collapsible-tabular-inlines-collapsed');
+
+ $(stackedInlinesSel).each(function() {
+ collapsibleInline(this, stackedInlinesCollapsed);
+ });
+
+ $(tabularInlinesSel).each(function() {
+ collapsibleInline(this, tabularInlinesCollapsed);
+ });
+
+ });
+
+ })(django.jQuery);
+}
diff --git a/backend/staticfiles/admin_interface/css/admin-interface-fix.css b/backend/staticfiles/admin_interface/css/admin-interface-fix.css
new file mode 100644
index 00000000..e4cb7eaa
--- /dev/null
+++ b/backend/staticfiles/admin_interface/css/admin-interface-fix.css
@@ -0,0 +1,610 @@
+.admin-interface {
+ overflow-x: hidden;
+}
+
+/* fix login */
+.admin-interface.login #container {
+ width: 100%;
+ max-width: 360px;
+ margin: 15px auto;
+ box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+}
+
+.admin-interface.login #content {
+ padding: 15px 30px 30px 30px;
+}
+
+@media (min-width:768px){
+ .admin-interface.login #container {
+ margin: 90px auto;
+ }
+}
+
+.admin-interface.login #header {
+ min-height: auto;
+ padding: 10px 30px;
+ line-height: 30px;
+ align-items: center;
+ justify-content: flex-start;
+}
+
+.admin-interface.login #header #branding h1 {
+ margin-right:0;
+}
+
+.admin-interface.login #header #branding h1 img.logo {
+ margin-right: 0;
+}
+
+.admin-interface.login #header #branding h1 img.logo+span {
+ display: block;
+}
+
+.admin-interface.login #login-form {
+ display: flex;
+ flex-direction: column;
+}
+
+.admin-interface.login .submit-row {
+ float: left;
+ width: 100%;
+ margin-top: 20px;
+ padding-top: 0;
+ padding-left: 0;
+ text-align: right;
+}
+
+.admin-interface.login .submit-row label {
+ display: none;
+}
+
+.admin-interface.login .submit-row input[type="submit"] {
+ width: 100%;
+ text-transform: uppercase;
+}
+
+.admin-interface.login #footer {
+ display: none;
+}
+/* end login fix*/
+
+.admin-interface #header {
+ height: auto;
+ min-height: 55px;
+ box-sizing: border-box;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+@media (max-width:1024px) {
+ .admin-interface #header {
+ align-items: start;
+ }
+}
+
+.admin-interface #branding h1 img.logo {
+ margin-top:10px;
+ margin-bottom:10px;
+ margin-right:15px;
+ display:inline-block !important; /* override inline display:none; */
+}
+
+.admin-interface #branding h1 span {
+ display: inline-block;
+}
+
+.admin-interface #branding h1 img.logo+span {
+ white-space:nowrap;
+}
+
+.admin-interface #user-tools {
+ margin-top: 10px;
+ margin-bottom: 10px;
+ white-space: nowrap;
+ align-self: flex-start;
+}
+
+.admin-interface #user-tools br {
+ display: none;
+}
+@media (max-width: 768px) {
+ .admin-interface #user-tools br {
+ display: block;
+ }
+}
+
+.admin-interface fieldset.collapse {
+ border: 1px solid transparent;
+}
+
+.admin-interface fieldset.collapse.collapsed a.collapse-toggle,
+.admin-interface fieldset.collapse a.collapse-toggle,
+.admin-interface .inline-group .inline-related fieldset.module a.collapse-toggle,
+.admin-interface .inline-group .inline-related fieldset.module.collapsed a.collapse-toggle {
+ font-weight: normal;
+ text-transform: lowercase;
+ font-size: 12px;
+ text-decoration: underline;
+ padding: 0 1px;
+}
+
+@media (min-width: 1024px) {
+ .admin-interface #changelist .actions .button,
+ .admin-interface #changelist .actions .action-counter {
+ margin-left: 8px;
+ }
+}
+
+.admin-interface #changelist .paginator {
+ margin-top:-1px; /* merge 2 borders into 1 */
+ line-height:42px;
+}
+
+.admin-interface .paginator a,
+.admin-interface .paginator a:link,
+.admin-interface .paginator a:visited,
+.admin-interface .paginator .this-page {
+ padding:7px 12px;
+}
+
+.admin-interface .paginator a,
+.admin-interface .paginator .this-page {
+ margin-left:0px;
+}
+
+.admin-interface .paginator .this-page,
+.admin-interface .paginator a.end {
+ margin-right:25px;
+}
+
+.admin-interface .paginator .this-page + a:not(.showall) {
+ margin-left:-25px;
+}
+
+body.admin-interface .paginator a.showall,
+body.admin-interface .paginator a.showall:link,
+body.admin-interface .paginator a.showall:visited {
+ margin-left:20px;
+}
+
+/* fix help text icon on newline */
+.admin-interface .inline-group thead th {
+ white-space:nowrap;
+}
+
+.admin-interface .inline-group thead th img {
+ vertical-align: -2px;
+ margin-left: 5px;
+}
+
+.admin-interface .inline-group .inlinechangelink {
+ background-size: contain;
+ padding-left: 15px;
+ margin-left: 10px;
+}
+
+.admin-interface .file-thumbnail > a {
+ display: inline-block;
+}
+
+.admin-interface .aligned p.file-upload {
+ display:table;
+}
+
+.admin-interface form .form-row p.file-upload > a {
+ margin-right:20px;
+}
+
+.admin-interface form .form-row p.file-upload .clearable-file-input {
+ display:inline-block;
+}
+
+.admin-interface form .form-row p.file-upload .clearable-file-input label {
+ padding-bottom:0px;
+ margin-left:2px;
+}
+
+.admin-interface form .form-row p.file-upload > input[type="file"] {
+ margin-top: 0px;
+}
+
+@media (max-width:767px){
+
+ .admin-interface form .form-row p.file-upload {
+ width: 100%;
+ }
+
+ .admin-interface form .form-row p.file-upload > a {
+ margin-right:0px;
+ display: block;
+ white-space: pre-wrap;
+ word-break: break-word;
+ }
+
+ .admin-interface form .form-row p.file-upload .clearable-file-input {
+ display: block;
+ margin-top: 10px;
+ margin-left: 0;
+ margin-bottom: -10px;
+ }
+
+ .admin-interface form .form-row p.file-upload > input[type="file"] {
+ display: block;
+ width: auto;
+ padding: 0px;
+ }
+
+ /* fix inline horizontal scroll caused by checkbox-row */
+ .admin-interface form .form-row > div.checkbox-row {
+ width: 100%;
+ }
+}
+
+
+/* FIX WIDE FIELDSET HELPS / ERROR MESSAGES */
+.admin-interface form .wide p.help,
+.admin-interface form .wide div.help {
+ padding-left: 50px;
+}
+
+.admin-interface form .wide input + p.help,
+.admin-interface form .wide input + div.help {
+ margin-left: 160px;
+}
+
+@media (max-width:767px){
+ .admin-interface form .form-row div.help {
+ display: block;
+ width: 100%;
+ }
+}
+
+.admin-interface form .wide ul.errorlist {
+ margin-left: 200px;
+}
+
+/* LIST FILTER */
+.admin-interface .module.filtered h2 {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+.admin-interface .module.filtered #changelist-filter {
+ min-width: 240px;
+}
+
+@media (max-width: 1024px) {
+ .admin-interface .module.filtered #changelist-filter {
+ min-width: 200px;
+ }
+}
+
+.admin-interface .module.filtered #changelist-filter h2 {
+ font-size: 11px;
+ padding: 10px 15px;
+}
+
+.admin-interface .module.filtered #changelist-filter h2 + h3 {
+ margin-top: 0px;
+}
+
+.admin-interface .module.filtered #changelist-filter h3 {
+ margin-top: 12px;
+ margin-bottom: 12px;
+}
+
+.admin-interface #changelist-form .results {
+ scrollbar-width: thin;
+}
+
+/* begin fix issue #13 - Datetime widget broken in long inlines */
+.admin-interface p.datetime {
+ white-space:nowrap;
+}
+/* end fix */
+
+/* begin fix lateral padding to align text with field labels */
+.admin-interface .module h2,
+.admin-interface.dashboard .module caption,
+.admin-interface.dashboard .module th,
+.admin-interface .module.filtered h2,
+.admin-interface .inline-group h2,
+.admin-interface #nav-sidebar .module caption,
+.admin-interface #nav-sidebar .module th {
+ padding-left: 10px;
+ padding-right: 10px;
+}
+/* end fix */
+
+/* begin fix restrict tabular-inline horizontal-scroll to inline-group instead of whole page */
+.admin-interface .inline-group[data-inline-type="tabular"] {
+ overflow-x:auto;
+}
+/* end fix */
+
+/* begin fix stacked-inline margin-bottom in responsive small viewport */
+.admin-interface .inline-group[data-inline-type="stacked"] .module {
+ margin-bottom:0px;
+}
+/* end fix */
+
+/* begin fix tabular inlines horizontal scroll */
+.admin-interface .inline-related.tabular {
+ overflow-x: scroll;
+ overflow-y: hidden;
+}
+.admin-interface .inline-related.tabular fieldset.module {
+ display: contents;
+ width: 100%;
+ white-space: nowrap;
+ position: relative;
+}
+.admin-interface .inline-related.tabular fieldset.module h2 {
+ position: sticky;
+ left: 0;
+}
+.admin-interface .inline-related.tabular fieldset.module table {
+ scrollbar-width: thin;
+}
+.admin-interface .inline-related.tabular fieldset.module table tbody tr {
+ position: relative;
+}
+/* end fix */
+
+.admin-interface .inline-related h3 {
+ padding:6px 10px;
+}
+
+/* begin fix issue #12 - Inlines bad delete buttons alignement */
+.admin-interface .inline-group .tabular thead th:last-child:not([class]):not([style]) {
+ text-align:right;
+}
+
+.admin-interface .inline-group .tabular tr td {
+ vertical-align: top;
+}
+
+.admin-interface .inline-group .tabular tr td.delete {
+ text-align:right;
+ padding-right:15px;
+ vertical-align: top;
+}
+
+.admin-interface .inline-group .tabular tr td input[type="checkbox"] {
+ margin: 7px 0px;
+}
+
+.admin-interface .inline-group .tabular tr td.delete a.inline-deletelink {
+ margin-top:4px;
+ overflow:hidden;
+ text-indent:9999px;
+}
+/* end fix */
+
+/* top-right buttons color on hover -> just a lighten grey */
+.admin-interface .object-tools a {
+ color:#FFFFFF;
+}
+.admin-interface .object-tools a:focus,
+.admin-interface .object-tools a:hover,
+.admin-interface .object-tools li:focus a,
+.admin-interface .object-tools li:hover a {
+ background-color:#AAAAAA;
+}
+
+/* improve responsive selector */
+
+/* fix [stacked, not-stacked] equalize horizontal and vertical select padding for selector */
+.admin-interface .selector .selector-available select,
+.admin-interface .selector .selector-chosen select {
+ padding: 7px 10px;
+ display: block;
+}
+
+/* fix [stacked, not-stacked] select options text overflow */
+.admin-interface .selector .selector-available select option,
+.admin-interface .selector .selector-chosen select option {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+/* fix [not-stacked] equalize selectors height by adding the height of the .selector-available filter-bar */
+.admin-interface .selector:not(.stacked) .selector-chosen select {
+ height: calc(46px + 17.2em) !important;
+}
+
+/* fix nav-sidebar (added in django 3.1.0) */
+.admin-interface #toggle-nav-sidebar {
+ top: 10px;
+ left: 0;
+ z-index: 20;
+ flex: 0 0 30px;
+ width: 30px;
+ height: 45px;
+ margin-top: 10px;
+ margin-right: -30px;
+ background-color: #FFFFFF;
+ font-size: 16px;
+ border: 1px solid #eaeaea;
+ border-left: none;
+ outline: none;
+ -webkit-box-shadow: 4px 4px 8px -4px #DBDBDB;
+ -moz-box-shadow: 4px 4px 8px -4px #DBDBDB;
+ box-shadow: 4px 4px 8px -4px #DBDBDB;
+ /*transition: left .3s;*/
+}
+
+.admin-interface .toggle-nav-sidebar::before {
+ margin-top: -2px;
+}
+
+.admin-interface .main > #nav-sidebar + .content,
+.admin-interface .main.shifted > #nav-sidebar + .content {
+ max-width: 100%;
+}
+
+/* hide nav-sidebar below 1280px to prevent horizontal overflow issues */
+@media (max-width:1279px) {
+ .admin-interface #nav-sidebar,
+ .admin-interface #toggle-nav-sidebar {
+ display: none;
+ }
+}
+
+.admin-interface #nav-sidebar {
+ flex: 0 0 360px;
+ left: -360px;
+ margin-left: -360px;
+ box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ padding: 40px 40px 40px 40px;
+ border-top: none;
+ border-bottom: none;
+ border-left: none;
+ scrollbar-width: thin;
+ /*transition: left .3s, margin-left .3s;*/
+}
+
+.admin-interface #nav-filter {
+ background-color: transparent;
+ border-radius: 4px;
+ height: 30px;
+ margin: 0 0 30px 0;
+ padding: 5px 6px;
+ outline-width: initial;
+}
+
+@media (min-width:1280px) {
+ .admin-interface #main.shifted > #toggle-nav-sidebar {
+ left: 359px;
+ }
+ .admin-interface #main.shifted > #nav-sidebar {
+ left: 0px;
+ margin-left: 0;
+ }
+ .admin-interface #main:not(.shifted) > .content {
+ max-width: 100%;
+ }
+ .admin-interface.change-list:not(.popup) #main.shifted > #nav-sidebar + .content,
+ .admin-interface.change-form:not(.popup) #main.shifted > #nav-sidebar + .content {
+ max-width: calc(100% - 360px);
+ }
+}
+
+/* fixed related widget and select2 */
+/* begin fix issue #10 - Related widget broken in long tabular inline */
+.admin-interface .related-widget-wrapper {
+ white-space: nowrap;
+}
+/* end fix */
+
+.admin-interface .related-widget-wrapper select + .related-widget-wrapper-link,
+.admin-interface .related-widget-wrapper .select2-container + .related-widget-wrapper-link {
+ margin-left: 12px !important;
+}
+
+@media (min-width: 768px) {
+ .admin-interface.change-form select {
+ min-width: 150px;
+ }
+}
+
+@media (min-width: 1024px) {
+ .admin-interface.change-form select {
+ min-width: 200px;
+ }
+}
+
+.admin-interface.change-form .inline-related.tabular select {
+ min-width: auto !important;
+}
+
+/* fixed time widget header border radius */
+.admin-interface .clockbox.module h2 {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+/* fix searchbar overriden padding */
+.admin-interface #changelist #changelist-search #searchbar {
+ padding: 2px 5px 3px 5px;
+}
+
+@media (min-width: 1024px) {
+ .admin-interface #changelist #changelist-search #searchbar,
+ .admin-interface #changelist #changelist-search input[type="submit"],
+ .admin-interface #changelist #changelist-search .quiet {
+ margin-left: 8px;
+ }
+ .admin-interface #changelist #changelist-search label img {
+ vertical-align: text-top;
+ margin-right: 0px;
+ }
+}
+
+@media (max-width: 1024px) {
+ .admin-interface #changelist #toolbar {
+ border-top: 1px solid #eee;
+ border-bottom: 1px solid #eee;
+ }
+ /* fixed changelist search size when there are search results and .quiet is visible */
+ .admin-interface #changelist-search label img {
+ margin-top: 2px;
+ }
+ .admin-interface #changelist-search .quiet {
+ margin: 0 0 0 10px;
+ align-self: center;
+ flex-basis: content;
+ }
+}
+
+@media (max-width: 767px) {
+ /* fixed responsive widgets */
+ .admin-interface .aligned.collapsed .form-row {
+ display: none;
+ }
+
+ .admin-interface .aligned .form-row > div {
+ display: flex;
+ max-width: 100vw;
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .admin-interface .aligned .form-row .help {
+ margin-left: 0;
+ }
+
+ .admin-interface .aligned .form-row .checkbox-row label {
+ margin: 10px 0 0 0;
+ padding: 0;
+ }
+
+ .admin-interface .aligned .form-row input[type="file"],
+ .admin-interface .aligned .form-row input[type="text"],
+ .admin-interface .aligned .form-row input[type="email"] {
+ width: 100%;
+ }
+
+ /* fix textarea horizontal scroll on Firefox */
+ .admin-interface .aligned .form-row textarea {
+ width: 100% !important;
+ flex: 0 1 auto;
+ }
+
+ .admin-interface .aligned .form-row .datetime input[type="text"] {
+ width: 50%;
+ }
+
+ .admin-interface .aligned .form-row span + .file-upload {
+ margin-top: 10px;
+ }
+
+ .admin-interface .aligned .form-row .file-upload input[type="file"] {
+ margin-top: 5px;
+ }
+}
diff --git a/backend/staticfiles/admin_interface/css/admin-interface.css b/backend/staticfiles/admin_interface/css/admin-interface.css
new file mode 100644
index 00000000..bfa9ba40
--- /dev/null
+++ b/backend/staticfiles/admin_interface/css/admin-interface.css
@@ -0,0 +1,497 @@
+@media (prefers-color-scheme: dark) {
+ :root .admin-interface {
+ --primary: #79aec8;
+ --secondary: #417690;
+ --accent: #f5dd5d;
+ --primary-fg: #fff;
+ --body-fg: #333;
+ --body-bg: #fff;
+ --body-quiet-color: #666;
+ --body-loud-color: #000;
+ --header-color: #ffc;
+ --header-branding-color: var(--accent);
+ --header-bg: var(--secondary);
+ --header-link-color: var(--primary-fg);
+ --breadcrumbs-fg: #c4dce8;
+ --breadcrumbs-link-fg: var(--body-bg);
+ --breadcrumbs-bg: var(--primary);
+ --link-fg: #447e9b;
+ --link-hover-color: #036;
+ --link-selected-fg: #5b80b2;
+ --hairline-color: #e8e8e8;
+ --border-color: #ccc;
+ --error-fg: #ba2121;
+ --message-success-bg: #dfd;
+ --message-warning-bg: #ffc;
+ --message-error-bg: #ffefef;
+ --darkened-bg: #f8f8f8;
+ --selected-bg: #e4e4e4;
+ --selected-row: #ffc;
+ --button-fg: #fff;
+ --button-bg: var(--primary);
+ --button-hover-bg: #609ab6;
+ --default-button-bg: var(--secondary);
+ --default-button-hover-bg: #205067;
+ --close-button-bg: #888;
+ --close-button-hover-bg: #747474;
+ --delete-button-bg: #ba2121;
+ --delete-button-hover-bg: #a41515;
+ --object-tools-fg: var(--button-fg);
+ --object-tools-bg: var(--close-button-bg);
+ --object-tools-hover-bg: var(--close-button-hover-bg);
+ }
+}
+
+.admin-interface #header {
+ background: var(--admin-interface-header-background-color);
+ color: var(--admin-interface-header-text-color);
+}
+
+.admin-interface #header + #main {
+ border-top: var(--admin-interface-main-border-top);
+}
+
+.admin-interface .environment-label {
+}
+
+.admin-interface .environment-label::before {
+ content: "";
+ display: inline-block;
+ width: 8px;
+ height: 8px;
+ background-color: var(--admin-interface-env-color);
+ border-radius: 100%;
+ margin-right: 6px;
+ box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+}
+
+.admin-interface .environment-label::after {
+ content: " - ";
+}
+
+@media (max-width: 1024px) {
+ .admin-interface .environment-label::after {
+ content: "";
+ }
+}
+
+.admin-interface .language-chooser {
+ display: inline-block;
+ position: absolute;
+ top: 15px;
+ right: 15px;
+ z-index: 10;
+}
+
+@media (min-width: 768px) {
+ .admin-interface .language-chooser {
+ right: 30px;
+ }
+}
+
+@media (min-width: 1024px) {
+ .admin-interface .language-chooser {
+ position: static;
+ float: right;
+ margin-left: 20px;
+ }
+}
+
+.admin-interface .language-chooser-hidden-form {
+ display: none;
+}
+
+.admin-interface .language-chooser-select-form {
+ display: inline-block;
+}
+
+.admin-interface #branding h1,
+.admin-interface.login #header h1,
+.admin-interface.login #header h1 a {
+ color: var(--admin-interface-title-color);
+}
+
+.admin-interface #branding h1 a {
+ color: inherit;
+}
+
+.admin-interface #branding h1 .logo.default {
+ background-color: transparent;
+ background-repeat: no-repeat;
+ background-position: center center;
+ background-size: 104px 36px;
+ background-image: var(--admin-interface-logo-default-background-image);
+}
+
+.admin-interface #branding h1 img.logo,
+.admin-interface.login #header #branding h1 img.logo {
+ max-width: var(--admin-interface-logo-max-width);
+ max-height: var(--admin-interface-logo-max-height);
+}
+
+.admin-interface #header #user-tools a {
+ color: var(--admin-interface-header-link-color);
+}
+
+.admin-interface #header #user-tools a:hover,
+.admin-interface #header #user-tools a:active {
+ color: var(--admin-interface-header-link-hover-color);
+ border-bottom-color: rgba(255, 255, 255, 0.5);
+}
+
+.admin-interface #nav-sidebar .current-app .section:link,
+.admin-interface #nav-sidebar .current-app .section:visited {
+ color: var(--admin-interface-module-link-selected-color);
+ font-weight: normal;
+}
+
+.admin-interface #nav-sidebar .current-app .section:focus,
+.admin-interface #nav-sidebar .current-app .section:hover {
+ color: var(--admin-interface-module-link-hover-color);
+}
+
+.admin-interface #nav-sidebar .current-model {
+ background: var(--admin-interface-module-background-selected-color);
+}
+
+.admin-interface #changelist table tbody tr.selected {
+ background-color: var(--admin-interface-module-background-selected-color);
+}
+
+.admin-interface .module h2,
+.admin-interface .module caption,
+.admin-interface .module.filtered h2 {
+ background: var(--admin-interface-module-background-color);
+ color: var(--admin-interface-module-text-color);
+}
+
+.admin-interface .module a.section:link,
+.admin-interface .module a.section:visited {
+ color: var(--admin-interface-module-link-color);
+}
+
+.admin-interface .module a.section:active,
+.admin-interface .module a.section:hover {
+ color: var(--admin-interface-module-link-hover-color);
+}
+
+.admin-interface div.breadcrumbs {
+ background: var(--admin-interface-module-background-color);
+ color: var(--admin-interface-module-text-color);
+}
+
+.admin-interface div.breadcrumbs a {
+ color: var(--admin-interface-module-link-color);
+}
+
+.admin-interface div.breadcrumbs a:active,
+.admin-interface div.breadcrumbs a:focus,
+.admin-interface div.breadcrumbs a:hover {
+ color: var(--admin-interface-module-link-hover-color);
+}
+
+.admin-interface fieldset.collapse a.collapse-toggle,
+.admin-interface fieldset.collapse.collapsed a.collapse-toggle,
+.admin-interface .inline-group .inline-related fieldset.module a.collapse-toggle,
+.admin-interface .inline-group .inline-related fieldset.module.collapsed a.collapse-toggle {
+ color: var(--admin-interface-module-link-color);
+}
+
+.admin-interface fieldset.collapse a.collapse-toggle:hover,
+.admin-interface fieldset.collapse a.collapse-toggle:active,
+.admin-interface fieldset.collapse.collapsed a.collapse-toggle:hover,
+.admin-interface fieldset.collapse.collapsed a.collapse-toggle:active,
+.admin-interface .inline-group .inline-related fieldset.module a.collapse-toggle:hover,
+.admin-interface .inline-group .inline-related fieldset.module a.collapse-toggle:active,
+.admin-interface .inline-group .inline-related fieldset.module.collapsed a.collapse-toggle:hover,
+.admin-interface .inline-group .inline-related fieldset.module.collapsed a.collapse-toggle:active {
+ color: var(--admin-interface-module-link-hover-color);
+}
+
+.admin-interface .inline-group h2 {
+ background: var(--admin-interface-module-background-color);
+ color: var(--admin-interface-module-text-color);
+}
+
+.admin-interface .selector .selector-chosen h2 {
+ border-color: var(--admin-interface-module-background-color);
+ background: var(--admin-interface-module-background-color);
+ color: var(--admin-interface-module-text-color);
+}
+
+.admin-interface .selector .selector-available h2,
+.admin-interface .selector .selector-chosen h2 {
+ border-bottom-left-radius: 0px;
+ border-bottom-right-radius: 0px;
+}
+
+.admin-interface .selector a.selector-chooseall:focus,
+.admin-interface .selector a.selector-chooseall:hover,
+.admin-interface .selector a.selector-clearall:focus,
+.admin-interface .selector a.selector-clearall:hover {
+ color: var(--admin-interface-generic-link-hover-color);
+}
+
+.admin-interface a:link,
+.admin-interface a:visited {
+ color: var(--admin-interface-generic-link-color);
+}
+
+.admin-interface a:hover {
+ color: var(--admin-interface-generic-link-hover-color);
+}
+
+.admin-interface thead th a,
+.admin-interface thead th a:link,
+.admin-interface thead th a:visited,
+.admin-interface thead th a:focus,
+.admin-interface thead th a:hover {
+ color: #666666;
+}
+
+.admin-interface .button,
+.admin-interface input[type=submit],
+.admin-interface input[type=button],
+.admin-interface .submit-row input,
+.admin-interface a.button {
+ background: var(--admin-interface-save-button-background-color);
+ color: var(--admin-interface-save-button-text-color);
+}
+
+.admin-interface .button:active,
+.admin-interface .button:focus,
+.admin-interface .button:hover,
+.admin-interface input[type=submit]:active,
+.admin-interface input[type=submit]:focus,
+.admin-interface input[type=submit]:hover,
+.admin-interface input[type=button]:active,
+.admin-interface input[type=button]:focus,
+.admin-interface input[type=button]:hover {
+ background: var(--admin-interface-save-button-background-hover-color);
+ color: var(--admin-interface-save-button-text-color);
+ outline: none;
+}
+
+.admin-interface .button.default,
+.admin-interface input[type=submit].default,
+.admin-interface .submit-row input.default {
+ background: var(--admin-interface-save-button-background-color);
+ color: var(--admin-interface-save-button-text-color);
+ outline: none;
+}
+
+.admin-interface .button.default:active,
+.admin-interface .button.default:focus,
+.admin-interface .button.default:hover,
+.admin-interface input[type=submit].default:active,
+.admin-interface input[type=submit].default:focus,
+.admin-interface input[type=submit].default:hover,
+.admin-interface.delete-confirmation form .cancel-link:hover {
+ background: var(--admin-interface-save-button-background-hover-color);
+ color: var(--admin-interface-save-button-text-color);
+ outline: none;
+}
+
+.admin-interface .submit-row a.deletelink:link,
+.admin-interface .submit-row a.deletelink:visited,
+.admin-interface.delete-confirmation form input[type="submit"] {
+ background: var(--admin-interface-delete-button-background-color);
+ color: var(--admin-interface-delete-button-text-color);
+}
+
+.admin-interface .submit-row a.deletelink:hover,
+.admin-interface.delete-confirmation form input[type="submit"]:hover {
+ background: var(--admin-interface-delete-button-background-hover-color);
+ color: var(--admin-interface-delete-button-text-color);
+}
+
+.admin-interface .paginator a,
+.admin-interface .paginator a:link,
+.admin-interface .paginator a:visited,
+.admin-interface .paginator .this-page {
+ border-radius: var(--admin-interface-module-border-radius);
+}
+
+.admin-interface .paginator a,
+.admin-interface .paginator a:link,
+.admin-interface .paginator a:visited {
+ background-color: #FFFFFF;
+ color: var(--admin-interface-generic-link-color);
+}
+
+.admin-interface .paginator a:hover,
+.admin-interface .paginator a:active {
+ background-color: #F8F8F8;
+ color: var(--admin-interface-generic-link-hover-color);
+}
+
+.admin-interface .paginator .this-page {
+ background-color: var(--admin-interface-module-background-color);
+ color: var(--admin-interface-module-link-color);
+}
+
+.admin-interface .paginator a.showall,
+.admin-interface .paginator a.showall:link,
+.admin-interface .paginator a.showall:visited {
+ color: var(--admin-interface-generic-link-color);
+}
+
+.admin-interface .paginator a.showall:hover,
+.admin-interface .paginator a.showall:active {
+ color: var(--admin-interface-generic-link-hover-color);
+}
+
+/* list-filter sticky */
+@media (min-width: 768px) {
+ .admin-interface.list-filter-sticky .module.filtered #changelist-filter {
+ position: sticky;
+ top: 30px;
+ float: right;
+ z-index: 30;
+ display: flex;
+ flex-direction: column;
+ overflow-y: auto;
+ scrollbar-width: thin;
+ height: 100%;
+ max-height: calc(100vh - 60px);
+ }
+ .admin-interface.list-filter-sticky.sticky-pagination .module.filtered #changelist-filter {
+ max-height: calc(100vh - 125px);
+ }
+
+ /* feature not available for django < 3.1.2 */
+ .admin-interface.list-filter-sticky .module.filtered #toolbar + #changelist-filter {
+ position: absolute;
+ top: 0px;
+ z-index: 30;
+ max-height: calc(100vh - 105px);
+ }
+ .admin-interface.list-filter-sticky.sticky-pagination .module.filtered #toolbar + #changelist-filter {
+ max-height: calc(100vh - 170px);
+ }
+}
+
+.admin-interface .module.filtered #changelist-filter {
+ border-radius: var(--admin-interface-module-border-radius);
+}
+
+.admin-interface .module.filtered #changelist-filter h3#changelist-filter-clear {
+ margin-bottom: 0;
+}
+
+.admin-interface .module.filtered #changelist-filter .changelist-filter-clear a {
+ font-size: 13px;
+ margin: .3em 0;
+ padding: 0 15px;
+}
+
+.admin-interface .module.filtered #changelist-filter .changelist-filter-clear a:focus,
+.admin-interface .module.filtered #changelist-filter .changelist-filter-clear a:hover,
+.admin-interface .module.filtered #changelist-filter #changelist-filter-clear a:focus,
+.admin-interface .module.filtered #changelist-filter #changelist-filter-clear a:hover {
+ color: #666;
+ text-decoration: none;
+}
+
+.admin-interface .module.filtered #changelist-filter .changelist-filter-clear a span {
+ font-weight: bold;
+}
+
+.admin-interface .module.filtered #changelist-filter li a:focus,
+.admin-interface .module.filtered #changelist-filter li a:hover {
+ color: #666;
+ text-decoration: none;
+}
+
+.admin-interface.list-filter-highlight .module.filtered #changelist-filter h3.active {
+ font-weight: bold;
+}
+
+.admin-interface.list-filter-highlight .module.filtered #changelist-filter ul.active li.selected {
+ color: var(--admin-interface-module-text-color);
+ background: var(--admin-interface-module-background-color);
+ margin-left: -10px;
+ padding-left: 5px;
+ margin-right: -10px;
+ border-left: 5px solid var(--admin-interface-module-background-color);
+ border-right: 5px solid var(--admin-interface-module-background-color);
+ border-radius: var(--admin-interface-module-border-radius);
+}
+
+.admin-interface.list-filter-highlight .module.filtered #changelist-filter ul.active li.selected a,
+.admin-interface.list-filter-highlight .module.filtered #changelist-filter ul.active li.selected a:link,
+.admin-interface.list-filter-highlight .module.filtered #changelist-filter ul.active li.selected a:visited,
+.admin-interface.list-filter-highlight .module.filtered #changelist-filter ul.active li.selected a:focus,
+.admin-interface.list-filter-highlight .module.filtered #changelist-filter ul.active li.selected a:hover {
+ background: inherit;
+ color: inherit;
+}
+
+.admin-interface .module.filtered #changelist-filter li.selected a,
+.admin-interface .module.filtered #changelist-filter li.selected a:link,
+.admin-interface .module.filtered #changelist-filter li.selected a:visited,
+.admin-interface .module.filtered #changelist-filter li.selected a:focus,
+.admin-interface .module.filtered #changelist-filter li.selected a:hover {
+ color: var(--admin-interface-generic-link-hover-color);
+}
+
+/* begin fix issue #11 - Inline border bottom should not be rounded */
+.admin-interface .module h2,
+.admin-interface.dashboard .module caption,
+.admin-interface #nav-sidebar .module th,
+.admin-interface #nav-sidebar .module caption,
+.admin-interface .module.filtered h2 {
+ border-radius: var(--admin-interface-module-border-radius);
+}
+
+.admin-interface .inline-group h2 {
+ border-bottom-left-radius: 0px;
+ border-bottom-right-radius: 0px;
+}
+
+.admin-interface .module.collapse.collapsed h2 {
+ /* fix collapsed inlines rounded bottom borders */
+ border-bottom-left-radius: var(--admin-interface-module-border-radius);
+ border-bottom-right-radius: var(--admin-interface-module-border-radius);
+}
+
+/* end fix */
+
+.admin-interface #content-related {
+ border-radius: var(--admin-interface-module-border-radius);
+}
+
+.admin-interface .select2-container--admin-autocomplete .select2-results__option--highlighted[aria-selected] {
+ background-color: var(--admin-interface-module-background-color);
+ color: var(--admin-interface-module-text-color);
+}
+
+.admin-interface #toggle-nav-sidebar {
+ border-top-right-radius: var(--admin-interface-module-border-radius);
+ border-bottom-right-radius: var(--admin-interface-module-border-radius);
+ color: var(--admin-interface-generic-link-color);
+}
+
+.admin-interface #toggle-nav-sidebar:focus,
+.admin-interface #toggle-nav-sidebar:hover,
+.admin-interface #toggle-nav-sidebar:active {
+ color: var(--admin-interface-generic-link-hover-color);
+}
+
+.admin-interface .calendar td.selected a,
+.admin-interface .calendar td a:active,
+.admin-interface .calendar td a:focus,
+.admin-interface .calendar td a:hover,
+.admin-interface .timelist a:active,
+.admin-interface .timelist a:focus,
+.admin-interface .timelist a:hover {
+ background: var(--admin-interface-module-background-color);
+}
+
+.admin-interface .calendarbox .calendarnav-previous,
+.admin-interface .calendarbox .calendarnav-next {
+ transition: none;
+ filter: invert(100%);
+}
diff --git a/backend/staticfiles/admin_interface/css/ckeditor.css b/backend/staticfiles/admin_interface/css/ckeditor.css
new file mode 100644
index 00000000..952120cb
--- /dev/null
+++ b/backend/staticfiles/admin_interface/css/ckeditor.css
@@ -0,0 +1,126 @@
+/*
+ckeditor + light theme
+https://github.com/Ikimea/ckeditor-light-theme
+*/
+
+.admin-interface .cke {
+ border: none;
+ box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+}
+
+.admin-interface .cke_inner,
+.admin-interface .cke_wysiwyg_frame {
+ border-top-left-radius: 4px;
+ border-top-right-radius: 4px;
+ border-bottom-left-radius: 4px;
+ border-bottom-right-radius: 4px;
+}
+
+.admin-interface .cke_inner {
+ border: 1px solid #CCCCCC;
+}
+
+.admin-interface .cke_chrome {
+ -moz-box-shadow: none;
+ -webkit-box-shadow: none;
+ box-shadow: none;
+}
+
+.admin-interface .cke_top {
+ background: #f8f8f8;
+ border-top: none;
+ border-top-left-radius: 4px;
+ border-top-right-radius: 4px;
+ border-bottom: 1px solid #EEEEEE;
+ padding-left: 10px;
+ padding-right: 10px;
+ -moz-box-shadow: none;
+ -webkit-box-shadow: none;
+ box-shadow: none;
+}
+
+.admin-interface .cke_toolgroup {
+ background: none;
+ -moz-box-shadow: none;
+ -webkit-box-shadow: none;
+ box-shadow: none;
+}
+
+.admin-interface .cke_bottom {
+ background: #f8f8f8;
+ border-top: 1px solid #EEEEEE;
+
+ border-bottom-left-radius: 4px;
+ border-bottom-right-radius: 4px;
+
+ -moz-box-shadow: none;
+ -webkit-box-shadow: none;
+ box-shadow: none;
+}
+
+.admin-interface .cke_source {
+ padding: 13px 15px;
+ box-sizing: border-box;
+}
+
+.admin-interface a.cke_button,
+.admin-interface a.cke_button:active,
+.admin-interface a.cke_button:hover,
+.admin-interface a.cke_button:focus {
+ box-shadow: none;
+ -moz-box-shadow: none;
+ -webkit-box-shadow: none;
+ background-image: none;
+ border-radius: 4px;
+ border: none;
+}
+
+.admin-interface a.cke_button:active,
+.admin-interface a.cke_button:hover,
+.admin-interface a.cke_button:focus {
+ background-color: #E8E8E8 !important;
+}
+
+.admin-interface a.cke_button.cke_button_on {
+ background-color: #CCCCCC !important;
+}
+
+.admin-interface a.cke_button.cke_button_disabled {
+ background-color: transparent !important;
+}
+
+.admin-interface .cke_resizer {
+ border-color: transparent #666666 transparent transparent;
+}
+
+@media (max-width: 767px){
+
+ .admin-interface .django-ckeditor-widget,
+ .admin-interface .cke {
+ width: 100% !important;
+ }
+
+ .admin-interface .cke_top {
+ padding-left: 10px;
+ padding-right: 10px;
+ }
+
+ .admin-interface .cke_toolbar {
+ height: auto;
+ }
+
+ .admin-interface .cke_contents {
+ height: auto;
+ }
+
+ .admin-interface .tabular .django-ckeditor-widget,
+ .admin-interface .tabular .cke {
+ width: 400px !important;
+ }
+
+ .admin-interface .tabular .cke_contents {
+ height: 90px !important;
+ }
+}
diff --git a/backend/staticfiles/admin_interface/css/form-controls.css b/backend/staticfiles/admin_interface/css/form-controls.css
new file mode 100644
index 00000000..43073670
--- /dev/null
+++ b/backend/staticfiles/admin_interface/css/form-controls.css
@@ -0,0 +1,95 @@
+/* sticky pagination */
+
+.admin-interface.sticky-pagination.change-list #content-main {
+ padding-bottom: 4.375rem;
+}
+
+.admin-interface.sticky-pagination.change-list .paginator {
+ width: 100%;
+ position: fixed;
+ bottom: 0;
+ right: 0;
+ z-index: 40;
+ box-sizing: border-box;
+ padding-left: 15px;
+ padding-right: 15px;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ border-radius: 0;
+ border-top: 1px solid #EEEEEE !important;
+ border-bottom: none;
+ margin: 0;
+}
+
+.admin-interface.sticky-pagination.change-list.popup .paginator {
+ padding-left: 20px;
+ padding-right: 20px;
+}
+
+@media (min-width:768px) {
+ .admin-interface.sticky-pagination.change-list:not(.popup) .paginator {
+ padding-left: 30px;
+ padding-right: 30px;
+ }
+}
+
+@media (min-width:1024px) {
+ .admin-interface.sticky-pagination.change-list:not(.popup) .paginator {
+ padding-left: 40px;
+ padding-right: 40px;
+ }
+}
+
+@media (min-width:1280px) {
+ .admin-interface.sticky-pagination.change-list:not(.popup) #main.shifted > #nav-sidebar + .content .paginator {
+ width: calc(100% - 360px);
+ }
+}
+
+/* sticky submit */
+
+@media (min-width:768px) {
+ .admin-interface.sticky-submit.change-form #content-main {
+ padding-bottom: 4.375rem;
+ }
+
+ .admin-interface.sticky-submit.change-form .submit-row:last-of-type {
+ width: 100%;
+ position: fixed;
+ bottom: 0;
+ right: 0;
+ z-index: 40;
+ box-sizing: border-box;
+ padding-left: 15px;
+ padding-right: 15px;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ border-radius: 0;
+ border-top: 1px solid #EEEEEE;
+ border-bottom: none !important;
+ margin: 0;
+ }
+
+ .admin-interface.sticky-submit.change-form.popup .submit-row:last-of-type {
+ padding-left: 20px;
+ padding-right: 20px;
+ }
+
+ .admin-interface.sticky-submit.change-form:not(.popup) .submit-row:last-of-type {
+ padding-left: 30px;
+ padding-right: 30px;
+ }
+}
+
+@media (min-width:1024px) {
+ .admin-interface.sticky-submit.change-form:not(.popup) .submit-row:last-of-type {
+ padding-left: 40px;
+ padding-right: 40px;
+ }
+}
+
+@media (min-width:1280px) {
+ .admin-interface.sticky-submit.change-form:not(.popup) #main.shifted > #nav-sidebar + .content .submit-row:last-of-type {
+ width: calc(100% - 359px);
+ }
+}
diff --git a/backend/staticfiles/admin_interface/css/import-export.css b/backend/staticfiles/admin_interface/css/import-export.css
new file mode 100644
index 00000000..8ea0a092
--- /dev/null
+++ b/backend/staticfiles/admin_interface/css/import-export.css
@@ -0,0 +1,7 @@
+/* Fix left/right scrolling broken with django-import-export #165 */
+.admin-interface table.import-preview {
+ display: block;
+ width: 100%;
+ max-width: 100%;
+ overflow: auto;
+}
diff --git a/backend/staticfiles/admin_interface/css/jquery.ui.tabs.css b/backend/staticfiles/admin_interface/css/jquery.ui.tabs.css
new file mode 100644
index 00000000..dfc2f94f
--- /dev/null
+++ b/backend/staticfiles/admin_interface/css/jquery.ui.tabs.css
@@ -0,0 +1,247 @@
+/*
+* jQuery UI CSS Framework
+* Copyright (c) 2010 AUTHORS.txt (http://jqueryui.com/about)
+* Dual licensed under the MIT (MIT-LICENSE.txt) and GPL (GPL-LICENSE.txt) licenses.
+* http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.2/themes/base/jquery.ui.core.css
+*/
+
+/*
+backward compatibility:
+.ui-tabs-selected: jquery ui < 1.10
+.ui-tabs-active classes jquery ui >= 1.10
+*/
+
+/* Layout helpers
+----------------------------------*/
+.ui-helper-hidden {
+ display: none;
+}
+
+.ui-helper-hidden-accessible {
+ position: absolute;
+ left: -99999999px;
+}
+
+.ui-helper-reset {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ outline: 0;
+ line-height: 1.3;
+ text-decoration: none;
+ font-size: 100%;
+ list-style: none;
+}
+
+.ui-helper-clearfix:after {
+ content: ".";
+ display: block;
+ height: 0;
+ clear: both;
+ visibility: hidden;
+}
+
+.ui-helper-clearfix {
+ display: inline-block;
+}
+
+/* required comment for clearfix to work in Opera \*/
+* html .ui-helper-clearfix {
+ height: 1%;
+}
+
+.ui-helper-clearfix {
+ display: block;
+}
+
+.ui-helper-zfix {
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ position: absolute;
+ opacity: 0;
+ filter: Alpha(Opacity=0);
+}
+
+.ui-state-disabled {
+ cursor: default !important;
+}
+
+.ui-icon {
+ display: block;
+ text-indent: -99999px;
+ overflow: hidden;
+ background-repeat: no-repeat;
+}
+
+/* http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.2/themes/base/jquery.ui.tabs.css */
+.ui-widget-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+}
+
+.ui-tabs {
+ position: relative;
+ padding: .2em;
+ zoom: 1;
+}
+
+/* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */
+.ui-tabs .ui-tabs-nav {
+ margin: 0;
+ padding: .2em .2em 0;
+}
+
+.ui-tabs .ui-tabs-nav li {
+ list-style: none;
+ float: left;
+ position: relative;
+ top: 1px;
+ margin: 0 .2em 1px 0;
+ border-bottom: 0 !important;
+ padding: 0;
+ white-space: nowrap;
+}
+
+.ui-tabs .ui-tabs-nav li a {
+ float: left;
+ padding: .5em 1em;
+ text-decoration: none;
+}
+
+.ui-tabs .ui-tabs-nav li.ui-tabs-active, .ui-tabs .ui-tabs-nav li.ui-tabs-selected {
+ margin-bottom: 0;
+ padding-bottom: 1px;
+}
+
+.ui-tabs .ui-tabs-nav li.ui-tabs-active a, .ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-state-processing a {
+ cursor: text;
+}
+
+.ui-tabs .ui-tabs-nav li a, .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a {
+ cursor: pointer;
+}
+
+/* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */
+.ui-tabs .ui-tabs-panel {
+ display: block;
+ border-width: 0;
+ padding: 1em 1.4em;
+ background: none;
+}
+
+.ui-tabs .ui-tabs-hide {
+ position: absolute;
+ display: none;
+}
+
+/* Custom tabs theme */
+.admin-interface .ui-tabs {
+ padding: 0;
+}
+
+.admin-interface .ui-tabs,
+.admin-interface .ui-tabs .ui-widget-header,
+.admin-interface .ui-tabs .ui-widget-header .ui-state-default {
+ border: none;
+ background: transparent;
+}
+
+.admin-interface .ui-tabs .ui-tabs-nav {
+ padding: 10px 0 0 10px;
+ border-bottom: none;
+}
+
+.admin-interface .ui-tabs .ui-tabs-nav li {
+ margin: 0 0 0 -1px;
+}
+
+.admin-interface .ui-tabs .ui-tabs-nav li.required {
+ font-weight: bold;
+}
+
+.admin-interface .ui-tabs .ui-tabs-nav li a {
+ border: 1px solid #eeeeee;
+ background-color: #f8f8f8;
+ border-bottom: none;
+ color: #666666;
+ padding: 7px 14px 8px 14px;
+ margin-top: 1px;
+ -moz-border-radius-topright: 4px;
+ -webkit-border-top-right-radius: 4px;
+ -moz-border-radius-topleft: 4px;
+ -webkit-border-top-left-radius: 4px;
+ border-top-left-radius: 4px;
+ border-top-right-radius: 4px;
+ font-size: 12px;
+ text-transform: uppercase;
+ outline: none;
+}
+
+.admin-interface .ui-tabs .ui-tabs-nav li.ui-tabs-active a,
+.admin-interface .ui-tabs .ui-tabs-nav li.ui-tabs-selected a {
+ padding: 8px 14px 8px 14px;
+ margin-top: 0px;
+ margin-bottom: -1px;
+ font-weight: bold;
+ background-color: #FFFFFF;
+ color: var(--admin-interface-module-background-color);
+ border-bottom: 1px solid #FFFFFF;
+}
+
+.admin-interface .ui-tabs .ui-tabs-panel {
+ border: 1px solid #eeeeee;
+ border-radius: 4px;
+ padding: 10px;
+ margin-bottom: 30px;
+ overflow: hidden;
+}
+
+.admin-interface .inline-group .tabular .ui-tabs .ui-tabs-panel {
+ padding: 8px;
+}
+
+.admin-interface .inline-group .tabular .ui-tabs .ui-tabs-nav {
+ padding-left: 4px;
+}
+
+.admin-interface .inline-group .tabular tr td {
+ vertical-align: top;
+}
+
+.admin-interface .inline-group .tabular tr.has_original td.original,
+.admin-interface .inline-group .tabular tr td.delete {
+ vertical-align: top;
+}
+
+.admin-interface .inline-group .tabular .datetime > input {
+ margin-right: 5px;
+}
+
+.admin-interface .inline-group .tabular .datetime br {
+ display: none;
+}
+
+.admin-interface #changelist .row1:not(.selected):hover,
+.admin-interface #changelist .row2:not(.selected):hover {
+ background: #f9f9f9;
+}
+
+.admin-interface .row2 {
+ background: #fcfcfc;
+}
+
+.admin-interface .row2 .ui-tabs .ui-tabs-nav li a {
+ background-color: #f5f5f5;
+ border: 1px solid #ebebeb;
+}
+
+.admin-interface .row2 .ui-tabs .ui-tabs-nav li.ui-tabs-active a,
+.admin-interface .row2 .ui-tabs .ui-tabs-nav li.ui-tabs-selected a {
+ background-color: #fcfcfc;
+ border-bottom: 1px solid #fcfcfc;
+}
diff --git a/backend/staticfiles/admin_interface/css/json-widget.css b/backend/staticfiles/admin_interface/css/json-widget.css
new file mode 100644
index 00000000..e0a999d1
--- /dev/null
+++ b/backend/staticfiles/admin_interface/css/json-widget.css
@@ -0,0 +1,27 @@
+/*
+django-json-widget support
+https://github.com/jmrivas86/django-json-widget
+*/
+
+.admin-interface div.jsoneditor {
+ border: 1px solid var(--admin-interface-module-background-color);
+ border-radius: var(--admin-interface-jsoneditor-border-radius);
+ overflow: var(--admin-interface-jsoneditor-overflow);
+}
+
+.admin-interface div.jsoneditor-menu {
+ background-color: var(--admin-interface-module-background-color);
+ border-bottom: 1px solid var(--admin-interface-module-background-color);
+}
+
+.admin-interface div.jsoneditor-menu a.jsoneditor-poweredBy {
+ color: var(--admin-interface-module-link-color);
+}
+
+.admin-interface div.jsoneditor-contextmenu ul li button.jsoneditor-selected,
+.admin-interface div.jsoneditor-contextmenu ul li button.jsoneditor-selected:focus,
+.admin-interface div.jsoneditor-contextmenu ul li button.jsoneditor-selected:hover {
+ background-color: var(--admin-interface-module-background-selected-color);
+ color: #000000;
+ font-weight: bold;
+}
diff --git a/backend/staticfiles/admin_interface/css/language-chooser.css b/backend/staticfiles/admin_interface/css/language-chooser.css
new file mode 100644
index 00000000..8459b9c1
--- /dev/null
+++ b/backend/staticfiles/admin_interface/css/language-chooser.css
@@ -0,0 +1,72 @@
+.admin-interface .language-chooser {
+ display: inline-block;
+ position: absolute;
+ top: 15px;
+ right: 15px;
+ z-index: 10;
+}
+
+@media (min-width: 768px) {
+ .admin-interface .language-chooser {
+ right: 30px;
+ }
+}
+
+@media (min-width: 1024px) {
+ .admin-interface .language-chooser {
+ position: static;
+ margin-left: 20px;
+ }
+}
+
+.admin-interface .language-chooser .language-chooser-hidden-form {
+ display: none;
+}
+
+.admin-interface .language-chooser .language-chooser-select-form {
+ display: inline-block;
+ position: relative;
+ z-index: 0;
+}
+
+.admin-interface .language-chooser select {
+ width: auto;
+ min-width: auto;
+}
+
+.admin-interface .language-chooser.minimal .language-chooser-select-form::after {
+ content: "";
+ position: absolute;
+ right: 2px;
+ top: 50%;
+ border: solid var(--admin-interface-header-text-color);
+ border-width: 0px 0px 1px 1px;
+ display: inline-block;
+ padding: 2px;
+ transform: rotate(-45deg);
+ -webkit-transform: rotate(-45deg);
+ pointer-events: none;
+ margin-top: -4px;
+}
+
+.admin-interface .language-chooser.minimal .language-chooser-select-form:hover select {
+ border-bottom: 1px solid transparent;
+ color: var(--admin-interface-header-link-hover-color);
+}
+
+.admin-interface .language-chooser.minimal select {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ background-color: transparent;
+ border: none;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.25);
+ border-radius: 0;
+ color: var(--admin-interface-header-link-color);
+ cursor: pointer;
+ font-weight: inherit;
+ font-size: inherit;
+ height: auto;
+ margin: 0;
+ padding: 0 15px 0 0;
+}
diff --git a/backend/staticfiles/admin_interface/css/list-filter-dropdown.css b/backend/staticfiles/admin_interface/css/list-filter-dropdown.css
new file mode 100644
index 00000000..207b3c7a
--- /dev/null
+++ b/backend/staticfiles/admin_interface/css/list-filter-dropdown.css
@@ -0,0 +1,27 @@
+/*
+list-filter-dropdown
+*/
+
+.admin-interface #changelist-filter .list-filter-dropdown {
+ margin-top: 15px;
+ margin-bottom: 15px;
+}
+
+.admin-interface #changelist-filter h2 + .list-filter-dropdown,
+.admin-interface #changelist-filter .list-filter-dropdown + .list-filter-dropdown {
+ margin-top: 5px;
+}
+
+.admin-interface #changelist-filter .list-filter-dropdown h3 {
+ margin-top: 0 !important;
+}
+
+.admin-interface #changelist-filter .list-filter-dropdown select {
+ background-color: #FFFFFF;
+ width: calc(100% - 30px);
+ margin-right: 15px;
+}
+
+.admin-interface.list-filter-highlight #changelist-filter .list-filter-dropdown h3.active + div select {
+ font-weight: bold;
+}
diff --git a/backend/staticfiles/admin_interface/css/modeltranslation.css b/backend/staticfiles/admin_interface/css/modeltranslation.css
new file mode 100644
index 00000000..9e1890b3
--- /dev/null
+++ b/backend/staticfiles/admin_interface/css/modeltranslation.css
@@ -0,0 +1,17 @@
+/*
+django-modeltranslation support
+https://github.com/deschler/django-modeltranslation
+*/
+
+.admin-interface #content h1 select {
+ text-transform: uppercase;
+ margin-left: 15px;
+ min-width: 50px;
+}
+
+.admin-interface .ui-tabs .ui-tabs-panel[id^=tab_id_] {
+ border: none;
+ border-top: 1px solid #eeeeee;
+ padding: 0;
+ margin-bottom: 0;
+}
diff --git a/backend/staticfiles/admin_interface/css/rangefilter.css b/backend/staticfiles/admin_interface/css/rangefilter.css
new file mode 100644
index 00000000..25b4f0ba
--- /dev/null
+++ b/backend/staticfiles/admin_interface/css/rangefilter.css
@@ -0,0 +1,25 @@
+.admin-interface #changelist-filter .admindatefilter {
+ border-bottom: 1px solid var(--hairline-color);
+}
+
+.admin-interface #changelist-filter .admindatefilter .button,
+.admin-interface #changelist-filter .admindatefilter .submit-row input,
+.admin-interface #changelist-filter .admindatefilter a.button,
+.admin-interface #changelist-filter .admindatefilter input[type="submit"],
+.admin-interface #changelist-filter .admindatefilter input[type="button"],
+.admin-interface #changelist-filter .admindatefilter input[type="reset"] {
+ background: var(--admin-interface-module-background-color);
+ color: var(--admin-interface-module-link-color);
+ padding: 6px 10px;
+ font-size: 12px;
+ margin-right: 4px;
+}
+
+.admin-interface #changelist-filter .admindatefilter .button:hover,
+.admin-interface #changelist-filter .admindatefilter .submit-row input:hover,
+.admin-interface #changelist-filter .admindatefilter a.button:hover,
+.admin-interface #changelist-filter .admindatefilter input[type="submit"]:hover,
+.admin-interface #changelist-filter .admindatefilter input[type="button"]:hover,
+.admin-interface #changelist-filter .admindatefilter input[type="reset"]:hover {
+ color: var(--admin-interface-module-link-hover-color);
+}
diff --git a/backend/staticfiles/admin_interface/css/recent-actions.css b/backend/staticfiles/admin_interface/css/recent-actions.css
new file mode 100644
index 00000000..6e251648
--- /dev/null
+++ b/backend/staticfiles/admin_interface/css/recent-actions.css
@@ -0,0 +1,10 @@
+.admin-interface.dashboard #content {
+ width: auto;
+ max-width: 600px;
+ margin-right: 0;
+ margin-left: 0;
+}
+
+.admin-interface.dashboard #content #recent-actions-module {
+ display: none;
+}
diff --git a/backend/staticfiles/admin_interface/css/related-modal.css b/backend/staticfiles/admin_interface/css/related-modal.css
new file mode 100644
index 00000000..015752f8
--- /dev/null
+++ b/backend/staticfiles/admin_interface/css/related-modal.css
@@ -0,0 +1,82 @@
+/*
+related modal + magnific popup customization
+https://github.com/dimsemenov/Magnific-Popup
+*/
+.admin-interface .related-modal.mfp-bg {
+ background-color: var(--admin-interface-related-modal-background-color);
+ opacity: var(--admin-interface-related-modal-background-opacity);
+}
+
+.admin-interface .related-modal .mfp-content {
+ height: 100%;
+ -webkit-box-shadow: 0px 5px 30px 0px rgba(0, 0, 0, 0.2);
+ -moz-box-shadow: 0px 5px 30px 0px rgba(0, 0, 0, 0.2);
+ box-shadow: 0px 5px 30px 0px rgba(0, 0, 0, 0.2);
+}
+
+.admin-interface .related-modal .mfp-container {
+ padding: 80px 80px 80px 80px;
+}
+
+.admin-interface .related-modal__nested .mfp-container {
+ padding: 40px 80px 40px 80px;
+}
+
+@media (max-width: 640px) {
+ .admin-interface .related-modal .mfp-container {
+ padding: 80px 20px 80px 20px;
+ }
+
+ .admin-interface .related-modal__nested .mfp-container {
+ padding: 40px 40px 40px 40px;
+ }
+}
+
+@media (max-height: 640px) {
+ .admin-interface .related-modal .mfp-container {
+ padding-top: 20px;
+ padding-bottom: 20px;
+ }
+
+ .admin-interface .related-modal__nested .mfp-container {
+ padding: 40px 40px 40px 40px;
+ }
+}
+
+.admin-interface .related-modal .related-modal-iframe-container {
+ display: block;
+ width: 100%;
+ height: 100%;
+ position: relative;
+ z-index: 100;
+ overflow: hidden;
+ border-radius: var(--admin-interface-related-modal-border-radius);
+}
+
+.admin-interface .related-modal #related-modal-iframe {
+ width: 100%;
+ height: 100%;
+ background-color: #FFFFFF;
+ background-repeat: no-repeat;
+ background-position: center center;
+ background-size: 30px 30px;
+ background-image: url("data:image/svg+xml;utf8,");
+ border: none;
+ margin: 0 auto;
+ padding: 0;
+ display: block;
+}
+
+.admin-interface .related-modal .mfp-close {
+ width: 40px;
+ height: 40px;
+ opacity: 1.0;
+ color: rgba(0, 0, 0, 0.4);
+ display: var(--admin-interface-related-modal-close-button-display);
+}
+
+.admin-interface .related-modal .mfp-close:hover,
+.admin-interface .related-modal .mfp-close:active {
+ color: rgba(0, 0, 0, 0.6);
+ top: 0;
+}
diff --git a/backend/staticfiles/admin_interface/css/rtl.css b/backend/staticfiles/admin_interface/css/rtl.css
new file mode 100644
index 00000000..30c0b9d0
--- /dev/null
+++ b/backend/staticfiles/admin_interface/css/rtl.css
@@ -0,0 +1,34 @@
+[dir="rtl"] .admin-interface,
+[dir="rtl"] .admin-interface * {
+ font-family: 'Vazir', sans-serif !important;
+}
+
+[dir="rtl"] .admin-interface .main .toggle-nav-sidebar.sticky {
+ left: auto !important;
+ right: 0px !important;
+ margin-right: 0px !important;
+ margin-left: 10px;
+ border: 1px solid #eaeaea !important;
+ border-right: none !important;
+ border-top-right-radius: 0px !important;
+ border-bottom-right-radius: 0px !important;
+ border-top-left-radius: 4px;
+ border-bottom-left-radius: 4px;
+ box-shadow: -4px 2px 8px -2px #DBDBDB !important;
+}
+
+[dir="rtl"] .admin-interface #main.shifted > #toggle-nav-sidebar {
+ right: 359px !important;
+}
+
+[dir="rtl"] .admin-interface #main > #nav-sidebar {
+ margin-right: -360px !important;
+ margin-left: 0px !important;
+ right: -320px !important;
+}
+
+[dir="rtl"] .admin-interface #main.shifted > #nav-sidebar {
+ border-left: 1px solid #eaeaea;
+ margin-right: 0px !important;
+ padding: 40px 0px 40px 40px !important;
+}
diff --git a/backend/staticfiles/admin_interface/css/sorl-thumbnail.css b/backend/staticfiles/admin_interface/css/sorl-thumbnail.css
new file mode 100644
index 00000000..44c61909
--- /dev/null
+++ b/backend/staticfiles/admin_interface/css/sorl-thumbnail.css
@@ -0,0 +1,67 @@
+/*
+sorl-thumbnail - improved AdminImageMixin widget layout
+https://github.com/mariocesar/sorl-thumbnail
+*/
+
+.admin-interface a.thumbnail + a {
+ font-weight: normal;
+}
+
+.admin-interface a.thumbnail + a + input[type="checkbox"] {
+ margin: 10px 0px 10px 18px;
+}
+
+@media (max-width: 767px){
+ .admin-interface a.thumbnail + a {
+ display: block;
+ margin-top: 3px;
+ white-space: pre-wrap;
+ word-break: break-word;
+ }
+ .admin-interface a.thumbnail + a + input[type="checkbox"] {
+ margin: 15px 0;
+ }
+}
+
+.admin-interface a.thumbnail ~ label {
+ color: #333;
+ font-size: 11px;
+ display: inline;
+ float: none;
+ margin-left: 2px;
+}
+
+.admin-interface.change-form div[style="float:left"] {
+ font-size: 11px;
+ font-weight: bold;
+ color: #666;
+ margin-bottom: 5px;
+}
+
+@media (max-width: 767px){
+ .admin-interface.change-form div[style="float:left"] {
+ font-size: 12px;
+ width: 100%;
+ }
+}
+
+.admin-interface .aligned .form-row a.thumbnail ~ input[type="file"] {
+ margin-top: 0px;
+}
+
+@media (max-width:767px){
+ .admin-interface .aligned .form-row a.thumbnail ~ input[type="file"] {
+ width: auto;
+ padding: 0px;
+ display: block;
+ margin-top: 3px;
+ }
+
+ .admin-interface div[style="float:left"] {
+ margin-bottom: 0px;
+ }
+
+ .admin-interface div[style="float:left"] + div.help {
+ margin-top: 0px !important;
+ }
+}
diff --git a/backend/staticfiles/admin_interface/css/streamfield.css b/backend/staticfiles/admin_interface/css/streamfield.css
new file mode 100644
index 00000000..71ce2dbe
--- /dev/null
+++ b/backend/staticfiles/admin_interface/css/streamfield.css
@@ -0,0 +1,200 @@
+/*
+django-streamfield support
+https://github.com/raagin/django-streamfield/
+*/
+
+.admin-interface .form-row.field-stream {
+ margin: 0;
+ padding: 0;
+ border-bottom: none;
+}
+
+.admin-interface .form-row.field-stream label[for=id_stream] {
+ display: none;
+}
+
+.admin-interface .streamfield_app {
+ clear: both;
+ width: 100%;
+}
+
+.admin-interface .streamfield_app .stream-help-text {
+ margin-bottom: 15px;
+ display: flex;
+ flex-direction: column;
+ clear: both;
+}
+
+.admin-interface .streamfield_app .stream-help-text .stream-help-text__title {
+ align-self: flex-end;
+ user-select: none;
+ padding: 8px;
+ padding-right: 0;
+ color: var(--admin-interface-generic-link-color);
+}
+
+.admin-interface .streamfield_app .stream-help-text .stream-help-text__title:hover {
+ color: var(--admin-interface-generic-link-hover-color);
+}
+
+.admin-interface .streamfield_app .stream-help-text .stream-help-text__content {
+ background: var(--admin-interface-module-background-selected-color);
+ border-radius: var(--admin-interface-module-border-radius);
+ border: 1px solid rgba(0,0,0,0.1);
+ padding: 15px;
+}
+
+.admin-interface .streamfield_app .stream-help-text .stream-help-text__content > ul {
+ margin: 0;
+ padding: 0;
+}
+
+.admin-interface .streamfield_app .collapse-handler {
+ user-select: none;
+ padding: 8px;
+ padding-right: 0;
+ margin: 0 0 5px 0;
+ color: var(--admin-interface-generic-link-color);
+ text-decoration: none;
+}
+
+.admin-interface .streamfield_app .collapse-handler:hover {
+ color: var(--admin-interface-generic-link-hover-color);
+ text-decoration: none;
+}
+
+.admin-interface .streamfield_app .stream-model-block {
+ position: relative;
+ box-shadow: none;
+ border: 1px solid rgba(0,0,0,0.1);
+ border-radius: var(--admin-interface-module-border-radius);
+ overflow: hidden;
+}
+
+.admin-interface .streamfield_app .stream-model-block,
+.admin-interface .streamfield_app .streamfield-models.collapsed .stream-model-block {
+ margin-bottom: 10px;
+}
+
+.admin-interface .streamfield_app .stream-model-block .streamblock__block__title {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ margin: 0;
+ padding: 10px 10px 10px 20px;
+}
+
+.admin-interface .streamfield_app .stream-model-block .streamblock__block__title span {
+ font-size: 18px;
+}
+
+.admin-interface .streamfield_app .stream-model-block .streamblock__block__title .streamblock__block-handle {
+ position: static;
+ right: 0;
+ top: 0;
+ color: var(--admin-interface-generic-link-color);
+}
+
+.admin-interface .streamfield_app .stream-model-block .streamblock__block__title .streamblock__block-handle:hover {
+ color: var(--admin-interface-generic-link-hover-color);
+}
+
+.admin-interface .streamfield_app .stream-model-block .streamblock__block__title .streamblock__block-handle {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.admin-interface .streamfield_app .stream-model-block .streamblock__block__title .streamblock__block-handle .block-move,
+.admin-interface .streamfield_app .stream-model-block .streamblock__block__title .streamblock__block-handle .block-delete {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 40px;
+ height: 40px;
+ font-weight: normal;
+ background: none;
+ flex-shrink: 0;
+ color: inherit;
+ font-size: 16px;
+}
+
+.admin-interface .streamfield_app .stream-model-block .streamblock__block__title .streamblock__block-handle .block-move {
+ cursor: move; /* fallback if grab cursor is unsupported */
+ cursor: grab;
+ cursor: -moz-grab;
+ cursor: -webkit-grab;
+}
+
+.admin-interface .streamfield_app .stream-model-block .streamblock__block__title .streamblock__block-handle .block-move:before {
+ content: "↕";
+
+ display: block;
+}
+
+.admin-interface .streamfield_app .stream-model-block .streamblock__block__title .streamblock__block-handle .block-delete:before {
+ content: "×";
+ display: block;
+ font-size: 18px;
+}
+
+.admin-interface .streamfield_app .block-fields > div {
+ margin-bottom: 15px;
+}
+
+.admin-interface .streamfield_app .stream-model-block .stream-model-block__content {
+ background-color: #f8f8f8;
+ padding: 20px;
+}
+
+.admin-interface .streamfield_app .stream-model-block .stream-model-block__content.no-subblocks.abstract-block {
+ display: none;
+}
+
+.admin-interface .streamfield_app .stream-insert-new-block {
+ margin-bottom: 20px;
+}
+
+.admin-interface .streamfield_app .stream-insert-new-block .add-new-block-button {
+ color: var(--admin-interface-generic-link-color);
+ text-decoration: none;
+}
+
+.admin-interface .streamfield_app .stream-insert-new-block .add-new-block-button:hover {
+ color: var(--admin-interface-generic-link-hover-color);
+ text-decoration: none;
+}
+
+.admin-interface .streamfield_app .stream-insert-new-block ul {
+ display: block;
+ width: 100%;
+ margin: 10px 0 0 0;
+ padding: 0;
+ user-select: none;
+}
+
+.admin-interface .streamfield_app .stream-insert-new-block ul li {
+ display: inline-block;
+ font-size: 12px;
+ margin: 0;
+ padding: 0;
+}
+
+.admin-interface .streamfield_app .stream-btn {
+ font-weight: normal;
+ text-decoration: none;
+ background-color: var(--admin-interface-generic-link-color);
+ padding: 6px 12px;
+ border-radius: 4px;
+}
+
+.admin-interface .streamfield_app .stream-btn:hover {
+ text-decoration: none;
+ background-color: var(--admin-interface-generic-link-hover-color);
+}
+
+.admin-interface .streamfield_app .stream-insert-new-block ul li .stream-btn {
+ margin-top: 5px;
+ margin-left: 5px;
+}
diff --git a/backend/staticfiles/admin_interface/css/tabbed-admin.css b/backend/staticfiles/admin_interface/css/tabbed-admin.css
new file mode 100644
index 00000000..010296c5
--- /dev/null
+++ b/backend/staticfiles/admin_interface/css/tabbed-admin.css
@@ -0,0 +1,37 @@
+/*
+django-tabbed-admin support
+https://github.com/omji/django-tabbed-admin
+*/
+
+/* Hide tabs until ready */
+/*
+.admin-interface #tabs ul {
+ display: none;
+}
+
+.admin-interface #tabs ul.ui-tabs-nav {
+ display: block;
+}
+*/
+
+.admin-interface .ui-tabs .ui-tabs-panel[id^=tabs] .module.aligned:last-child {
+ margin-bottom: 0;
+}
+
+.admin-interface .ui-tabs .ui-tabs-panel[id^=tabs] .module.aligned:last-child .form-row:last-child {
+ border-bottom: none;
+}
+
+@media (max-width: 350px){
+ .admin-interface .ui-tabs .ui-tabs-panel[id^=tabs] .vTextField,
+ .admin-interface .inline-related .vTextField {
+ width: 17em;
+ }
+}
+
+@media (max-width: 767px){
+ /* fix horizontal overflow - responsive.css:563 */
+ .admin-interface .ui-tabs .ui-tabs-panel[id^=tabs] .aligned .form-row > div:not([class]) {
+ width: 100% !important;
+ }
+}
diff --git a/backend/staticfiles/admin_interface/css/tabbed-changeform.css b/backend/staticfiles/admin_interface/css/tabbed-changeform.css
new file mode 100644
index 00000000..a0f6718f
--- /dev/null
+++ b/backend/staticfiles/admin_interface/css/tabbed-changeform.css
@@ -0,0 +1,67 @@
+.admin-interface .tabbed-changeform-tabs {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ overflow-x: auto;
+ overflow-y: hidden;
+ scrollbar-width: thin;
+ padding-bottom: 15px;
+}
+
+@-moz-document url-prefix() {
+ .admin-interface .tabbed-changeform-tabs {
+ padding-bottom: 13px;
+ }
+}
+
+.admin-interface .tabbed-changeform-tabs .tabbed-changeform-tablink {
+ appearance: none;
+ -webkit-appearance: none;
+ border: 1px solid transparent;
+ border-bottom: 1px solid var(--border-color);
+ border-radius: var(--admin-interface-module-border-radius);
+ border-bottom-left-radius: 0px;
+ border-bottom-right-radius: 0px;
+ flex-shrink: 0;
+ flex-grow: 0;
+ cursor: pointer;
+ padding: 10px 15px;
+ margin: 0;
+ background-color: var(--admin-interface-module-header-text-color);
+ color: var(--admin-interface-generic-link-color);
+ font-size: 13px;
+ font-weight: bold;
+ outline: none !important;
+}
+
+.admin-interface .tabbed-changeform-tabs .tabbed-changeform-tablink + .tabbed-changeform-tablink {
+ margin-left: -1px;
+}
+
+.admin-interface .tabbed-changeform-tabs .tabbed-changeform-tablink:hover {
+ color: var(--admin-interface-generic-link-hover-color);
+}
+
+.admin-interface .tabbed-changeform-tabs .tabbed-changeform-tablink:focus {
+ color: var(--admin-interface-generic-link-hover-color);
+}
+
+.admin-interface .tabbed-changeform-tabs .tabbed-changeform-tablink.active {
+ border: 1px solid var(--border-color);
+ border-bottom: 1px solid transparent;
+ color: var(--admin-interface-generic-link-active-color);
+}
+
+.admin-interface .tabbed-changeform-tabs-remaining-space {
+ flex: 1;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.admin-interface .tabbed-changeform-tabcontent {
+ display: none;
+ padding: 1em 0;
+}
+
+.admin-interface .tabbed-changeform-tabcontent.active {
+ display: block;
+}
diff --git a/backend/staticfiles/admin_interface/css/tinymce.css b/backend/staticfiles/admin_interface/css/tinymce.css
new file mode 100644
index 00000000..76ed1b41
--- /dev/null
+++ b/backend/staticfiles/admin_interface/css/tinymce.css
@@ -0,0 +1,3 @@
+.admin-interface textarea.tinymce ~ p.help {
+ margin-top:5px !important;
+}
diff --git a/backend/staticfiles/admin_interface/favico/favico-0.3.10-patched.js b/backend/staticfiles/admin_interface/favico/favico-0.3.10-patched.js
new file mode 100644
index 00000000..bc791352
--- /dev/null
+++ b/backend/staticfiles/admin_interface/favico/favico-0.3.10-patched.js
@@ -0,0 +1,913 @@
+/**
+ * @license MIT or GPL-2.0
+ * @fileOverview Favico animations
+ * @author Miroslav Magda, http://blog.ejci.net
+ * @source: https://github.com/ejci/favico.js
+ * @version 0.3.10
+ */
+
+/**
+ * Create new favico instance
+ * @param {Object} Options
+ * @return {Object} Favico object
+ * @example
+ * var favico = new Favico({
+ * bgColor : '#d00',
+ * textColor : '#fff',
+ * fontFamily : 'sans-serif',
+ * fontStyle : 'bold',
+ * type : 'circle',
+ * position : 'down',
+ * animation : 'slide',
+ * elementId: false,
+ * element: null,
+ * dataUrl: function(url){},
+ * win: window
+ * });
+ */
+(function () {
+
+ var Favico = (function (opt) {
+ 'use strict';
+ opt = (opt) ? opt : {};
+ var _def = {
+ bgColor: '#d00',
+ textColor: '#fff',
+ fontFamily: 'sans-serif', //Arial,Verdana,Times New Roman,serif,sans-serif,...
+ fontStyle: 'bold', //normal,italic,oblique,bold,bolder,lighter,100,200,300,400,500,600,700,800,900
+ type: 'circle',
+ position: 'down', // down, up, left, leftup (upleft)
+ animation: 'slide',
+ elementId: false,
+ element: null,
+ dataUrl: false,
+ win: window
+ };
+ var _opt, _orig, _h, _w, _canvas, _context, _img, _ready, _lastBadge, _running, _readyCb, _stop, _browser, _animTimeout, _drawTimeout, _doc;
+
+ _browser = {};
+ _browser.ff = typeof InstallTrigger != 'undefined';
+ _browser.chrome = !!window.chrome;
+ _browser.opera = !!window.opera || navigator.userAgent.indexOf('Opera') >= 0;
+ _browser.ie = /*@cc_on!@*/false;
+ _browser.safari = Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0;
+ _browser.supported = (_browser.chrome || _browser.ff || _browser.opera);
+
+ var _queue = [];
+ _readyCb = function () {
+ };
+ _ready = _stop = false;
+ /**
+ * Initialize favico
+ */
+ var init = function () {
+ //merge initial options
+ _opt = merge(_def, opt);
+ _opt.bgColor = hexToRgb(_opt.bgColor);
+ _opt.textColor = hexToRgb(_opt.textColor);
+ _opt.position = _opt.position.toLowerCase();
+ _opt.animation = (animation.types['' + _opt.animation]) ? _opt.animation : _def.animation;
+
+ _doc = _opt.win.document;
+
+ var isUp = _opt.position.indexOf('up') > -1;
+ var isLeft = _opt.position.indexOf('left') > -1;
+
+ //transform the animations
+ if (isUp || isLeft) {
+ for (var a in animation.types) {
+ for (var i = 0; i < animation.types[a].length; i++) {
+ var step = animation.types[a][i];
+
+ if (isUp) {
+ if (step.y < 0.6) {
+ step.y = step.y - 0.4;
+ } else {
+ step.y = step.y - 2 * step.y + (1 - step.w);
+ }
+ }
+
+ if (isLeft) {
+ if (step.x < 0.6) {
+ step.x = step.x - 0.4;
+ } else {
+ step.x = step.x - 2 * step.x + (1 - step.h);
+ }
+ }
+
+ animation.types[a][i] = step;
+ }
+ }
+ }
+ _opt.type = (type['' + _opt.type]) ? _opt.type : _def.type;
+
+ _orig = link. getIcons();
+ //create temp canvas
+ _canvas = document.createElement('canvas');
+ //create temp image
+ _img = document.createElement('img');
+ var lastIcon = _orig[_orig.length - 1];
+ if (lastIcon.hasAttribute('href')) {
+ _img.setAttribute('crossOrigin', 'anonymous');
+ //get width/height
+ _img.onload = function () {
+ _h = (_img.height > 0) ? _img.height : 32;
+ _w = (_img.width > 0) ? _img.width : 32;
+ _canvas.height = _h;
+ _canvas.width = _w;
+ _context = _canvas.getContext('2d');
+ icon.ready();
+ };
+ _img.setAttribute('src', lastIcon.getAttribute('href'));
+ } else {
+ _h = 32;
+ _w = 32;
+ _img.height = _h;
+ _img.width = _w;
+ _canvas.height = _h;
+ _canvas.width = _w;
+ _context = _canvas.getContext('2d');
+ icon.ready();
+ }
+
+ };
+ /**
+ * Icon namespace
+ */
+ var icon = {};
+ /**
+ * Icon is ready (reset icon) and start animation (if ther is any)
+ */
+ icon.ready = function () {
+ _ready = true;
+ icon.reset();
+ _readyCb();
+ };
+ /**
+ * Reset icon to default state
+ */
+ icon.reset = function () {
+ //reset
+ if (!_ready) {
+ return;
+ }
+ _queue = [];
+ _lastBadge = false;
+ _running = false;
+ _context.clearRect(0, 0, _w, _h);
+ _context.drawImage(_img, 0, 0, _w, _h);
+ //_stop=true;
+ link.setIcon(_canvas);
+ //webcam('stop');
+ //video('stop');
+ window.clearTimeout(_animTimeout);
+ window.clearTimeout(_drawTimeout);
+ };
+ /**
+ * Start animation
+ */
+ icon.start = function () {
+ if (!_ready || _running) {
+ return;
+ }
+ var finished = function () {
+ _lastBadge = _queue[0];
+ _running = false;
+ if (_queue.length > 0) {
+ _queue.shift();
+ icon.start();
+ } else {
+
+ }
+ };
+ if (_queue.length > 0) {
+ _running = true;
+ var run = function () {
+ // apply options for this animation
+ ['type', 'animation', 'bgColor', 'textColor', 'fontFamily', 'fontStyle'].forEach(function (a) {
+ if (a in _queue[0].options) {
+ _opt[a] = _queue[0].options[a];
+ }
+ });
+ animation.run(_queue[0].options, function () {
+ finished();
+ }, false);
+ };
+ if (_lastBadge) {
+ animation.run(_lastBadge.options, function () {
+ run();
+ }, true);
+ } else {
+ run();
+ }
+ }
+ };
+
+ /**
+ * Badge types
+ */
+ var type = {};
+ var options = function (opt) {
+ opt.n = ((typeof opt.n) === 'number') ? Math.abs(opt.n | 0) : opt.n;
+ opt.x = _w * opt.x;
+ opt.y = _h * opt.y;
+ opt.w = _w * opt.w;
+ opt.h = _h * opt.h;
+ opt.len = ("" + opt.n).length;
+ return opt;
+ };
+
+ /**
+ * Generate circle
+ * @param {Object} opt Badge options
+ */
+ type.circle = function (opt) {
+ opt = options(opt);
+ var more = false;
+ if (opt.len === 2) {
+ opt.x = opt.x - opt.w * 0.4;
+ opt.w = opt.w * 1.4;
+ more = true;
+ } else if (opt.len >= 3) {
+ opt.x = opt.x - opt.w * 0.65;
+ opt.w = opt.w * 1.65;
+ more = true;
+ }
+
+ // begin patch
+ // draw a smaller badge without text
+ opt.w = 14;
+ opt.h = 14;
+ opt.x = (_w - opt.w);
+ opt.y = (_h - opt.h);
+ // end patch
+
+ _context.clearRect(0, 0, _w, _h);
+ _context.drawImage(_img, 0, 0, _w, _h);
+ _context.beginPath();
+ _context.font = _opt.fontStyle + " " + Math.floor(opt.h * (opt.n > 99 ? 0.85 : 1)) + "px " + _opt.fontFamily;
+ _context.textAlign = 'center';
+ if (more) {
+ _context.moveTo(opt.x + opt.w / 2, opt.y);
+ _context.lineTo(opt.x + opt.w - opt.h / 2, opt.y);
+ _context.quadraticCurveTo(opt.x + opt.w, opt.y, opt.x + opt.w, opt.y + opt.h / 2);
+ _context.lineTo(opt.x + opt.w, opt.y + opt.h - opt.h / 2);
+ _context.quadraticCurveTo(opt.x + opt.w, opt.y + opt.h, opt.x + opt.w - opt.h / 2, opt.y + opt.h);
+ _context.lineTo(opt.x + opt.h / 2, opt.y + opt.h);
+ _context.quadraticCurveTo(opt.x, opt.y + opt.h, opt.x, opt.y + opt.h - opt.h / 2);
+ _context.lineTo(opt.x, opt.y + opt.h / 2);
+ _context.quadraticCurveTo(opt.x, opt.y, opt.x + opt.h / 2, opt.y);
+ } else {
+ _context.arc(opt.x + opt.w / 2, opt.y + opt.h / 2, opt.h / 2, 0, 2 * Math.PI);
+ }
+ _context.fillStyle = 'rgba(' + _opt.bgColor.r + ',' + _opt.bgColor.g + ',' + _opt.bgColor.b + ',' + opt.o + ')';
+ _context.fill();
+ _context.closePath();
+ _context.beginPath();
+ _context.stroke();
+ _context.fillStyle = 'rgba(' + _opt.textColor.r + ',' + _opt.textColor.g + ',' + _opt.textColor.b + ',' + opt.o + ')';
+ //_context.fillText((more) ? '9+' : opt.n, Math.floor(opt.x + opt.w / 2), Math.floor(opt.y + opt.h - opt.h * 0.15));
+ if ((typeof opt.n) === 'number' && opt.n > 999) {
+ _context.fillText(((opt.n > 9999) ? 9 : Math.floor(opt.n / 1000)) + 'k+', Math.floor(opt.x + opt.w / 2), Math.floor(opt.y + opt.h - opt.h * 0.2));
+ } else {
+ _context.fillText(opt.n, Math.floor(opt.x + opt.w / 2), Math.floor(opt.y + opt.h - opt.h * 0.15));
+ }
+ _context.closePath();
+ };
+ /**
+ * Generate rectangle
+ * @param {Object} opt Badge options
+ */
+ type.rectangle = function (opt) {
+ opt = options(opt);
+ var more = false;
+ if (opt.len === 2) {
+ opt.x = opt.x - opt.w * 0.4;
+ opt.w = opt.w * 1.4;
+ more = true;
+ } else if (opt.len >= 3) {
+ opt.x = opt.x - opt.w * 0.65;
+ opt.w = opt.w * 1.65;
+ more = true;
+ }
+ _context.clearRect(0, 0, _w, _h);
+ _context.drawImage(_img, 0, 0, _w, _h);
+ _context.beginPath();
+ _context.font = _opt.fontStyle + " " + Math.floor(opt.h * (opt.n > 99 ? 0.9 : 1)) + "px " + _opt.fontFamily;
+ _context.textAlign = 'center';
+ _context.fillStyle = 'rgba(' + _opt.bgColor.r + ',' + _opt.bgColor.g + ',' + _opt.bgColor.b + ',' + opt.o + ')';
+ _context.fillRect(opt.x, opt.y, opt.w, opt.h);
+ _context.fillStyle = 'rgba(' + _opt.textColor.r + ',' + _opt.textColor.g + ',' + _opt.textColor.b + ',' + opt.o + ')';
+ //_context.fillText((more) ? '9+' : opt.n, Math.floor(opt.x + opt.w / 2), Math.floor(opt.y + opt.h - opt.h * 0.15));
+ if ((typeof opt.n) === 'number' && opt.n > 999) {
+ _context.fillText(((opt.n > 9999) ? 9 : Math.floor(opt.n / 1000)) + 'k+', Math.floor(opt.x + opt.w / 2), Math.floor(opt.y + opt.h - opt.h * 0.2));
+ } else {
+ _context.fillText(opt.n, Math.floor(opt.x + opt.w / 2), Math.floor(opt.y + opt.h - opt.h * 0.15));
+ }
+ _context.closePath();
+ };
+
+ /**
+ * Set badge
+ */
+ var badge = function (number, opts) {
+ opts = ((typeof opts) === 'string' ? {
+ animation: opts
+ } : opts) || {};
+ _readyCb = function () {
+ try {
+ if (typeof (number) === 'number' ? (number > 0) : (number !== '')) {
+ var q = {
+ type: 'badge',
+ options: {
+ n: number
+ }
+ };
+ if ('animation' in opts && animation.types['' + opts.animation]) {
+ q.options.animation = '' + opts.animation;
+ }
+ if ('type' in opts && type['' + opts.type]) {
+ q.options.type = '' + opts.type;
+ }
+ ['bgColor', 'textColor'].forEach(function (o) {
+ if (o in opts) {
+ q.options[o] = hexToRgb(opts[o]);
+ }
+ });
+ ['fontStyle', 'fontFamily'].forEach(function (o) {
+ if (o in opts) {
+ q.options[o] = opts[o];
+ }
+ });
+ _queue.push(q);
+ if (_queue.length > 100) {
+ throw new Error('Too many badges requests in queue.');
+ }
+ icon.start();
+ } else {
+ icon.reset();
+ }
+ } catch (e) {
+ throw new Error('Error setting badge. Message: ' + e.message);
+ }
+ };
+ if (_ready) {
+ _readyCb();
+ }
+ };
+
+ /**
+ * Set image as icon
+ */
+ var image = function (imageElement) {
+ _readyCb = function () {
+ try {
+ var w = imageElement.width;
+ var h = imageElement.height;
+ var newImg = document.createElement('img');
+ var ratio = (w / _w < h / _h) ? (w / _w) : (h / _h);
+ newImg.setAttribute('crossOrigin', 'anonymous');
+ newImg.onload=function(){
+ _context.clearRect(0, 0, _w, _h);
+ _context.drawImage(newImg, 0, 0, _w, _h);
+ link.setIcon(_canvas);
+ };
+ newImg.setAttribute('src', imageElement.getAttribute('src'));
+ newImg.height = (h / ratio);
+ newImg.width = (w / ratio);
+ } catch (e) {
+ throw new Error('Error setting image. Message: ' + e.message);
+ }
+ };
+ if (_ready) {
+ _readyCb();
+ }
+ };
+ /**
+ * Set the icon from a source url. Won't work with badges.
+ */
+ var rawImageSrc = function (url) {
+ _readyCb = function() {
+ link.setIconSrc(url);
+ };
+ if (_ready) {
+ _readyCb();
+ }
+ };
+ /**
+ * Set video as icon
+ */
+ var video = function (videoElement) {
+ _readyCb = function () {
+ try {
+ if (videoElement === 'stop') {
+ _stop = true;
+ icon.reset();
+ _stop = false;
+ return;
+ }
+ //var w = videoElement.width;
+ //var h = videoElement.height;
+ //var ratio = (w / _w < h / _h) ? (w / _w) : (h / _h);
+ videoElement.addEventListener('play', function () {
+ drawVideo(this);
+ }, false);
+
+ } catch (e) {
+ throw new Error('Error setting video. Message: ' + e.message);
+ }
+ };
+ if (_ready) {
+ _readyCb();
+ }
+ };
+ /**
+ * Set video as icon
+ */
+ var webcam = function (action) {
+ //UR
+ if (!window.URL || !window.URL.createObjectURL) {
+ window.URL = window.URL || {};
+ window.URL.createObjectURL = function (obj) {
+ return obj;
+ };
+ }
+ if (_browser.supported) {
+ var newVideo = false;
+ navigator.getUserMedia = navigator.getUserMedia || navigator.oGetUserMedia || navigator.msGetUserMedia || navigator.mozGetUserMedia || navigator.webkitGetUserMedia;
+ _readyCb = function () {
+ try {
+ if (action === 'stop') {
+ _stop = true;
+ icon.reset();
+ _stop = false;
+ return;
+ }
+ newVideo = document.createElement('video');
+ newVideo.width = _w;
+ newVideo.height = _h;
+ navigator.getUserMedia({
+ video: true,
+ audio: false
+ }, function (stream) {
+ newVideo.src = URL.createObjectURL(stream);
+ newVideo.play();
+ drawVideo(newVideo);
+ }, function () {
+ });
+ } catch (e) {
+ throw new Error('Error setting webcam. Message: ' + e.message);
+ }
+ };
+ if (_ready) {
+ _readyCb();
+ }
+ }
+
+ };
+
+ var setOpt = function (key, value) {
+ var opts = key;
+ if (!(value == null && Object.prototype.toString.call(key) == '[object Object]')) {
+ opts = {};
+ opts[key] = value;
+ }
+
+ var keys = Object.keys(opts);
+ for (var i = 0; i < keys.length; i++) {
+ if (keys[i] == 'bgColor' || keys[i] == 'textColor') {
+ _opt[keys[i]] = hexToRgb(opts[keys[i]]);
+ } else {
+ _opt[keys[i]] = opts[keys[i]];
+ }
+ }
+
+ _queue.push(_lastBadge);
+ icon.start();
+ };
+
+ /**
+ * Draw video to context and repeat :)
+ */
+ function drawVideo(video) {
+ if (video.paused || video.ended || _stop) {
+ return false;
+ }
+ //nasty hack for FF webcam (Thanks to Julian Ćwirko, kontakt@redsunmedia.pl)
+ try {
+ _context.clearRect(0, 0, _w, _h);
+ _context.drawImage(video, 0, 0, _w, _h);
+ } catch (e) {
+
+ }
+ _drawTimeout = setTimeout(function () {
+ drawVideo(video);
+ }, animation.duration);
+ link.setIcon(_canvas);
+ }
+
+ var link = {};
+ /**
+ * Get icons from HEAD tag or create a new element
+ */
+ link.getIcons = function () {
+ var elms = [];
+ //get link element
+ var getLinks = function () {
+ var icons = [];
+ var links = _doc.getElementsByTagName('head')[0].getElementsByTagName('link');
+ for (var i = 0; i < links.length; i++) {
+ if ((/(^|\s)icon(\s|$)/i).test(links[i].getAttribute('rel'))) {
+ icons.push(links[i]);
+ }
+ }
+ return icons;
+ };
+ if (_opt.element) {
+ elms = [_opt.element];
+ } else if (_opt.elementId) {
+ //if img element identified by elementId
+ elms = [_doc.getElementById(_opt.elementId)];
+ elms[0].setAttribute('href', elms[0].getAttribute('src'));
+ } else {
+ //if link element
+ elms = getLinks();
+ if (elms.length === 0) {
+ elms = [_doc.createElement('link')];
+ elms[0].setAttribute('rel', 'icon');
+ _doc.getElementsByTagName('head')[0].appendChild(elms[0]);
+ }
+ }
+ elms.forEach(function(item) {
+ item.setAttribute('type', 'image/png');
+ });
+ return elms;
+ };
+ link.setIcon = function (canvas) {
+ var url = canvas.toDataURL('image/png');
+ link.setIconSrc(url);
+ };
+ link.setIconSrc = function (url) {
+ if (_opt.dataUrl) {
+ //if using custom exporter
+ _opt.dataUrl(url);
+ }
+ if (_opt.element) {
+ _opt.element.setAttribute('href', url);
+ _opt.element.setAttribute('src', url);
+ } else if (_opt.elementId) {
+ //if is attached to element (image)
+ var elm = _doc.getElementById(_opt.elementId);
+ elm.setAttribute('href', url);
+ elm.setAttribute('src', url);
+ } else {
+ //if is attached to fav icon
+ if (_browser.ff || _browser.opera) {
+ //for FF we need to "recreate" element, atach to dom and remove old
+ //var originalType = _orig.getAttribute('rel');
+ var old = _orig[_orig.length - 1];
+ var newIcon = _doc.createElement('link');
+ _orig = [newIcon];
+ //_orig.setAttribute('rel', originalType);
+ if (_browser.opera) {
+ newIcon.setAttribute('rel', 'icon');
+ }
+ newIcon.setAttribute('rel', 'icon');
+ newIcon.setAttribute('type', 'image/png');
+ _doc.getElementsByTagName('head')[0].appendChild(newIcon);
+ newIcon.setAttribute('href', url);
+ if (old.parentNode) {
+ old.parentNode.removeChild(old);
+ }
+ } else {
+ _orig.forEach(function(icon) {
+ icon.setAttribute('href', url);
+ });
+ }
+ }
+ };
+
+ //http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb#answer-5624139
+ //HEX to RGB convertor
+ function hexToRgb(hex) {
+ var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
+ hex = hex.replace(shorthandRegex, function (m, r, g, b) {
+ return r + r + g + g + b + b;
+ });
+ var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+ return result ? {
+ r: parseInt(result[1], 16),
+ g: parseInt(result[2], 16),
+ b: parseInt(result[3], 16)
+ } : false;
+ }
+
+ /**
+ * Merge options
+ */
+ function merge(def, opt) {
+ var mergedOpt = {};
+ var attrname;
+ for (attrname in def) {
+ mergedOpt[attrname] = def[attrname];
+ }
+ for (attrname in opt) {
+ mergedOpt[attrname] = opt[attrname];
+ }
+ return mergedOpt;
+ }
+
+ /**
+ * Cross-browser page visibility shim
+ * http://stackoverflow.com/questions/12536562/detect-whether-a-window-is-visible
+ */
+ function isPageHidden() {
+ return _doc.hidden || _doc.msHidden || _doc.webkitHidden || _doc.mozHidden;
+ }
+
+ /**
+ * @namespace animation
+ */
+ var animation = {};
+ /**
+ * Animation "frame" duration
+ */
+ animation.duration = 40;
+ /**
+ * Animation types (none,fade,pop,slide)
+ */
+ animation.types = {};
+ animation.types.fade = [{
+ x: 0.4,
+ y: 0.4,
+ w: 0.6,
+ h: 0.6,
+ o: 0.0
+ }, {
+ x: 0.4,
+ y: 0.4,
+ w: 0.6,
+ h: 0.6,
+ o: 0.1
+ }, {
+ x: 0.4,
+ y: 0.4,
+ w: 0.6,
+ h: 0.6,
+ o: 0.2
+ }, {
+ x: 0.4,
+ y: 0.4,
+ w: 0.6,
+ h: 0.6,
+ o: 0.3
+ }, {
+ x: 0.4,
+ y: 0.4,
+ w: 0.6,
+ h: 0.6,
+ o: 0.4
+ }, {
+ x: 0.4,
+ y: 0.4,
+ w: 0.6,
+ h: 0.6,
+ o: 0.5
+ }, {
+ x: 0.4,
+ y: 0.4,
+ w: 0.6,
+ h: 0.6,
+ o: 0.6
+ }, {
+ x: 0.4,
+ y: 0.4,
+ w: 0.6,
+ h: 0.6,
+ o: 0.7
+ }, {
+ x: 0.4,
+ y: 0.4,
+ w: 0.6,
+ h: 0.6,
+ o: 0.8
+ }, {
+ x: 0.4,
+ y: 0.4,
+ w: 0.6,
+ h: 0.6,
+ o: 0.9
+ }, {
+ x: 0.4,
+ y: 0.4,
+ w: 0.6,
+ h: 0.6,
+ o: 1.0
+ }];
+ animation.types.none = [{
+ x: 0.4,
+ y: 0.4,
+ w: 0.6,
+ h: 0.6,
+ o: 1
+ }];
+ animation.types.pop = [{
+ x: 1,
+ y: 1,
+ w: 0,
+ h: 0,
+ o: 1
+ }, {
+ x: 0.9,
+ y: 0.9,
+ w: 0.1,
+ h: 0.1,
+ o: 1
+ }, {
+ x: 0.8,
+ y: 0.8,
+ w: 0.2,
+ h: 0.2,
+ o: 1
+ }, {
+ x: 0.7,
+ y: 0.7,
+ w: 0.3,
+ h: 0.3,
+ o: 1
+ }, {
+ x: 0.6,
+ y: 0.6,
+ w: 0.4,
+ h: 0.4,
+ o: 1
+ }, {
+ x: 0.5,
+ y: 0.5,
+ w: 0.5,
+ h: 0.5,
+ o: 1
+ }, {
+ x: 0.4,
+ y: 0.4,
+ w: 0.6,
+ h: 0.6,
+ o: 1
+ }];
+ animation.types.popFade = [{
+ x: 0.75,
+ y: 0.75,
+ w: 0,
+ h: 0,
+ o: 0
+ }, {
+ x: 0.65,
+ y: 0.65,
+ w: 0.1,
+ h: 0.1,
+ o: 0.2
+ }, {
+ x: 0.6,
+ y: 0.6,
+ w: 0.2,
+ h: 0.2,
+ o: 0.4
+ }, {
+ x: 0.55,
+ y: 0.55,
+ w: 0.3,
+ h: 0.3,
+ o: 0.6
+ }, {
+ x: 0.50,
+ y: 0.50,
+ w: 0.4,
+ h: 0.4,
+ o: 0.8
+ }, {
+ x: 0.45,
+ y: 0.45,
+ w: 0.5,
+ h: 0.5,
+ o: 0.9
+ }, {
+ x: 0.4,
+ y: 0.4,
+ w: 0.6,
+ h: 0.6,
+ o: 1
+ }];
+ animation.types.slide = [{
+ x: 0.4,
+ y: 1,
+ w: 0.6,
+ h: 0.6,
+ o: 1
+ }, {
+ x: 0.4,
+ y: 0.9,
+ w: 0.6,
+ h: 0.6,
+ o: 1
+ }, {
+ x: 0.4,
+ y: 0.9,
+ w: 0.6,
+ h: 0.6,
+ o: 1
+ }, {
+ x: 0.4,
+ y: 0.8,
+ w: 0.6,
+ h: 0.6,
+ o: 1
+ }, {
+ x: 0.4,
+ y: 0.7,
+ w: 0.6,
+ h: 0.6,
+ o: 1
+ }, {
+ x: 0.4,
+ y: 0.6,
+ w: 0.6,
+ h: 0.6,
+ o: 1
+ }, {
+ x: 0.4,
+ y: 0.5,
+ w: 0.6,
+ h: 0.6,
+ o: 1
+ }, {
+ x: 0.4,
+ y: 0.4,
+ w: 0.6,
+ h: 0.6,
+ o: 1
+ }];
+ /**
+ * Run animation
+ * @param {Object} opt Animation options
+ * @param {Object} cb Callabak after all steps are done
+ * @param {Object} revert Reverse order? true|false
+ * @param {Object} step Optional step number (frame bumber)
+ */
+ animation.run = function (opt, cb, revert, step) {
+ var animationType = animation.types[isPageHidden() ? 'none' : _opt.animation];
+ if (revert === true) {
+ step = (typeof step !== 'undefined') ? step : animationType.length - 1;
+ } else {
+ step = (typeof step !== 'undefined') ? step : 0;
+ }
+ cb = (cb) ? cb : function () {
+ };
+ if ((step < animationType.length) && (step >= 0)) {
+ type[_opt.type](merge(opt, animationType[step]));
+ _animTimeout = setTimeout(function () {
+ if (revert) {
+ step = step - 1;
+ } else {
+ step = step + 1;
+ }
+ animation.run(opt, cb, revert, step);
+ }, animation.duration);
+
+ link.setIcon(_canvas);
+ } else {
+ cb();
+ return;
+ }
+ };
+ //auto init
+ init();
+ return {
+ badge: badge,
+ video: video,
+ image: image,
+ rawImageSrc: rawImageSrc,
+ webcam: webcam,
+ setOpt: setOpt,
+ reset: icon.reset,
+ browser: {
+ supported: _browser.supported
+ }
+ };
+ });
+
+ // AMD / RequireJS
+ if (typeof define !== 'undefined' && define.amd) {
+ define([], function () {
+ return Favico;
+ });
+ }
+ // CommonJS
+ else if (typeof module !== 'undefined' && module.exports) {
+ module.exports = Favico;
+ }
+ // included directly via