diff --git a/openedx/core/djangoapps/ace_common/template_context.py b/openedx/core/djangoapps/ace_common/template_context.py index b2254ba2ae51..ac359258a475 100644 --- a/openedx/core/djangoapps/ace_common/template_context.py +++ b/openedx/core/djangoapps/ace_common/template_context.py @@ -1,13 +1,21 @@ """ Context dictionary for templates that use the ace_common base template. """ +from math import floor + from django.conf import settings from django.core.urlresolvers import NoReverseMatch from django.urls import reverse from edxmako.shortcuts import marketing_link +from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.theming.helpers import get_config_value_from_site_or_settings +DEFAULT_COLOR_DICT = { + 'primary': '#3E99D4', + 'secondary': '#1197EA' +} + def get_base_template_context(site): """ @@ -36,4 +44,233 @@ def get_base_template_context(site): 'CONTACT_MAILING_ADDRESS', site=site, site_config_name='contact_mailing_address'), 'social_media_urls': get_config_value_from_site_or_settings('SOCIAL_MEDIA_FOOTER_URLS', site=site), 'mobile_store_urls': get_config_value_from_site_or_settings('MOBILE_STORE_URLS', site=site), + + # Context processor values for dynamic theming + 'edly_colors_config': get_theme_colors(), + 'edly_fonts_config': configuration_helpers.get_dict('FONTS', {}), + 'edly_branding_config': configuration_helpers.get_dict('BRANDING', {}), } + + +def get_theme_colors(): + color_dict = configuration_helpers.get_dict('COLORS', DEFAULT_COLOR_DICT) + primary = Colour(str(color_dict.get('primary'))) + secondary = Colour(str(color_dict.get('secondary'))) + + primary_hover = color_dict.get('primary-hover') + primary_rgb = color_dict.get('primary-rgb') + primary_lighten_5p = color_dict.get('primary-lighten-5p') + primary_lighten_10p = color_dict.get('primary-lighten-10p') + primary_darken_5p = color_dict.get('primary-darken-5p') + primary_darken_10p = color_dict.get('primary-darken-10p') + + secondary_hover = color_dict.get('secondary-hover') + secondary_rgb = color_dict.get('secondary-rgb') + secondary_lighten_5p = color_dict.get('secondary-lighten-5p') + secondary_lighten_10p = color_dict.get('secondary-lighten-10p') + secondary_darken_5p = color_dict.get('secondary-darken-5p') + secondary_darken_10p = color_dict.get('secondary-darken-10p') + + colours = { + 'primary': color_dict.get('primary'), + 'secondary': color_dict.get('secondary'), + 'primary-hover': get_hover_color(primary_hover, primary), + 'primary-rgb': get_rgb_color(primary_rgb, primary), + 'primary-lighten-5p': get_lighten_color(primary_lighten_5p, primary, 0.05), + 'primary-lighten-10p': get_lighten_color(primary_lighten_10p, primary, 0.1), + 'primary-darken-5p': get_darken_color(primary_darken_5p, primary, 0.05), + 'primary-darken-10p': get_darken_color(primary_darken_10p, primary, 0.1), + 'secondary-hover': get_hover_color(secondary_hover, secondary), + 'secondary-rgb': get_rgb_color(secondary_rgb, secondary), + 'secondary-lighten-5p': get_lighten_color(secondary_lighten_5p, secondary, 0.05), + 'secondary-lighten-10p': get_lighten_color(secondary_lighten_10p, secondary, 0.1), + 'secondary-darken-5p': get_darken_color(secondary_darken_5p, secondary, 0.05), + 'secondary-darken-10p': get_darken_color(secondary_darken_10p, secondary, 0.1), + } + + return colours + + +def get_hover_color(color_string, color_object): + return color_string if color_string else get_darken_color('', color_object, 0.5) + + +def get_lighten_color(color_string, color_object, scale): + return color_string if color_string else color_object.lighten(scale).hex + + +def get_darken_color(color_string, color_object, scale): + return color_string if color_string else color_object.darken(scale).hex + + +def get_rgb_color(color_string, color_object): + return color_string if color_string else ','.join([str(i) for i in color_object.rgb]) + + +class Colour(object): + def __init__(self, *args): + """ + Parse the initialising argument(s): + + The arguments might be: + - three integers corresponding to RGB values out of 255 + - an RGB tuple or list + - a greyscale percentage + - greyscale value out of 255 + - a 3 digit hexadecimal string + - or a 6 digit hexadecimal string + """ + + def _colour_convert(): + """ + Post-process parsed red, green and blue values into hex + """ + r = self.red + g = self.green + b = self.blue + self.hex = '#' + format(int(floor(r)), '02X') + format(int(floor(g)), '02X') + format(int(floor(b)), '02X') + self.rgb = (r, g, b) + _hue_convert() + + def _validate_and_parse_rgb_arguments(args): + if (max(args) > 255) or (min(args) < 0): + raise ValueError('RGB values must be between 0 and 255') + + self.red, self.green, self.blue = args + _colour_convert() + + def _validate_gray_scale_input(args): + if args[0] < 0 or args[0] > 255: + raise ValueError('Greyscale value must be either out of 1 or 255') + if args[0] <= 1: + self.red = self.green = self.blue = args[0] * 255 + else: + self.red = self.green = self.blue = args[0] + _colour_convert() + + def _validate_hex_input(args): + string = args[0] + if len(string) in [4, 7] and string[0] == '#': + string = string[1:] + + hexerror = "Hex string must be in the form 'RGB', '#RGB', 'RRGGBB'" \ + " or '#RRGGBB', and each digit must be a valid hexadecimal digit" + if len(string) not in [3, 6]: + raise TypeError(hexerror) + + if len(string) == 3: + try: + self.red = int(string[0], 16) * 17 + self.green = int(string[1], 16) * 17 + self.blue = int(string[2], 16) * 17 + except ValueError: + raise ValueError(hexerror + '3') + elif len(string) == 6: + try: + self.red = int(string[0:2], 16) + self.green = int(string[2:4], 16) + self.blue = int(string[4:6], 16) + except ValueError: + raise ValueError(hexerror + '6') + _colour_convert() + + def _calculate_lightness_values(max_rgb, min_rgb, red, green, blue): + return (max_rgb + min_rgb) / 2., max_rgb, (red + green + blue) / 3. + + def _calculate_saturation_values(hue, rgb_difference, min_rgb, lightness, value, intensity): + if rgb_difference == 0: + hue_saturation_lightness = hue_saturation_value = hue_saturation_intensity = 0 + else: + hue_saturation_lightness = rgb_difference / (1 - abs((2 * lightness) - 1)) + hue_saturation_value = rgb_difference / value + hue_saturation_intensity = 1 - (float(min_rgb) / intensity) + + self.hsl = (hue, hue_saturation_lightness, lightness) + self.hsv = (hue, hue_saturation_value, value) + self.hsi = (hue, hue_saturation_intensity, intensity) + + def _hue_convert(): + """ + Calculates hue saturation value, intensity, lightness. + + HSV: hue saturation value + HSI: hue saturation intensity + HSL: hue saturation lightness + """ + red = self.red / 255. + green = self.green / 255. + blue = self.blue / 255. + max_rgb = max(red, green, blue) + min_rgb = min(red, green, blue) + rgb_difference = float(max_rgb - min_rgb) + if rgb_difference == 0: + hue = 0 + elif max_rgb == red: + hue = ((green - blue) / rgb_difference) % 6 + elif max_rgb == green: + hue = ((blue - red) / rgb_difference) + 2 + elif max_rgb == blue: + hue = ((red - green) / rgb_difference) + 4 + + hue *= 60 + self.hue = hue + lightness, value, intensity = _calculate_lightness_values(max_rgb, min_rgb, red, green, blue) + _calculate_saturation_values(hue, rgb_difference, min_rgb, lightness, value, intensity) + + if len(args) == 1 and type(args[0]) in [tuple, list]: + args = args[0] + if len(args) == 3: + _validate_and_parse_rgb_arguments(args) + elif len(args) == 1 and type(args[0]) in [int, float, int]: + _validate_gray_scale_input(args) + elif len(args) == 1 and type(args[0]) == str: + _validate_hex_input(args) + else: + raise TypeError( + 'Input arguments must either be 3 RGB values out' + 'of 255, a greyscale value out of either 1 or 255, or a hexadecimal string' + ) + + def __str__(self): + return 'rgba' + self.__repr__()[6:] + + def __repr__(self): + return 'Colour({},{},{})'.format(self.red, self.green, self.blue) + + def _trans(self, transparency, other_colored_background): + """ + Returns a Colour object representing the colour when the calling + colour has a transparency out of 1 against an other coloured + background. + """ + if transparency < 0 or transparency > 1: + raise ValueError('Transparency must be between 0 and 1') + + red = (self.red * transparency) + (other_colored_background.red * (1 - transparency)) + green = (self.green * transparency) + (other_colored_background.green * (1 - transparency)) + blue = (self.blue * transparency) + (other_colored_background.blue * (1 - transparency)) + return Colour(red, green, blue) + + def lighten(self, lighten_factor): + """ + Lighten the colour by provided lighten factor. + """ + if lighten_factor < 0 or lighten_factor > 1: + raise ValueError('Lighten factor must be between 0 and 1') + + return Colour('FFF')._trans(lighten_factor, self) + + def darken(self, darken_factor): + """ + Darken the colour by provided darken factor. + """ + if darken_factor < 0 or darken_factor > 1: + raise ValueError('Darken factor must be between 0 and 1') + + return Colour('000')._trans(darken_factor, self) + + def trans(self, trans_factor): + """ + Make the colour transparent by the provided transparent factor. + """ + return self.darken(-1 * trans_factor) if trans_factor < 0 else self.lighten(trans_factor)