Implemented first time setup wizard. There are still some problems to be solved.

This commit is contained in:
Tiberiu Chibici 2018-12-10 23:23:00 +02:00
parent 34e358a9c4
commit 899de7adf5
22 changed files with 283 additions and 49 deletions

View File

@ -7,7 +7,7 @@ from django.conf import settings as dj_settings
from .management.appconfig import global_prefs
from .management.jobs.synchronize import schedule_synchronize_global
from .scheduler import initialize_scheduler
from django.db.utils import OperationalError
def __initialize_logger():
log_dir = os.path.join(dj_settings.DATA_DIR, 'logs')
@ -29,8 +29,12 @@ def __initialize_logger():
def main():
__initialize_logger()
if global_prefs['hidden__initialized']:
initialize_scheduler()
schedule_synchronize_global()
try:
if global_prefs['hidden__initialized']:
initialize_scheduler()
schedule_synchronize_global()
except OperationalError:
# Settings table is not created when running migrate or makemigrations, so just don't do anything in this case.
pass
logging.info('Initialization complete.')

View File

@ -4,6 +4,8 @@ from dynamic_preferences.registries import global_preferences_registry
from dynamic_preferences.users.registries import user_preferences_registry
from YtManagerApp.models import VIDEO_ORDER_CHOICES
from django.conf import settings
import os
# we create some section objects to link related preferences together
@ -83,7 +85,7 @@ class AutoDeleteWatched(BooleanPreference):
@user_preferences_registry.register
class AutoDownloadEnabled(BooleanPreference):
section = downloader
name = 'download_enabled'
name = 'auto_enabled'
default = True
required = True
@ -91,7 +93,7 @@ class AutoDownloadEnabled(BooleanPreference):
@user_preferences_registry.register
class DownloadGlobalLimit(IntegerPreference):
section = downloader
name = 'download_global_limit'
name = 'global_limit'
default = None
required = False
@ -107,7 +109,7 @@ class DownloadGlobalSizeLimit(IntegerPreference):
@user_preferences_registry.register
class DownloadSubscriptionLimit(IntegerPreference):
section = downloader
name = 'download_limit_per_subscription'
name = 'limit_per_subscription'
default = 5
required = False
@ -115,7 +117,7 @@ class DownloadSubscriptionLimit(IntegerPreference):
@user_preferences_registry.register
class DownloadMaxAttempts(IntegerPreference):
section = downloader
name = 'download_max_attempts'
name = 'max_download_attempts'
default = 3
required = True
@ -133,7 +135,7 @@ class DownloadOrder(ChoicePreference):
class DownloadPath(StringPreference):
section = downloader
name = 'download_path'
default = None
default = os.path.join(settings.DATA_DIR, 'downloads')
required = False

View File

@ -149,7 +149,7 @@ def synchronize_subscription(subscription: Subscription):
def schedule_synchronize_global():
trigger = CronTrigger.from_crontab(global_prefs['synchronization_schedule'])
trigger = CronTrigger.from_crontab(global_prefs['scheduler__synchronization_schedule'])
job = scheduler.scheduler.add_job(synchronize, trigger, max_instances=1, coalesce=True)
log.info('Scheduled synchronize job job=%s', job.id)

View File

@ -1,7 +1,8 @@
import logging
import sys
from apscheduler.schedulers.background import BackgroundScheduler
from django.conf import settings
from YtManagerApp.management.appconfig import global_prefs
scheduler: BackgroundScheduler = None
@ -13,7 +14,7 @@ def initialize_scheduler():
executors = {
'default': {
'type': 'threadpool',
'max_workers': settings.SCHEDULER_CONCURRENCY
'max_workers': global_prefs['scheduler__concurrency']
}
}
job_defaults = {

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -5,7 +5,7 @@
<div class="container">
<h1>Done!</h1>
<p>The application is now ready to use!</p>
<p>The setup is finished, and the application is now ready to use!</p>
{% crispy form %}
</div>

View File

@ -0,0 +1,66 @@
{% extends "YtManagerApp/master_default.html" %}
{% load crispy_forms_tags %}
{% load static %}
{% block body %}
<div class="container">
<h1>Step 1: Set up YouTube API key (optional)</h1>
<p>This program uses the YouTube API in order to obtain information about videos, channels and playlists.
In order to access this API, YouTube requires that we register on their site and request an API key. YouTube
uses this key in order to limit the number of requests made to their platform, in order to prevent abuses.</p>
<p>To use this program, it is <em>recommended</em> that you create an API key for your own account. While a key
is already provided the developer, there is a chance that the quota limits will be reached, which will prevent
this program from reaching YouTube. Follow the steps below, or press <strong>Skip</strong> to use the provided key.</p>
<h4>
<a data-toggle="collapse" href="#HowToObtainKey" role="button" aria-expanded="false" aria-controls="HowToObtainKey">
<span class="typcn typcn-arrow-sorted-down"></span>How to obtain a key
</a>
</h4>
<div class="collapse" id="HowToObtainKey">
<ol>
<li>Visit <a href="https://developers.google.com/">https://developers.google.com/</a>, log in or create an account, if necessary.</li>
<li>Go to <a href="https://console.developers.google.com/project"></a>, and click on the <strong>Create project</strong> button.
<img class="d-block" src="{% static 'YtManagerApp/img/first_time/ytapi_create_project.png' %}">
</li>
<li>Give a name to the project, and then click on <strong>Create</strong>. Wait for a few seconds, until Google finishes creating the project.
<img class="d-block" src="{% static 'YtManagerApp/img/first_time/ytapi_project_name.png' %}">
</li>
<li>Make sure the newly created project is selected in the top bar and then, in the left sidebar,
go to <strong>APIs & Services</strong> - <strong>Library</strong>.
<img class="d-block" src="{% static 'YtManagerApp/img/first_time/ytapi_goto_apis.png' %}">
</li>
<li>Find and click on the <strong>YouTube Data API v3</strong> from the list.
<img class="d-block" src="{% static 'YtManagerApp/img/first_time/ytapi_select_ytapi.png' %}">
</li>
<li>Click <strong>Enable</strong> to enable the YouTube API for your account.
<img class="d-block" src="{% static 'YtManagerApp/img/first_time/ytapi_enable_ytapi.png' %}">
</li>
<li>In the navigation sidebar, go to the <strong>Credentials</strong> page.
<img class="d-block" src="{% static 'YtManagerApp/img/first_time/ytapi_goto_credentials.png' %}">
</li>
<li>Click on <strong>Create credentials</strong>.
<img class="d-block" src="{% static 'YtManagerApp/img/first_time/ytapi_create_credential.png' %}">
</li>
<li>Fill the requested information; we will need access to the <strong>YouTube Data v3</strong>,
we will be calling the API from a <strong>Web server</strong>, and we will access the <strong>Public data</strong>.
After filling the information, click on the <strong>What credentials do I need?</strong> button.
<img class="d-block" src="{% static 'YtManagerApp/img/first_time/ytapi_create_credential_options.png' %}">
</li>
<li>Copy the created key in the box below, and then hit <strong>Done</strong>.
<img class="d-block" src="{% static 'YtManagerApp/img/first_time/ytapi_done.png' %}">
</li>
</ol>
</div>
{% crispy form %}
<a href="{% url 'first_time_2' %}">Skip</a>
</div>
{% endblock body %}

View File

@ -0,0 +1,15 @@
{% extends "YtManagerApp/master_default.html" %}
{% load crispy_forms_tags %}
{% load static %}
{% block body %}
<div class="container">
<h1>Step 2: Create an administrator account</h1>
{% crispy form %}
</div>
{% endblock body %}

View File

@ -0,0 +1,17 @@
{% extends "YtManagerApp/master_default.html" %}
{% load crispy_forms_tags %}
{% load static %}
{% block body %}
<div class="container">
<h1>Step 3: Configure the server</h1>
<p>Here you can customize some basic options for the application. There are many more options which can be changed in the settings page.</p>
{% crispy form %}
</div>
{% endblock body %}

View File

@ -24,6 +24,7 @@ from .views.auth import ExtendedLoginView, RegisterView, RegisterDoneView
from .views.index import index, ajax_get_tree, ajax_get_videos, CreateFolderModal, UpdateFolderModal, DeleteFolderModal, \
CreateSubscriptionModal, UpdateSubscriptionModal, DeleteSubscriptionModal, ImportSubscriptionsModal
from .views.settings import SettingsView
from .views import first_time
urlpatterns = [
# Authentication URLs
@ -59,4 +60,11 @@ urlpatterns = [
path('', index, name='home'),
path('settings/', SettingsView.as_view(), name='settings'),
# 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/step3_config', first_time.Step3ConfigureView.as_view(), name='first_time_3'),
path('first_time/done', first_time.DoneView.as_view(), name='first_time_done'),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@ -3,49 +3,165 @@ from crispy_forms.layout import Layout, HTML, Submit
from django import forms
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from django.views.generic import UpdateView
from django.views.generic import UpdateView, FormView
from django.shortcuts import render, redirect
from YtManagerApp.views.auth import RegisterView
from YtManagerApp.models import UserSettings
from YtManagerApp.management.appconfig import global_prefs
from django.http import HttpResponseForbidden
class SettingsForm(forms.ModelForm):
class Meta:
model = UserSettings
exclude = ['user']
class ProtectInitializedMixin(object):
def get(self, request, *args, **kwargs):
if global_prefs['hidden__initialized']:
return redirect('home')
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
if global_prefs['hidden__initialized']:
return HttpResponseForbidden()
return super().post(request, *args, **kwargs)
#
# 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):
template_name = 'YtManagerApp/first_time_setup/step0_welcome.html'
form_class = Step0WelcomeForm
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.form_class = 'form-horizontal'
self.helper.label_class = 'col-lg-3'
self.helper.field_class = 'col-lg-9'
self.helper.layout = Layout(
'mark_deleted_as_watched',
'delete_watched',
HTML('<h2>Download settings</h2>'),
'auto_download',
'download_path',
'download_file_pattern',
'download_format',
'download_order',
'download_global_limit',
'download_subscription_limit',
HTML('<h2>Subtitles download settings</h2>'),
'download_subtitles',
'download_subtitles_langs',
'download_subtitles_all',
'download_autogenerated_subtitles',
'download_subtitles_format',
Submit('submit', value='Save')
'api_key',
Submit('submit', value='Continue'),
)
class SettingsView(LoginRequiredMixin, UpdateView):
form_class = SettingsForm
model = UserSettings
template_name = 'YtManagerApp/settings.html'
success_url = reverse_lazy('home')
class Step1ApiKeyView(ProtectInitializedMixin, FormView):
template_name = 'YtManagerApp/first_time_setup/step1_apikey.html'
form_class = Step1ApiKeyForm
success_url = reverse_lazy('first_time_2')
def get_object(self, queryset=None):
obj, _ = self.model.objects.get_or_create(user=self.request.user)
return obj
def form_valid(self, form):
key = form.cleaned_data['api_key']
# TODO: validate key
if key is not None and len(key) > 0:
global_prefs['general__youtube_api_key'] = key
#
# Step 2: create admin user
#
class Step2CreateAdminUserView(ProtectInitializedMixin, RegisterView):
template_name = 'YtManagerApp/first_time_setup/step2_admin.html'
success_url = reverse_lazy('first_time_3')
#
# 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('<h3>Server settings</h3>'),
'sync_schedule',
'allow_registrations',
HTML('<h3>User settings</h3>'),
'auto_download',
'download_location',
Submit('submit', value='Continue'),
)
class Step3ConfigureView(ProtectInitializedMixin, FormView):
template_name = 'YtManagerApp/first_time_setup/step3_configure.html'
form_class = Step3ConfigureForm
success_url = reverse_lazy('first_time_done')
def form_valid(self, form):
allow_registrations = form.cleaned_data['allow_registrations']
if allow_registrations is not None:
global_prefs['general__allow_registrations'] = allow_registrations
sync_schedule = form.cleaned_data['sync_schedule']
if sync_schedule is not None and len(sync_schedule) > 0:
global_prefs['scheduler__synchronization_schedule'] = sync_schedule
auto_download = form.cleaned_data['auto_download']
if auto_download is not None:
self.request.user.preferences['downloader__auto_enabled'] = auto_download
download_location = form.cleaned_data['download_location']
if download_location is not None and len(download_location) > 0:
self.request.user.preferences['downloader__download_path'] = download_location
# Set initialized to true
global_prefs['hidden__initialized'] = True
return super().form_valid(form)
#
# Done screen
#
class DoneForm(forms.Form):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.layout = Layout(
Submit('submit', value='Finish')
)
class DoneView(FormView):
template_name = 'YtManagerApp/first_time_setup/done.html'
form_class = DoneForm
success_url = reverse_lazy('home')

View File

@ -5,11 +5,12 @@ from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q
from django.http import HttpRequest, HttpResponseBadRequest, JsonResponse
from django.shortcuts import render
from django.shortcuts import render, redirect
from django.views.generic import CreateView, UpdateView, DeleteView, FormView
from django.views.generic.edit import FormMixin
from django.conf import settings
from YtManagerApp.management.videos import get_videos
from YtManagerApp.management.appconfig import global_prefs
from YtManagerApp.models import Subscription, SubscriptionFolder, VIDEO_ORDER_CHOICES, VIDEO_ORDER_MAPPING
from YtManagerApp.utils import youtube, subscription_file_parser
from YtManagerApp.views.controls.modal import ModalMixin
@ -94,6 +95,10 @@ def __tree_sub_id(sub_id):
def index(request: HttpRequest):
if not global_prefs['hidden__initialized']:
return redirect('first_time_0')
context = {
'config_errors': settings.CONFIG_ERRORS,
'config_warnings': settings.CONFIG_WARNINGS,