WIP: Attach invoice PDF to payment emails #104418
3
.gitmodules
vendored
@ -2,3 +2,6 @@
|
|||||||
path = assets_shared
|
path = assets_shared
|
||||||
url = https://projects.blender.org/infrastructure/web-assets.git
|
url = https://projects.blender.org/infrastructure/web-assets.git
|
||||||
branch = v2
|
branch = v2
|
||||||
|
[submodule "playbooks/shared"]
|
||||||
|
path = playbooks/shared
|
||||||
|
url = https://projects.blender.org/infrastructure/web-playbooks
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit 448320696057165a83cc91eee8854f561a952ff4
|
Subproject commit c47e6b36b73b393337caac122f2fa9fa363580bb
|
@ -12,9 +12,10 @@ from comments.models import Comment
|
|||||||
from comments.queries import get_annotated_comments
|
from comments.queries import get_annotated_comments
|
||||||
from comments.views.common import comments_to_template_type
|
from comments.views.common import comments_to_template_type
|
||||||
import common.queries
|
import common.queries
|
||||||
|
from common.mixins import PaginatedViewMixin
|
||||||
|
|
||||||
|
|
||||||
class PostList(ListView):
|
class PostList(PaginatedViewMixin):
|
||||||
model = Post
|
model = Post
|
||||||
context_object_name = 'posts'
|
context_object_name = 'posts'
|
||||||
paginate_by = 12
|
paginate_by = 12
|
||||||
|
@ -8,6 +8,7 @@ from django.contrib import admin
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.base import Model
|
from django.db.models.base import Model
|
||||||
from django.http.request import HttpRequest
|
from django.http.request import HttpRequest
|
||||||
|
from django.views.generic import ListView
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
import looper.model_mixins
|
import looper.model_mixins
|
||||||
@ -171,3 +172,21 @@ class SetModifiedByViewMixin:
|
|||||||
obj = super().get_object(*args, **kwargs)
|
obj = super().get_object(*args, **kwargs)
|
||||||
obj._modified_by_user_id = self.request.user.pk
|
obj._modified_by_user_id = self.request.user.pk
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
class PaginatedViewMixin(ListView):
|
||||||
|
"""A custom Paginator that shows 3 clickable items (plus prev and next), instead
|
||||||
|
of the default 2.
|
||||||
|
"""
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
paginator = context['paginator']
|
||||||
|
page_obj = context['page_obj']
|
||||||
|
current_page = page_obj.number
|
||||||
|
|
||||||
|
# Determine the range of pages to show
|
||||||
|
start_page = max(current_page - 2, 1)
|
||||||
|
end_page = min(current_page + 2, paginator.num_pages)
|
||||||
|
|
||||||
|
context['page_range'] = range(start_page, end_page + 1)
|
||||||
|
return context
|
||||||
|
BIN
common/static/common/images/welcome/film-thumbnail-charge.webp
Normal file
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
BIN
common/static/common/images/welcome/film-thumbnail-gold.webp
Normal file
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
BIN
common/static/common/images/welcome/film-thumbnail-wing-it.webp
Normal file
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
@ -1,50 +1,59 @@
|
|||||||
// TODO: make script more generic if multiple subnavs are added
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const navGlobalLinkTraining = document.querySelector('.js-nav-global-link-training');
|
const navGlobalSubnavLinks = document.querySelectorAll('.js-nav-global-subnav-link');
|
||||||
const navSubnavTraining = document.querySelector('.js-nav-subnav-training');
|
|
||||||
|
|
||||||
function positionNavSubnavTraining() {
|
if (!navGlobalSubnavLinks || navGlobalSubnavLinks.length === 0) {
|
||||||
// Get 'navGlobalLinkTraining' position left
|
return;
|
||||||
const navGlobalLinkTrainingRect = navGlobalLinkTraining.getBoundingClientRect();
|
|
||||||
const navGlobalLinkTrainingPositionLeft = navGlobalLinkTrainingRect.left;
|
|
||||||
|
|
||||||
// Position 'navSubnavTraining'
|
|
||||||
navSubnavTraining.style.left = navGlobalLinkTrainingPositionLeft + 'px';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideNavSubnavTraining() {
|
function showNavSubnavPopover(navSubnavPopover, link) {
|
||||||
navSubnavTraining.classList.remove('show');
|
positionNavSubnavPopover(navSubnavPopover, link);
|
||||||
|
navSubnavPopover.classList.add('show');
|
||||||
}
|
}
|
||||||
|
|
||||||
function showNavSubnavTraining() {
|
function hideNavSubnavPopover(navSubnavPopover) {
|
||||||
navSubnavTraining.classList.add('show');
|
navSubnavPopover.classList.remove('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
function positionNavSubnavPopover(navSubnavPopover, link) {
|
||||||
|
// Get the link's left position
|
||||||
|
const navGlobalSubnavLinkRect = link.getBoundingClientRect();
|
||||||
|
const navGlobalSubnavLinkPositionLeft = navGlobalSubnavLinkRect.left;
|
||||||
|
|
||||||
|
// Position 'navSubnavPopover'
|
||||||
|
navSubnavPopover.style.left = navGlobalSubnavLinkPositionLeft + 'px';
|
||||||
}
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
positionNavSubnavTraining();
|
navGlobalSubnavLinks.forEach(link => {
|
||||||
|
const navGlobalSubnavLinkAttr = link.getAttribute('data-subnav');
|
||||||
|
const navSubnavPopover = document.querySelector(navGlobalSubnavLinkAttr);
|
||||||
|
|
||||||
// Create event 'navGlobalLinkTraining' on mouseover
|
if (!navSubnavPopover) {
|
||||||
navGlobalLinkTraining.addEventListener('mouseover', function() {
|
return;
|
||||||
showNavSubnavTraining();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create event 'navGlobalLinkTraining' on mouseleave
|
|
||||||
navGlobalLinkTraining.addEventListener('mouseleave', function(e) {
|
|
||||||
|
|
||||||
// Check if mouse has left 'navSubnavTraining' area
|
|
||||||
if (!navSubnavTraining.contains(e.relatedTarget)) {
|
|
||||||
hideNavSubnavTraining();
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Create event 'navSubnavTraining' on mouseleave
|
// Create event 'navGlobalSubnavLink' on mouseover
|
||||||
navSubnavTraining.addEventListener('mouseleave', function() {
|
link.addEventListener('mouseover', function() {
|
||||||
hideNavSubnavTraining();
|
showNavSubnavPopover(navSubnavPopover, link);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reposition 'navSubnavTraining' on window resize
|
// Create event 'navGlobalSubnavLink' on mouseleave
|
||||||
window.addEventListener('resize', function() {
|
link.addEventListener('mouseleave', function(e) {
|
||||||
positionNavSubnavTraining();
|
// Check if mouse has left 'navSubnavPopover' area
|
||||||
|
if (!navSubnavPopover.contains(e.relatedTarget)) {
|
||||||
|
hideNavSubnavPopover(navSubnavPopover);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create event 'navSubnavPopover' on mouseleave
|
||||||
|
navSubnavPopover.addEventListener('mouseleave', function() {
|
||||||
|
hideNavSubnavPopover(navSubnavPopover);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reposition 'navSubnavPopover' on window resize
|
||||||
|
window.addEventListener('resize', function() {
|
||||||
|
hideNavSubnavPopover(navSubnavPopover);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,18 +3,17 @@ function markAsRead(event) {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const element = event.currentTarget;
|
const element = event.currentTarget;
|
||||||
const url = element.dataset.markReadUrl;
|
const url = element.dataset.markReadUrl;
|
||||||
|
|
||||||
if (element.dataset.isRead === 'true') return;
|
if (element.dataset.isRead === 'true') return;
|
||||||
|
|
||||||
ajax.jsonRequest('POST', url).then(() => {
|
ajax.jsonRequest('POST', url).then(() => {
|
||||||
if (element.href) {
|
if (element.href) {
|
||||||
window.location.href = element.href;
|
window.location.href = element.href;
|
||||||
} else {
|
} else {
|
||||||
element
|
// TODO: @web-assets optionally make js notifications mark as read named function part of web-assets, that can be called on project level
|
||||||
.closest('.activity-list-item-wrapper')
|
element.closest('.js-notifications-item')
|
||||||
.querySelectorAll('.unread')
|
// Set notifications item parent is read
|
||||||
.forEach((i) => {
|
.classList.add('is-read');
|
||||||
i.classList.remove('unread');
|
|
||||||
});
|
|
||||||
|
|
||||||
const tooltip = bootstrap.Tooltip.getInstance(event.target);
|
const tooltip = bootstrap.Tooltip.getInstance(event.target);
|
||||||
tooltip.dispose();
|
tooltip.dispose();
|
||||||
@ -29,15 +28,9 @@ function markAllAsRead(event) {
|
|||||||
const url = element.dataset.markAllReadUrl;
|
const url = element.dataset.markAllReadUrl;
|
||||||
|
|
||||||
ajax.jsonRequest('POST', url).then(() => {
|
ajax.jsonRequest('POST', url).then(() => {
|
||||||
document.querySelectorAll('.unread').forEach((i) => {
|
document.querySelectorAll('.js-notifications-item').forEach((i) => {
|
||||||
i.classList.remove('unread');
|
// Set notifications items is read
|
||||||
|
i.classList.add('is-read');
|
||||||
if (
|
|
||||||
i.closest('.activity-list-item-wrapper') &&
|
|
||||||
i.closest('.activity-list-item-wrapper').querySelector('.markasread')
|
|
||||||
) {
|
|
||||||
i.closest('.activity-list-item-wrapper').querySelector('.markasread').remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.querySelector('.notifications-counter')) {
|
if (document.querySelector('.notifications-counter')) {
|
||||||
document.querySelector('.notifications-counter').remove();
|
document.querySelector('.notifications-counter').remove();
|
||||||
|
@ -369,20 +369,23 @@ function toggleNavDrawer() {
|
|||||||
const navDrawerBtnToggle = document.querySelector('.js-nav-drawer-btn-toggle');
|
const navDrawerBtnToggle = document.querySelector('.js-nav-drawer-btn-toggle');
|
||||||
const navDrawerHelper = document.querySelector('.js-nav-drawer-helper');
|
const navDrawerHelper = document.querySelector('.js-nav-drawer-helper');
|
||||||
|
|
||||||
navDrawerBtnToggle.addEventListener('click', function() {
|
// Check if 'navDrawerBtnToggle' exists
|
||||||
// Check if navDrawerBtnToggle is active
|
if (navDrawerBtnToggle) {
|
||||||
if (this.classList.contains('active')) {
|
navDrawerBtnToggle.addEventListener('click', function() {
|
||||||
// Show 'navDrawerHelper'
|
// Check if navDrawerBtnToggle is active
|
||||||
this.classList.remove('active');
|
if (this.classList.contains('active')) {
|
||||||
|
// Show 'navDrawerHelper'
|
||||||
|
this.classList.remove('active');
|
||||||
|
|
||||||
navDrawerHelper.classList.remove('show');
|
navDrawerHelper.classList.remove('show');
|
||||||
} else {
|
} else {
|
||||||
// Hide 'navDrawerHelper'
|
// Hide 'navDrawerHelper'
|
||||||
this.classList.add('active');
|
this.classList.add('active');
|
||||||
|
|
||||||
navDrawerHelper.classList.add('show');
|
navDrawerHelper.classList.add('show');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create function init
|
// Create function init
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/* Breadcrumb. */
|
/* Breadcrumb. */
|
||||||
.breadcrumb-item
|
.breadcrumb-item
|
||||||
&::before
|
&::before
|
||||||
padding-top: var(--spacer-1)
|
padding-top: var(--spacer-1)
|
||||||
|
|
||||||
/* Button group. */
|
/* Button group. */
|
||||||
.button-toolbar
|
.button-toolbar
|
||||||
|
@ -203,7 +203,33 @@ pre
|
|||||||
/* Dropdown */
|
/* Dropdown */
|
||||||
.dropdown-menu
|
.dropdown-menu
|
||||||
&.dropdown-menu-notification
|
&.dropdown-menu-notification
|
||||||
max-width: 72.0rem
|
max-width: 64.0rem
|
||||||
|
top: 1.2rem !important // Optically aligned vertically with subnavs
|
||||||
|
width: 100%
|
||||||
|
|
||||||
|
// TODO: @web-assets optionally create dropdown or floating notifications box in web-assets
|
||||||
|
.notifications-item
|
||||||
|
color: var(--box-text-color)
|
||||||
|
line-height: 1.5
|
||||||
|
|
||||||
|
&.is-read
|
||||||
|
color: var(--color-text-secondary)
|
||||||
|
|
||||||
|
.notifications-item-time
|
||||||
|
.date
|
||||||
|
font-family: var(--font-family-mono)
|
||||||
|
|
||||||
|
+media-md
|
||||||
|
font-size: 1.4rem
|
||||||
|
|
||||||
|
.notifications-list
|
||||||
|
background-color: transparent
|
||||||
|
box-shadow: none
|
||||||
|
font-size: var(--fs-sm)
|
||||||
|
+padding(0, x)
|
||||||
|
|
||||||
|
&.theme-dark
|
||||||
|
background-color: transparent
|
||||||
|
|
||||||
&.dropdown-menu-other-contributors
|
&.dropdown-menu-other-contributors
|
||||||
.dropdown-item
|
.dropdown-item
|
||||||
@ -480,6 +506,13 @@ input
|
|||||||
&:focus
|
&:focus
|
||||||
color: var(--color-text-primary) !important
|
color: var(--color-text-primary) !important
|
||||||
|
|
||||||
|
.notifications-list-activity
|
||||||
|
.notifications-item-dot
|
||||||
|
display: none
|
||||||
|
|
||||||
|
.notifications-item-nav
|
||||||
|
display: none
|
||||||
|
|
||||||
/* Payment. */
|
/* Payment. */
|
||||||
.braintree-heading
|
.braintree-heading
|
||||||
color: var(--color-text)
|
color: var(--color-text)
|
||||||
@ -622,6 +655,93 @@ button,
|
|||||||
filter: blur(var(--filter-blur-value))
|
filter: blur(var(--filter-blur-value))
|
||||||
transform: scale(1.1)
|
transform: scale(1.1)
|
||||||
|
|
||||||
|
.training-group
|
||||||
|
--training-group-item-content-width: 100%
|
||||||
|
--training-group-item-nav-width: 100%
|
||||||
|
|
||||||
|
+media-xl
|
||||||
|
--training-group-item-content-width: 40.0rem
|
||||||
|
--training-group-item-nav-width: 30.0rem
|
||||||
|
|
||||||
|
+media-xxl
|
||||||
|
--training-group-item-content-width: 54.0rem
|
||||||
|
--training-group-item-nav-width: 40.0rem
|
||||||
|
|
||||||
|
.training-group-item
|
||||||
|
+padding(3, x)
|
||||||
|
|
||||||
|
&:last-child
|
||||||
|
+media-xl
|
||||||
|
padding-right: 0
|
||||||
|
|
||||||
|
.training-group-item-content
|
||||||
|
width: var(--training-group-item-content-width)
|
||||||
|
|
||||||
|
.box
|
||||||
|
background-color: var(--color-bg-tertiary)
|
||||||
|
+padding(3)
|
||||||
|
|
||||||
|
.comment-input-div
|
||||||
|
&.form-control
|
||||||
|
background-color: var(--color-bg-primary)
|
||||||
|
|
||||||
|
.replies
|
||||||
|
.comment
|
||||||
|
background-color: var(--color-bg-secondary)
|
||||||
|
|
||||||
|
.top-level-comment
|
||||||
|
background-color: var(--color-bg-primary)
|
||||||
|
|
||||||
|
.training-group-item-content-detail
|
||||||
|
width: 100%
|
||||||
|
|
||||||
|
.cards-item-title
|
||||||
|
font-size: var(--fs-h4)
|
||||||
|
line-height: var(--lh-base)
|
||||||
|
|
||||||
|
+media-lg
|
||||||
|
.training-group-item-content-detail-inner
|
||||||
|
max-width: 114.0rem
|
||||||
|
|
||||||
|
+media-xl
|
||||||
|
width: calc(100% - var(--training-group-item-nav-width))
|
||||||
|
|
||||||
|
.cards
|
||||||
|
--cards-items-per-row: 4
|
||||||
|
|
||||||
|
+media-xxl
|
||||||
|
.cards
|
||||||
|
--cards-items-per-row: 5
|
||||||
|
|
||||||
|
// TODO: revise training-group-item-nav display toggle on medium and small screens
|
||||||
|
.training-group-item-nav
|
||||||
|
+margin(3, bottom)
|
||||||
|
width: var(--training-group-item-nav-width)
|
||||||
|
|
||||||
|
.training-group-item-video
|
||||||
|
background-color: black
|
||||||
|
+margin(3, bottom)
|
||||||
|
padding: 0
|
||||||
|
width: 100%
|
||||||
|
|
||||||
|
+media-xl
|
||||||
|
align-items: center
|
||||||
|
display: flex
|
||||||
|
height: 100vh
|
||||||
|
justify-content: center
|
||||||
|
left: 0
|
||||||
|
margin-bottom: 0
|
||||||
|
position: sticky
|
||||||
|
top: 0
|
||||||
|
width: calc(100% - var(--training-group-item-content-width) - var(--training-group-item-nav-width))
|
||||||
|
|
||||||
|
.training-header-img-helper
|
||||||
|
align-items: center
|
||||||
|
overflow: hidden
|
||||||
|
|
||||||
|
img
|
||||||
|
object-fit: cover
|
||||||
|
width: 100%
|
||||||
|
|
||||||
/* Type */
|
/* Type */
|
||||||
::selection
|
::selection
|
||||||
@ -629,6 +749,10 @@ button,
|
|||||||
background-color: var(--color-accent)
|
background-color: var(--color-accent)
|
||||||
|
|
||||||
.markdown-text
|
.markdown-text
|
||||||
|
// Style inline links also if they're not in a paragraph
|
||||||
|
a
|
||||||
|
text-decoration: underline
|
||||||
|
|
||||||
hr
|
hr
|
||||||
clear: both
|
clear: both
|
||||||
|
|
||||||
@ -678,6 +802,7 @@ button,
|
|||||||
.spoiler-alert
|
.spoiler-alert
|
||||||
@include border-radius($border-radius)
|
@include border-radius($border-radius)
|
||||||
backdrop-filter: blur(var(--filter-blur-value))
|
backdrop-filter: blur(var(--filter-blur-value))
|
||||||
|
-webkit-backdrop-filter: blur(var(--filter-blur-value))
|
||||||
background: rgba(0, 0, 0, 0.4)
|
background: rgba(0, 0, 0, 0.4)
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
height: 100%
|
height: 100%
|
||||||
@ -726,13 +851,15 @@ button,
|
|||||||
[data-tooltip]
|
[data-tooltip]
|
||||||
&:hover
|
&:hover
|
||||||
&:before, &:after
|
&:before, &:after
|
||||||
|
color: var(--color-text)
|
||||||
display: block
|
display: block
|
||||||
position: absolute
|
position: absolute
|
||||||
color: var(--color-text-primary)
|
|
||||||
&:before
|
&:before
|
||||||
border-radius: var(--spacer-1)
|
background-color: var(--color-bg-primary)
|
||||||
|
border-radius: var(--border-radius)
|
||||||
content: attr(title)
|
content: attr(title)
|
||||||
background-color: var(--color-bg-primary-subtle)
|
|
||||||
margin-top: var(--spacer)
|
|
||||||
padding: var(--spacer)
|
|
||||||
font-size: var(--fs-sm)
|
font-size: var(--fs-sm)
|
||||||
|
+fw-normal
|
||||||
|
margin-top: var(--spacer)
|
||||||
|
padding: var(--spacer-2)
|
||||||
|
word-break: break-word
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
&::-webkit-scrollbar-thumb
|
&::-webkit-scrollbar-thumb
|
||||||
background: $bar-color
|
background: $bar-color
|
||||||
border-radius: $border-radius
|
border-radius: var(--border-radius)
|
||||||
border: 4px solid $bg-color
|
border: 4px solid $bg-color
|
||||||
|
|
||||||
// TODO: Fix style
|
// TODO: Fix style
|
||||||
@ -50,7 +50,7 @@
|
|||||||
|
|
||||||
.nav-drawer
|
.nav-drawer
|
||||||
background: var(--navbar-bg)
|
background: var(--navbar-bg)
|
||||||
border-radius: $border-radius
|
border-radius: var(--border-radius)
|
||||||
border-width: 0 0 1px
|
border-width: 0 0 1px
|
||||||
display: none
|
display: none
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
@ -111,8 +111,12 @@
|
|||||||
|
|
||||||
.nav-drawer-body
|
.nav-drawer-body
|
||||||
@include media-breakpoint-up(md)
|
@include media-breakpoint-up(md)
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
gap: var(--spacer-1)
|
||||||
height: 100%
|
height: 100%
|
||||||
max-height: calc(100vh - var(--nav-global-navbar-height))
|
max-height: calc(100vh - var(--nav-global-navbar-height))
|
||||||
|
+padding(2, bottom)
|
||||||
|
|
||||||
.nav-drawer-list
|
.nav-drawer-list
|
||||||
@include media-breakpoint-down(sm)
|
@include media-breakpoint-down(sm)
|
||||||
@ -145,15 +149,27 @@
|
|||||||
transition: margin-left var(--nav-drawer-animation-duration)
|
transition: margin-left var(--nav-drawer-animation-duration)
|
||||||
|
|
||||||
.drawer-nav-group, .drawer-nav-header
|
.drawer-nav-group, .drawer-nav-header
|
||||||
h1, .h1, h2, .h2, h3, .h3, h4, .h4, h5, .h5, p, a
|
h1, .h1, h2, .h2, h3, .h3, h4, .h4, h5, .h5, h6, .h6, p, a
|
||||||
margin-bottom: 0
|
margin-bottom: 0
|
||||||
|
|
||||||
|
.drawer-nav-group
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
gap: var(--spacer-1)
|
||||||
|
+padding(1, bottom)
|
||||||
|
|
||||||
.drawer-nav-list
|
.drawer-nav-list
|
||||||
background: var(--navbar-bg)
|
background-color: var(--color-bg-secondary)
|
||||||
|
border-radius: var(--border-radius)
|
||||||
color: var(--color-text-secondary)
|
color: var(--color-text-secondary)
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
gap: var(--spacer-1)
|
||||||
list-style: none
|
list-style: none
|
||||||
margin: var(--spacer) / 4 0
|
+margin(2, x)
|
||||||
padding: var(--spacer) / 4 0
|
+margin(1, y)
|
||||||
|
+padding(1, x)
|
||||||
|
+padding(2, y)
|
||||||
|
|
||||||
&.training
|
&.training
|
||||||
.drawer-nav-section
|
.drawer-nav-section
|
||||||
@ -182,29 +198,40 @@
|
|||||||
background-color: var(--color-accent)
|
background-color: var(--color-accent)
|
||||||
|
|
||||||
.drawer-nav-section-icon
|
.drawer-nav-section-icon
|
||||||
::before
|
&::before
|
||||||
background-color: var(--color-accent)
|
background-color: var(--color-accent)
|
||||||
|
|
||||||
&:first-of-type
|
&:first-of-type
|
||||||
.drawer-nav-section-icon
|
.drawer-nav-section-icon
|
||||||
::after
|
&::after
|
||||||
content: none
|
content: none
|
||||||
|
|
||||||
&:last-of-type
|
&:last-of-type
|
||||||
.drawer-nav-section-icon
|
.drawer-nav-section-icon
|
||||||
::before
|
&::before
|
||||||
content: none
|
content: none
|
||||||
|
|
||||||
.drawer-nav-section-link
|
.drawer-nav-section-link
|
||||||
align-items: center
|
align-items: center
|
||||||
|
border-radius: var(--border-radius)
|
||||||
color: inherit
|
color: inherit
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: row
|
flex-direction: row
|
||||||
flex-grow: 1
|
flex-grow: 1
|
||||||
padding: calc(var(--spacer) * 0.5) var(--spacer)
|
padding: var(--spacer-1)
|
||||||
transition: $transition-base
|
margin: 0 var(--spacer-1)
|
||||||
|
transition: background-color var(--transition-speed), color var(--transition-speed)
|
||||||
width: 100%
|
width: 100%
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background: var(--color-bg-primary)
|
||||||
|
color: var(--color-text-primary)
|
||||||
|
|
||||||
|
&.active
|
||||||
|
background: var(--color-bg-primary)
|
||||||
|
color: var(--color-text-primary)
|
||||||
|
+fw-bold
|
||||||
|
|
||||||
.drawer-nav-section-icon-progress .progress
|
.drawer-nav-section-icon-progress .progress
|
||||||
transition: $transition-base
|
transition: $transition-base
|
||||||
|
|
||||||
@ -214,36 +241,13 @@
|
|||||||
|
|
||||||
h4, .h4
|
h4, .h4
|
||||||
margin-bottom: 0
|
margin-bottom: 0
|
||||||
line-height: 1.5
|
|
||||||
color: var(--color-text-secondary)
|
|
||||||
transition: $transition-base
|
|
||||||
|
|
||||||
span
|
span
|
||||||
line-height: 1
|
line-height: 1
|
||||||
|
|
||||||
&::before
|
|
||||||
background: var(--color-bg-primary)
|
|
||||||
border-radius: $border-radius
|
|
||||||
content: close-quote
|
|
||||||
height: calc(100% - var(--spacer) / 2)
|
|
||||||
left: var(--spacer) / 2
|
|
||||||
opacity: 0
|
|
||||||
position: absolute
|
|
||||||
top: var(--spacer) / 4
|
|
||||||
transition: $transition-base
|
|
||||||
width: calc(100% - var(--spacer))
|
|
||||||
pointer-events: none
|
|
||||||
|
|
||||||
&:hover
|
&:hover
|
||||||
text-decoration: none
|
text-decoration: none
|
||||||
|
|
||||||
.drawer-nav-section-icon-progress .progress
|
|
||||||
stroke: var(--color-accent)
|
|
||||||
|
|
||||||
// TODO: fix drawer-nav-section-link ::before
|
|
||||||
&::before
|
|
||||||
opacity: 1
|
|
||||||
|
|
||||||
// Fix before overflowing content on hover
|
// Fix before overflowing content on hover
|
||||||
i,
|
i,
|
||||||
p
|
p
|
||||||
@ -252,13 +256,6 @@
|
|||||||
>i
|
>i
|
||||||
margin-right: (var(--spacer) / 2)
|
margin-right: (var(--spacer) / 2)
|
||||||
|
|
||||||
&.active
|
|
||||||
h4, .h4
|
|
||||||
.drawer-nav-section-icon
|
|
||||||
&-progress
|
|
||||||
.progress
|
|
||||||
stroke: var(--color-accent)
|
|
||||||
|
|
||||||
.subtitle
|
.subtitle
|
||||||
color:
|
color:
|
||||||
font-size: var(--fs-xs)
|
font-size: var(--fs-xs)
|
||||||
@ -276,10 +273,9 @@
|
|||||||
opacity: 1
|
opacity: 1
|
||||||
|
|
||||||
.drawer-nav-header
|
.drawer-nav-header
|
||||||
margin-top: -$spacer / 4
|
border-bottom: var(--border-width) solid var(--color-bg-alt)
|
||||||
border-bottom: var(--border-width) solid var(--box-bg-color)
|
+margin(3, x)
|
||||||
margin-bottom: $spacer / 4
|
+padding(2, y)
|
||||||
padding: $spacer
|
|
||||||
|
|
||||||
@include media-breakpoint-down(sm)
|
@include media-breakpoint-down(sm)
|
||||||
padding: $spacer / 2 $spacer
|
padding: $spacer / 2 $spacer
|
||||||
@ -294,8 +290,9 @@
|
|||||||
position: absolute
|
position: absolute
|
||||||
width: 24px
|
width: 24px
|
||||||
|
|
||||||
h5
|
span
|
||||||
font-size: var(--fs-sm)
|
font-size: var(--fs-sm)
|
||||||
|
+fw-bold
|
||||||
left: 50%
|
left: 50%
|
||||||
line-height: 0
|
line-height: 0
|
||||||
position: absolute
|
position: absolute
|
||||||
@ -334,20 +331,18 @@ $circle-circumference: $circle-diameter * 3.14
|
|||||||
.progress
|
.progress
|
||||||
fill: none
|
fill: none
|
||||||
//not using the $progress-bg here as it's too strong, meant for overlaying images
|
//not using the $progress-bg here as it's too strong, meant for overlaying images
|
||||||
// TODO: @web-sasets check variable $highlight-white-strong replacement
|
stroke: var(--color-accent)
|
||||||
// stroke: $highlight-white-strong
|
|
||||||
stroke: var(--color-text-secondary)
|
|
||||||
stroke-dasharray: $circle-circumference
|
stroke-dasharray: $circle-circumference
|
||||||
stroke-dashoffset: calc((1 - var(--progress-fraction, 0)) * #{$circle-circumference}px)
|
stroke-dashoffset: calc((1 - var(--progress-fraction, 0)) * #{$circle-circumference}px)
|
||||||
stroke-linecap: round
|
stroke-linecap: round
|
||||||
stroke-width: 3px
|
stroke-width: 2px
|
||||||
|
|
||||||
.background
|
.background
|
||||||
fill: none
|
fill: none
|
||||||
// stroke: $highlight-white
|
stroke: currentColor
|
||||||
stroke: var(--color-text-secondary)
|
|
||||||
stroke-linecap: round
|
stroke-linecap: round
|
||||||
stroke-width: 3px
|
stroke-width: 1px
|
||||||
|
opacity: .33
|
||||||
|
|
||||||
.drawer-nav-dropdown-wrapper
|
.drawer-nav-dropdown-wrapper
|
||||||
@include button-float
|
@include button-float
|
||||||
@ -361,33 +356,32 @@ $circle-circumference: $circle-diameter * 3.14
|
|||||||
|
|
||||||
.drawer-nav-dropdown
|
.drawer-nav-dropdown
|
||||||
align-items: center
|
align-items: center
|
||||||
|
border-radius: var(--border-radius)
|
||||||
color: var(--nav-global-color-text)
|
color: var(--nav-global-color-text)
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
display: flex
|
display: flex
|
||||||
flex-grow: 1
|
flex-grow: 1
|
||||||
margin-bottom: 0
|
margin: 0 var(--spacer-2)
|
||||||
max-width: 100%
|
max-width: 100%
|
||||||
padding: $spacer / 2 $spacer
|
padding: var(--spacer-1) var(--spacer-2)
|
||||||
position: relative
|
position: relative
|
||||||
transition: $transition-base
|
transition: background-color var(--transition-speed), color var(--transition-speed)
|
||||||
user-select: none
|
user-select: none
|
||||||
|
|
||||||
&.dropdown
|
&:hover
|
||||||
max-width: calc(100% - 44px)
|
|
||||||
|
|
||||||
&::before
|
|
||||||
// background: $highlight-white
|
|
||||||
background: var(--color-bg-primary)
|
background: var(--color-bg-primary)
|
||||||
border-radius: $border-radius
|
color: var(--color-text-primary)
|
||||||
content: close-quote
|
|
||||||
height: calc(100% - #{$spacer / 2})
|
&.active
|
||||||
left: $spacer / 2
|
background: var(--color-bg-primary)
|
||||||
opacity: 0
|
color: var(--color-text-primary)
|
||||||
position: absolute
|
+fw-bold
|
||||||
top: $spacer / 4
|
|
||||||
transition: $transition-base
|
i
|
||||||
width: calc(100% - #{$spacer})
|
color: var(--color-text-primary)
|
||||||
pointer-events: none
|
|
||||||
|
&+.icon
|
||||||
|
color: var(--color-text-primary)
|
||||||
|
|
||||||
&.collapsed
|
&.collapsed
|
||||||
i
|
i
|
||||||
@ -396,9 +390,6 @@ $circle-circumference: $circle-diameter * 3.14
|
|||||||
&:hover
|
&:hover
|
||||||
text-decoration: none
|
text-decoration: none
|
||||||
|
|
||||||
&::before
|
|
||||||
opacity: 1
|
|
||||||
|
|
||||||
// Fix before overflowing content on hover
|
// Fix before overflowing content on hover
|
||||||
i,
|
i,
|
||||||
span
|
span
|
||||||
@ -414,13 +405,11 @@ $circle-circumference: $circle-diameter * 3.14
|
|||||||
flex-grow: 0
|
flex-grow: 0
|
||||||
flex-shrink: 1
|
flex-shrink: 1
|
||||||
justify-content: center
|
justify-content: center
|
||||||
margin-left: - $spacer / 2
|
+padding(3, x)
|
||||||
min-width: calc(var(--spacer) * 3)
|
margin-left: 0
|
||||||
padding: $spacer / 2 $spacer * .75
|
+margin(right, 2)
|
||||||
|
|
||||||
.drawer-nav-dropdown-text
|
.drawer-nav-dropdown-text
|
||||||
font-weight: normal
|
|
||||||
font-variation-settings: "wght" 400
|
|
||||||
margin-right: auto
|
margin-right: auto
|
||||||
|
|
||||||
.overflow-text
|
.overflow-text
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
.bg-filter-blur
|
.bg-filter-blur
|
||||||
background-color: transparent
|
background-color: transparent
|
||||||
backdrop-filter: blur(24px)
|
backdrop-filter: blur(24px)
|
||||||
|
-webkit-backdrop-filter: blur(24px)
|
||||||
position: relative
|
position: relative
|
||||||
|
|
||||||
&::before
|
&::before
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
$container-max-widths: (sm: 100%, md: 100%, lg: 100%, xl: 1320px, xxl: 1600px)
|
$container-max-widths: (sm: 100%, md: 100%, lg: 100%, xl: 1320px, xxl: 1600px)
|
||||||
|
|
||||||
|
// Redeclare $grid-breakpoints 'xl' and 'xxl' with web-assets defaults to override obsolete Bootstrap breakpoints coming from flat, pre-compiled vendor files
|
||||||
|
$grid-breakpoints: (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1320px, xxl: 1680px)
|
||||||
|
|
||||||
$container-width: map-get($container-max-widths, 'xl')
|
$container-width: map-get($container-max-widths, 'xl')
|
||||||
|
|
||||||
$font-path: "/static/assets/fonts"
|
$font-path: "/static/assets/fonts"
|
||||||
|
@ -18,12 +18,16 @@ html[data-theme="dark"]
|
|||||||
/* Breadcrumb */
|
/* Breadcrumb */
|
||||||
.breadcrumb-item
|
.breadcrumb-item
|
||||||
&.active
|
&.active
|
||||||
--btn-color: var(--color-text)
|
--btn-color: var(--color-text-secondary)
|
||||||
|
|
||||||
span
|
span
|
||||||
opacity: 1
|
opacity: 1
|
||||||
|
|
||||||
/* Button */
|
/* Button */
|
||||||
|
.btn
|
||||||
|
&.active
|
||||||
|
@extend .btn-primary
|
||||||
|
|
||||||
.btn-admin
|
.btn-admin
|
||||||
@extend .btn-link
|
@extend .btn-link
|
||||||
|
|
||||||
@ -66,9 +70,6 @@ a
|
|||||||
background-color: transparent
|
background-color: transparent
|
||||||
box-shadow: none
|
box-shadow: none
|
||||||
|
|
||||||
.cards
|
|
||||||
--grid-gap-size: calc(var(--spacer) * 2)
|
|
||||||
|
|
||||||
.cards-item
|
.cards-item
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
@ -165,6 +166,15 @@ textarea
|
|||||||
&.form-control
|
&.form-control
|
||||||
min-height: calc(var(--spacer) * 5)
|
min-height: calc(var(--spacer) * 5)
|
||||||
|
|
||||||
|
/* Grid. */
|
||||||
|
// TODO: consider moving to web-assets
|
||||||
|
.container-fluid
|
||||||
|
.row
|
||||||
|
+margin(0, x)
|
||||||
|
|
||||||
|
.row
|
||||||
|
width: 100%
|
||||||
|
|
||||||
/* Hero. */
|
/* Hero. */
|
||||||
.hero-content
|
.hero-content
|
||||||
h1
|
h1
|
||||||
@ -233,6 +243,18 @@ textarea
|
|||||||
.nav-global-icon-dropdown-toggle
|
.nav-global-icon-dropdown-toggle
|
||||||
margin-left: 0
|
margin-left: 0
|
||||||
|
|
||||||
|
/* Notifications. */
|
||||||
|
.notifications
|
||||||
|
--border-width: .1rem
|
||||||
|
|
||||||
|
.notifications-item-content
|
||||||
|
em
|
||||||
|
font-style: normal
|
||||||
|
+fw-bold
|
||||||
|
|
||||||
|
.notifications-list
|
||||||
|
width: 100%
|
||||||
|
|
||||||
/* Pagination */
|
/* Pagination */
|
||||||
// Fix hover colours when btn is not in box for web-assets
|
// Fix hover colours when btn is not in box for web-assets
|
||||||
.pagination
|
.pagination
|
||||||
|
@ -130,6 +130,12 @@ def get_s3_post_url_and_fields(
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
_storages = {
|
||||||
|
's3': S3Boto3CustomStorage(),
|
||||||
|
'fs': nginx_secure_links.storages.FileStorage(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class DynamicStorageFieldFile(FieldFile):
|
class DynamicStorageFieldFile(FieldFile):
|
||||||
"""Defines which storage the file is located at."""
|
"""Defines which storage the file is located at."""
|
||||||
|
|
||||||
@ -137,9 +143,9 @@ class DynamicStorageFieldFile(FieldFile):
|
|||||||
"""Choose between S3 and file system storage depending on `source_storage`."""
|
"""Choose between S3 and file system storage depending on `source_storage`."""
|
||||||
super().__init__(instance, *args, **kwargs)
|
super().__init__(instance, *args, **kwargs)
|
||||||
if instance.source_storage is None: # S3 is default
|
if instance.source_storage is None: # S3 is default
|
||||||
self.storage = S3Boto3CustomStorage()
|
self.storage = _storages['s3']
|
||||||
elif instance.source_storage == 'fs':
|
elif instance.source_storage == 'fs':
|
||||||
self.storage = nginx_secure_links.storages.FileStorage()
|
self.storage = _storages['fs']
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@ -152,9 +158,9 @@ class CustomFileField(models.FileField):
|
|||||||
def pre_save(self, model_instance, add):
|
def pre_save(self, model_instance, add):
|
||||||
"""Choose between S3 and file system storage depending on `source_storage`."""
|
"""Choose between S3 and file system storage depending on `source_storage`."""
|
||||||
if model_instance.source_storage is None:
|
if model_instance.source_storage is None:
|
||||||
storage = S3Boto3CustomStorage()
|
storage = _storages['s3']
|
||||||
elif model_instance.source_storage == 'fs':
|
elif model_instance.source_storage == 'fs':
|
||||||
storage = nginx_secure_links.storages.FileStorage()
|
storage = _storages['fs']
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
self.storage = storage
|
self.storage = storage
|
||||||
|
@ -17,13 +17,11 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{{ post.title }}
|
{{ post.title }}
|
||||||
</h3>
|
</h3>
|
||||||
{% comment %}
|
{% if post.excerpt %}
|
||||||
<div class="cards-item-excerpt">
|
<div class="cards-item-excerpt">
|
||||||
<p>
|
<p>{{ post.excerpt }}</p>
|
||||||
{{ post.excerpt }}
|
</div>
|
||||||
</p>
|
{% endif %}
|
||||||
</div>
|
|
||||||
{% endcomment %}
|
|
||||||
<div class="d-flex cards-item-extra">
|
<div class="d-flex cards-item-extra">
|
||||||
{% if not post.is_published %}
|
{% if not post.is_published %}
|
||||||
<span class="badge badge-danger me-3">Unpublished</span>
|
<span class="badge badge-danger me-3">Unpublished</span>
|
||||||
|
@ -4,34 +4,34 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="fs-xs letter-spacing lh-1 text-muted text-uppercase mb-3">
|
<div class="fs-xs letter-spacing lh-1 text-muted text-uppercase mb-3">
|
||||||
Highlighted trainings
|
Training Highlights
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<a class="col-6 d-flex pb-3" href="/training/geometry-nodes-from-scratch">
|
<a class="col-6 d-flex align-items-center pb-3" href="/training/geometry-nodes-from-scratch">
|
||||||
<div class="me-3 nav-subnav-item-img">
|
<div class="me-3 nav-subnav-item-img">
|
||||||
<div class="bg-center bg-cover rounded" style="background-image: url('{% static 'common/images/welcome/training-thumbnail-geometry-nodes-orig.jpg' %}');"></div>
|
<div class="bg-center bg-cover rounded" style="background-image: url('{% static 'common/images/welcome/training-thumbnail-geometry-nodes-orig.jpg' %}');"></div>
|
||||||
</div>
|
</div>
|
||||||
<h6 class="fs-sm fw-normal lh-sm mb-0">Geometry nodes from scratch</h6>
|
<h6 class="fw-normal lh-xs mb-0">Geometry Nodes from Scratch</h6>
|
||||||
</a>
|
</a>
|
||||||
<a class="col-6 d-flex pb-3" href="/training/procedural-shading">
|
<a class="col-6 d-flex align-items-center pb-3" href="/training/procedural-shading">
|
||||||
<div class="me-3 nav-subnav-item-img">
|
<div class="me-3 nav-subnav-item-img">
|
||||||
<div class="bg-center bg-cover rounded" style="background-image: url('{% static 'common/images/welcome/training-thumbnail-procedural-shading-orig.jpg' %}');"></div>
|
<div class="bg-center bg-cover rounded" style="background-image: url('{% static 'common/images/welcome/training-thumbnail-procedural-shading-orig.jpg' %}');"></div>
|
||||||
</div>
|
</div>
|
||||||
<h6 class="fs-sm fw-normal lh-sm mb-0">Procedural shading: Fundamentals and beyond</h6>
|
<h6 class="fw-normal lh-xs mb-0">Procedural Shading Fundamentals</h6>
|
||||||
</a>
|
</a>
|
||||||
<a class="col-6 d-flex pb-3" href="/training/stylized-character-workflow">
|
<a class="col-6 d-flex align-items-center pb-3" href="/training/stylized-character-workflow">
|
||||||
<div class="me-3 nav-subnav-item-img">
|
<div class="me-3 nav-subnav-item-img">
|
||||||
<div class="bg-center bg-cover rounded" style="background-image: url('{% static 'common/images/welcome/training-thumbnail-stylized-character-workflow-orig.jpg' %}' );"></div>
|
<div class="bg-center bg-cover rounded" style="background-image: url('{% static 'common/images/welcome/training-thumbnail-stylized-character-workflow-orig.jpg' %}' );"></div>
|
||||||
</div>
|
</div>
|
||||||
<h6 class="fs-sm fw-normal lh-sm mb-0">Stylized character workflow</h6>
|
<h6 class="fw-normal lh-xs mb-0">Stylized Character Workflow</h6>
|
||||||
</a>
|
</a>
|
||||||
<a class="col-6 d-flex pb-3" href="/training/animation-fundamentals">
|
<a class="col-6 d-flex align-items-center pb-3" href="/training/animation-fundamentals">
|
||||||
<div class="me-3 nav-subnav-item-img">
|
<div class="me-3 nav-subnav-item-img">
|
||||||
<div class="bg-center bg-cover rounded" style="background-image: url('{% static 'common/images/welcome/training-thumbnail-animation-fundamentals-orig.jpg' %}');"></div>
|
<div class="bg-center bg-cover rounded" style="background-image: url('{% static 'common/images/welcome/training-thumbnail-animation-fundamentals-orig.jpg' %}');"></div>
|
||||||
</div>
|
</div>
|
||||||
<h6 class="fs-sm fw-normal lh-sm mb-0">Animation fundamentals</h6>
|
<h6 class="fw-normal lh-xs mb-0">Animation Fundamentals</h6>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
@ -47,7 +47,7 @@
|
|||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
<a class="btn btn-secondary btn-sm" href="/training/?training_date_desc%5Bmenu%5D%5Btype%5D=course#all-training">Course</a>
|
<a class="btn btn-secondary btn-sm" href="/training/?training_date_desc%5Bmenu%5D%5Btype%5D=course#all-training">Course</a>
|
||||||
<a class="btn btn-secondary btn-sm" href="/training/?training_date_desc%5Bmenu%5D%5Btype%5D=documentation#all-training">Documentation</a>
|
<a class="btn btn-secondary btn-sm" href="/training/?training_date_desc%5Bmenu%5D%5Btype%5D=documentation#all-training">Documentation</a>
|
||||||
<a class="btn btn-secondary btn-sm" href="/training/?training_date_desc%5Bmenu%5D%5Btype%5D=production%20lesson#all-training">Production lesson</a>
|
<a class="btn btn-secondary btn-sm" href="/training/?training_date_desc%5Bmenu%5D%5Btype%5D=production%20lesson#all-training">Production Lesson</a>
|
||||||
<a class="btn btn-secondary btn-sm" href="/training/?training_date_desc[menu][type]=workshop#all-training">Worskhop</a>
|
<a class="btn btn-secondary btn-sm" href="/training/?training_date_desc[menu][type]=workshop#all-training">Worskhop</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -74,3 +74,54 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="d-md-block d-none fade js-nav-subnav-film nav-subnav position-absolute">
|
||||||
|
<div class="bg-filter-blur bg-noise box mt-2">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="fs-xs letter-spacing lh-1 text-muted text-uppercase mb-3">
|
||||||
|
Film Highlights
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<a class="col-6 d-flex align-items-center pb-3" href="/films/gold/">
|
||||||
|
<div class="me-3 nav-subnav-item-img">
|
||||||
|
<div class="bg-center bg-cover rounded" style="background-image: url('{% static 'common/images/welcome/film-thumbnail-gold.webp' %}');"></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h6 class="fw-normal lh-xs mb-0">Gold</h6>
|
||||||
|
<small class="badge badge-sm badge-primary">In Production</small>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a class="col-6 d-flex align-items-center pb-3" href="/films/wing-it/">
|
||||||
|
<div class="me-3 nav-subnav-item-img">
|
||||||
|
<div class="bg-center bg-cover rounded" style="background-image: url('{% static 'common/images/welcome/film-thumbnail-wing-it.webp' %}');"></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h6 class="fw-normal lh-xs mb-0">Wing It!</h6>
|
||||||
|
<small class="text-muted">2023</small>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a class="col-6 d-flex align-items-center" href="/films/charge/">
|
||||||
|
<div class="me-3 nav-subnav-item-img">
|
||||||
|
<div class="bg-center bg-cover rounded" style="background-image: url('{% static 'common/images/welcome/film-thumbnail-charge.webp' %}' );"></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h6 class="fw-normal lh-xs mb-0">Charge</h6>
|
||||||
|
<small class="text-muted">2022</small>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a class="col-6 d-flex align-items-center" href="/films/sprite-fright/">
|
||||||
|
<div class="me-3 nav-subnav-item-img">
|
||||||
|
<div class="bg-center bg-cover rounded" style="background-image: url('{% static 'common/images/welcome/film-thumbnail-sprite-fright.webp' %}');"></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h6 class="fw-normal lh-xs mb-0">Sprite Fright</h6>
|
||||||
|
<small class="text-muted">2021</small>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@ -22,10 +22,10 @@
|
|||||||
|
|
||||||
<ul class="nav-global-nav-links nav-global-dropdown js-dropdown-menu" id="nav-global-nav-links">
|
<ul class="nav-global-nav-links nav-global-dropdown js-dropdown-menu" id="nav-global-nav-links">
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'film-list' %}" class="{% if '/films' in request.path %}is-active{% endif %}">Films</a>
|
<a href="{% url 'film-list' %}" data-subnav=".js-nav-subnav-film" class="js-nav-global-subnav-link {% if '/films' in request.path %}is-active{% endif %}">Films</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'training-home' %}" class="js-nav-global-link-training {% if '/training' in request.path %}is-active{% endif %}">Training</a>
|
<a href="{% url 'training-home' %}" data-subnav=".js-nav-subnav-training" class="js-nav-global-subnav-link {% if '/training' in request.path %}is-active{% endif %}">Training</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'character-list' %}" class="{% if '/characters' in request.path %}is-active{% endif %}">Characters</a>
|
<a href="{% url 'character-list' %}" class="{% if '/characters' in request.path %}is-active{% endif %}">Characters</a>
|
||||||
|
@ -7,29 +7,29 @@
|
|||||||
<span>{{ user.notifications_unread.count }}</span>
|
<span>{{ user.notifications_unread.count }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown-menu dropdown-menu-end dropdown-menu-notification">
|
<div class="dropdown-menu dropdown-menu-end dropdown-menu-notification mt-2 p-0 theme-dark">
|
||||||
<div class="btn-row">
|
<div class="bg-filter-blur bg-noise box pt-3">
|
||||||
<a href="{% url 'user-notification' verbs='commented,replied to' %}" class="dropdown-item flex">
|
<div class="align-items-center d-flex">
|
||||||
<span>Notifications</span>
|
<div class="flex-grow-1 fs-xs letter-spacing lh-1 text-muted text-uppercase">Notifications</div>
|
||||||
</a>
|
<a class="btn btn-link dropdown-item flex-grow-0" data-bs-toggle="tooltip" data-placement="top" data-mark-all-read-url="{% url 'api-notifications-mark-read' %}" title="Mark all as read">
|
||||||
<a class="btn btn-link dropdown-item flex-grow-0" data-bs-toggle="tooltip" data-placement="top" data-mark-all-read-url="{% url 'api-notifications-mark-read' %}" title="Mark all as read">
|
<i class="i-check me-0"></i>
|
||||||
<i class="i-check me-0"></i>
|
</a>
|
||||||
|
</div>
|
||||||
|
<ul class="notifications-list mb-3">
|
||||||
|
{% for notification in user.notifications.all|slice:":10" %}
|
||||||
|
{% with action=notification.action %}
|
||||||
|
{% include 'users/components/nav_action.html' %}
|
||||||
|
{% endwith %}
|
||||||
|
{% empty %}
|
||||||
|
<p class="mb-0 text-muted">
|
||||||
|
No notifications yet
|
||||||
|
</p>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<a href="{% url 'user-notification' %}">
|
||||||
|
<span>See all notifications</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown-menu-nested">
|
|
||||||
{% for notification in user.notifications.all|slice:":10" %}
|
|
||||||
{% with action=notification.action %}
|
|
||||||
{% include 'users/components/nav_action.html' %}
|
|
||||||
{% endwith %}
|
|
||||||
{% empty %}
|
|
||||||
<p class="px-2 py-2 text-center text-muted">
|
|
||||||
No notifications yet
|
|
||||||
</p>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
<a href="{% url 'user-notification' %}" class="dropdown-item text-sm">
|
|
||||||
<span>See all notifications</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
@ -72,8 +72,8 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://twitter.com/BlenderStudio_" title="Follow Blender Studio on Twitter" target="_blank" class="social-icons__twitter">
|
<a href="https://x.com/BlenderStudio_" title="Follow Blender Studio on X" target="_blank" class="social-icons__twitter">
|
||||||
<i class="i-twitter"></i>Twitter
|
<i class="i-twitter"></i>X
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<a href="{{ href }}" class="drawer-nav-section-link justify-content-between {% if active %}active{% endif %}" data-bs-tooltip="tooltip-overflow"
|
<a href="{{ href }}" class="drawer-nav-section-link justify-content-between {% if active %}active{% endif %}" data-bs-tooltip="tooltip-overflow"
|
||||||
data-placement="top" title="{{ title }}">
|
data-placement="top" title="{{ title }}">
|
||||||
<div class="nav-drawer-section-progress-wrapper">
|
<div class="nav-drawer-section-progress-wrapper">
|
||||||
<h5>{{ nth }}</h5>
|
<span>{{ nth }}</span>
|
||||||
{% comment %} TODO(Anna): Fix fraction calculation {% endcomment %}
|
{% comment %} TODO(Anna): Fix fraction calculation {% endcomment %}
|
||||||
<svg width="40" height="40" class="drawer-nav-section-icon-progress" style="--progress-fraction: {% if finished %} 1.0 {% else %} {{ progress_fraction }} {% endif %}">
|
<svg width="40" height="40" class="drawer-nav-section-icon-progress" style="--progress-fraction: {% if finished %} 1.0 {% else %} {{ progress_fraction }} {% endif %}">
|
||||||
<circle class="background" cx="20" cy="20" r="14" />
|
<circle class="background" cx="20" cy="20" r="14" />
|
||||||
|
@ -6,19 +6,21 @@
|
|||||||
<a href="?page=1">First</a>
|
<a href="?page=1">First</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li class="page-item">
|
|
||||||
<a href="?page={{ page_obj.previous_page_number }}">{{ page_obj.previous_page_number }}</a>
|
<li class="page-item page-prev">
|
||||||
|
<a href="?page={{ page_obj.previous_page_number }}"><i class="i-chevron-left"></i> Previous</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<li class="active disabled page-item">
|
<li class="page-item page-current">
|
||||||
<a href="#">{{ page_obj.number }}</a>
|
<a href="#">Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
{% if page_obj.has_next %}
|
{% if page_obj.has_next %}
|
||||||
<li class="page-item">
|
<li class="page-item page-next">
|
||||||
<a href="?page={{ page_obj.next_page_number }}">{{ page_obj.next_page_number }}</a>
|
<a href="?page={{ page_obj.next_page_number }}">Next <i class="i-chevron-right"></i></a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
{% if not page_obj.next_page_number == page_obj.paginator.num_pages %}
|
{% if not page_obj.next_page_number == page_obj.paginator.num_pages %}
|
||||||
<li class="page-item page-last">
|
<li class="page-item page-last">
|
||||||
<a href="?page={{ page_obj.paginator.num_pages }}">Last</a>
|
<a href="?page={{ page_obj.paginator.num_pages }}">Last</a>
|
||||||
@ -26,4 +28,4 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
@ -1,10 +1,9 @@
|
|||||||
#!/bin/sh -ex
|
#!/bin/sh -ex
|
||||||
|
|
||||||
git fetch origin main:production
|
git fetch origin main:production && git push origin production
|
||||||
git push origin production
|
|
||||||
|
|
||||||
pushd playbooks
|
pushd playbooks
|
||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
./ansible.sh -i environments/production deploy.yaml
|
./ansible.sh -i environments/production shared/deploy.yaml
|
||||||
deactivate
|
deactivate
|
||||||
popd
|
popd
|
||||||
|
@ -40,9 +40,8 @@ or [venv](https://docs.python.org/3.10/library/venv.html) will do.
|
|||||||
- Download the CloudFront key file and save it to the project directory (it should be named `pk-APK***.pem`);
|
- Download the CloudFront key file and save it to the project directory (it should be named `pk-APK***.pem`);
|
||||||
- Set `AWS_CLOUDFRONT_KEY_ID='APK***'` where `APK***` is from the name of the key file above.
|
- Set `AWS_CLOUDFRONT_KEY_ID='APK***'` where `APK***` is from the name of the key file above.
|
||||||
6. In the project folder, run migrations and load additional plans data:
|
6. In the project folder, run migrations and load additional plans data:
|
||||||
|
- `./manage.py migrate`
|
||||||
./manage.py migrate
|
- `./manage.py loaddata team_plans`
|
||||||
./manage.py loaddata team_plans
|
|
||||||
|
|
||||||
7. Create a superuser: `echo "from django.contrib.auth import get_user_model; User = get_user_model(); User.objects.create_superuser('admin', 'admin@example.com', 'password')" | python manage.py shell`
|
7. Create a superuser: `echo "from django.contrib.auth import get_user_model; User = get_user_model(); User.objects.create_superuser('admin', 'admin@example.com', 'password')" | python manage.py shell`
|
||||||
8. Run the server: `./manage.py runserver 8001`. The project will be available at
|
8. Run the server: `./manage.py runserver 8001`. The project will be available at
|
||||||
@ -53,7 +52,7 @@ or [venv](https://docs.python.org/3.10/library/venv.html) will do.
|
|||||||
The default domain is `example.com`; change it to `studio.local:8001`. This will make
|
The default domain is `example.com`; change it to `studio.local:8001`. This will make
|
||||||
it possible to immediately view objects created/edited via admin on site.
|
it possible to immediately view objects created/edited via admin on site.
|
||||||
11. Set up the [Blender ID server](#blender-id-authentication) for authentication
|
11. Set up the [Blender ID server](#blender-id-authentication) for authentication
|
||||||
and [MeiliSerach server](#search) for the search functionality.
|
and [MeiliSearch server](#search) for the search functionality.
|
||||||
12. Setup for video processing jobs. Download ngrok (https://ngrok.com/).
|
12. Setup for video processing jobs. Download ngrok (https://ngrok.com/).
|
||||||
- Run `./ngrok http 8010`
|
- Run `./ngrok http 8010`
|
||||||
- Update `.env`:
|
- Update `.env`:
|
||||||
|
@ -13,6 +13,7 @@ const search = instantsearch({
|
|||||||
return {
|
return {
|
||||||
query: indexUiState.query,
|
query: indexUiState.query,
|
||||||
sortBy: indexUiState && indexUiState.sortBy,
|
sortBy: indexUiState && indexUiState.sortBy,
|
||||||
|
media_type: indexUiState.menu && indexUiState.menu.media_type,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
routeToState(routeState) {
|
routeToState(routeState) {
|
||||||
@ -20,6 +21,9 @@ const search = instantsearch({
|
|||||||
[indexName]: {
|
[indexName]: {
|
||||||
query: routeState.query,
|
query: routeState.query,
|
||||||
sortBy: routeState.sortBy,
|
sortBy: routeState.sortBy,
|
||||||
|
menu: {
|
||||||
|
media_type: routeState.media_type,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -117,16 +121,21 @@ const renderHits = (renderOptions, isFirstRender) => {
|
|||||||
<div class="cards-item-thumbnail">
|
<div class="cards-item-thumbnail">
|
||||||
<img aria-label="${item.name}" loading=lazy src="${item.thumbnail_url || fileIconURL}">
|
<img aria-label="${item.name}" loading=lazy src="${item.thumbnail_url || fileIconURL}">
|
||||||
</div>
|
</div>
|
||||||
<h3 class="cards-item-title fs-6 lh-base overflow-text" data-tooltip="tooltip-overflow" data-placement="top" title="${item.name}">
|
<h3 class="cards-item-title fs-6 lh-base" data-tooltip="tooltip-overflow" data-placement="top" title="${item.name}">
|
||||||
${instantsearch.highlight({
|
<span class="overflow-text">
|
||||||
attribute: 'name',
|
${instantsearch.highlight({
|
||||||
hit: item,
|
attribute: 'name',
|
||||||
})}
|
hit: item,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
</h3>
|
</h3>
|
||||||
<div class="cards-item-extra">
|
<div class="cards-item-extra">
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<i class="i-clock x-sm"></i> ${timeDifference(epochToDate(item.timestamp))}
|
${item.media_type}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="i-clock x-sm"></i> ${timeDifference(epochToDate(item.timestamp))}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
${
|
${
|
||||||
@ -179,6 +188,47 @@ const renderHits = (renderOptions, isFirstRender) => {
|
|||||||
|
|
||||||
const customHits = instantsearch.connectors.connectInfiniteHits(renderHits);
|
const customHits = instantsearch.connectors.connectInfiniteHits(renderHits);
|
||||||
|
|
||||||
|
// -------- FILTERS -------- //
|
||||||
|
|
||||||
|
// 1. Create a render function
|
||||||
|
const renderMenuSelect = (renderOptions, isFirstRender) => {
|
||||||
|
const { items, canRefine, refine, widgetParams } = renderOptions;
|
||||||
|
|
||||||
|
if (isFirstRender) {
|
||||||
|
const select = document.createElement('select');
|
||||||
|
|
||||||
|
select.setAttribute('class', 'form-control');
|
||||||
|
select.addEventListener('change', (event) => {
|
||||||
|
refine(event.target.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
widgetParams.container.insertAdjacentElement('afterbegin', select);
|
||||||
|
// widgetParams.container.appendChild(select);
|
||||||
|
}
|
||||||
|
|
||||||
|
const select = widgetParams.container.querySelector('select');
|
||||||
|
|
||||||
|
select.disabled = !canRefine;
|
||||||
|
|
||||||
|
select.innerHTML = `
|
||||||
|
<option value="">${widgetParams.placeholder}</option>
|
||||||
|
${items
|
||||||
|
.map(
|
||||||
|
(item) =>
|
||||||
|
`<option
|
||||||
|
value="${item.value}"
|
||||||
|
${item.isRefined ? 'selected' : ''}
|
||||||
|
>
|
||||||
|
${titleCase(item.label)}
|
||||||
|
</option>`
|
||||||
|
)
|
||||||
|
.join('')}
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. Create the custom widget
|
||||||
|
const customMenuSelect = instantsearch.connectors.connectMenu(renderMenuSelect);
|
||||||
|
|
||||||
// -------- CONFIGURE -------- //
|
// -------- CONFIGURE -------- //
|
||||||
|
|
||||||
const renderConfigure = (renderOptions, isFirstRender) => {};
|
const renderConfigure = (renderOptions, isFirstRender) => {};
|
||||||
@ -202,6 +252,11 @@ search.addWidgets([
|
|||||||
{ label: 'Date (old first)', value: 'studio_date_asc' },
|
{ label: 'Date (old first)', value: 'studio_date_asc' },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
customMenuSelect({
|
||||||
|
container: document.querySelector('#searchMedia'),
|
||||||
|
attribute: 'media_type',
|
||||||
|
placeholder: 'All Types',
|
||||||
|
}),
|
||||||
customConfigure({
|
customConfigure({
|
||||||
container: document.querySelector('#hits'),
|
container: document.querySelector('#hits'),
|
||||||
searchParameters: {
|
searchParameters: {
|
||||||
|
@ -14,28 +14,28 @@
|
|||||||
<header class="navbar navbar-secondary" role="navigation">
|
<header class="navbar navbar-secondary" role="navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
<li class="nav-item nav-parent show-on-scroll" data-link="/">
|
|
||||||
<a class="nav-link" href="/">Home</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item" data-link="{% url 'film-detail' film_slug=film.slug %}">
|
<li class="nav-item" data-link="{% url 'film-detail' film_slug=film.slug %}">
|
||||||
<a class="nav-link" href="{% url 'film-detail' film_slug=film.slug %}">{{ film.title }}</a>
|
<a class="nav-link" href="{% url 'film-detail' film_slug=film.slug %}"><strong>{{ film.title }}</strong></a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-separator"><i class="i-chevron-right"></i></li>
|
||||||
|
<li class="nav-item {% if current_collection or '/all-artwork/' in request.path %}active{% endif %}" data-link="{% url 'film-gallery' film_slug=film.slug %}">
|
||||||
|
<a class="nav-link" href="{% url 'film-all-assets' film_slug=film.slug %}">Content Gallery</a>
|
||||||
|
</li>
|
||||||
|
{% if film.show_production_logs_nav_link %}
|
||||||
|
<li class="nav-item {% if date_list or production_log.name or '/production-log' in request.path %}active{% endif %}">
|
||||||
|
<a class="nav-link" href="{% url 'film-production-logs' film_slug=film.slug %}">Production Logs</a>
|
||||||
|
</li>
|
||||||
|
{% if user.is_staff and user_can_edit_production_log %}
|
||||||
|
<li>
|
||||||
|
{% include 'films/components/admin/production_log_manage.html' %}
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
{% for flatpage in film.flatpages.all %}
|
{% for flatpage in film.flatpages.all %}
|
||||||
<li class="nav-item" data-link="{% url 'film-flatpage' film_slug=film.slug page_slug=flatpage.slug %}">
|
<li class="nav-item" data-link="{% url 'film-flatpage' film_slug=film.slug page_slug=flatpage.slug %}">
|
||||||
<a class="nav-link" href="{% url 'film-flatpage' film_slug=film.slug page_slug=flatpage.slug %}">{{ flatpage.title|title }}</a>
|
<a class="nav-link" href="{% url 'film-flatpage' film_slug=film.slug page_slug=flatpage.slug %}">{{ flatpage.title|title }}</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<li class="nav-item {% if current_collection %}active{% endif %}" data-link="{% url 'film-gallery' film_slug=film.slug %}">
|
|
||||||
<a class="nav-link" href="{% url 'film-gallery' film_slug=film.slug %}">Content Gallery</a>
|
|
||||||
</li>
|
|
||||||
{% if film.show_production_logs_nav_link %}
|
|
||||||
<li class="nav-item {% if date_list or production_log.name %}active{% endif %}">
|
|
||||||
<a class="nav-link" href="{% url 'film-production-logs' film_slug=film.slug %}">Production Logs</a>
|
|
||||||
</li>
|
|
||||||
<li class="ms-3">
|
|
||||||
{% include 'films/components/admin/production_log_manage.html' %}
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% if user_has_production_credit or credit %}
|
{% if user_has_production_credit or credit %}
|
||||||
<li class="nav-item" data-link="{% url 'production-credit' film_slug=film.slug %}">
|
<li class="nav-item" data-link="{% url 'production-credit' film_slug=film.slug %}">
|
||||||
<a href="{% url 'production-credit' film_slug=film.slug %}" class="nav-link">
|
<a href="{% url 'production-credit' film_slug=film.slug %}" class="nav-link">
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
{% if user.is_staff and user_can_edit_production_log %}
|
<a data-bs-toggle="dropdown" class="btn btn-admin nav-link">
|
||||||
<a data-bs-toggle="dropdown" class="btn btn-admin">
|
|
||||||
<i class="i-more-vertical"></i>
|
<i class="i-more-vertical"></i>
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown-menu dropdown-menu-end">
|
<div class="dropdown-menu dropdown-menu-end">
|
||||||
@ -7,4 +6,3 @@
|
|||||||
<a href="{% url 'admin:films_productionlog_changelist' %}?film__id__exact={{ film.id }}"
|
<a href="{% url 'admin:films_productionlog_changelist' %}?film__id__exact={{ film.id }}"
|
||||||
class="dropdown-item">Manage Production Logs</a>
|
class="dropdown-item">Manage Production Logs</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
@ -5,7 +5,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<h3 class="cards-item-title">
|
<h3 class="cards-item-title">
|
||||||
<span class="me-2">{{ film.title }}</span>
|
<span class="me-2">{{ film.title }}</span>
|
||||||
{% if film.status == "2_released" %}
|
{% if film.status == "1_prod" %}
|
||||||
|
<span class="badge badge-primary">
|
||||||
|
{{ film.get_status_display }}
|
||||||
|
</span>
|
||||||
|
{% elif film.status == "2_released" %}
|
||||||
<span class="badge">
|
<span class="badge">
|
||||||
{{ film.release_date|date:"Y" }}
|
{{ film.release_date|date:"Y" }}
|
||||||
</span>
|
</span>
|
||||||
|
@ -10,7 +10,8 @@
|
|||||||
{% if film.youtube_link != "" %}
|
{% if film.youtube_link != "" %}
|
||||||
{# Class 'video-modal-link' is needed for js #}
|
{# Class 'video-modal-link' is needed for js #}
|
||||||
<button class="btn btn-accent video-modal-link" data-bs-toggle="modal" data-bs-target="#videoModal" data-video="{{ film.youtube_link }}">
|
<button class="btn btn-accent video-modal-link" data-bs-toggle="modal" data-bs-target="#videoModal" data-video="{{ film.youtube_link }}">
|
||||||
Watch {{ film.title }}
|
<i class="i-youtube"></i>
|
||||||
|
<span>Watch {{ film.title }}</span>
|
||||||
</button>
|
</button>
|
||||||
<a class="btn btn-link" href="{% url 'film-gallery' film.slug %}">Explore Content Gallery</a>
|
<a class="btn btn-link" href="{% url 'film-gallery' film.slug %}">Explore Content Gallery</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -2,41 +2,48 @@
|
|||||||
{% load common_extras %}
|
{% load common_extras %}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="pb-4 pt-2">
|
<div class="py-2">
|
||||||
{% if user_can_edit_production_log %}
|
|
||||||
<a href="{{ production_log.admin_url }}" class="btn btn-admin mb-3">
|
|
||||||
<i class="i-edit"></i>
|
|
||||||
<span>Edit</span>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<h2>
|
<h2 class="mb-0">
|
||||||
<a href="{{ production_log.url }}" class="text-white">
|
<a href="{{ production_log.url }}" class="text-white">
|
||||||
{{ production_log.name }}
|
{{ production_log.name }}
|
||||||
</a>
|
</a>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{% if production_log.start_date %}
|
<div class="text-muted mb-2">
|
||||||
<div class="text-muted mb-2">
|
<ul class="list-inline mb-0">
|
||||||
<small>
|
{% if production_log.start_date %}
|
||||||
|
<li>
|
||||||
{{ production_log.start_date|date:'N jS, Y' }}
|
{{ production_log.start_date|date:'N jS, Y' }}
|
||||||
</small>
|
</li>
|
||||||
</div>
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
|
{% if user_can_edit_production_log %}
|
||||||
|
<li>
|
||||||
|
-
|
||||||
|
<a href="{{ production_log.admin_url }}" class="text-muted text-underline">
|
||||||
|
Edit
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if production_log.summary %}
|
{% if production_log.summary %}
|
||||||
<div>
|
<div>
|
||||||
<span class="fw-bold">This week on {{ film.title }}:</span>
|
<strong>This week on {{ film.title }}</strong>
|
||||||
{% with_shortcodes production_log.summary|markdown %}
|
{% with_shortcodes production_log.summary|markdown %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if production_log.youtube_link != "" %}
|
{% if production_log.youtube_link != "" %}
|
||||||
<a href="{{ production_log.youtube_link }}" class="btn btn-primary mt-3 video-modal-link" data-bs-toggle="modal"
|
<a href="{{ production_log.youtube_link }}" class="btn btn-primary mt-3 video-modal-link" data-bs-toggle="modal"
|
||||||
data-bs-target="#videoModal" data-video="{{ production_log.youtube_link }}">Watch Video</a>
|
data-bs-target="#videoModal" data-video="{{ production_log.youtube_link }}">
|
||||||
|
<i class="i-youtube"></i>
|
||||||
|
<span>Watch Video</span>
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@ -66,56 +73,60 @@
|
|||||||
<div>
|
<div>
|
||||||
{% for entry in production_log.log_entries.all %}
|
{% for entry in production_log.log_entries.all %}
|
||||||
<div class="pb-4">
|
<div class="pb-4">
|
||||||
<div class="row ">
|
<div class="row">
|
||||||
<div class="col-md-2">
|
<div class="col-md-12">
|
||||||
{% include 'common/components/cards/card_profile.html' with user=entry.user title=entry.author_role %}
|
<div class="d-flex">
|
||||||
|
<div>
|
||||||
{% with entry_author=entry.author|default:entry.user contributors=entry.contributors first_contributor=entry.contributors|first %}
|
{% include 'common/components/cards/card_profile.html' with user=entry.user title=entry.author_role %}
|
||||||
{% if contributors|length > 1 or contributors|length == 1 and first_contributor.pk != entry_author.pk %}
|
</div>
|
||||||
<h4 class="fs-6 fw-normal lh-base mt-3 text-muted">Other contributors:</h4>
|
<div class="ms-4 pt-1">
|
||||||
<div class="align-items-center contributors d-flex mb-1">
|
{% with entry_author=entry.author|default:entry.user contributors=entry.contributors first_contributor=entry.contributors|first %}
|
||||||
<div class="d-flex flex-wrap">
|
{% if contributors|length > 1 or contributors|length == 1 and first_contributor.pk != entry_author.pk %}
|
||||||
{% for contributor in contributors %}
|
<div class="align-items-center contributors d-flex mb-1">
|
||||||
{% if contributor.pk != entry_author.pk %}
|
<div class="d-flex flex-wrap">
|
||||||
{% include 'users/components/avatar.html' with user=contributor %}
|
{% for contributor in contributors %}
|
||||||
{% endif %}
|
{% if contributor.pk != entry_author.pk %}
|
||||||
{% endfor %}
|
{% include 'users/components/avatar.html' with user=contributor %}
|
||||||
</div>
|
{% endif %}
|
||||||
</div>
|
{% endfor %}
|
||||||
{% endif %}
|
</div>
|
||||||
{% endwith %}
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
{% if user_can_edit_production_log_entry %}
|
||||||
|
<div class="ms-auto">
|
||||||
|
<a href="{{ entry.admin_url }}" class="btn btn-admin">
|
||||||
|
<i class="i-edit"></i>
|
||||||
|
<span>Edit</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-10">
|
</div>
|
||||||
|
<div class="row ">
|
||||||
|
<div class="col-md-12">
|
||||||
<div class="flex-column-reverse flex-md-row row">
|
<div class="flex-column-reverse flex-md-row row">
|
||||||
<div class="col-md-8 pb-3">
|
<div class="col-md-9 py-3">
|
||||||
<p>{{ entry.description }}</p>
|
<p>{{ entry.description }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% if user_can_edit_production_log_entry %}
|
|
||||||
<div class="col-md-4 d-flex justify-content-md-end">
|
|
||||||
<div class="pb-3">
|
|
||||||
<a href="{{ entry.admin_url }}" class="btn btn-admin">
|
|
||||||
<i class="i-edit"></i>
|
|
||||||
<span>Edit</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="files">
|
<div class="files">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card-layout-card-transparent cards">
|
<div class="card-layout-card-transparent cards cards-4">
|
||||||
{% for asset in entry.assets.all|slice:':3' %}
|
{% for asset in entry.assets.all|slice:':8' %}
|
||||||
{% if asset.is_published %}
|
{% if asset.is_published %}
|
||||||
{% include "common/components/file.html" with card_sizes="col" aspect_ratio="16:9" asset=asset site_context="production_logs" %}
|
{% include "common/components/file.html" with card_sizes="col" aspect_ratio="16:9" asset=asset site_context="production_logs" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if entry.assets.count > 3 %}
|
{% if entry.assets.count > 8 %}
|
||||||
<div class="collapse" id="entry-{{ entry.id }}">
|
<div class="collapse" id="entry-{{ entry.id }}">
|
||||||
<div class="card-layout-card-transparent cards">
|
<div class="card-layout-card-transparent cards cards-4">
|
||||||
{% for asset in entry.assets.all|slice:'3:' %}
|
{% for asset in entry.assets.all|slice:'8:' %}
|
||||||
{% if asset.is_published %}
|
{% if asset.is_published %}
|
||||||
{% include "common/components/file.html" with card_sizes="col" aspect_ratio="16:9" asset=asset site_context="production_logs" %}
|
{% include "common/components/file.html" with card_sizes="col" aspect_ratio="16:9" asset=asset site_context="production_logs" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -133,15 +144,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr style="border-width: 1px;">
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if user_can_edit_production_log %}
|
{% if user_can_edit_production_log %}
|
||||||
<div class="mt-3 text-center">
|
<div class="mt-3 text-right">
|
||||||
<a class="btn btn-admin px-5" href="{% url 'admin:films_productionlogentry_add' %}?production_log={{ production_log.pk }}">
|
<a class="btn btn-admin" href="{% url 'admin:films_productionlogentry_add' %}?production_log={{ production_log.pk }}">
|
||||||
<i class="i-plus me-2"></i>
|
<i class="i-plus"></i>
|
||||||
<span>Add Entry</span>
|
<span>Add Entry</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -36,14 +36,14 @@
|
|||||||
<!-- Latest Updates -->
|
<!-- Latest Updates -->
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col text-center">
|
<div class="col text-center">
|
||||||
<h1>This week in Production</h1>
|
<h1>Production Logs</h1>
|
||||||
<p class="mb-0">Check out what the team has been working these days on {{ film.title }}.
|
<p class="mb-0">Check out the latest updates on {{ film.title }}.
|
||||||
<a href="{% url 'film-production-logs' film.slug %}">See all production logs</a>
|
<a href="{% url 'film-production-logs' film.slug %}">See all production logs</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{% for production_log in production_logs_page|slice:":1" %}
|
{% for production_log in production_logs_page|slice:":4" %}
|
||||||
{% include 'films/components/production_log_entry.html' %}
|
{% include 'films/components/production_log_entry.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,7 +45,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="drawer-nav-dropdown-wrapper">
|
<div class="drawer-nav-dropdown-wrapper">
|
||||||
<a class="drawer-nav-dropdown fw-bold" href="{% url 'film-all-assets' film_slug=film.slug %}">
|
<a class="drawer-nav-dropdown" href="{% url 'film-all-assets' film_slug=film.slug %}">
|
||||||
<i class="i-search me-2"></i>
|
<i class="i-search me-2"></i>
|
||||||
All Artwork
|
All Artwork
|
||||||
</a>
|
</a>
|
||||||
@ -133,17 +133,17 @@
|
|||||||
{% block nested_nav_drawer_inner %}
|
{% block nested_nav_drawer_inner %}
|
||||||
<div class="drawer-nav-group">
|
<div class="drawer-nav-group">
|
||||||
<div class="drawer-nav-dropdown-wrapper">
|
<div class="drawer-nav-dropdown-wrapper">
|
||||||
<a class="drawer-nav-dropdown fw-bold" href="{% url 'film-gallery' film_slug=film.slug %}"
|
<a class="drawer-nav-dropdown" href="{% url 'film-all-assets' film_slug=film.slug %}"
|
||||||
data-bs-tooltip="tooltip-overflow" data-placement="top" title="Featured Artwork">
|
data-bs-tooltip="tooltip-overflow" data-placement="top" title="Search Project">
|
||||||
<i class="i-star me-2"></i>
|
<i class="i-search me-2"></i>
|
||||||
<span class="overflow-text">Featured Artwork</span>
|
<span class="overflow-text">Search Project</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="drawer-nav-dropdown-wrapper">
|
<div class="drawer-nav-dropdown-wrapper">
|
||||||
<a class="drawer-nav-dropdown fw-bold" href="{% url 'film-all-assets' film_slug=film.slug %}"
|
<a class="drawer-nav-dropdown" href="{% url 'film-gallery' film_slug=film.slug %}"
|
||||||
data-bs-tooltip="tooltip-overflow" data-placement="top" title="All Artwork">
|
data-bs-tooltip="tooltip-overflow" data-placement="top" title="Featured Artwork">
|
||||||
<i class="i-search me-2"></i>
|
<i class="i-star me-2"></i>
|
||||||
<span class="overflow-text">All Artwork</span>
|
<span class="overflow-text">Featured Artwork</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -151,7 +151,7 @@
|
|||||||
{% for collection, child_collections in collections.items %}
|
{% for collection, child_collections in collections.items %}
|
||||||
<div class="drawer-nav-dropdown-wrapper">
|
<div class="drawer-nav-dropdown-wrapper">
|
||||||
{% if child_collections %}
|
{% if child_collections %}
|
||||||
<a class="drawer-nav-dropdown fw-bold dropdown" href="{{ collection.url }}"
|
<a class="drawer-nav-dropdown dropdown" href="{{ collection.url }}"
|
||||||
data-bs-tooltip="tooltip-overflow" data-placement="top" title="{{ collection.name }}">
|
data-bs-tooltip="tooltip-overflow" data-placement="top" title="{{ collection.name }}">
|
||||||
<span class="drawer-nav-dropdown-text overflow-text">
|
<span class="drawer-nav-dropdown-text overflow-text">
|
||||||
{{ collection.name }}
|
{{ collection.name }}
|
||||||
@ -162,7 +162,7 @@
|
|||||||
<i class="i-chevron-down"></i>
|
<i class="i-chevron-down"></i>
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a class="drawer-nav-dropdown fw-bold" href="{{ collection.url }}" data-bs-tooltip="tooltip-overflow"
|
<a class="drawer-nav-dropdown" href="{{ collection.url }}" data-bs-tooltip="tooltip-overflow"
|
||||||
data-placement="top" title="{{ collection.name }}">
|
data-placement="top" title="{{ collection.name }}">
|
||||||
<span class="drawer-nav-dropdown-text overflow-text">
|
<span class="drawer-nav-dropdown-text overflow-text">
|
||||||
{{ collection.name }}
|
{{ collection.name }}
|
||||||
|
@ -15,13 +15,11 @@
|
|||||||
{% block toolbar %}
|
{% block toolbar %}
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
{% include "search/components/input.html" with sm=True %}
|
{% include "search/components/input.html" %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto mb-3 mb-md-0 d-md-flex d-none">
|
<div class="col-auto mb-3 mb-md-0 d-md-flex d-none">
|
||||||
<div class="input-group input-group-sm" id="sorting">
|
<div class="input-group" id="searchMedia"></div>
|
||||||
<label class="input-group-text pe-0" for="searchLicence">Sort by:</label>
|
<div class="input-group ms-3" id="sorting"></div>
|
||||||
{% comment %} INPUT (Js) {% endcomment %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock toolbar %}
|
{% endblock toolbar %}
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
{# TODO: Remove template #}
|
||||||
|
{# This template is used by ProductionLogView, which is not reachable by any route. #}
|
||||||
|
{# The route has been replaced by ProductionLogPaginatedView #}
|
||||||
{% extends 'films/base_films.html' %}
|
{% extends 'films/base_films.html' %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
@ -15,22 +18,17 @@
|
|||||||
<p>Follow the latest updates and progress on {{ film.title }}.</p>
|
<p>Follow the latest updates and progress on {{ film.title }}.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 d-flex justify-content-md-end">
|
<div class="col-md-6 d-flex justify-content-md-end">
|
||||||
{% with previous_month=date_list.1 %}
|
{% include "common/components/navigation/pagination.html" %}
|
||||||
{% include 'films/components/pagination_dates.html' %}
|
|
||||||
{% endwith %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if latest_month|length %}
|
{% if object_list %}
|
||||||
<div>
|
<div>
|
||||||
{% for production_log in latest_month %}
|
{% for production_log in object_list %}
|
||||||
{% include 'films/components/production_log_entry.html' %}
|
{% include 'films/components/production_log_entry.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
{% include "common/components/navigation/pagination.html" %}
|
||||||
{% with previous_month=date_list.1 %}
|
|
||||||
{% include 'films/components/pagination_dates.html' %}
|
|
||||||
{% endwith %}
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col text-center">
|
<div class="col text-center">
|
||||||
|
39
films/templates/films/productionlog_list.html
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{% extends 'films/base_films.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title_prepend %}Production Logs - {{ film.title }} - {% endblock title_prepend %}
|
||||||
|
|
||||||
|
{% block bodyclasses %}spacer has-secondary-nav{% endblock bodyclasses %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section class="mb-5">
|
||||||
|
<div class="container pt-4">
|
||||||
|
<!-- Latest Updates -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<h1 class="mb-1">Latest on {{ film.title }}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 d-flex justify-content-md-end">
|
||||||
|
{% include "common/components/navigation/pagination.html" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if object_list %}
|
||||||
|
<div>
|
||||||
|
{% for production_log in object_list %}
|
||||||
|
{% include 'films/components/production_log_entry.html' %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% include "common/components/navigation/pagination.html" %}
|
||||||
|
{% else %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col text-center">
|
||||||
|
<div class="bg-secondary py-4 rounded">
|
||||||
|
<h3 class="mb-0">No Weeklies to show</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock content %}
|
@ -26,7 +26,7 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
'<slug:film_slug>/production-logs/',
|
'<slug:film_slug>/production-logs/',
|
||||||
production_log.ProductionLogView.as_view(),
|
production_log.ProductionLogPaginatedView.as_view(),
|
||||||
name='film-production-logs',
|
name='film-production-logs',
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
|
@ -4,9 +4,10 @@ from django.http import Http404
|
|||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.urls.base import reverse
|
from django.urls.base import reverse
|
||||||
from django.views.generic import dates, detail
|
from django.views.generic import dates, detail, ListView
|
||||||
|
|
||||||
from common.queries import has_active_subscription
|
from common.queries import has_active_subscription
|
||||||
|
from common.mixins import PaginatedViewMixin
|
||||||
from films.models import Film, ProductionLog
|
from films.models import Film, ProductionLog
|
||||||
from films.queries import (
|
from films.queries import (
|
||||||
get_next_production_log,
|
get_next_production_log,
|
||||||
@ -197,3 +198,13 @@ class ProductionLogMonthView(_ProductionLogViewMixin, LandingPageMixin, dates.Mo
|
|||||||
# Make sure `date_list` is an actual list, not a QuerySet, otherwise `|last` won't work
|
# Make sure `date_list` is an actual list, not a QuerySet, otherwise `|last` won't work
|
||||||
context['date_list'] = list(date_list)
|
context['date_list'] = list(date_list)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class ProductionLogPaginatedView(_ProductionLogViewMixin, LandingPageMixin, PaginatedViewMixin):
|
||||||
|
model = ProductionLog
|
||||||
|
context_object_name = 'production_log'
|
||||||
|
paginate_by = 4
|
||||||
|
|
||||||
|
def get_queryset(self) -> QuerySet:
|
||||||
|
film = get_object_or_404(Film, slug=self.kwargs['film_slug'], is_published=True)
|
||||||
|
return get_production_logs(film)
|
||||||
|
@ -22,7 +22,7 @@ To set it up use the following commands:
|
|||||||
|
|
||||||
python3.10 -m venv .venv
|
python3.10 -m venv .venv
|
||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
pip install -r requirements.txt
|
pip install -r shared/requirements.txt
|
||||||
|
|
||||||
## First time install
|
## First time install
|
||||||
|
|
||||||
@ -38,8 +38,8 @@ One of these variables is `meili_master_key`, which can be generated using the f
|
|||||||
After encrypting `meili_master_key` and saving in the above mentioned `99_vault.yaml`,
|
After encrypting `meili_master_key` and saving in the above mentioned `99_vault.yaml`,
|
||||||
run the installation playbooks:
|
run the installation playbooks:
|
||||||
|
|
||||||
./ansible.sh -i environments/production install.yaml --vault-id production@prompt
|
./ansible.sh -i environments/production shared/install.yaml --vault-id production@prompt
|
||||||
./ansible.sh -i environments/production setup_certificate.yaml
|
./ansible.sh -i environments/production shared/setup_certificate.yaml
|
||||||
|
|
||||||
These vaulted variables are written to the configuration files at the target host,
|
These vaulted variables are written to the configuration files at the target host,
|
||||||
so they shouldn't be required after the installation is complete,
|
so they shouldn't be required after the installation is complete,
|
||||||
@ -56,7 +56,7 @@ editing it and then restarting the affected services:
|
|||||||
|
|
||||||
### Encrypting variables
|
### Encrypting variables
|
||||||
|
|
||||||
Let's say one of the config templates used by `install.yaml` refers to a variable named `sentry_dsn`,
|
Let's say one of the config templates used by `shared/install.yaml` refers to a variable named `sentry_dsn`,
|
||||||
and for **production** we want this variable to have the following value: `https://foo@bar.example.com/1234`.
|
and for **production** we want this variable to have the following value: `https://foo@bar.example.com/1234`.
|
||||||
To encrypt this value, use the following command:
|
To encrypt this value, use the following command:
|
||||||
|
|
||||||
@ -96,8 +96,21 @@ When you need to deploy something, make sure to commit and push your changes bot
|
|||||||
git fetch origin main:production && git push origin production
|
git fetch origin main:production && git push origin production
|
||||||
```
|
```
|
||||||
|
|
||||||
3. navigate to the playbooks and run `deploy.yaml`
|
3. navigate to the playbooks and run `shared/deploy.yaml`
|
||||||
|
|
||||||
```
|
```
|
||||||
./ansible.sh -i environments/production deploy.yaml
|
./ansible.sh -i environments/production shared/deploy.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Periodic tasks
|
||||||
|
|
||||||
|
Blender Studio is using systemd timers for periodic tasks such as cleaning up old sessions,
|
||||||
|
processing account deletion requests and charging subscriptions.
|
||||||
|
|
||||||
|
To install or update these, use the following playbook:
|
||||||
|
|
||||||
|
./ansible.sh -i environments/production shared/install.yaml --tags=services
|
||||||
|
|
||||||
|
To view existing timers at the target host, the following can be used:
|
||||||
|
|
||||||
|
systemctl list-units --type=timer | grep blender-studio
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
[ssh_connection]
|
|
||||||
ssh_args = -o ServerAliveInterval=30 -o ControlMaster=auto -o ControlPersist=60s
|
|
||||||
pipelining = True
|
|
||||||
|
|
||||||
[defaults]
|
|
||||||
error_on_missing_handler = True
|
|
||||||
error_on_undefined_vars = True
|
|
||||||
# Use the YAML callback plugin.
|
|
||||||
stdout_callback = yaml
|
|
||||||
# Use the stdout_callback when running ad-hoc commands.
|
|
||||||
bin_ansible_callbacks = True
|
|
||||||
ansible_python_interpreter = /usr/bin/python3
|
|
1
playbooks/ansible.cfg
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
shared/ansible.cfg
|
@ -1,8 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# This script will only work when called by a user that also exists
|
|
||||||
# at the target host and is capable of `sudo`.
|
|
||||||
|
|
||||||
# "-K" is necessary because ansible has to prompt
|
|
||||||
# for a password when becoming a required user.
|
|
||||||
.venv/bin/ansible-playbook -K $@
|
|
1
playbooks/ansible.sh
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
shared/ansible.sh
|
@ -1,39 +0,0 @@
|
|||||||
---
|
|
||||||
- name: restart service
|
|
||||||
become: true
|
|
||||||
become_user: root
|
|
||||||
ansible.builtin.systemd: name={{ item }} daemon_reload=yes state=restarted enabled=yes
|
|
||||||
with_items:
|
|
||||||
- "{{ service_name }}"
|
|
||||||
tags:
|
|
||||||
- always
|
|
||||||
|
|
||||||
- name: restart background
|
|
||||||
become: true
|
|
||||||
become_user: root
|
|
||||||
ansible.builtin.systemd: name={{ item }} daemon_reload=yes state=restarted enabled=yes
|
|
||||||
with_items:
|
|
||||||
- "{{ background_service_name }}"
|
|
||||||
tags:
|
|
||||||
- always
|
|
||||||
|
|
||||||
- name: reload service
|
|
||||||
become: true
|
|
||||||
become_user: root
|
|
||||||
ansible.builtin.systemd: name={{ item }} daemon_reload=yes state=reloaded enabled=yes
|
|
||||||
with_items:
|
|
||||||
- "{{ service_name }}"
|
|
||||||
tags:
|
|
||||||
- always
|
|
||||||
|
|
||||||
- name: test nginx
|
|
||||||
become: true
|
|
||||||
become_user: root
|
|
||||||
ansible.builtin.command: nginx -t
|
|
||||||
|
|
||||||
- name: reload nginx
|
|
||||||
become: true
|
|
||||||
become_user: root
|
|
||||||
ansible.builtin.systemd: name=nginx state=reloaded enabled=yes
|
|
||||||
tags:
|
|
||||||
- always
|
|
@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Adding {{ mailto }} to /etc/aliases
|
|
||||||
ansible.builtin.lineinfile:
|
|
||||||
path: /etc/aliases
|
|
||||||
regexp: "^{{ user }}: "
|
|
||||||
line: "{{ user }}: {{ mailto }}"
|
|
||||||
state: present
|
|
||||||
backup: true
|
|
||||||
tags:
|
|
||||||
- aliases
|
|
||||||
|
|
||||||
- name: Calling newaliases
|
|
||||||
ansible.builtin.command: newaliases
|
|
||||||
changed_when: false
|
|
||||||
tags:
|
|
||||||
- aliases
|
|
@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Enabling forwarding of journal to syslog
|
|
||||||
become: true
|
|
||||||
ansible.builtin.lineinfile:
|
|
||||||
path: /etc/systemd/journald.conf
|
|
||||||
regexp: ForwardToSyslog
|
|
||||||
line: ForwardToSyslog=yes
|
|
||||||
state: present
|
|
||||||
backup: true
|
|
||||||
tags:
|
|
||||||
- journald
|
|
||||||
- syslog
|
|
||||||
|
|
||||||
- name: Restarting journald
|
|
||||||
become: true
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: systemd-journald
|
|
||||||
daemon_reload: true
|
|
||||||
state: restarted
|
|
||||||
enabled: true
|
|
||||||
tags:
|
|
||||||
- journald
|
|
||||||
- syslog
|
|
@ -1,11 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Coming up next
|
|
||||||
ansible.builtin.debug:
|
|
||||||
msg: Deploying branch {{ branch }} of {{ source_url }} to {{ env }} ({{ domain }})
|
|
||||||
tags:
|
|
||||||
- debug
|
|
||||||
- name: Installing required packages
|
|
||||||
ansible.builtin.apt: name={{ item }} state=present
|
|
||||||
with_items:
|
|
||||||
# acl is required to avoid "Failed to set permissions on the temporary files"
|
|
||||||
- acl
|
|
@ -1,14 +0,0 @@
|
|||||||
---
|
|
||||||
- name: "Writing {{ conf_d }}{{ conf_f }}"
|
|
||||||
vars:
|
|
||||||
conf_d: /etc/nginx/conf.d/
|
|
||||||
conf_f: log-format-upstreaminfo.conf
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: "templates/nginx/conf.d/{{ conf_f }}"
|
|
||||||
dest: "{{ conf_d }}{{ conf_f }}"
|
|
||||||
backup: true
|
|
||||||
mode: 0664
|
|
||||||
tags:
|
|
||||||
- nginx
|
|
||||||
- config
|
|
||||||
- log_format
|
|
@ -1,11 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Disabling nginx server tokens
|
|
||||||
ansible.builtin.lineinfile:
|
|
||||||
path: "{{ nginx_conf_dir }}/nginx.conf"
|
|
||||||
regexp: "server_tokens "
|
|
||||||
line: "\tserver_tokens off;" # noqa: no-tabs
|
|
||||||
state: present
|
|
||||||
backup: true
|
|
||||||
tags:
|
|
||||||
- nginx
|
|
||||||
- server_tokens
|
|
@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Enabling site configuration {{ config_file }}
|
|
||||||
ansible.builtin.file:
|
|
||||||
dest: "{{ nginx_conf_dir }}/sites-enabled/{{ config_file }}"
|
|
||||||
src: "{{ nginx_conf_dir }}/sites-available/{{ config_file }}"
|
|
||||||
state: link
|
|
||||||
force: true
|
|
@ -1,39 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Installing required packages
|
|
||||||
ansible.builtin.apt: name={{ item }} state=present
|
|
||||||
with_items:
|
|
||||||
- certbot
|
|
||||||
- git
|
|
||||||
- nginx
|
|
||||||
- python3-certbot-nginx
|
|
||||||
tags:
|
|
||||||
- nginx
|
|
||||||
- certbot
|
|
||||||
|
|
||||||
- name: Registering certbot
|
|
||||||
ansible.builtin.shell: >
|
|
||||||
certbot -n register --agree-tos --email {{ certbot.email }} &&
|
|
||||||
touch /etc/letsencrypt/.registered-{{ certbot.email }}
|
|
||||||
args:
|
|
||||||
creates: /etc/letsencrypt/.registered-{{ certbot.email }}
|
|
||||||
tags:
|
|
||||||
- nginx
|
|
||||||
- certbot
|
|
||||||
- register
|
|
||||||
|
|
||||||
- name: Getting a certificate for {{ domain }}
|
|
||||||
changed_when: true
|
|
||||||
ansible.builtin.command: >
|
|
||||||
certbot -n --nginx -d {{ domain }} --redirect
|
|
||||||
# N.B.: in certain cases certbot ignores systemd nginx
|
|
||||||
# (see https://github.com/certbot/certbot/issues/5486),
|
|
||||||
# which can be fixed with addition of the following hooks, but these hooks, in turn,
|
|
||||||
# produce a noticeable downtime for all services, leaving previous uwsgi connections
|
|
||||||
# producing "Connection reset"s until upstream uwsgi is restarted.
|
|
||||||
# --pre-hook "nginx -t && systemctl stop nginx"
|
|
||||||
# --post-hook "nginx -t && killall nginx; systemctl start nginx"
|
|
||||||
ignore_errors: false
|
|
||||||
tags:
|
|
||||||
- nginx
|
|
||||||
- certbot
|
|
||||||
- issue
|
|
@ -1,13 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Remove the default site
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ nginx_conf_dir }}/sites-enabled/default"
|
|
||||||
state: absent
|
|
||||||
- name: Remove the default site
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ nginx_conf_dir }}/sites-enabled/default.conf"
|
|
||||||
state: absent
|
|
||||||
- name: Remove the default configuration
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ nginx_conf_dir }}/conf.d/default.conf"
|
|
||||||
state: absent
|
|
@ -1,25 +0,0 @@
|
|||||||
---
|
|
||||||
- hosts: http
|
|
||||||
gather_facts: true
|
|
||||||
become: true
|
|
||||||
become_user: "{{ user }}"
|
|
||||||
roles: [common]
|
|
||||||
vars:
|
|
||||||
playbook_type: deploy
|
|
||||||
lock_file_path: /tmp/deploy-{{project_slug}}.lock
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
- import_tasks: tasks/pull.yaml
|
|
||||||
- import_tasks: tasks/deploy.yaml
|
|
||||||
|
|
||||||
pre_tasks:
|
|
||||||
- stat: path={{lock_file_path}}
|
|
||||||
register: lock_file
|
|
||||||
|
|
||||||
- fail: msg="Another deploy was already started by {{lock_file.stat.pw_name}} {{((ansible_date_time.epoch|float - lock_file.stat.mtime) / 60)|int}}min ago.\nAdd '-e override_lock=true' to override if the deploy was abandoned."
|
|
||||||
when: lock_file.stat.exists|bool and override_lock is undefined
|
|
||||||
|
|
||||||
- copy: dest={{lock_file_path}} content="{{ansible_user_id}} locked at {{now(fmt='%Y-%m-%d %H:%M:%S')}}"
|
|
||||||
|
|
||||||
post_tasks:
|
|
||||||
- file: path={{lock_file_path}} state=absent
|
|
@ -1,21 +0,0 @@
|
|||||||
---
|
|
||||||
- hosts: localhost
|
|
||||||
connection: local
|
|
||||||
gather_facts: false
|
|
||||||
tasks:
|
|
||||||
- name: Creating download directory
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ maxminddb_download_path }}"
|
|
||||||
mode: u+rwx
|
|
||||||
state: directory
|
|
||||||
register: ttt
|
|
||||||
tags:
|
|
||||||
- maxminddb
|
|
||||||
|
|
||||||
- name: Downloading and extracting MaxMind database from {{ maxminddb_url }}
|
|
||||||
ansible.builtin.unarchive:
|
|
||||||
src: "{{ maxminddb_url }}?suffix=tar.gz&license_key={{ maxminddb_license_key }}&edition_id={{ maxminddb_edition }}"
|
|
||||||
dest: "{{ maxminddb_download_path }}"
|
|
||||||
remote_src: true
|
|
||||||
tags:
|
|
||||||
- maxminddb
|
|
@ -1 +1,10 @@
|
|||||||
env: production
|
env: production
|
||||||
|
domain: studio.blender.org
|
||||||
|
host: web-production-1.hz-nbg1.blender.internal
|
||||||
|
allowed_hosts: "{{ domain }}"
|
||||||
|
|
||||||
|
db_host: db-postgres-production-1.hz-nbg1.blender.internal
|
||||||
|
|
||||||
|
ssl_only: true
|
||||||
|
|
||||||
|
admins: 'Anna Sirota: anna@blender.org'
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
http:
|
ingress:
|
||||||
hosts:
|
hosts:
|
||||||
web-studio:
|
lb-production-1.hz-nbg1.blender.internal:
|
||||||
|
|
||||||
https:
|
application:
|
||||||
hosts:
|
hosts:
|
||||||
sintel.blender.org:
|
web-production-1.hz-nbg1.blender.internal:
|
||||||
|
@ -1,77 +0,0 @@
|
|||||||
---
|
|
||||||
- import_playbook: download_maxmind_db.yaml
|
|
||||||
|
|
||||||
- hosts: http
|
|
||||||
gather_facts: false
|
|
||||||
become: true
|
|
||||||
roles: [common]
|
|
||||||
tasks:
|
|
||||||
- name: Installing required packages
|
|
||||||
ansible.builtin.apt: name={{ item }} state=present
|
|
||||||
with_items:
|
|
||||||
- git
|
|
||||||
- libjpeg-dev
|
|
||||||
- libpq-dev
|
|
||||||
- libxml2-dev
|
|
||||||
- libxslt-dev
|
|
||||||
- nginx
|
|
||||||
- postfix # to be able to configure /etc/aliases for cron
|
|
||||||
- postgresql-client
|
|
||||||
- python3
|
|
||||||
- python3-pip
|
|
||||||
- python3-virtualenv
|
|
||||||
- uwsgi
|
|
||||||
- uwsgi-plugin-python3
|
|
||||||
- vim
|
|
||||||
- zlib1g
|
|
||||||
- zlib1g-dev
|
|
||||||
|
|
||||||
- name: Creating user "{{ user }}:{{ group }}"
|
|
||||||
ansible.builtin.user:
|
|
||||||
name: "{{ user }}"
|
|
||||||
group: "{{ group }}"
|
|
||||||
|
|
||||||
- import_tasks: common/tasks/add_alias.yaml
|
|
||||||
|
|
||||||
# In systemd coming with Ubuntu 22.04 Standard{Output,Error}=syslog is no longer supported
|
|
||||||
# however, we still need it because log aggregation relies on syslog, not journald.
|
|
||||||
# This is why ForwardToSyslog is enabled system-wide.
|
|
||||||
- import_tasks: common/tasks/journald/forward_to_syslog.yaml
|
|
||||||
|
|
||||||
- name: Creating various directories
|
|
||||||
ansible.builtin.file: path={{ item }} state=directory owner={{ user }} group={{ group }} recurse=yes
|
|
||||||
with_items:
|
|
||||||
- "{{ dir.errors }}"
|
|
||||||
- "{{ dir.config }}"
|
|
||||||
- "{{ dir.media }}"
|
|
||||||
- "{{ dir.source }}"
|
|
||||||
- "{{ dir.pipeline_docs }}"
|
|
||||||
|
|
||||||
- import_tasks: tasks/pull.yaml
|
|
||||||
|
|
||||||
- name: Creating {{ env_file }}
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: templates/dotenv
|
|
||||||
dest: "{{ env_file }}"
|
|
||||||
mode: 0644
|
|
||||||
backup: true
|
|
||||||
tags:
|
|
||||||
- dotenv
|
|
||||||
|
|
||||||
- import_tasks: tasks/copy_maxmind_db.yaml
|
|
||||||
|
|
||||||
- import_tasks: tasks/configure_uwsgi.yaml
|
|
||||||
|
|
||||||
- import_tasks: tasks/deploy.yaml
|
|
||||||
become: true
|
|
||||||
become_user: "{{ user }}"
|
|
||||||
|
|
||||||
- import_tasks: tasks/configure_nginx.yaml
|
|
||||||
tags:
|
|
||||||
- nginx
|
|
||||||
|
|
||||||
- import_tasks: tasks/setup_other_services.yaml
|
|
||||||
tags:
|
|
||||||
- services
|
|
||||||
|
|
||||||
- import_playbook: install_meilisearch.yaml
|
|
@ -11,7 +11,7 @@
|
|||||||
tags:
|
tags:
|
||||||
- meilisearch
|
- meilisearch
|
||||||
|
|
||||||
- hosts: http
|
- hosts: application
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
become: true
|
become: true
|
||||||
tasks:
|
tasks:
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
ansible==5.2.0
|
|
||||||
ansible-compat==2.1.0
|
|
||||||
ansible-core==2.12.1
|
|
||||||
ansible-lint==6.2.2
|
|
||||||
attrs==21.4.0
|
|
||||||
bracex==2.3.post1
|
|
||||||
cffi==1.15.0
|
|
||||||
commonmark==0.9.1
|
|
||||||
cryptography==36.0.1
|
|
||||||
enrich==1.2.7
|
|
||||||
iniconfig==1.1.1
|
|
||||||
Jinja2==3.0.3
|
|
||||||
jsonschema==4.6.0
|
|
||||||
MarkupSafe==2.0.1
|
|
||||||
packaging==21.3
|
|
||||||
pathspec==0.9.0
|
|
||||||
pluggy==1.0.0
|
|
||||||
py==1.11.0
|
|
||||||
pycparser==2.21
|
|
||||||
Pygments==2.12.0
|
|
||||||
pyparsing==3.0.6
|
|
||||||
pyrsistent==0.18.1
|
|
||||||
pytest==7.1.2
|
|
||||||
PyYAML==6.0
|
|
||||||
resolvelib==0.5.4
|
|
||||||
rich==12.4.4
|
|
||||||
ruamel.yaml==0.17.21
|
|
||||||
ruamel.yaml.clib==0.2.6
|
|
||||||
subprocess-tee==0.3.5
|
|
||||||
tomli==2.0.1
|
|
||||||
wcmatch==8.4
|
|
||||||
yamllint==1.26.3
|
|
@ -1,42 +0,0 @@
|
|||||||
---
|
|
||||||
- hosts: https
|
|
||||||
gather_facts: false
|
|
||||||
become: true
|
|
||||||
roles: [common]
|
|
||||||
tasks:
|
|
||||||
- name: Creating {{ nginx_temp_path }}
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ nginx_temp_path }}"
|
|
||||||
state: directory
|
|
||||||
owner: "{{ nginx.user }}"
|
|
||||||
group: "{{ nginx.group }}"
|
|
||||||
recurse: true
|
|
||||||
when: nginx_temp_path is defined
|
|
||||||
|
|
||||||
- name: Creating errors directory
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ dir.errors }}"
|
|
||||||
state: directory
|
|
||||||
owner: "{{ nginx.user }}"
|
|
||||||
group: "{{ nginx.group }}"
|
|
||||||
recurse: true
|
|
||||||
tags:
|
|
||||||
- error-pages
|
|
||||||
|
|
||||||
- import_tasks: tasks/copy_error_pages.yaml
|
|
||||||
|
|
||||||
- import_tasks: common/tasks/nginx/add_upstreaminfo_log_format.yaml
|
|
||||||
|
|
||||||
- name: Copying nginx config
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: templates/nginx/https.conf
|
|
||||||
dest: "{{ nginx_conf_dir }}/sites-available/{{ service_name }}.conf"
|
|
||||||
mode: 0644
|
|
||||||
|
|
||||||
- import_tasks: common/tasks/nginx/enable_site.yaml
|
|
||||||
vars:
|
|
||||||
config_file: "{{ service_name }}.conf"
|
|
||||||
|
|
||||||
- import_tasks: common/tasks/nginx/get_certificate.yaml
|
|
||||||
notify:
|
|
||||||
- test nginx
|
|
1
playbooks/shared
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 2380780e143c20a77852bdd8d92da3a274726210
|
@ -1,27 +0,0 @@
|
|||||||
---
|
|
||||||
- import_tasks: common/tasks/nginx/remove_default_site.yaml
|
|
||||||
- import_tasks: common/tasks/nginx/disable_server_tokens.yaml
|
|
||||||
|
|
||||||
- name: Copying nginx config snippets
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: "{{ item }}"
|
|
||||||
dest: "{{ dir.config }}/{{ item|basename }}"
|
|
||||||
mode: 0644
|
|
||||||
backup: true
|
|
||||||
with_fileglob:
|
|
||||||
- ../templates/nginx/snippets/*
|
|
||||||
|
|
||||||
- name: Copying nginx configs
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: templates/nginx/http.conf
|
|
||||||
dest: "{{ nginx_conf_dir }}/sites-available/{{ service_name }}.conf"
|
|
||||||
mode: 0644
|
|
||||||
backup: true
|
|
||||||
register: nginx_config
|
|
||||||
|
|
||||||
- import_tasks: common/tasks/nginx/enable_site.yaml
|
|
||||||
vars:
|
|
||||||
config_file: "{{ service_name }}.conf"
|
|
||||||
notify:
|
|
||||||
- test nginx
|
|
||||||
- reload nginx
|
|
@ -1,14 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Copying uWSGI config files
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: "{{ item.src }}"
|
|
||||||
dest: "{{ item.dest }}"
|
|
||||||
mode: 0644
|
|
||||||
backup: true
|
|
||||||
loop:
|
|
||||||
- { src: templates/uwsgi/uwsgi.ini, dest: "/etc/uwsgi/{{ service_name }}.ini" }
|
|
||||||
- { src: templates/uwsgi/uwsgi.service, dest: "/etc/systemd/system/{{ service_name }}.service" }
|
|
||||||
notify:
|
|
||||||
- restart service
|
|
||||||
tags:
|
|
||||||
- uwsgi
|
|
@ -1,24 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Copying error pages
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: templates/nginx/errors/{{ item.src }}
|
|
||||||
dest: "{{ dir.errors }}/{{ item.dest }}"
|
|
||||||
mode: 0644
|
|
||||||
loop:
|
|
||||||
- src: 4xx.html
|
|
||||||
dest: 401.html
|
|
||||||
title: Unauthorized
|
|
||||||
- src: 4xx.html
|
|
||||||
dest: 403.html
|
|
||||||
title: Forbidden
|
|
||||||
- src: 4xx.html
|
|
||||||
dest: 404.html
|
|
||||||
title: Page Not Found
|
|
||||||
message: The page you requested does not exist
|
|
||||||
- src: 4xx.html
|
|
||||||
dest: 405.html
|
|
||||||
title: Method Not Allowed
|
|
||||||
- src: 5xx.html
|
|
||||||
dest: 5xx.html
|
|
||||||
tags:
|
|
||||||
- error-pages
|
|
@ -1,12 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Copying MaxMind DB
|
|
||||||
ansible.builtin.copy:
|
|
||||||
src: "{{ item.src }}"
|
|
||||||
dest: "{{ dir.source }}"
|
|
||||||
owner: "{{ nginx.user }}"
|
|
||||||
group: "{{ group }}"
|
|
||||||
mode: u+rw,g+r
|
|
||||||
with_community.general.filetree: "{{ maxminddb_download_path }}"
|
|
||||||
when: '"GeoLite2-Country.mmdb" in item.get("src", "")'
|
|
||||||
tags:
|
|
||||||
- maxminddb
|
|
@ -1,71 +0,0 @@
|
|||||||
---
|
|
||||||
- import_tasks: copy_error_pages.yaml
|
|
||||||
become: true
|
|
||||||
become_user: "{{ user }}"
|
|
||||||
|
|
||||||
- name: Deleting {{ dir.source }}/.venv if it exists
|
|
||||||
become: true
|
|
||||||
become_user: "{{ user }}"
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ dir.source }}/.venv"
|
|
||||||
state: absent
|
|
||||||
when: delete_venv
|
|
||||||
tags:
|
|
||||||
- pip
|
|
||||||
|
|
||||||
- name: Install Python requirements in {{ dir.source }}/.venv
|
|
||||||
become: true
|
|
||||||
become_user: "{{ user }}"
|
|
||||||
ansible.builtin.pip:
|
|
||||||
requirements: "{{ dir.source }}/requirements_prod.txt"
|
|
||||||
virtualenv: "{{ dir.source }}/.venv"
|
|
||||||
virtualenv_command: python{{ python_version }} -m venv
|
|
||||||
chdir: "{{ dir.source }}"
|
|
||||||
tags:
|
|
||||||
- pip
|
|
||||||
|
|
||||||
- name: Preparing to run database migrations
|
|
||||||
become: true
|
|
||||||
become_user: "{{ user }}"
|
|
||||||
tags:
|
|
||||||
- migrate
|
|
||||||
block:
|
|
||||||
- name: Displaying database migrations
|
|
||||||
import_tasks: managepy.yaml
|
|
||||||
vars:
|
|
||||||
command: migrate --plan
|
|
||||||
- name: Displaying database migrations
|
|
||||||
ansible.builtin.debug:
|
|
||||||
var: management_command_output.stdout
|
|
||||||
|
|
||||||
- name: Confirming database migrations
|
|
||||||
when:
|
|
||||||
- management_command_output.stdout | length > 0
|
|
||||||
- ('No planned migration operations' not in management_command_output.stdout)
|
|
||||||
ansible.builtin.pause:
|
|
||||||
prompt: Press return to continue. Press Ctrl+c and then "a" to abort.
|
|
||||||
|
|
||||||
- name: Running database migrations
|
|
||||||
when:
|
|
||||||
- management_command_output.stdout | length > 0
|
|
||||||
- ('No planned migration operations' not in management_command_output.stdout)
|
|
||||||
import_tasks: managepy.yaml
|
|
||||||
vars:
|
|
||||||
command: migrate
|
|
||||||
changed_when:
|
|
||||||
- management_command_output.stdout | length > 0
|
|
||||||
- ('No planned migration operations' not in management_command_output.stdout)
|
|
||||||
|
|
||||||
- import_tasks: managepy.yaml
|
|
||||||
become: true
|
|
||||||
become_user: "{{ user }}"
|
|
||||||
vars:
|
|
||||||
command: collectstatic --noinput
|
|
||||||
changed_when: true
|
|
||||||
tags:
|
|
||||||
- collectstatic
|
|
||||||
notify:
|
|
||||||
- reload service
|
|
||||||
- restart background
|
|
||||||
- test nginx
|
|
||||||
- reload nginx
|
|
@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Running Django management command "{{ command }}"
|
|
||||||
ansible.builtin.shell:
|
|
||||||
cmd: >
|
|
||||||
bash -ca 'source {{ env_file }} &&
|
|
||||||
{{ dir.source }}/.venv/bin/python {{ dir.source }}/manage.py {{ command }}'
|
|
||||||
chdir: "{{ dir.source }}"
|
|
||||||
register: management_command_output
|
|
@ -1,11 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Pulling latest branch "{{ branch }}"
|
|
||||||
become: true
|
|
||||||
become_user: "{{ user }}"
|
|
||||||
ansible.builtin.git:
|
|
||||||
repo: "{{ source_url }}"
|
|
||||||
dest: "{{ dir.source }}"
|
|
||||||
accept_hostkey: true
|
|
||||||
version: "{{ branch }}"
|
|
||||||
tags:
|
|
||||||
- git
|
|
@ -1,29 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Copying systemd services
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: "{{ item }}"
|
|
||||||
dest: /etc/systemd/system/{{ service_name }}-{{ item|basename }}
|
|
||||||
mode: 0644
|
|
||||||
backup: true
|
|
||||||
with_fileglob:
|
|
||||||
- ../templates/other-services/*.service
|
|
||||||
|
|
||||||
- name: Copying systemd timers
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: "{{ item }}"
|
|
||||||
dest: /etc/systemd/system/{{ service_name }}-{{ item|basename }}
|
|
||||||
mode: 0644
|
|
||||||
backup: true
|
|
||||||
with_fileglob:
|
|
||||||
- ../templates/other-services/*.timer
|
|
||||||
|
|
||||||
- name: Enabling systemd timers
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: "{{ service_name }}-{{ item|basename }}"
|
|
||||||
state: restarted
|
|
||||||
enabled: true
|
|
||||||
daemon_reload: true
|
|
||||||
tags:
|
|
||||||
- timers
|
|
||||||
with_fileglob:
|
|
||||||
- ../templates/other-services/*.timer
|
|
@ -1,3 +0,0 @@
|
|||||||
[virtualenvs]
|
|
||||||
in-project = true
|
|
||||||
create = false
|
|
46
playbooks/templates/nginx/application.conf
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
{% extends "templates/nginx/base_application.conf" %}
|
||||||
|
{% block extra_locations %}
|
||||||
|
|
||||||
|
rewrite ^/spring /films/spring permanent;
|
||||||
|
rewrite ^/sprite-fright /films/sprite-fright permanent;
|
||||||
|
rewrite ^/charge$ /films/charge permanent;
|
||||||
|
rewrite ^/films/heist/(.*)$ /films/charge/$1 permanent;
|
||||||
|
rewrite ^/wing-it /films/wing-it permanent;
|
||||||
|
rewrite ^/films/pet-projects/(.*)$ /films/wing-it/$1 permanent;
|
||||||
|
|
||||||
|
location /robots.txt {
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
return 200 "";
|
||||||
|
}
|
||||||
|
|
||||||
|
location /favicon.ico {
|
||||||
|
root {{ dir.static }}/common/images/favicon;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2024-02-01 (anna): someone is hotlinking old static, this will stop requests from hammering the app
|
||||||
|
location /static-studio/ {
|
||||||
|
add_header Cache-Control "public,max-age=31536000";
|
||||||
|
return 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
charset utf-8;
|
||||||
|
proxy_connect_timeout {{ proxy_timeout }};
|
||||||
|
proxy_send_timeout {{ proxy_timeout }};
|
||||||
|
proxy_read_timeout {{ proxy_timeout }};
|
||||||
|
send_timeout {{ proxy_timeout }};
|
||||||
|
keepalive_requests 1000;
|
||||||
|
keepalive_timeout {{ keepalive_timeout }};
|
||||||
|
|
||||||
|
# The Pipelines and Tools documentation (output of the blender-studio-pipeline repo)
|
||||||
|
location /pipeline {
|
||||||
|
alias {{ dir.pipeline_docs }};
|
||||||
|
index index.html;
|
||||||
|
try_files $uri.html $uri $uri/ =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Meilisearch
|
||||||
|
location {{ meilisearch_endpoint }} {
|
||||||
|
rewrite {{ meilisearch_endpoint }}(.*) /$1 break;
|
||||||
|
proxy_pass http://{{ meilisearch_host }};
|
||||||
|
}
|
||||||
|
{% endblock extra_locations %}
|
@ -1,5 +0,0 @@
|
|||||||
log_format upstreaminfo
|
|
||||||
'$remote_addr - $remote_user [$time_local] "$request" '
|
|
||||||
'$status $body_bytes_sent "$http_referer" "$http_user_agent" '
|
|
||||||
'$request_length $request_time [upstream-{{ env }}-service-1234] [upstream-{{ env }}-service-1234] $upstream_addr '
|
|
||||||
'$upstream_response_length $upstream_response_time $upstream_status $request_id';
|
|
@ -1,76 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta http-equiv="refresh" content="5">
|
|
||||||
<title>{{ project_name }} — blender.org</title>
|
|
||||||
|
|
||||||
<style type='text/css'>
|
|
||||||
html, body {
|
|
||||||
background-color: #f8f8f8;
|
|
||||||
color: #4d4e53;
|
|
||||||
display: flex;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
section {
|
|
||||||
width: 600px;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
article {
|
|
||||||
background-color: #fff;
|
|
||||||
box-shadow: rgba(0, 0, 0, 0.25) 0px 1px 4px -1px;
|
|
||||||
border-radius: 3px;
|
|
||||||
padding: 30px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
background: linear-gradient(to right, #0cc, violet);
|
|
||||||
background-clip: text;
|
|
||||||
-webkit-bg-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
font-weight: 300;
|
|
||||||
font-size: 1.6em;
|
|
||||||
margin: 0 auto 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
color: #78b13f;
|
|
||||||
padding: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
small {
|
|
||||||
border-top: thin solid rgba(0,0,0,0.1);
|
|
||||||
color: #9E9FA2;
|
|
||||||
display: block;
|
|
||||||
padding-top: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.check {
|
|
||||||
border-bottom: 3px solid #78b13f;
|
|
||||||
border-right: 3px solid #78b13f;
|
|
||||||
display: inline-block;
|
|
||||||
height: 10px;
|
|
||||||
margin-right: 10px;
|
|
||||||
transform: rotate(45deg) translateY(-2px);
|
|
||||||
width: 5px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<section>
|
|
||||||
<article>
|
|
||||||
<h1>{{ project_name }}</h1>
|
|
||||||
<p><span class="check"></span> We're updating the server, it should be back within a minute.</p>
|
|
||||||
<small>This page automatically refreshes and tries again for you.</small>
|
|
||||||
</article>
|
|
||||||
</section>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
@ -1,75 +0,0 @@
|
|||||||
# uWSGI connection to the Studio Django app.
|
|
||||||
upstream studio_{{ env }}_app {
|
|
||||||
server {{ uwsgi_socket }};
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
server_name {{ domain }};
|
|
||||||
|
|
||||||
set_real_ip_from 0.0.0.0/0;
|
|
||||||
set_real_ip_from ::/0;
|
|
||||||
real_ip_header X-Forwarded-For;
|
|
||||||
real_ip_recursive on;
|
|
||||||
|
|
||||||
if ($host !~* ^({{ domain }})$) {
|
|
||||||
return 444;
|
|
||||||
}
|
|
||||||
|
|
||||||
access_log /var/log/nginx/{{ domain }}-access.log;
|
|
||||||
error_log /var/log/nginx/{{ domain }}-error.log;
|
|
||||||
|
|
||||||
rewrite ^/spring /films/spring permanent;
|
|
||||||
rewrite ^/sprite-fright /films/sprite-fright permanent;
|
|
||||||
rewrite ^/charge$ /films/charge permanent;
|
|
||||||
rewrite ^/films/heist/(.*)$ /films/charge/$1 permanent;
|
|
||||||
rewrite ^/wing-it /films/wing-it permanent;
|
|
||||||
rewrite ^/films/pet-projects/(.*)$ /films/wing-it/$1 permanent;
|
|
||||||
|
|
||||||
location /robots.txt {
|
|
||||||
add_header Content-Type text/plain;
|
|
||||||
return 200 "";
|
|
||||||
}
|
|
||||||
|
|
||||||
location /favicon.ico {
|
|
||||||
root {{ dir.static }}/common/images/favicon;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
include {{ dir.config }}/studio.common.conf;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
uwsgi_pass studio_{{ env }}_app;
|
|
||||||
# FIXME(anna): move to uwsgi_params after LB proxying is no more.
|
|
||||||
uwsgi_param HOST $host;
|
|
||||||
include {{ dir.config }}/studio_uwsgi_params;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
# Proxied server config. Assumes the TLS termination is done upstream.
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name 10.129.22.239;
|
|
||||||
server_name cloudbalance.blender.org;
|
|
||||||
server_name cloud.blender.org;
|
|
||||||
|
|
||||||
include {{ dir.config }}/studio.common.conf;
|
|
||||||
|
|
||||||
# Only allow HTTP traffic from our private network.
|
|
||||||
allow 10.129.0.0/16;
|
|
||||||
deny all;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
uwsgi_pass studio_{{ env }}_app;
|
|
||||||
include {{ dir.config }}/studio_uwsgi_params;
|
|
||||||
|
|
||||||
# Override some parameters to let uWSGI know the client is
|
|
||||||
# using HTTPS.
|
|
||||||
uwsgi_param REQUEST_SCHEME https;
|
|
||||||
uwsgi_param HTTPS true;
|
|
||||||
|
|
||||||
# Parameters that are not in the default studio_uwsgi_params,
|
|
||||||
# but are documented on https://uwsgi-docs.readthedocs.io/en/latest/Vars.html
|
|
||||||
uwsgi_param UWSGI_SCHEME https;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,90 +0,0 @@
|
|||||||
# This will be updated by certbot.
|
|
||||||
# N.B.: map always sets global variables visible across the server blocks.
|
|
||||||
# Use unique variable name unless expecting map results to be shared across all servers.
|
|
||||||
map $remote_addr ${{ nginx_var_prefix }}_allowed {
|
|
||||||
default 0;
|
|
||||||
127.0.0.1 1; # For local connections
|
|
||||||
{% if allowed_ips is defined %}{% for ip in allowed_ips %}
|
|
||||||
{{ ip }} 1;
|
|
||||||
{% endfor %}{% endif %}
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
server_name {{ domain }};
|
|
||||||
|
|
||||||
# Avoid DisallowedHost "Invalid HTTP_HOST header" errors
|
|
||||||
if ($host !~* ^({{ domain }})$) {
|
|
||||||
return 444;
|
|
||||||
}
|
|
||||||
|
|
||||||
{% if env != 'production' %}{% for ip in allowed_ips %}
|
|
||||||
allow {{ ip }};
|
|
||||||
{% endfor %}
|
|
||||||
deny all;
|
|
||||||
{% endif %}
|
|
||||||
access_log /var/log/nginx/studio.blender.org-access.log upstreaminfo_forwarded_for;
|
|
||||||
error_log /var/log/nginx/{{ domain }}-error.log;
|
|
||||||
|
|
||||||
#location /robots.txt {
|
|
||||||
# add_header Content-Type text/plain;
|
|
||||||
# return 200 "User-agent: *\nDisallow: /\n";
|
|
||||||
#}
|
|
||||||
|
|
||||||
client_max_body_size {{ client_max_body_size }};
|
|
||||||
{% if nginx_temp_path is defined %}
|
|
||||||
client_body_temp_path {{ nginx_temp_path }} 1 2;
|
|
||||||
proxy_temp_path {{ nginx_temp_path }} 1 2;
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
location / {
|
|
||||||
set $maintenance 0;
|
|
||||||
if (-f {{ dir.errors }}/maintenance_on) {
|
|
||||||
set $maintenance 1;
|
|
||||||
}
|
|
||||||
if (${{ nginx_var_prefix }}_allowed != 1) {
|
|
||||||
set $maintenance "${maintenance}1";
|
|
||||||
}
|
|
||||||
if ($maintenance = 11) {
|
|
||||||
return 503;
|
|
||||||
}
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
|
|
||||||
proxy_pass http://{{ host }}/;
|
|
||||||
proxy_connect_timeout 600s;
|
|
||||||
proxy_read_timeout 600s;
|
|
||||||
proxy_send_timeout 600s;
|
|
||||||
proxy_ssl_name $host;
|
|
||||||
proxy_ssl_server_name on;
|
|
||||||
send_timeout 600s;
|
|
||||||
}
|
|
||||||
|
|
||||||
error_page 403 /errors/403.html;
|
|
||||||
error_page 404 /errors/404.html;
|
|
||||||
error_page 405 /errors/405.html;
|
|
||||||
error_page 500 501 502 503 504 /errors/5xx.html;
|
|
||||||
location ^~ /errors/ {
|
|
||||||
alias {{ dir.errors | regex_replace('\\/*$', '/') }};
|
|
||||||
internal;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Some other security related headers
|
|
||||||
# NOTE: Avoiding use of includeSubdomains in case "blender.org" alias causes it
|
|
||||||
# to match every subdomain of blender.org, at least until we are ready -- Dan
|
|
||||||
add_header Strict-Transport-Security "max-age=63072000; preload";
|
|
||||||
add_header X-Frame-Options "SAMEORIGIN";
|
|
||||||
add_header X-Xss-Protection "1; mode=block";
|
|
||||||
add_header X-Content-Type-Options "nosniff";
|
|
||||||
{% if "127.0.0.1" in host %}
|
|
||||||
location /media/ {
|
|
||||||
# must end with a slash in order to work
|
|
||||||
alias {{ dir.media | regex_replace('\\/*$', '/') }};
|
|
||||||
}
|
|
||||||
location /static/ {
|
|
||||||
# must end with a slash in order to work
|
|
||||||
alias {{ dir.static | regex_replace('\\/*$', '/') }};
|
|
||||||
}
|
|
||||||
{% endif %}
|
|
||||||
}
|
|
1
playbooks/templates/nginx/ingress.conf
Normal file
@ -0,0 +1 @@
|
|||||||
|
{% extends "templates/nginx/base_ingress.conf" %}
|
@ -1,43 +0,0 @@
|
|||||||
# This file is intended to be included in a server{} block.
|
|
||||||
# It contains the configuration that's common to both the name-based
|
|
||||||
# (public; this server does HTTPS) and IP-based (behind proxy; this
|
|
||||||
# server assumes the proxy does HTTPS) server blocks.
|
|
||||||
charset utf-8;
|
|
||||||
client_max_body_size {{ client_max_body_size }};
|
|
||||||
proxy_connect_timeout 600;
|
|
||||||
proxy_send_timeout 600;
|
|
||||||
proxy_read_timeout 600;
|
|
||||||
send_timeout 600;
|
|
||||||
keepalive_requests 1000;
|
|
||||||
keepalive_timeout 600s;
|
|
||||||
|
|
||||||
error_page 403 /errors/403.html;
|
|
||||||
error_page 404 /errors/404.html;
|
|
||||||
error_page 405 /errors/405.html;
|
|
||||||
error_page 500 501 502 503 504 /errors/5xx.html;
|
|
||||||
location ^~ /errors/ {
|
|
||||||
alias {{ dir.errors | regex_replace('\\/*$', '/') }};
|
|
||||||
internal;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Django media, unused as we use S3 buckets for the media.
|
|
||||||
location {{ media_url }} {
|
|
||||||
alias {{ dir.media | regex_replace('\\/*$', '/') }};
|
|
||||||
}
|
|
||||||
|
|
||||||
location {{ static_url }} {
|
|
||||||
alias {{ dir.static | regex_replace('\\/*$', '/') }};
|
|
||||||
}
|
|
||||||
|
|
||||||
# The Pipelines and Tools documentation (output of the blender-studio-pipeline repo)
|
|
||||||
location /pipeline {
|
|
||||||
alias {{ dir.pipeline_docs }};
|
|
||||||
index index.html;
|
|
||||||
try_files $uri.html $uri $uri/ =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Meilisearch
|
|
||||||
location {{ meilisearch_endpoint }} {
|
|
||||||
rewrite {{ meilisearch_endpoint }}(.*) /$1 break;
|
|
||||||
proxy_pass http://{{ meilisearch_host }};
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
uwsgi_param QUERY_STRING $query_string;
|
|
||||||
uwsgi_param REQUEST_METHOD $request_method;
|
|
||||||
uwsgi_param CONTENT_TYPE $content_type;
|
|
||||||
uwsgi_param CONTENT_LENGTH $content_length;
|
|
||||||
|
|
||||||
uwsgi_param REQUEST_URI $request_uri;
|
|
||||||
uwsgi_param PATH_INFO $document_uri;
|
|
||||||
uwsgi_param DOCUMENT_ROOT $document_root;
|
|
||||||
uwsgi_param SERVER_PROTOCOL $server_protocol;
|
|
||||||
uwsgi_param REQUEST_SCHEME $scheme;
|
|
||||||
uwsgi_param HTTPS $https if_not_empty;
|
|
||||||
|
|
||||||
uwsgi_param REMOTE_ADDR $remote_addr;
|
|
||||||
uwsgi_param REMOTE_PORT $remote_port;
|
|
||||||
uwsgi_param SERVER_PORT $server_port;
|
|
||||||
uwsgi_param SERVER_NAME $server_name;
|
|
@ -1,6 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=restart {{ project_name }} background worker
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
ExecStart=/bin/systemctl restart {{ background_service_name }}.service
|
|
@ -1,6 +0,0 @@
|
|||||||
[Timer]
|
|
||||||
OnActiveSec=48h
|
|
||||||
OnUnitActiveSec=48h
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
@ -1,18 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description={{ project_name }} background worker
|
|
||||||
After=syslog.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
User={{ user }}
|
|
||||||
Group={{ group }}
|
|
||||||
EnvironmentFile={{ env_file }}
|
|
||||||
ExecStart={{ dir.source }}/.venv/bin/python {{ dir.source }}/manage.py process_tasks
|
|
||||||
ExecStop=kill -s SIGTSTP $MAINPID
|
|
||||||
Restart=always
|
|
||||||
|
|
||||||
Type=idle
|
|
||||||
SyslogIdentifier={{ service_name }}
|
|
||||||
NotifyAccess=all
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
@ -1,9 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Clear expired {{ project_name }} sessions
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
User={{ user }}
|
|
||||||
Group={{ group }}
|
|
||||||
ExecStart={{ dir.source }}/.venv/bin/python {{ dir.source }}/manage.py clearsessions
|
|
||||||
SyslogIdentifier={{ service_name }}
|
|
@ -1,7 +0,0 @@
|
|||||||
[Timer]
|
|
||||||
OnCalendar=daily
|
|
||||||
RandomizedDelaySec=5h
|
|
||||||
AccuracySec=1us
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
@ -1,10 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Deleted completed tasks of {{ project_name }}
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
User={{ user }}
|
|
||||||
Group={{ group }}
|
|
||||||
EnvironmentFile={{ env_file }}
|
|
||||||
ExecStart={{ dir.source }}/.venv/bin/python {{ dir.source }}/manage.py delete_completed_tasks
|
|
||||||
SyslogIdentifier={{ service_name }}
|
|
@ -1,7 +0,0 @@
|
|||||||
[Timer]
|
|
||||||
OnCalendar=daily
|
|
||||||
RandomizedDelaySec=5h
|
|
||||||
AccuracySec=1us
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
@ -9,18 +9,22 @@ client_max_body_size: 5500M
|
|||||||
python_version: "3.10"
|
python_version: "3.10"
|
||||||
delete_venv: false # set to true if venv has to be re-created from scratch
|
delete_venv: false # set to true if venv has to be re-created from scratch
|
||||||
|
|
||||||
|
# Set to true if ingress == application:
|
||||||
|
# meaning that SSL is terminated by and Django app is run on the same host.
|
||||||
|
single_host: false
|
||||||
|
|
||||||
dir:
|
dir:
|
||||||
source: "/opt/{{ service_name }}"
|
source: "/opt/{{ service_name }}"
|
||||||
static: "/var/www/{{ service_name }}/static"
|
static: "/var/www/{{ service_name }}/static"
|
||||||
media: "/var/www/{{ service_name }}/media"
|
media: "/var/www/{{ service_name }}/media"
|
||||||
errors: "/var/www/{{ service_name }}/html/errors"
|
errors: "/var/www/{{ service_name }}/html/errors"
|
||||||
config: /etc/nginx/snippets
|
|
||||||
pipeline_docs: "/var/www/blender-studio-pipeline-{{ env }}"
|
pipeline_docs: "/var/www/blender-studio-pipeline-{{ env }}"
|
||||||
|
|
||||||
env_file: "{{ dir.source }}/.env"
|
env_file: "{{ dir.source }}/.env"
|
||||||
uwsgi_pid: "{{ dir.source }}/{{ service_name }}.pid"
|
uwsgi_pid: "{{ dir.source }}/{{ service_name }}.pid"
|
||||||
uwsgi_module: studio.wsgi
|
uwsgi_module: studio.wsgi
|
||||||
uwsgi_socket: "unix://{{ dir.source }}/studio.sock"
|
uwsgi_processes: 8
|
||||||
|
uwsgi_socket: "{{ dir.source }}/uwsgi.sock"
|
||||||
host: web-studio.internal
|
host: web-studio.internal
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
@ -29,19 +33,26 @@ nginx:
|
|||||||
nginx_conf_dir: /etc/nginx
|
nginx_conf_dir: /etc/nginx
|
||||||
# Studio workflows include heavy uploads, so client temp path must have plenty of disk space
|
# Studio workflows include heavy uploads, so client temp path must have plenty of disk space
|
||||||
nginx_temp_path: /data/nginx/tmp
|
nginx_temp_path: /data/nginx/tmp
|
||||||
# For prepending to variable names in cases when they have to be set outside server block,
|
|
||||||
# e.g. for use in a `map $something ... {}`.
|
|
||||||
nginx_var_prefix: "{{ service_name|regex_replace('-', '_') }}"
|
|
||||||
user: "studio-{{ env }}"
|
user: "studio-{{ env }}"
|
||||||
group: "{{ nginx.group }}"
|
group: "{{ nginx.group }}"
|
||||||
|
rate_limit:
|
||||||
|
name: 'hundred_per_minute'
|
||||||
|
size: '10m'
|
||||||
|
rate: '100r/m'
|
||||||
|
burst: 50
|
||||||
|
delay: 10
|
||||||
|
keepalive_timeout: "600s"
|
||||||
|
|
||||||
mailto: cron@blender.org
|
aliases: null # This project doesn't use cron
|
||||||
certbot:
|
certbot:
|
||||||
email: root@blender.org
|
email: root@blender.org
|
||||||
|
|
||||||
source_url: https://projects.blender.org/studio/{{ project_slug }}.git
|
source_url: https://projects.blender.org/studio/{{ project_slug }}.git
|
||||||
branch: production
|
branch: production
|
||||||
|
|
||||||
|
ssl_only: false
|
||||||
|
ca_certificate: /usr/local/share/ca-certificates/cloud-init-ca-cert-1.crt
|
||||||
|
|
||||||
meilisearch_version: 0.25.2
|
meilisearch_version: 0.25.2
|
||||||
meilisearch_user: meilisearch
|
meilisearch_user: meilisearch
|
||||||
meilisearch_group: "{{ group }}"
|
meilisearch_group: "{{ group }}"
|
||||||
@ -53,14 +64,20 @@ meilisearch_database: "{{ meilisearch_home }}/data.ms"
|
|||||||
meilisearch_bin: meilisearch-{{ meilisearch_version }}
|
meilisearch_bin: meilisearch-{{ meilisearch_version }}
|
||||||
meilisearch_bin_path: /usr/bin/{{ meilisearch_bin }}
|
meilisearch_bin_path: /usr/bin/{{ meilisearch_bin }}
|
||||||
|
|
||||||
maxminddb_edition: GeoLite2-Country
|
maxmind_license_key: 'SET-IN-VAULT'
|
||||||
maxminddb_url: https://download.maxmind.com/app/geoip_download
|
maxmind:
|
||||||
maxminddb_path: /opt/maxmind
|
edition: GeoLite2-Country
|
||||||
maxminddb_download_path: /tmp/maxmind
|
url: https://download.maxmind.com/app/geoip_download
|
||||||
|
path: /opt/maxmind
|
||||||
|
download_path: /tmp/maxmind
|
||||||
|
license_key: "{{ maxmind_license_key }}"
|
||||||
|
|
||||||
media_url: /media/
|
media_url: /media/
|
||||||
static_url: /static/
|
static_url: /static/
|
||||||
|
|
||||||
|
db_user: "studio_{{ env }}"
|
||||||
|
db_name: "studio_{{ env }}"
|
||||||
|
|
||||||
allowed_hosts: "{{ domain }},cloudbalance.blender.org,cloud.blender.org"
|
allowed_hosts: "{{ domain }},cloudbalance.blender.org,cloud.blender.org"
|
||||||
|
|
||||||
# The following variables should be encrypted with Ansible Vault
|
# The following variables should be encrypted with Ansible Vault
|
||||||
@ -69,4 +86,28 @@ allowed_hosts: "{{ domain }},cloudbalance.blender.org,cloud.blender.org"
|
|||||||
|
|
||||||
# sentry_dsn:
|
# sentry_dsn:
|
||||||
# meili_master_key:
|
# meili_master_key:
|
||||||
# maxminddb_license_key:
|
|
||||||
|
include_common_services:
|
||||||
|
- background
|
||||||
|
- background-restart
|
||||||
|
- clearsessions
|
||||||
|
- delete-completed-tasks
|
||||||
|
- notify-email@
|
||||||
|
|
||||||
|
# Override required packages list
|
||||||
|
packages_common:
|
||||||
|
- git
|
||||||
|
- libjpeg-dev
|
||||||
|
- libpq-dev
|
||||||
|
- libxml2-dev
|
||||||
|
- libxslt-dev
|
||||||
|
- nginx
|
||||||
|
- postgresql-client
|
||||||
|
- python3-pip
|
||||||
|
- python{{ python_version }}
|
||||||
|
- python{{ python_version }}-dev
|
||||||
|
- python{{ python_version }}-distutils
|
||||||
|
- python{{ python_version }}-venv
|
||||||
|
- vim
|
||||||
|
- zlib1g
|
||||||
|
- zlib1g-dev
|
||||||
|
@ -81,7 +81,6 @@ pypdf==4.2.0
|
|||||||
pypng==0.20220715.0
|
pypng==0.20220715.0
|
||||||
python-bidi==0.4.2
|
python-bidi==0.4.2
|
||||||
python-dateutil==2.8.2
|
python-dateutil==2.8.2
|
||||||
python-dotenv==0.21.0
|
|
||||||
python-monkey-business==1.0.0
|
python-monkey-business==1.0.0
|
||||||
python-stdnum==1.18
|
python-stdnum==1.18
|
||||||
pytz==2022.7.1
|
pytz==2022.7.1
|
||||||
|
@ -34,6 +34,7 @@ pycodestyle==2.7.0
|
|||||||
pydocstyle==6.1.1
|
pydocstyle==6.1.1
|
||||||
pyflakes==2.3.1
|
pyflakes==2.3.1
|
||||||
Pygments==2.13.0
|
Pygments==2.13.0
|
||||||
|
python-dotenv==0.21.0
|
||||||
responses==0.25.3
|
responses==0.25.3
|
||||||
snowballstemmer==2.2.0
|
snowballstemmer==2.2.0
|
||||||
tblib==3.0.0
|
tblib==3.0.0
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div class="input-group w-100 {% if sm %}input-group-sm{% endif %}" id="search-container">
|
<div class="input-group w-100 {% if sm %}input-group-sm{% endif %}" id="search-container">
|
||||||
<input autocapitalize="none" autocomplete="off" autocorrect="off" id="searchInput" class="flex-grow-1 form-control me-3" placeholder="Search tags and kewords" spellcheck="false" type="text">
|
<input autocapitalize="none" autocomplete="off" autocorrect="off" id="searchInput" class="flex-grow-1 form-control me-3" placeholder="Search tags or keywords..." spellcheck="false" type="text">
|
||||||
<div class="btn-row input-group-addon">
|
<div class="btn-row input-group-addon">
|
||||||
<button class="btn" id="clearSearchBtn" title="Cancel"><i class="i-cancel"></i></button>
|
|
||||||
<button class="btn btn-primary" id="searchBtn" type="submit"><i class="i-search"></i></button>
|
<button class="btn btn-primary" id="searchBtn" type="submit"><i class="i-search"></i></button>
|
||||||
|
<button class="btn btn-link" id="clearSearchBtn" title="Cancel"><i class="i-cancel"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,7 +9,6 @@ import os
|
|||||||
import pathlib
|
import pathlib
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
import braintree
|
import braintree
|
||||||
import dj_database_url
|
import dj_database_url
|
||||||
import meilisearch
|
import meilisearch
|
||||||
@ -17,10 +16,14 @@ import meilisearch
|
|||||||
import common.upload_paths
|
import common.upload_paths
|
||||||
|
|
||||||
|
|
||||||
# Load variables from .env, if available
|
try:
|
||||||
path = os.path.dirname(os.path.abspath(__file__)) + '/../.env'
|
from dotenv import load_dotenv
|
||||||
if os.path.isfile(path):
|
# Load variables from .env, if available
|
||||||
load_dotenv(path)
|
path = os.path.dirname(os.path.abspath(__file__)) + '/../.env'
|
||||||
|
if os.path.isfile(path):
|
||||||
|
load_dotenv(path)
|
||||||
|
except ImportError: # This is expected: there should be no python-dotenv in production
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _get(name: str, default=None, coerse_to=None):
|
def _get(name: str, default=None, coerse_to=None):
|
||||||
@ -310,25 +313,32 @@ LOGGING = {
|
|||||||
'formatters': {
|
'formatters': {
|
||||||
'default': {'format': '%(asctime)-15s %(levelname)8s %(name)s %(message)s'},
|
'default': {'format': '%(asctime)-15s %(levelname)8s %(name)s %(message)s'},
|
||||||
'verbose': {
|
'verbose': {
|
||||||
'format': '%(asctime)-15s %(levelname)8s %(name)s %(process)d %(thread)d %(message)s'
|
'format': (
|
||||||
|
'%(asctime)s %(levelname)8s [%(filename)s:%(lineno)d '
|
||||||
|
'%(funcName)s] %(name)s %(message)s '
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'handlers': {
|
'handlers': {
|
||||||
'console': {
|
'console': {
|
||||||
'class': 'logging.StreamHandler',
|
'class': 'logging.StreamHandler',
|
||||||
'formatter': 'default', # Set to 'verbose' in production
|
'formatter': 'verbose',
|
||||||
'stream': 'ext://sys.stderr',
|
},
|
||||||
|
'mail_admins': {
|
||||||
|
'level': 'ERROR',
|
||||||
|
'class': 'django.utils.log.AdminEmailHandler',
|
||||||
|
'include_html': True,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'loggers': {
|
'loggers': {
|
||||||
'asyncio': {'level': 'WARNING'},
|
'asyncio': {'level': 'WARNING'},
|
||||||
'django': {'level': 'WARNING'},
|
'django': {'level': 'INFO'},
|
||||||
'urllib3': {'level': 'WARNING'},
|
'urllib3': {'level': 'WARNING'},
|
||||||
'search': {'level': 'DEBUG'},
|
'search': {'level': 'DEBUG'},
|
||||||
'static_assets': {'level': 'DEBUG'},
|
'static_assets': {'level': 'DEBUG'},
|
||||||
'looper': {'level': 'DEBUG'},
|
'looper': {'level': 'DEBUG'},
|
||||||
},
|
},
|
||||||
'root': {'level': 'WARNING', 'handlers': ['console']},
|
'root': {'level': 'INFO', 'handlers': ['console', 'mail_admins']},
|
||||||
}
|
}
|
||||||
|
|
||||||
SITE_ID = 1
|
SITE_ID = 1
|
||||||
@ -512,8 +522,14 @@ GATEWAYS = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Optional Sentry configuration
|
if os.environ.get('ADMINS') is not None:
|
||||||
|
# Expects the following format:
|
||||||
|
# ADMINS='J Doe: jane@example.com, John Dee: john@example.com'
|
||||||
|
ADMINS = [[_.strip() for _ in adm.split(':')] for adm in os.environ.get('ADMINS').split(',')]
|
||||||
|
EMAIL_SUBJECT_PREFIX = f'[{ALLOWED_HOSTS[0]}]'
|
||||||
|
SERVER_EMAIL = f'django@{ALLOWED_HOSTS[0]}'
|
||||||
|
|
||||||
|
# Optional Sentry configuration
|
||||||
SENTRY_DSN = _get('SENTRY_DSN')
|
SENTRY_DSN = _get('SENTRY_DSN')
|
||||||
if SENTRY_DSN:
|
if SENTRY_DSN:
|
||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
WSGI config for training project.
|
WSGI config for Blender Studio project.
|
||||||
|
|
||||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
@ -8,18 +8,9 @@ https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import os.path
|
|
||||||
import pathlib
|
|
||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application
|
from django.core.wsgi import get_wsgi_application
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
BASE_DIR = pathlib.Path(__file__).absolute().parent.parent
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'studio.settings')
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'studio.settings')
|
||||||
|
|
||||||
# Load variables from .env, if available
|
|
||||||
path = BASE_DIR / '.env'
|
|
||||||
if os.path.isfile(path):
|
|
||||||
load_dotenv(path)
|
|
||||||
|
|
||||||
application = get_wsgi_application()
|
application = get_wsgi_application()
|
||||||
|
@ -125,13 +125,14 @@ class SectionAdmin(mixins.ViewOnSiteMixin, admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(flatpages.TrainingFlatPage)
|
@admin.register(flatpages.TrainingFlatPage)
|
||||||
class TrainingFlatPageAdmin(mixins.ViewOnSiteMixin, admin.ModelAdmin):
|
class TrainingFlatPageAdmin(mixins.ViewOnSiteMixin, admin.ModelAdmin):
|
||||||
|
save_on_top = True
|
||||||
autocomplete_fields = ['training', 'attachments']
|
autocomplete_fields = ['training', 'attachments']
|
||||||
list_display = ('title', 'training', 'view_link')
|
list_display = ('title', 'training', 'view_link')
|
||||||
list_filter = [
|
list_filter = [
|
||||||
'training',
|
'training',
|
||||||
]
|
]
|
||||||
prepopulated_fields = {'slug': ('slug',)}
|
prepopulated_fields = {'slug': ('slug',)}
|
||||||
raw_id_fields = ['training', 'attachments']
|
raw_id_fields = ['training']
|
||||||
|
|
||||||
|
|
||||||
@admin.register(progress.UserSectionProgress)
|
@admin.register(progress.UserSectionProgress)
|
||||||
|
@ -7,28 +7,22 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% block training_header_image %}
|
<div class="container-fluid pt-2 pt-md-3">
|
||||||
{% endblock training_header_image %}
|
|
||||||
|
|
||||||
<div class="container pt-2 pt-md-3">
|
|
||||||
<div class="d-md-none mb-3 pt-2 row">
|
<div class="d-md-none mb-3 pt-2 row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<button class="btn js-nav-drawer-btn-toggle"><i class="i-list"></i> Content</button>
|
<button class="btn js-nav-drawer-btn-toggle"><i class="i-list"></i> Content</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row training-group">
|
||||||
<div class="col-lg-3 col-md-4 mb-3 fade-xs js-nav-drawer-helper nav-drawer-helper">
|
<div class="training-group-item training-group-item-nav fade-xs js-nav-drawer-helper nav-drawer-helper">
|
||||||
<nav class="nav-drawer-nested">
|
<nav class="nav-drawer-nested">
|
||||||
<div class="nav-drawer-body">
|
<div class="nav-drawer-body">
|
||||||
{% block nested_nav_drawer_inner %}
|
{% block nested_nav_drawer_inner %}{% endblock nested_nav_drawer_inner %}
|
||||||
{% endblock nested_nav_drawer_inner %}
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-lg-9 col-md-8">
|
|
||||||
{% block nexted_content %}
|
{% block nexted_content %}{% endblock nexted_content %}
|
||||||
{% endblock nexted_content %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
@ -66,9 +66,9 @@
|
|||||||
|
|
||||||
{% block nested_nav_drawer_header %}
|
{% block nested_nav_drawer_header %}
|
||||||
<div class="drawer-nav-header">
|
<div class="drawer-nav-header">
|
||||||
<p class="mb-1 text-muted">
|
<h6 class="mb-1 text-muted fw-normal">
|
||||||
{% firstof training.type.label training.type|capfirst %}
|
<small>{% firstof training.type.label training.type|capfirst %}</small>
|
||||||
</p>
|
</h6>
|
||||||
<a class="fw-bold" href="{{ navigation.overview_url }}">{{ training.name }}</a>
|
<a class="fw-bold" href="{{ navigation.overview_url }}">{{ training.name }}</a>
|
||||||
</div>
|
</div>
|
||||||
{% endblock nested_nav_drawer_header %}
|
{% endblock nested_nav_drawer_header %}
|
||||||
@ -87,7 +87,7 @@
|
|||||||
{% if chapter_navigation.is_published or request.user.is_superuser or request.user.is_staff %}
|
{% if chapter_navigation.is_published or request.user.is_superuser or request.user.is_staff %}
|
||||||
<div class="drawer-nav-dropdown-wrapper">
|
<div class="drawer-nav-dropdown-wrapper">
|
||||||
<a href="{{ chapter_navigation.url }}"
|
<a href="{{ chapter_navigation.url }}"
|
||||||
class="drawer-nav-dropdown dropdown fw-bold {% if chapter_navigation.current %} active{% endif %}"
|
class="drawer-nav-dropdown dropdown {% if chapter_navigation.current %} active{% endif %}"
|
||||||
data-bs-tooltip="tooltip-overflow" data-placement="top" title="{{ chapter_navigation.name }}">
|
data-bs-tooltip="tooltip-overflow" data-placement="top" title="{{ chapter_navigation.name }}">
|
||||||
<span class="drawer-nav-dropdown-text overflow-text">{{ chapter_navigation.name }}</span>
|
<span class="drawer-nav-dropdown-text overflow-text">{{ chapter_navigation.name }}</span>
|
||||||
{% if not chapter_navigation.is_published %}
|
{% if not chapter_navigation.is_published %}
|
||||||
|
@ -19,64 +19,70 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block nexted_content %}
|
{% block nexted_content %}
|
||||||
{% if chapter.thumbnail %}
|
<div class="training-group-item-content-detail">
|
||||||
<div class="row mb-3">
|
<div class="training-group-item-content-detail-inner">
|
||||||
<div class="col">
|
{% if chapter.thumbnail %}
|
||||||
{% if section.is_free or request.user|has_active_subscription %}
|
<div class="row mb-3">
|
||||||
{% firstof chapter.picture_header chapter.thumbnail as header %}
|
<div class="col">
|
||||||
{% include "common/components/helpers/image_set.html" with alt=chapter.name classes="img-fluid img-width-100 rounded" img_source=header xsmall_width="600" small_width="800" medium_width="1000" large_width="1200" xlarge_width="1920" %}
|
{% if section.is_free or request.user|has_active_subscription %}
|
||||||
{% else %}
|
{% firstof chapter.picture_header chapter.thumbnail as header %}
|
||||||
{% include 'common/components/content_locked.html' with background=training.picture_header %}
|
{% include "common/components/helpers/image_set.html" with alt=chapter.name classes="img-fluid img-width-100 rounded" img_source=header xsmall_width="600" small_width="800" medium_width="1000" large_width="1200" xlarge_width="1920" %}
|
||||||
{% endif %}
|
{% else %}
|
||||||
</div>
|
{% include 'common/components/content_locked.html' with background=training.picture_header %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="align-items-start row">
|
||||||
|
<div class="col-12 col-md mb-3">
|
||||||
|
<div class="d-md-block d-none">
|
||||||
|
<p class="small text-muted">{{ training.name }}</p>
|
||||||
|
<h2 class="mb-0">{{ chapter.name }}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-auto mb-2 mb-md-0 mt-0 mt-md-3">
|
||||||
|
<div class="button-toolbar-container">
|
||||||
|
<div class="button-toolbar">
|
||||||
|
{% if user.is_staff %}
|
||||||
|
<a href="{{ chapter.admin_url }}" class="btn btn-admin">
|
||||||
|
<i class="i-edit"></i>
|
||||||
|
<span>Edit</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<button data-bs-toggle="dropdown" class="btn btn-link">
|
||||||
|
<i class="i-more-vertical"></i>
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu dropdown-menu-end">
|
||||||
|
<a href="https://projects.blender.org/studio/blender-studio/issues/new" target="_blank" class="dropdown-item">
|
||||||
|
<i class="i-flag"></i>
|
||||||
|
<span>Report Problem</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if chapter.description %}
|
||||||
|
<section class="mb-3 row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="markdown-text">
|
||||||
|
{% with_shortcodes chapter.description|markdown %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
<div class="mb-3 row">
|
||||||
<div class="mb-3 row ">
|
<div class="col-12">
|
||||||
<div class="col">
|
<div class="cards card-layout-card-transparent files">
|
||||||
<div class="align-items-start row">
|
{% for section in chapter.sections.all %}
|
||||||
<div class="col-12 col-md mb-3">
|
{% if section.is_published %}
|
||||||
<div class="d-md-block d-none">
|
{% include "common/components/file_section.html" with section=section %}
|
||||||
<p class="small text-muted">{{ training.name }}</p>
|
{% endif %}
|
||||||
<h2 class="mb-0">{{ chapter.name }}</h2>
|
{% endfor %}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-auto mb-2 mb-md-0 mt-0 mt-md-3">
|
|
||||||
<div class="button-toolbar-container">
|
|
||||||
<div class="button-toolbar">
|
|
||||||
{% if user.is_staff %}
|
|
||||||
<a href="{{ chapter.admin_url }}" class="btn btn-admin">
|
|
||||||
<i class="i-edit"></i>
|
|
||||||
<span>Edit</span>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<button data-bs-toggle="dropdown" class="btn btn-link">
|
|
||||||
<i class="i-more-vertical"></i>
|
|
||||||
</button>
|
|
||||||
<div class="dropdown-menu dropdown-menu-end">
|
|
||||||
<a href="https://projects.blender.org/studio/blender-studio/issues/new" target="_blank" class="dropdown-item">
|
|
||||||
<i class="i-flag"></i>
|
|
||||||
<span>Report Problem</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if chapter.description %}
|
|
||||||
<section class="markdown-text mb-3">{% with_shortcodes chapter.description|markdown %}</section>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="cards card-layout-card-transparent files">
|
|
||||||
{% for section in chapter.sections.all %}
|
|
||||||
{% if section.is_published %}
|
|
||||||
{% include "common/components/file_section.html" with section=section %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -19,45 +19,35 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block nexted_content %}
|
{% block nexted_content %}
|
||||||
<div class="row mb-3">
|
<div class="training-group-item training-group-item-video">
|
||||||
{% if section.preview_youtube_link %}
|
{% if section.preview_youtube_link %}
|
||||||
<div class="col">
|
{% include 'common/components/video_player_embed.html' with url=section.preview_youtube_link rounded=True %}
|
||||||
<div class="overflow-hidden rounded">
|
|
||||||
{% include 'common/components/video_player_embed.html' with url=section.preview_youtube_link rounded=True %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% elif video %}
|
{% elif video %}
|
||||||
<div class="col">
|
{% if section.is_free or request.user|has_active_subscription %}
|
||||||
<div class="overflow-hidden rounded">
|
{% if user.is_anonymous %}
|
||||||
{% if section.is_free or request.user|has_active_subscription %}
|
{% include 'common/components/video_player.html' with url=video.source.url poster=section.thumbnail_m_url tracks=section.static_asset.video.tracks.all loop=section.static_asset.video.loop %}
|
||||||
{% if user.is_anonymous %}
|
{% else %}
|
||||||
{% include 'common/components/video_player.html' with url=video.source.url poster=section.thumbnail_m_url tracks=section.static_asset.video.tracks.all loop=section.static_asset.video.loop %}
|
{% include 'common/components/video_player.html' with url=video.source.url progress_url=video.progress_url start_position=video.start_position poster=section.thumbnail_m_url tracks=section.static_asset.video.tracks.all loop=section.static_asset.video.loop %}
|
||||||
{% else %}
|
{% endif %}
|
||||||
{% include 'common/components/video_player.html' with url=video.source.url progress_url=video.progress_url start_position=video.start_position poster=section.thumbnail_m_url tracks=section.static_asset.video.tracks.all loop=section.static_asset.video.loop %}
|
{% else %}
|
||||||
{% endif %}
|
{% include 'common/components/content_locked.html' with background=section.thumbnail_m_url %}
|
||||||
{% else %}
|
{% endif %}
|
||||||
{% include 'common/components/content_locked.html' with background=section.thumbnail_m_url %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="col">
|
{% if section.is_free or request.user|has_active_subscription %}
|
||||||
<div class="overflow-hidden rounded">
|
{% if section.thumbnail %}
|
||||||
{% if section.is_free or request.user|has_active_subscription %}
|
{% include "common/components/helpers/image_set.html" with alt=section.name classes="img-fluid img-width-100" img_source=section.thumbnail xsmall_width="600" small_width="800" medium_width="1000" large_width="1200" xlarge_width="1920" %}
|
||||||
{% if section.thumbnail %}
|
{% endif %}
|
||||||
{% include "common/components/helpers/image_set.html" with alt=section.name classes="img-fluid img-width-100 rounded" img_source=section.thumbnail xsmall_width="600" small_width="800" medium_width="1000" large_width="1200" xlarge_width="1920" %}
|
{% else %}
|
||||||
{% endif %}
|
<div class="col">
|
||||||
{% else %}
|
{% include 'common/components/content_locked.html' with background=training.picture_header %}
|
||||||
{% include 'common/components/content_locked.html' with background=training.picture_header %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="training-group-item training-group-item-content">
|
||||||
<div class="col">
|
<div class="box">
|
||||||
<div class="align-items-start row">
|
<div class="row">
|
||||||
<div class="col-12 col-md mb-2 mb-md-3">
|
<div class="col mb-2 mb-md-3">
|
||||||
<div class="d-md-block d-none">
|
<div class="d-md-block d-none">
|
||||||
<p class="small text-muted">{{ chapter.name }}</p>
|
<p class="small text-muted">{{ chapter.name }}</p>
|
||||||
<h2>{{ section.name }}</h2>
|
<h2>{{ section.name }}</h2>
|
||||||
@ -92,7 +82,7 @@
|
|||||||
<section class="mb-3 markdown-text">
|
<section class="mb-3 markdown-text">
|
||||||
{% with_shortcodes section.text|markdown_unsafe %}
|
{% with_shortcodes section.text|markdown_unsafe %}
|
||||||
</section>
|
</section>
|
||||||
<section class="mb-3">
|
<section>
|
||||||
{% include 'comments/components/comment_section.html' %}
|
{% include 'comments/components/comment_section.html' %}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,24 +11,23 @@
|
|||||||
{% javascript 'training' %}
|
{% javascript 'training' %}
|
||||||
{% endblock scripts %}
|
{% endblock scripts %}
|
||||||
|
|
||||||
{% block training_header_image %}
|
|
||||||
{% if training.picture_header_url %}
|
|
||||||
<div class="container">
|
|
||||||
<div class="mt-3 row">
|
|
||||||
<div class="col">
|
|
||||||
<img src="{{ training.picture_header_url }}" class="img-fluid img-width-100 rounded" alt="{{ training.name }}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock training_header_image %}
|
|
||||||
|
|
||||||
{% block nexted_content %}
|
{% block nexted_content %}
|
||||||
<section>
|
|
||||||
<div class="row">
|
<section class="training-group-item training-group-item-content-detail">
|
||||||
<div class="col">
|
<div class="row training-group-item-content-detail-inner">
|
||||||
<div class="row align-items-start mb-2">
|
<div class="col">
|
||||||
<div class="col-12 col-md">
|
{% if training.picture_header_url %}
|
||||||
|
<div class="mb-3 row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="training-header-img-helper">
|
||||||
|
<img src="{{ training.picture_header_url }}" class="img-fluid img-width-100 rounded" alt="{{ training.name }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="align-items-start mb-3 row">
|
||||||
|
<div class="col-12 col-md">
|
||||||
<h1 class="mb-0">{{ training.name }}</h1>
|
<h1 class="mb-0">{{ training.name }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-auto">
|
<div class="col-12 col-md-auto">
|
||||||
@ -36,9 +35,6 @@
|
|||||||
{% if training.is_free %}
|
{% if training.is_free %}
|
||||||
{% include "common/components/cards/pill.html" with label='Free' %}
|
{% include "common/components/cards/pill.html" with label='Free' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for tag in training.tags_list %}
|
|
||||||
{% include 'common/components/cards/pill.html' with label=tag %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="button-toolbar justify-content-end">
|
<div class="button-toolbar justify-content-end">
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
@ -67,7 +63,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<section class="markdown-text mb-3">{% with_shortcodes training.summary_rendered %}</section>
|
|
||||||
|
{% if training.tags_list %}
|
||||||
|
<div class="mb-3 row">
|
||||||
|
<div class="col-12">
|
||||||
|
{% for tag in training.tags_list %}
|
||||||
|
{% include 'common/components/cards/pill.html' with label=tag %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<section class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="markdown-text">
|
||||||
|
{% with_shortcodes training.summary_rendered %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|