storage backend reworked

This commit is contained in:
vabene1111 2018-05-25 16:49:10 +02:00
parent c5986c6e7f
commit 4871b6194c
21 changed files with 278 additions and 128 deletions

View File

@ -17,7 +17,7 @@ class EmojiWidget(forms.TextInput):
class EditRecipeForm(forms.ModelForm):
class Meta:
model = Recipe
fields = ('name', 'category', 'keywords', 'path')
fields = ('name', 'category', 'keywords', 'path', 'storage')
labels = {
'name': _('Name'),
@ -42,8 +42,20 @@ class KeywordForm(forms.ModelForm):
widgets = {'icon': EmojiWidget}
class MonitorForm(forms.Form):
path = forms.CharField(label=_('Path'))
class StorageForm(forms.ModelForm):
username = forms.CharField(widget=forms.TextInput(attrs={'autocomplete': 'new-password'}), required=False)
password = forms.CharField(widget=forms.TextInput(attrs={'autocomplete': 'new-password', 'type': 'password'}), required=False)
token = forms.CharField(widget=forms.TextInput(attrs={'autocomplete': 'new-password', 'type': 'password'}), required=False)
class Meta:
model = Storage
fields = ('name', 'method', 'username', 'password', 'token', 'url')
class SyncForm(forms.ModelForm):
class Meta:
model = Sync
fields = ('storage', 'path')
class BatchEditForm(forms.Form):

View File

@ -8,22 +8,11 @@ from django.conf import settings
from cookbook.models import Recipe, Sync, RecipeImport, SyncLog
def sync_all():
monitors = Sync.objects.all()
for monitor in monitors:
ret = import_all(monitor)
if not ret:
return ret
return True
def import_all(monitor):
url = "https://api.dropboxapi.com/2/files/list_folder"
headers = {
"Authorization": "Bearer " + settings.DROPBOX_API_KEY,
"Authorization": "Bearer " + monitor.storage.token,
"Content-Type": "application/json"
}
@ -40,15 +29,15 @@ def import_all(monitor):
return r
import_count = 0
for recipe in recipes['entries']:
for recipe in recipes['entries']: # TODO check if has_more is set and import that as well
path = recipe['path_lower']
if not Recipe.objects.filter(path=path).exists() and not RecipeImport.objects.filter(path=path).exists():
name = os.path.splitext(recipe['name'])[0]
new_recipe = RecipeImport(name=name, path=path)
new_recipe = RecipeImport(name=name, path=path, storage=monitor.storage)
new_recipe.save()
import_count += 1
log_entry = SyncLog(status='SUCCESS', msg='Imported ' + str(import_count) + ' recipes', monitor=monitor)
log_entry = SyncLog(status='SUCCESS', msg='Imported ' + str(import_count) + ' recipes', sync=monitor)
log_entry.save()
monitor.last_checked = datetime.now()

View File

@ -1,4 +1,4 @@
# Generated by Django 2.0.5 on 2018-05-14 08:50
# Generated by Django 2.0.5 on 2018-05-25 13:11
from django.db import migrations, models
import django.db.models.deletion
@ -17,6 +17,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=64, unique=True)),
('icon', models.CharField(blank=True, max_length=1, null=True)),
('description', models.TextField(blank=True, default='')),
('created_by', models.IntegerField(default=0)),
('created_at', models.DateTimeField(auto_now_add=True)),
@ -28,6 +29,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=64, unique=True)),
('icon', models.CharField(blank=True, max_length=1, null=True)),
('description', models.TextField(blank=True, default='')),
('created_by', models.IntegerField(default=0)),
('created_at', models.DateTimeField(auto_now_add=True)),
@ -44,7 +46,7 @@ class Migration(migrations.Migration):
('created_by', models.IntegerField(default=0)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='cookbook.Category')),
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='cookbook.Category')),
('keywords', models.ManyToManyField(blank=True, to='cookbook.Keyword')),
],
),
@ -57,6 +59,18 @@ class Migration(migrations.Migration):
('created_at', models.DateTimeField(auto_now_add=True)),
],
),
migrations.CreateModel(
name='Storage',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=128)),
('method', models.CharField(choices=[('DB', 'Dropbox')], default='DB', max_length=128)),
('username', models.CharField(blank=True, max_length=128, null=True)),
('password', models.CharField(blank=True, max_length=128, null=True)),
('token', models.CharField(blank=True, max_length=512, null=True)),
('url', models.URLField(blank=True, null=True)),
],
),
migrations.CreateModel(
name='Sync',
fields=[
@ -65,6 +79,7 @@ class Migration(migrations.Migration):
('last_checked', models.DateTimeField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('storage', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='cookbook.Storage')),
],
),
migrations.CreateModel(
@ -77,4 +92,9 @@ class Migration(migrations.Migration):
('monitor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.Sync')),
],
),
migrations.AddField(
model_name='recipe',
name='storage',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='cookbook.Storage'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.0.5 on 2018-05-25 13:58
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0001_initial'),
]
operations = [
migrations.RenameField(
model_name='synclog',
old_name='monitor',
new_name='sync',
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 2.0.5 on 2018-05-14 08:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='category',
name='icon',
field=models.CharField(blank=True, max_length=4, null=True),
),
]

View File

@ -1,23 +0,0 @@
# Generated by Django 2.0.5 on 2018-05-14 09:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0002_category_icon'),
]
operations = [
migrations.AddField(
model_name='keyword',
name='icon',
field=models.CharField(blank=True, max_length=1, null=True),
),
migrations.AlterField(
model_name='category',
name='icon',
field=models.CharField(blank=True, max_length=1, null=True),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 2.0.5 on 2018-05-25 14:05
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0002_auto_20180525_1558'),
]
operations = [
migrations.AddField(
model_name='recipeimport',
name='storage',
field=models.ForeignKey(default=0, on_delete=django.db.models.deletion.PROTECT, to='cookbook.Storage'),
preserve_default=False,
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 2.0.5 on 2018-05-15 18:35
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0003_auto_20180514_1121'),
]
operations = [
migrations.AlterField(
model_name='recipe',
name='category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='cookbook.Category'),
),
]

View File

@ -1,6 +1,36 @@
from django.db import models
class Storage(models.Model):
DROPBOX = 'DB'
STORAGE_TYPES = ((DROPBOX, 'Dropbox'),)
name = models.CharField(max_length=128)
method = models.CharField(choices=STORAGE_TYPES, max_length=128, default=DROPBOX)
username = models.CharField(max_length=128, blank=True, null=True)
password = models.CharField(max_length=128, blank=True, null=True)
token = models.CharField(max_length=512, blank=True, null=True)
url = models.URLField(blank=True, null=True)
def __str__(self):
return self.name
class Sync(models.Model):
storage = models.ForeignKey(Storage, on_delete=models.PROTECT)
path = models.CharField(max_length=512, default="")
last_checked = models.DateTimeField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class SyncLog(models.Model):
sync = models.ForeignKey(Sync, on_delete=models.CASCADE)
status = models.CharField(max_length=32)
msg = models.TextField(default="")
created_at = models.DateTimeField(auto_now_add=True)
class Keyword(models.Model):
name = models.CharField(max_length=64, unique=True)
icon = models.CharField(max_length=1, blank=True, null=True)
@ -28,6 +58,7 @@ class Category(models.Model):
class Recipe(models.Model):
name = models.CharField(max_length=128)
path = models.CharField(max_length=512, default="")
storage = models.ForeignKey(Storage, on_delete=models.PROTECT)
link = models.CharField(max_length=512, default="")
category = models.ForeignKey(Category, blank=True, on_delete=models.SET_NULL, null=True)
keywords = models.ManyToManyField(Keyword, blank=True)
@ -45,19 +76,6 @@ class Recipe(models.Model):
class RecipeImport(models.Model):
name = models.CharField(max_length=128)
storage = models.ForeignKey(Storage, on_delete=models.PROTECT)
path = models.CharField(max_length=512, default="")
created_at = models.DateTimeField(auto_now_add=True)
class Sync(models.Model):
path = models.CharField(max_length=512, default="")
last_checked = models.DateTimeField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class SyncLog(models.Model):
monitor = models.ForeignKey(Sync, on_delete=models.CASCADE)
status = models.CharField(max_length=32)
msg = models.TextField(default="")
created_at = models.DateTimeField(auto_now_add=True)

View File

@ -39,8 +39,17 @@ class KeywordTable(tables.Table):
fields = ('id', 'icon', 'name')
class StorageTable(tables.Table):
id = tables.LinkColumn('edit_storage', args=[A('id')])
class Meta:
model = Storage
template_name = 'generic/table_template.html'
fields = ('id', 'name', 'method')
class ImportLogTable(tables.Table):
monitor_id = tables.LinkColumn('edit_monitor', args=[A('monitor_id')])
sync_id = tables.LinkColumn('edit_sync', args=[A('sync_id')])
@staticmethod
def render_status(value):
@ -52,20 +61,24 @@ class ImportLogTable(tables.Table):
class Meta:
model = SyncLog
template_name = 'generic/table_template.html'
fields = ('status', 'msg', 'monitor_id', 'created_at')
fields = ('status', 'msg', 'sync_id', 'created_at')
class MonitoredPathTable(tables.Table):
id = tables.LinkColumn('edit_monitor', args=[A('id')])
class SyncTable(tables.Table):
id = tables.LinkColumn('edit_sync', args=[A('id')])
@staticmethod
def render_path(value):
return format_html('<code>%s</code>' % value)
@staticmethod
def render_storage(value):
return format_html('<span class="badge badge-success">%s</span>' % value)
class Meta:
model = Sync
template_name = 'generic/table_template.html'
fields = ('id', 'path', 'last_checked')
fields = ('id', 'path', 'storage', 'last_checked')
class RecipeImportTable(tables.Table):

View File

@ -65,6 +65,8 @@
class="fas fa-archive"></i> {% trans 'Category' %}</a>
<a class="dropdown-item" href="{% url 'new_keyword' %}"><i
class="fas fa-tags"></i> {% trans 'Keyword' %}</a>
<a class="dropdown-item" href="{% url 'new_storage' %}"><i
class="fas fa-database"></i> {% trans 'Storage Backend' %}</a>
</div>
</li>
<li class="nav-item dropdown">
@ -81,6 +83,8 @@
class="far fa-file-alt"></i> {% trans 'New Recipes' %}</a>
<a class="dropdown-item" href="{% url 'list_import_log' %}"><i
class="fas fa-history"></i> {% trans 'Import Log' %}</a>
<a class="dropdown-item" href="{% url 'list_storage' %}"><i
class="fas fa-database"></i> {% trans 'Storage Backends' %}</a>
</div>
</li>
<li class="nav-item dropdown">

View File

@ -24,7 +24,7 @@
</div>
<br/>
<a href="{% url 'api_dropbox_sync' %}" class="btn btn-warning">{% trans 'Sync Now!' %}</a>
<a href="{% url 'batch_sync_wait' %}" class="btn btn-warning">{% trans 'Sync Now!' %}</a>
<br/><br/>
{% render_table monitored_paths %}

View File

@ -0,0 +1,26 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{% trans 'Importing Recipes' %}{% endblock %}
{% block content %}
<h3>
{% trans 'Importing Recipes' %}
</h3>
<br/>
<br/>
<br/>
<br/>
<div class="text-center">
<i class="fas fa-sync fa-spin fa-10x"></i>
</div>
<script>
$(document).ready(function () {
window.location.href = "http://192.168.178.27:8000/cookbook/api/sync_all"
});
</script>
{% endblock %}

View File

@ -13,6 +13,17 @@
<h3>{% trans 'Edit' %} {{ title }}</h3>
{% if form.Meta.model|get_class == 'Storage' %} <!-- TODO make one include for this text block -->
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading"><i class="far fa-exclamation-triangle"></i> {% trans 'Security Warning' %}</h4>
<p>{% blocktrans %}
The <b>Password and Token</b> field are stored as <b>plain text</b> inside the database.
This is necessary because they are needed to make API requests, but it also increases the risk of someone stealing it. <br/>
To limit the possible damage use read only tokens or accounts if available or create separate accounts with limited access (only to recipes).
{% endblocktrans %}</p>
</div>
{% endif %}
<form action="." method="post">
{% csrf_token %}
{{ form|crispy }}

View File

@ -13,6 +13,17 @@
<h3>{% trans 'New' %} {{ title }} </h3>
{% if form.Meta.model|get_class == 'Storage' %} <!-- TODO make one include for this text block -->
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading"><i class="far fa-exclamation-triangle"></i> {% trans 'Security Warning' %}</h4>
<p>{% blocktrans %}
The <b>Password and Token</b> field are stored as <b>plain text</b> inside the database.
This is necessary because they are needed to make API requests, but it also increases the risk of someone stealing it. <br/>
To limit the possible damage use read only tokens or accounts if available or create separate accounts with limited access (only to recipes).
{% endblocktrans %}</p>
</div>
{% endif %}
<form action="." method="post">
{% csrf_token %}
{{ form|crispy }}

View File

@ -12,32 +12,37 @@ urlpatterns = [
path('new/recipe_import/<int:import_id>/', new.create_new_recipe, name='new_recipe_import'),
path('new/category/', new.CategoryCreate.as_view(), name='new_category'),
path('new/keyword/', new.KeywordCreate.as_view(), name='new_keyword'),
path('new/storage/', new.StorageCreate.as_view(), name='new_storage'),
path('list/keyword', lists.keyword, name='list_keyword'),
path('list/category', lists.category, name='list_category'),
path('list/import_log', lists.sync_log, name='list_import_log'),
path('list/import', lists.recipe_import, name='list_import'),
path('list/storage', lists.storage, name='list_storage'),
path('edit/recipe/<int:pk>/', edit.RecipeUpdate.as_view(), name='edit_recipe'),
path('edit/keyword/<int:pk>/', edit.KeywordUpdate.as_view(), name='edit_keyword'),
path('edit/category/<int:pk>/', edit.CategoryUpdate.as_view(), name='edit_category'),
path('edit/monitor/<int:pk>/', edit.MonitorUpdate.as_view(), name='edit_monitor'),
path('edit/sync/<int:pk>/', edit.SyncUpdate.as_view(), name='edit_sync'),
path('edit/import/<int:pk>/', edit.ImportUpdate.as_view(), name='edit_import'),
path('edit/storage/<int:pk>/', edit.StorageUpdate.as_view(), name='edit_storage'),
path('redirect/delete/<slug:name>/<int:pk>/', edit.delete_redirect, name='redirect_delete'),
path('delete/recipe/<int:pk>/', edit.RecipeDelete.as_view(), name='delete_recipe'),
path('delete/keyword/<int:pk>/', edit.KeywordDelete.as_view(), name='delete_keyword'),
path('delete/category/<int:pk>/', edit.CategoryDelete.as_view(), name='delete_category'),
path('delete/monitor/<int:pk>/', edit.MonitorDelete.as_view(), name='delete_monitor'),
path('delete/sync/<int:pk>/', edit.MonitorDelete.as_view(), name='delete_sync'),
path('delete/import/<int:pk>/', edit.ImportDelete.as_view(), name='delete_import'),
path('delete/storage/<int:pk>/', edit.StorageDelete.as_view(), name='delete_storage'),
path('batch/monitor', batch.batch_monitor, name='batch_monitor'),
path('batch/sync', batch.batch_monitor, name='batch_monitor'), # TODO move to generic "new" view
path('batch/edit', batch.batch_edit, name='batch_edit'),
path('batch/import/all', batch.batch_import_all, name='batch_import_all'),
path('batch/sync/wait', batch.sync_wait, name='batch_sync_wait'),
path('api/get_file_link/<int:recipe_id>/', api.get_file_link, name='api_get_file_link'),
path('api/sync_all/', api.dropbox_sync, name='api_dropbox_sync'),
path('api/sync_all/', api.sync_all, name='api_sync'),
path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'),
]

View File

@ -4,13 +4,15 @@ from django.utils.translation import gettext as _
from django.contrib.auth.decorators import login_required
from django.shortcuts import redirect
from cookbook.models import Recipe
from cookbook.models import Recipe, Sync, Storage
from cookbook.helper import dropbox
@login_required
def get_file_link(request, recipe_id):
recipe = Recipe.objects.get(id=recipe_id)
if recipe.storage.method == Storage.DROPBOX:
if recipe.link == "":
response = dropbox.get_share_link(recipe.path) # TODO response validation
recipe.link = response['url']
@ -20,9 +22,17 @@ def get_file_link(request, recipe_id):
@login_required
def dropbox_sync(request):
ret = dropbox.sync_all()
if ret:
def sync_all(request):
monitors = Sync.objects.all()
error = False
for monitor in monitors:
if monitor.storage.method == Storage.DROPBOX:
ret = dropbox.import_all(monitor)
if not ret:
error = True
if not error:
messages.add_message(request, messages.SUCCESS, _('Sync successful!'))
return redirect('list_import')
else:

View File

@ -5,36 +5,42 @@ from django.contrib.auth.decorators import login_required
from django.shortcuts import redirect, render
from django_tables2 import RequestConfig
from cookbook.forms import MonitorForm, BatchEditForm, RecipeImport
from cookbook.forms import SyncForm, BatchEditForm, RecipeImport
from cookbook.models import Recipe, Category, Sync
from cookbook.tables import MonitoredPathTable
from cookbook.tables import SyncTable
from django.utils.translation import gettext as _, ngettext
@login_required
def batch_monitor(request):
if request.method == "POST":
form = MonitorForm(request.POST)
form = SyncForm(request.POST)
if form.is_valid():
new_path = Sync()
new_path.path = form.cleaned_data['path']
new_path.storage = form.cleaned_data['storage']
new_path.last_checked = datetime.now()
new_path.save()
return redirect('batch_monitor')
else:
form = MonitorForm()
form = SyncForm()
monitored_paths = MonitoredPathTable(Sync.objects.all())
monitored_paths = SyncTable(Sync.objects.all())
RequestConfig(request, paginate={'per_page': 25}).configure(monitored_paths)
return render(request, 'batch/monitor.html', {'form': form, 'monitored_paths': monitored_paths})
@login_required
def sync_wait(request):
return render(request, 'batch/waiting.html')
@login_required
def batch_import_all(request):
imports = RecipeImport.objects.all()
for new_recipe in imports:
recipe = Recipe(name=new_recipe.name, path=new_recipe.path)
recipe = Recipe(name=new_recipe.name, path=new_recipe.path, storage=new_recipe.storage)
recipe.save()
new_recipe.delete()

View File

@ -5,23 +5,23 @@ from django.urls import reverse_lazy, reverse
from django.utils.translation import gettext as _
from django.views.generic import UpdateView, DeleteView
from cookbook.forms import EditRecipeForm, CategoryForm, KeywordForm
from cookbook.models import Recipe, Category, Sync, Keyword, RecipeImport
from cookbook.forms import EditRecipeForm, CategoryForm, KeywordForm, StorageForm, SyncForm
from cookbook.models import Recipe, Category, Sync, Keyword, RecipeImport, Storage
class MonitorUpdate(LoginRequiredMixin, UpdateView):
class SyncUpdate(LoginRequiredMixin, UpdateView):
template_name = "generic\edit_template.html"
model = Sync
fields = ['path']
form_class = SyncForm
# TODO add msg box
def get_success_url(self):
return reverse('edit_monitor', kwargs={'pk': self.object.pk})
return reverse('edit_sync', kwargs={'pk': self.object.pk})
def get_context_data(self, **kwargs):
context = super(MonitorUpdate, self).get_context_data(**kwargs)
context['title'] = _("Monitor")
context = super(SyncUpdate, self).get_context_data(**kwargs)
context['title'] = _("Sync")
return context
@ -57,6 +57,22 @@ class KeywordUpdate(LoginRequiredMixin, UpdateView):
return context
class StorageUpdate(LoginRequiredMixin, UpdateView):
template_name = "generic\edit_template.html"
model = Storage
form_class = StorageForm
# TODO add msg box
def get_success_url(self):
return reverse('edit_storage', kwargs={'pk': self.object.pk})
def get_context_data(self, **kwargs):
context = super(StorageUpdate, self).get_context_data(**kwargs)
context['title'] = _("Storage Backend")
return context
class ImportUpdate(LoginRequiredMixin, UpdateView):
template_name = "generic\edit_template.html"
model = RecipeImport
@ -154,3 +170,14 @@ class KeywordDelete(LoginRequiredMixin, DeleteView):
context = super(KeywordDelete, self).get_context_data(**kwargs)
context['title'] = _("Keyword")
return context
class StorageDelete(LoginRequiredMixin, DeleteView):
template_name = "generic\delete_template.html"
model = Storage
success_url = reverse_lazy('list_storage')
def get_context_data(self, **kwargs):
context = super(StorageDelete, self).get_context_data(**kwargs)
context['title'] = _("Storage Backend")
return context

View File

@ -4,8 +4,8 @@ from django.shortcuts import render
from django_tables2 import RequestConfig
from django.utils.translation import gettext as _
from cookbook.models import Category, Keyword, SyncLog, RecipeImport
from cookbook.tables import CategoryTable, KeywordTable, ImportLogTable, RecipeImportTable
from cookbook.models import Category, Keyword, SyncLog, RecipeImport, Storage
from cookbook.tables import CategoryTable, KeywordTable, ImportLogTable, RecipeImportTable, StorageTable
@login_required
@ -38,3 +38,11 @@ def recipe_import(request):
RequestConfig(request, paginate={'per_page': 25}).configure(table)
return render(request, 'generic/list_template.html', {'title': _("Import"), 'table': table})
@login_required
def storage(request):
table = StorageTable(Storage.objects.all())
RequestConfig(request, paginate={'per_page': 25}).configure(table)
return render(request, 'generic/list_template.html', {'title': _("Storage Backend"), 'table': table})

View File

@ -6,7 +6,7 @@ from django.urls import reverse_lazy
from django.utils.translation import gettext as _
from django.views.generic import CreateView
from cookbook.forms import ImportRecipeForm, RecipeImport, CategoryForm, KeywordForm
from cookbook.forms import ImportRecipeForm, RecipeImport, CategoryForm, KeywordForm, Storage, StorageForm
from cookbook.models import Category, Keyword, Recipe
@ -46,6 +46,18 @@ class KeywordCreate(LoginRequiredMixin, CreateView):
return context
class StorageCreate(LoginRequiredMixin, CreateView):
template_name = "generic\\new_template.html"
model = Storage
form_class = StorageForm
success_url = reverse_lazy('list_storage')
def get_context_data(self, **kwargs):
context = super(StorageCreate, self).get_context_data(**kwargs)
context['title'] = _("Storage Backend")
return context
@login_required
def create_new_recipe(request, import_id):
if request.method == "POST":