WIP: Attach invoice PDF to payment emails #104418

Draft
Anna Sirota wants to merge 4 commits from attach-invoice-pdf into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
104 changed files with 1051 additions and 1571 deletions
Showing only changes of commit 884a565f33 - Show all commits

3
.gitmodules vendored
View File

@ -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

View File

@ -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

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -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);
});
}); });
} }

View File

@ -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();

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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" />

View File

@ -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>

View File

@ -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

View File

@ -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`:

View File

@ -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>&nbsp; ${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: {

View File

@ -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">

View File

@ -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 %}

View File

@ -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>

View File

@ -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 %}

View File

@ -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>

View File

@ -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>

View File

@ -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 }}

View File

@ -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 %}

View File

@ -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">

View 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 %}

View File

@ -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(

View File

@ -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)

View File

@ -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

View File

@ -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
View File

@ -0,0 +1 @@
shared/ansible.cfg

View File

@ -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
View File

@ -0,0 +1 @@
shared/ansible.sh

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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:

View File

@ -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

View File

@ -11,7 +11,7 @@
tags: tags:
- meilisearch - meilisearch
- hosts: http - hosts: application
gather_facts: false gather_facts: false
become: true become: true
tasks: tasks:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,3 +0,0 @@
[virtualenvs]
in-project = true
create = false

View 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 %}

View File

@ -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';

File diff suppressed because one or more lines are too long

View File

@ -1,76 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="refresh" content="5">
<title>{{ project_name }} &mdash; 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>

View File

@ -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;
}
}

View File

@ -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 %}
}

View File

@ -0,0 +1 @@
{% extends "templates/nginx/base_ingress.conf" %}

View File

@ -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 }};
}

View File

@ -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;

View File

@ -1,6 +0,0 @@
[Unit]
Description=restart {{ project_name }} background worker
[Service]
Type=oneshot
ExecStart=/bin/systemctl restart {{ background_service_name }}.service

View File

@ -1,6 +0,0 @@
[Timer]
OnActiveSec=48h
OnUnitActiveSec=48h
[Install]
WantedBy=timers.target

View File

@ -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

View File

@ -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 }}

View File

@ -1,7 +0,0 @@
[Timer]
OnCalendar=daily
RandomizedDelaySec=5h
AccuracySec=1us
[Install]
WantedBy=timers.target

View File

@ -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 }}

View File

@ -1,7 +0,0 @@
[Timer]
OnCalendar=daily
RandomizedDelaySec=5h
AccuracySec=1us
[Install]
WantedBy=timers.target

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -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>

View File

@ -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>

Some files were not shown because too many files have changed in this diff Show More