diff --git a/app/YtManager/settings.py b/app/YtManager/settings.py index 7c384e3..52ee338 100644 --- a/app/YtManager/settings.py +++ b/app/YtManager/settings.py @@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/1.11/ref/settings/ """ import os +import sys import logging from os.path import dirname as up @@ -128,7 +129,6 @@ BASE_DIR = up(os.path.dirname(__file__)) # Base dir of the ap CONFIG_DIR = os.getenv("YTSM_CONFIG_DIR", os.path.join(PROJECT_ROOT, "config")) DATA_DIR = os.getenv("YTSM_DATA_DIR", os.path.join(PROJECT_ROOT, "data")) -os.chdir(DATA_DIR) STATIC_ROOT = os.path.join(PROJECT_ROOT, "static") MEDIA_ROOT = os.path.join(DATA_DIR, 'media') @@ -154,6 +154,16 @@ CONFIG_ERRORS = [] CONFIG_WARNINGS = [] +# +# Config parser options +# +CFG_PARSER_OPTS = { + 'PROJECT_ROOT' : PROJECT_ROOT, + 'BASE_DIR' : BASE_DIR, + 'CONFIG_DIR' : CONFIG_DIR, + 'DATA_DIR' : DATA_DIR, +} + # # Load globals from config.ini # @@ -189,7 +199,7 @@ def get_global_opt(name, cfgparser, env_variable=None, fallback=None, boolean=Fa # Get from config parser if boolean: try: - return cfgparser.getboolean('global', name, fallback=fallback) + return cfgparser.getboolean('global', name, fallback=fallback, vars=CFG_PARSER_OPTS) except ValueError: CONFIG_WARNINGS.append(f'config.ini file: Value set for option global.{name} is not valid! ' f'Valid options: true, false, on, off.') @@ -197,12 +207,12 @@ def get_global_opt(name, cfgparser, env_variable=None, fallback=None, boolean=Fa if integer: try: - return cfgparser.getint('global', name, fallback=fallback) + return cfgparser.getint('global', name, fallback=fallback, vars=CFG_PARSER_OPTS) except ValueError: CONFIG_WARNINGS.append(f'config.ini file: Value set for option global.{name} must be an integer number! ') return fallback - return cfgparser.get('global', name, fallback=fallback) + return cfgparser.get('global', name, fallback=fallback, vars=CFG_PARSER_OPTS) def load_config_ini(): @@ -210,6 +220,13 @@ def load_config_ini(): from YtManagerApp.utils.extended_interpolation_with_env import ExtendedInterpolatorWithEnv import dj_database_url + try: + os.makedirs(DATA_DIR, exist_ok=True) + logging.info(f"Using data directory {DATA_DIR}") + except OSError as e: + print(f'CRITICAL ERROR! Cannot create data directory {DATA_DIR}! {e}', file=sys.stderr) + return; + cfg = ConfigParser(allow_no_value=True, interpolation=ExtendedInterpolatorWithEnv()) cfg_file = os.path.join(CONFIG_DIR, "config.ini") @@ -235,7 +252,7 @@ def load_config_ini(): } if cfg.has_option('global', 'DatabaseURL'): - DATABASES['default'] = dj_database_url.parse(cfg.get('global', 'DatabaseURL'), conn_max_age=600) + DATABASES['default'] = dj_database_url.parse(cfg.get('global', 'DatabaseURL', vars=CFG_PARSER_OPTS), conn_max_age=600) else: DATABASES['default'] = { diff --git a/app/YtManagerApp/static/YtManagerApp/css/style.css b/app/YtManagerApp/static/YtManagerApp/css/style.css index 5c9b046..c3a4aca 100644 --- a/app/YtManagerApp/static/YtManagerApp/css/style.css +++ b/app/YtManagerApp/static/YtManagerApp/css/style.css @@ -1,6 +1,6 @@ #main_body { margin-bottom: 4rem; - margin-top: 2rem; } + margin-top: 0; } #main_footer { position: fixed; diff --git a/app/YtManagerApp/static/YtManagerApp/css/style.css.map b/app/YtManagerApp/static/YtManagerApp/css/style.css.map index af57370..a16ab80 100644 --- a/app/YtManagerApp/static/YtManagerApp/css/style.css.map +++ b/app/YtManagerApp/static/YtManagerApp/css/style.css.map @@ -1,6 +1,6 @@ { "version": 3, -"mappings": "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", +"mappings": "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", "sources": ["style.scss"], "names": [], "file": "style.css" diff --git a/app/YtManagerApp/static/YtManagerApp/css/style.scss b/app/YtManagerApp/static/YtManagerApp/css/style.scss index 5b14f45..33a9fb9 100644 --- a/app/YtManagerApp/static/YtManagerApp/css/style.scss +++ b/app/YtManagerApp/static/YtManagerApp/css/style.scss @@ -2,7 +2,7 @@ $accent-color: #007bff; #main_body { margin-bottom: 4rem; - margin-top: 2rem; + margin-top: 0; } #main_footer { diff --git a/app/YtManagerApp/urls.py b/app/YtManagerApp/urls.py index 614b2e1..3ca0bef 100644 --- a/app/YtManagerApp/urls.py +++ b/app/YtManagerApp/urls.py @@ -63,8 +63,7 @@ urlpatterns = [ # First time setup path('first_time/step0_welcome', first_time.Step0WelcomeView.as_view(), name='first_time_0'), path('first_time/step1_apikey', first_time.Step1ApiKeyView.as_view(), name='first_time_1'), - path('first_time/step2_admin', first_time.Step2CreateAdminUserView.as_view(), name='first_time_2'), - path('first_time/step2_login', first_time.Step2LoginAdminUserView.as_view(), name='first_time_2_login'), + path('first_time/step2_admin', first_time.Step2SetupAdminUserView.as_view(), name='first_time_2'), path('first_time/step3_config', first_time.Step3ConfigureView.as_view(), name='first_time_3'), path('first_time/done', first_time.DoneView.as_view(), name='first_time_done'), diff --git a/app/YtManagerApp/views/auth.py b/app/YtManagerApp/views/auth.py index a95e8fb..1a39429 100644 --- a/app/YtManagerApp/views/auth.py +++ b/app/YtManagerApp/views/auth.py @@ -1,57 +1,17 @@ -from crispy_forms.helper import FormHelper -from crispy_forms.layout import Submit -from django import forms from django.contrib.auth import login, authenticate -from django.contrib.auth.forms import AuthenticationForm, UserCreationForm from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.models import User from django.contrib.auth.views import LoginView from django.urls import reverse_lazy from django.views.generic import FormView, TemplateView - -class ExtendedAuthenticationForm(AuthenticationForm): - remember_me = forms.BooleanField(label='Remember me', required=False, initial=False) - - def clean(self): - remember_me = self.cleaned_data.get('remember_me') - if remember_me: - expiry = 3600 * 24 * 30 - else: - expiry = 0 - self.request.session.set_expiry(expiry) - - return super().clean() +from .forms.auth import ExtendedAuthenticationForm, ExtendedUserCreationForm class ExtendedLoginView(LoginView): form_class = ExtendedAuthenticationForm -class ExtendedUserCreationForm(UserCreationForm): - email = forms.EmailField(required=False, - label='E-mail address', - help_text='The e-mail address is optional, but it is the only way to recover a lost ' - 'password.') - first_name = forms.CharField(max_length=30, required=False, - label='First name') - last_name = forms.CharField(max_length=150, required=False, - label='Last name') - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.helper = FormHelper() - self.helper.label_class = 'col-3' - self.helper.field_class = 'col-9' - self.helper.form_class = 'form-horizontal' - self.helper.form_method = 'post' - self.helper.form_action = reverse_lazy('register') - self.helper.add_input(Submit('submit', 'register')) - - class Meta(UserCreationForm.Meta): - fields = ['username', 'email', 'first_name', 'last_name'] - - class RegisterView(FormView): template_name = 'registration/register.html' form_class = ExtendedUserCreationForm diff --git a/app/YtManagerApp/views/first_time.py b/app/YtManagerApp/views/first_time.py index adc58cc..beae807 100644 --- a/app/YtManagerApp/views/first_time.py +++ b/app/YtManagerApp/views/first_time.py @@ -1,30 +1,28 @@ -from crispy_forms.helper import FormHelper -from crispy_forms.layout import Layout, HTML, Submit, Column -from django import forms -from django.contrib.auth import authenticate, login -from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib.auth.models import User -from django.urls import reverse_lazy -from django.views.generic import UpdateView, FormView -from django.shortcuts import render, redirect -from YtManagerApp.views.auth import RegisterView, ExtendedUserCreationForm, ExtendedAuthenticationForm -from YtManagerApp.models import UserSettings - -from YtManagerApp.management.appconfig import global_prefs -from django.http import HttpResponseForbidden - import logging +from django.contrib.auth import authenticate, login +from django.contrib.auth.models import User +from django.http import HttpResponseForbidden +from django.shortcuts import redirect +from django.urls import reverse_lazy +from django.views.generic import FormView + +from YtManagerApp.management.appconfig import global_prefs +from YtManagerApp.views.forms.auth import ExtendedAuthenticationForm +from YtManagerApp.views.forms.first_time import WelcomeForm, ApiKeyForm, PickAdminUserForm, ServerConfigForm, DoneForm, UserCreationForm logger = logging.getLogger("FirstTimeWizard") -class ProtectInitializedMixin(object): - +class WizardStepMixin(object): def get(self, request, *args, **kwargs): + + # Prevent access if application is already initialized if global_prefs['hidden__initialized']: - logger.debug(f"Attempted to access {request.path}, but first time setup already run. Redirected to home page.") + logger.debug(f"Attempted to access {request.path}, but first time setup already run. Redirected to home " + f"page.") return redirect('home') + return super().get(request, *args, **kwargs) def post(self, request, *args, **kwargs): @@ -37,44 +35,25 @@ class ProtectInitializedMixin(object): # # Step 0: welcome screen # -class Step0WelcomeForm(forms.Form): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.helper = FormHelper() - self.helper.layout = Layout( - Submit('submit', value='Continue') - ) - - -class Step0WelcomeView(ProtectInitializedMixin, FormView): +class Step0WelcomeView(WizardStepMixin, FormView): template_name = 'YtManagerApp/first_time_setup/step0_welcome.html' - form_class = Step0WelcomeForm + form_class = WelcomeForm success_url = reverse_lazy('first_time_1') # # Step 1: setup API key # -class Step1ApiKeyForm(forms.Form): - api_key = forms.CharField(label="YouTube API Key:") - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.helper = FormHelper() - self.helper.layout = Layout( - 'api_key', - Column( - Submit('submit', value='Continue'), - HTML('Skip') - ) - ) - - -class Step1ApiKeyView(ProtectInitializedMixin, FormView): +class Step1ApiKeyView(WizardStepMixin, FormView): template_name = 'YtManagerApp/first_time_setup/step1_apikey.html' - form_class = Step1ApiKeyForm + form_class = ApiKeyForm success_url = reverse_lazy('first_time_2') + def get_initial(self): + initial = super().get_initial() + initial['api_key'] = global_prefs['general__youtube_api_key'] + return initial + def form_valid(self, form): key = form.cleaned_data['api_key'] # TODO: validate key @@ -85,101 +64,87 @@ class Step1ApiKeyView(ProtectInitializedMixin, FormView): # # Step 2: create admin user # -class Step2CreateAdminUserView(ProtectInitializedMixin, FormView): +class Step2SetupAdminUserView(WizardStepMixin, FormView): template_name = 'YtManagerApp/first_time_setup/step2_admin.html' success_url = reverse_lazy('first_time_3') - form_class = ExtendedUserCreationForm + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__form_class = UserCreationForm + + def get_form_class(self): + return self.__form_class def get(self, request, *args, **kwargs): + have_users = User.objects.count() > 0 + have_admin = User.objects.filter(is_superuser=True).count() > 0 + # Skip if admin is already logged in if request.user.is_authenticated and request.user.is_superuser: logger.debug("Admin user already exists and is logged in!") return redirect(self.success_url) # Check if an admin user already exists - if User.objects.filter(is_superuser=True).count() > 0: - logger.warn("Admin user already exists! Will redirect to login page!") - return redirect('first_time_2_login') + elif have_admin: + logger.debug("Admin user already exists and is not logged in!") + self.__form_class = ExtendedAuthenticationForm + + elif have_users and 'register' not in kwargs: + logger.debug("There are users but no admin!") + self.__form_class = PickAdminUserForm + + else: + logger.debug("No admin user exists, will register a new account!") + self.__form_class = UserCreationForm return super().get(request, *args, **kwargs) - def form_valid(self, form): - user = form.save() - user.is_staff = True - user.is_superuser = True - user.save() - - username = form.cleaned_data.get('username') - password = form.cleaned_data.get('password1') - user = authenticate(username=username, password=password) - login(self.request, user) - - return super().form_valid(form) - - -# -# Step 2: create admin user -# -class Step2LoginAdminUserView(ProtectInitializedMixin, FormView): - template_name = 'YtManagerApp/first_time_setup/step2_admin.html' - success_url = reverse_lazy('first_time_3') - form_class = ExtendedAuthenticationForm + def form_invalid(self, form): + print("FORM INVALID!") def form_valid(self, form): - login(self.request, form.get_user()) + if isinstance(form, ExtendedAuthenticationForm): + login(self.request, form.get_user()) + + elif isinstance(form, UserCreationForm): + user = form.save() + user.is_staff = True + user.is_superuser = True + user.save() + + username = form.cleaned_data.get('username') + password = form.cleaned_data.get('password1') + user = authenticate(username=username, password=password) + login(self.request, user) + + elif isinstance(form, PickAdminUserForm): + user = form.cleaned_data['admin_user'] + user.is_staff = True + user.is_superuser = True + user.save() + + return redirect('first_time_2', assigned_success='1') + return super().form_valid(form) # # Step 3: configure server # -class Step3ConfigureForm(forms.Form): - - allow_registrations = forms.BooleanField( - label="Allow user registrations", - help_text="Disabling this option will prevent anyone from registering to the site.", - initial=True, - required=False - ) - - sync_schedule = forms.CharField( - label="Synchronization schedule", - help_text="How often should the application look for new videos.", - initial="5 * * * *", - required=True - ) - - auto_download = forms.BooleanField( - label="Download videos automatically", - required=False - ) - - download_location = forms.CharField( - label="Download location", - help_text="Location on the server where videos are downloaded.", - required=True - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.helper = FormHelper() - self.helper.layout = Layout( - HTML('