[func] tournament pages

main
yaemiku 2022-10-05 21:24:27 +02:00
parent a94c612af7
commit 9d4ca3f16b
Signed by: podlaskizbs
GPG Key ID: ADC039636B3E4AAB
81 changed files with 1212 additions and 48 deletions

View File

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@ -1,3 +0,0 @@
from django.db import models
# Create your models here.

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -1,3 +0,0 @@
from django.shortcuts import render
# Create your views here.

View File

@ -55,12 +55,14 @@ INSTALLED_APPS = [
'db.atu',
'db.mbkb',
'theme',
'core'
'core',
'tournamentpages'
] + [
'tailwind',
'tinymce',
'filebrowser',
'admin_ordering',
'tabbed_admin',
'django_browser_reload'
]
@ -169,3 +171,5 @@ TINYMCE_SPELLCHECKER = True
FILEBROWSER_DIRECTORY = ''
FILEBROWSER_EXCLUDE = [r'^_versions']
TABBED_ADMIN_USE_JQUERY_UI = True

View File

@ -21,6 +21,7 @@ from filebrowser.sites import site
urlpatterns = [
path('', include('core.urls')),
path('turnieje/', include('tournamentpages.urls')),
path('admin/filebrowser/', site.urls),
path('admin/', admin.site.urls),
path('tinymce/', include('tinymce.urls')),

View File

@ -1233,6 +1233,21 @@ select {
padding-bottom: 7px;
}
.tpage-nav ul li:hover {
font-weight: 700;
-webkit-text-decoration-line: underline;
text-decoration-line: underline;
}
.tpage-nav ul li.active {
-webkit-text-decoration-line: none;
text-decoration-line: none;
}
.tpage-nav ul li.active a {
font-weight: 600;
}
.sr-only {
position: absolute;
width: 1px;
@ -1406,6 +1421,10 @@ select {
resize: both;
}
.flex-row {
flex-direction: row;
}
.flex-col {
flex-direction: column;
}
@ -1505,6 +1524,11 @@ select {
background-color: rgb(250 250 249 / var(--tw-bg-opacity));
}
.bg-green-100 {
--tw-bg-opacity: 1;
background-color: rgb(220 252 231 / var(--tw-bg-opacity));
}
.bg-gray-50 {
--tw-bg-opacity: 1;
background-color: rgb(249 250 251 / var(--tw-bg-opacity));
@ -1795,19 +1819,6 @@ h4 {
font-size: 1.25em;
}
#atu {
transition: all 0.2s ease, visibility 0s;
border-radius: 0px;
background: repeat padding-box border-box 0% 0% / auto auto scroll
linear-gradient(180deg, rgba(8, 50, 4, 0.5) 0%, rgba(8, 50, 4, 0.5) 100%),
no-repeat padding-box border-box 78% 59%/200% scroll url("/static/atu.jpg");
display: block;
}
.target\:block:target {
display: block;
}
.active\:ring-2:active {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
@ -1818,6 +1829,10 @@ h4 {
display: block;
}
.prose-h1\:font-medium :is(:where(h1):not(:where([class~="not-prose"] *))) {
font-weight: 500;
}
.prose-h2\:mb-2 :is(:where(h2):not(:where([class~="not-prose"] *))) {
margin-bottom: 0.5rem;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
#tabs{clear:both}.ui-state-default a.errortab{background-color:red;color:white}.ui-tabs .ui-tabs-panel{overflow:hidden}

View File

@ -0,0 +1 @@
.ui-tabs{background-color:#fff;border:0;min-width:960px}.ui-widget textarea,.ui-widget input,.ui-widget select{font-family:Arial,sans-serif}.ui-widget{font-family:Arial,sans-serif;font-size:1em}.ui-tabs .ui-tabs-panel{background-color:#fff;padding:5px 4px 0}.ui-tabs .ui-tabs-anchor{font-size:9pt}.ui-widget-content{background:none}.ui-tabs .ui-widget-content a{color:#309BBF}.ui-tabs .ui-widget-content a:hover{color:#444}.ui-tabs .ui-widget-header{background-image:linear-gradient(#E5E5E5,#DBDBDB)}.ui-tabs .ui-state-default{background-image:linear-gradient(#CEE9F2,#E1F0F5)}.ui-tabs .ui-state-active{background-image:linear-gradient(#CEE9F2,#fff);border-color:#BBB}.ui-tabs .ui-state-active a{color:#444}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,27 @@
Copyright (c) 2015, Guillaume Pousseo
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the <ORGANIZATION> nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,6 @@
from .admin import TabbedModelAdmin
__author__ = 'Guillaume Pousseo'
__all__ = [
"TabbedModelAdmin"
]

View File

@ -0,0 +1,154 @@
from django.conf import settings
from django.contrib.admin.options import ModelAdmin
from django.utils.translation import gettext_lazy as _
from .settings import USE_JQUERY_UI, JQUERY_UI_CSS, JQUERY_UI_JS
class TabbedModelAdmin(ModelAdmin):
tabs = None
formatted_tabs = {}
# Needs a specific template to display the tabs
change_form_template = 'tabbed_admin/change_form.html'
def get_fieldsets(self, request, obj=None):
"""
Overwrites BaseModelAdmin fieldsets to add fieldsets passed by the
tabs.
If the tabs attribute is not set, use the default ModelAdmin method.
"""
tabs_fieldsets = self.get_formatted_tabs(request, obj)['fieldsets']
if self.tabs is not None:
self.fieldsets = ()
self.fieldsets = self.add_tabbed_item(tabs_fieldsets, self.fieldsets)
return super(TabbedModelAdmin, self).get_fieldsets(request, obj)
def get_inline_instances(self, request, obj=None):
"""
Overwrites BaseModelAdmin fieldsets to add fieldsets passed by the
tabs.
If the tabs attribute is not set, use the default ModelAdmin method.
"""
if self.tabs is not None:
self.inlines = ()
tabs_inlines = self.get_formatted_tabs(request, obj)['inlines']
self.inlines = self.add_tabbed_item(tabs_inlines, self.inlines)
try:
# django >=1.7
return super(TabbedModelAdmin, self)\
.get_inline_instances(request, obj)
except TypeError:
return super(TabbedModelAdmin, self).get_inline_instances(request)
def add_tabbed_item(self, items_to_add, collection):
"""
Adds tabbed items (inlines or fieldsets) to their corresponding
attribute.
"""
if items_to_add:
for item in items_to_add:
if item not in collection:
if type(collection) == tuple:
collection = collection + (item, )
elif type(collection) == list:
collection.append(item)
return collection
def get_tabs(self, request, obj=None):
"""
Returns the tabs attribute.
"""
return self.tabs
def get_formatted_tabs(self, request, obj=None):
"""
Returns the formated tabs attribute.
"""
if self.tabs:
self.parse_fieldsets_inlines_from_tabs(request, obj)
return self.formatted_tabs
def parse_fieldsets_inlines_from_tabs(self, request, obj=None):
"""
Parses the self.tabs attribute. Checks its integrity and populates
self._tabs_fieldsets and self._tabs_inlines attributes.
"""
tabs_fieldsets = ()
tabs_inlines = ()
self.formatted_tabs['fields'] = []
for tab in self.get_tabs(request, obj):
# Checks that each tab if formated with 2 arguments, verbose name
# of the tab and the tab configuration.
if type(tab) not in [tuple, list]:
raise TypeError(
_(u'Each tab entry must be either a list or a tuple'))
if len(tab) != 2:
raise ValueError(
_(u'Each tabs entry must contain 2 arguments: a verbose name and the tab setup.'))
if type(tab[1]) not in [tuple, list]:
raise TypeError(
_(u'A tab definition must be either a list or a tuple'))
# So far all went well, lets parse the tab configuration, we go
# through each item. If its a tuple or a list we consider its a
# fieldset, otherwise its a tuple.
formatted_tab = {'name': tab[0], 'entries': []}
for tab_entry in tab[1]:
formatted_tab_entry = {}
if type(tab_entry) in [tuple, list]:
tabs_fieldsets = tabs_fieldsets + (tab_entry, )
formatted_tab_entry = {'type': 'fieldset',
'name': tab_entry[0],
'config': tab_entry[1]}
else:
tabs_inlines = tabs_inlines + (tab_entry, )
formatted_tab_entry = {'type': 'inline',
'name': tab_entry.__name__}
formatted_tab['entries'].append(formatted_tab_entry)
self.formatted_tabs['fields'].append(formatted_tab)
self.formatted_tabs['fieldsets'] = tabs_fieldsets
self.formatted_tabs['inlines'] = tabs_inlines
def add_view(self, request, form_url='', extra_context=None):
"""
Overwrites add_view to insert the tabs config for the template.
"""
if extra_context is None:
extra_context = {}
extra_context.update({'tabs': self.get_formatted_tabs(request)})
return super(TabbedModelAdmin, self)\
.add_view(request, form_url=form_url, extra_context=extra_context)
def change_view(self, request, object_id, form_url='', extra_context=None):
"""
Overwrites change_view to insert the tabs config for the template.
"""
try:
# django 1.4
change_view = super(TabbedModelAdmin, self)\
.change_view(request, object_id, form_url=form_url,
extra_context=extra_context)
except TypeError:
# django 1.3
change_view = super(TabbedModelAdmin, self)\
.change_view(request, object_id, extra_context=extra_context)
if hasattr(change_view, 'context_data'):
change_view.context_data.update(
{'tabs': self.get_formatted_tabs(request,
change_view.context_data.get('original'))}
)
return change_view
class Media:
"""
Extends media class to add custom jquery ui if
TABBED_ADMIN_USE_JQUERY_UI is set to True.
"""
if 'grappelli' in settings.INSTALLED_APPS:
css = {'all': ("tabbed_admin/css/tabbed_grappelli_admin.css", )}
if USE_JQUERY_UI:
css = {
'all': (JQUERY_UI_CSS, 'tabbed_admin/css/tabbed_admin.css', )}
js = (JQUERY_UI_JS,)

View File

@ -0,0 +1,6 @@
# No particular version to test anything
Django
django-grappelli
django-tabbed-admin
django-gipsy

View File

@ -0,0 +1,18 @@
# coding: utf-8
# DJANGO IMPORTS
from django.conf import settings
# Activate the library jquery ui
USE_JQUERY_UI = getattr(settings, "TABBED_ADMIN_USE_JQUERY_UI", False)
USE_GRAPPELLI = getattr(settings, "TABBED_ADMIN_USE_GRAPPELLI", False)
# Default jquery ui css and js
DEFAULT_JQUERY_UI_CSS = 'tabbed_admin/css/jquery-ui-1.11.4.min.css'
DEFAULT_JQUERY_UI_JS = 'tabbed_admin/js/jquery-ui-1.11.4.min.js'
# User ability to override the default css and js
JQUERY_UI_CSS = getattr(
settings, "TABBED_ADMIN_JQUERY_UI_CSS", DEFAULT_JQUERY_UI_CSS)
JQUERY_UI_JS = getattr(
settings, "TABBED_ADMIN_JQUERY_UI_JS", DEFAULT_JQUERY_UI_JS)

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
#tabs{clear:both}.ui-state-default a.errortab{background-color:red;color:white}.ui-tabs .ui-tabs-panel{overflow:hidden}

View File

@ -0,0 +1 @@
.ui-tabs{background-color:#fff;border:0;min-width:960px}.ui-widget textarea,.ui-widget input,.ui-widget select{font-family:Arial,sans-serif}.ui-widget{font-family:Arial,sans-serif;font-size:1em}.ui-tabs .ui-tabs-panel{background-color:#fff;padding:5px 4px 0}.ui-tabs .ui-tabs-anchor{font-size:9pt}.ui-widget-content{background:none}.ui-tabs .ui-widget-content a{color:#309BBF}.ui-tabs .ui-widget-content a:hover{color:#444}.ui-tabs .ui-widget-header{background-image:linear-gradient(#E5E5E5,#DBDBDB)}.ui-tabs .ui-state-default{background-image:linear-gradient(#CEE9F2,#E1F0F5)}.ui-tabs .ui-state-active{background-image:linear-gradient(#CEE9F2,#fff);border-color:#BBB}.ui-tabs .ui-state-active a{color:#444}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,104 @@
{% extends "admin/change_form.html" %}
{% load i18n admin_modify admin_urls tabbed_admin_tags %}
{% block content %}
<div id="content-main">
{% block object-tools %}{{ block.super }}{% endblock %}
<form {% if has_file_field %}enctype="multipart/form-data" {% endif %}action="{{ form_url }}" method="post" id="{{ opts.model_name }}_form" novalidate>{% csrf_token %}{% block form_top %}{% endblock %}
<div>
{% if is_popup %}<input type="hidden" name="{{ is_popup_var }}" value="1" />{% endif %}
{% if to_field %}<input type="hidden" name="{{ to_field_var }}" value="{{ to_field }}" />{% endif %}
{% if save_on_top %}{% block submit_buttons_top %}{% submit_row %}{% endblock %}{% endif %}
{% if errors %}
<p class="errornote">
{% if errors|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %}
</p>
{{ adminform.form.non_field_errors }}
{% endif %}
<!-- start admin_tabs stuff -->
<div id="tabs">
<ul>
{% for tab in tabs.fields %}
<li><a href="#tabs-{{ forloop.counter }}" id="for_tabs-{{ forloop.counter }}">{{ tab.name }}</a></li>
{% endfor %}
</ul>
{% for tab in tabs.fields %}
<div id="tabs-{{ forloop.counter }}" class="{{ tab.name }}">
{% for entry in tab.entries %}
{% render_tab_fieldsets_inlines entry %}
{% endfor %}
</div>
{% endfor %}
</div>
<script type="text/javascript">
(function($) {
$(window).scrollTop()
$('#tabs').tabs({
{% if add %}
// when adding, don't select a tab by default, we'll do it ourselves
// by finding the first available tab.
selected: -1
{% endif %}
});
// disable tabs marked as such in page_config
var enabled_tabs = [];
var disabled_tabs = [];
{% for tab in page_config %}
{% if tab.enabled %}
enabled_tabs.push({{ forloop.counter0 }});
{% else %}
disabled_tabs.push({{ forloop.counter0 }});
{% endif %}
{% endfor %}
for (var i = 0; i < disabled_tabs.length; i++) {
$('#tabs').tabs("disable", disabled_tabs[i]);
}
// enable the first non-disabled tab in add view
{% if add %}
$('#tabs').tabs("option", "active", enabled_tabs[0]);
{% endif %}
// Hightlight tabs with errors inside
$('#tabs > div').each(function() {
if($(this).find('.errorlist').length) {
$('#tabs #for_' + this.id).addClass("errortab");
}
});
$("#tabs").on('tabsactivate', function(event, ui) {
var scrollPos = $(window).scrollTop();
var hash = ui.newTab.children("li a").attr("href");
window.location.hash = hash;
$(window).scrollTop(scrollPos);
});
if ($('.errornote').length) {
$('.errornote').addClass('tabbed-errornote');
}
})(django.jQuery);
</script>
<!-- end admin_tabs stuff -->
{% block after_field_sets %}{% endblock %}
{% block after_related_objects %}{% endblock %}
{% block submit_buttons_bottom %}{% submit_row %}{% endblock %}
{% if adminform and add and adminform.first_field and adminform.first_field.id_for_label %}
<script type="text/javascript">document.getElementById("{{ adminform.first_field.id_for_label }}").focus();</script>
{% endif %}
{% prepopulated_fields_js %}
</div>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
from django import template
from django.contrib.admin.helpers import Fieldset
from django.template.loader import render_to_string
from django.core.exceptions import ImproperlyConfigured
register = template.Library()
@register.simple_tag(takes_context=True)
def render_tab_fieldsets_inlines(context, entry):
"""
Render the fieldsets and inlines for a tab.
"""
template = "admin/includes/fieldset.html"
admin_form = context['adminform']
if 'request' not in context:
raise ImproperlyConfigured(
'"request" missing from context. Add django.core.context'
'_processors.request to your'
'TEMPLATE_CONTEXT_PROCESSORS')
request = context['request']
obj = context.get('original', None)
readonly_fields = admin_form.model_admin.get_readonly_fields(request, obj)
inline_matching = {}
if "inline_admin_formsets" in context:
inline_matching = dict((inline.opts.__class__.__name__, inline)
for inline in context["inline_admin_formsets"])
if entry['type'] == 'fieldset':
name = entry['name']
f = Fieldset(
admin_form.form,
name,
readonly_fields=readonly_fields,
model_admin=admin_form.model_admin,
**entry['config']
)
context["fieldset"] = f
return render_to_string(template, context.flatten(), request=request)
elif entry['type'] == 'inline':
try:
inline_admin_formset = inline_matching[entry["name"]]
context["inline_admin_formset"] = inline_admin_formset
return render_to_string(inline_admin_formset.opts.template,
context.flatten(), request=request)
except KeyError: # The user does not have the permission
pass
return ''

View File

View File

@ -0,0 +1,42 @@
from django.contrib import admin
from tabbed_admin import TabbedModelAdmin
from tabbed_admin.tests.models import Band, Musician, Concert, Album, Interview
class MusicianInline(admin.StackedInline):
model = Musician
class ConcertInline(admin.TabularInline):
model = Concert
class AlbumInline(admin.TabularInline):
model = Album
class InterviewInline(admin.TabularInline):
model = Interview
class BandAdmin(TabbedModelAdmin):
model = Band
tab_overview = (
(None, {
'fields': ('name', 'bio', 'style')
}),
MusicianInline,
('Contact', {
'fields': ('agent', 'phone', 'email')
})
)
tab_ressources = (
ConcertInline,
AlbumInline
)
tabs = [
('Overview', tab_overview),
('Ressources', tab_ressources)
]

View File

@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
from django.db import models
class Band(models.Model):
STYLE_ROCK = 1
STYLE_FUNK = 2
STYLE_JAZZ = 3
STYLE_OVERRIDE = 4
name = models.CharField(max_length=100)
bio = models.TextField(blank=True, null=True)
style = models.CharField(max_length=100, choices=(
(STYLE_ROCK, 'Rock'),
(STYLE_FUNK, 'Funk'),
(STYLE_JAZZ, 'Jazz'),
(STYLE_OVERRIDE, 'Override')
))
agent = models.CharField(max_length=100, blank=True, null=True)
phone = models.CharField(max_length=100, blank=True, null=True)
email = models.CharField(max_length=100, blank=True, null=True)
address = models.CharField(max_length=100, blank=True, null=True)
website = models.CharField(max_length=100, blank=True, null=True)
twitter = models.CharField(max_length=100, blank=True, null=True)
facebook = models.CharField(max_length=100, blank=True, null=True)
class Musician(models.Model):
band = models.ForeignKey(Band)
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
specialty = models.CharField(max_length=100, choices=(
(1, 'Vocal'),
(2, 'Guitar'),
(3, 'Bass'),
(4, 'Drums')
))
class Concert(models.Model):
band = models.ForeignKey(Band)
location = models.CharField(max_length=100)
date = models.DateField()
class Album(models.Model):
band = models.ForeignKey(Band)
name = models.CharField(max_length=100)
date = models.DateField()
class Interview(models.Model):
band = models.ForeignKey(Band)
media_name = models.CharField(max_length=100)
date = models.DateField()

View File

@ -0,0 +1,74 @@
import os
import sys
from django.conf import settings
DIRNAME = os.path.dirname(__file__)
settings.configure(
DEBUG=True,
DATABASE_ENGINE='sqlite3',
DATABASE_NAME=os.path.join(DIRNAME, 'database.db'),
DATABASES={
'default': {
'ENGINE': 'django.db.backends.sqlite3'
}
},
ROOT_URLCONF='tabbed_admin.tests.urls',
MIDDLEWARE_CLASSES=(),
TEMPLATE_CONTEXT_PROCESSORS=[
'django.template.context_processors.request'
],
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth'
],
},
},
],
INSTALLED_APPS=(
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.admin',
'tabbed_admin',
'tabbed_admin.tests'
)
)
try:
# Django < 1.8
from django.test.simple import DjangoTestSuiteRunner
test_runner = DjangoTestSuiteRunner(verbosity=1)
except ImportError:
# Django >= 1.8
from django.test.runner import DiscoverRunner
test_runner = DiscoverRunner(verbosity=1)
try:
# Django < 1.7
from django.core.management import setup_environ
setup_environ(settings)
failures = test_runner.run_tests(['tabbed_admin'])
except:
# Django >= 1.7
import django
django.setup()
if __name__ == "__main__":
from django.test.utils import get_runner
os.environ['DJANGO_SETTINGS_MODULE'] = 'tabbed_admin.runtests'
TestRunner = get_runner(settings)
test_runner = TestRunner()
failures = test_runner.run_tests(['tabbed_admin'])
if failures:
sys.exit(failures)

View File

@ -0,0 +1,240 @@
from django.conf import settings
from django.contrib.admin.sites import AdminSite
from django.template import Context
from django.test import TestCase
from django.test.client import RequestFactory, Client
from django.test.utils import override_settings
from tabbed_admin.settings import USE_JQUERY_UI
from tabbed_admin.templatetags.tabbed_admin_tags import render_tab_fieldsets_inlines
from tabbed_admin.tests.admin import BandAdmin, InterviewInline
from tabbed_admin.tests.models import Band
class MockRequest(object):
pass
class MockSuperUser(object):
is_active = True
is_staff = True
def has_perm(self, perm):
return True
request = RequestFactory()
request.user = MockSuperUser()
request.csrf_processing_done = True
class TabbedModelAdminTest(TestCase):
def setUp(self):
self.site = AdminSite()
def test_fieldsets_inline_attribute_populated(self):
"""
Tests if self.inlines and self.fieldsets are correcly populated from
the self.tabs attribute.
"""
admin = BandAdmin(Band, self.site)
self.assertIsNone(admin.fieldsets)
self.assertEqual(0, len(admin.inlines))
fieldsets = admin.get_fieldsets(request)
inlines = admin.get_inline_instances(request)
self.assertNotEqual(0, len(fieldsets))
self.assertNotEqual(0, len(inlines))
self.assertNotEqual(0, len(admin.fieldsets))
self.assertNotEqual(0, len(admin.inlines))
def test_fieldsets_inlines_overriden_by_tabs(self):
"""
Tests if when set by default, fieldsets and inlines are properly
overriden.
"""
class TestBandAdmin(BandAdmin):
fieldsets = (
('Social', {
'fields': ('website', 'twitter', 'facebook')
})
)
inlines = (
InterviewInline,
)
admin = TestBandAdmin(Band, self.site)
self.assertEqual(admin.get_fieldsets(request),
admin.formatted_tabs['fieldsets'])
inlines = admin.get_inline_instances(request)
inlines = admin.inlines
for inline in inlines:
self.assertIn(inline, admin.formatted_tabs['inlines'])
def test_get_tabs_overrides_tabs_attribute(self):
"""
Tests if get_tabs method successfully overrides the self.tabs and returns it.
"""
single_tab = [('Overview', BandAdmin.tab_overview)]
class TestBandAdmin(BandAdmin):
def get_tabs(self, request, obj=None):
"""
Returns the tabs attribute.
"""
tabs = self.tabs
if obj is not None and obj.style == Band.STYLE_OVERRIDE:
tabs = single_tab
self.tabs = tabs
return super(TestBandAdmin, self).get_tabs(request, obj)
admin = TestBandAdmin(Band, self.site)
band = Band.objects.create(name="Test band", style=Band.STYLE_JAZZ)
tabs = admin.get_tabs(request, band)
self.assertEqual(len(tabs), 2)
self.assertNotEqual(tabs, single_tab)
band.style = Band.STYLE_OVERRIDE
tabs = admin.get_tabs(request, band)
self.assertEqual(len(tabs), 1)
self.assertEqual(tabs, single_tab)
def test_dynamically_add_fieldsets_inlines_to_tabs(self):
"""
Tests overriding dynamically tabs via get_tabs.
"""
added_fieldset = ('Social', {
'fields': ('website', 'twitter', 'facebook')
})
added_inline = InterviewInline
class TestBandAdmin(BandAdmin):
def get_tabs(self, request, obj=None):
tabs = self.tabs
tab_overview = self.tab_overview + (added_fieldset, )
tab_ressources = self.tab_ressources + (added_inline, )
tabs = [
('Overview', tab_overview),
('Ressources', tab_ressources)
]
self.tabs = tabs
return super(TestBandAdmin, self).get_tabs(request, obj)
original_admin = BandAdmin(Band, self.site)
self.assertNotIn(added_fieldset, original_admin.get_fieldsets(request))
self.assertNotIn(added_inline, original_admin.tab_ressources)
admin = TestBandAdmin(Band, self.site)
inlines_classes = []
inlines = admin.get_inline_instances(request)
for inline in inlines:
inlines_classes.append(inline.__class__)
self.assertIn(added_inline, inlines_classes)
def test_version_previous_to_django(self):
"""
Tests overriding dynamically tabs via get_tabs.
"""
added_fieldset = ('Social', {
'fields': ('website', 'twitter', 'facebook')
})
added_inline = InterviewInline
class TestBandAdmin(BandAdmin):
def get_tabs(self, request, obj=None):
tabs = self.tabs
tab_overview = self.tab_overview + (added_fieldset, )
tab_ressources = self.tab_ressources + (added_inline, )
tabs = [
('Overview', tab_overview),
('Ressources', tab_ressources)
]
self.tabs = tabs
return super(TestBandAdmin, self).get_tabs(request, obj)
original_admin = BandAdmin(Band, self.site)
self.assertNotIn(added_fieldset, original_admin.get_fieldsets(request))
self.assertNotIn(added_inline, original_admin.tab_ressources)
admin = TestBandAdmin(Band, self.site)
inlines_classes = []
inlines = admin.get_inline_instances(request)
for inline in inlines:
inlines_classes.append(inline.__class__)
self.assertIn(added_inline, inlines_classes)
self.assertIn(added_fieldset, admin.get_fieldsets(request))
self.assertIn(added_fieldset, admin.get_fieldsets(request))
def test_medias_method_with_default_settings(self):
"""
Tests that the media method is retrning the proper static files when settings.TABBED_ADMIN_USE_JQUERY_UI
is True or False.
"""
self.assertEqual(False, USE_JQUERY_UI)
admin = BandAdmin(Band, self.site)
medias = admin.media
self.assertEqual({}, medias._css)
for js in medias._js:
self.assertNotIn(js, 'tabbed_admin')
def test_medias_method_with_grappelli(self):
"""
Tests if the right css ile is triggered when grappelli is installed.
"""
try:
import grappelli
except ImportError:
return
settings.INSTALLED_APPS += ('grappelli', )
self.assertIn('grappelli', settings.INSTALLED_APPS)
admin = BandAdmin(Band, self.site)
medias = admin.media
self.assertTrue(len(medias._css) > 0)
self.assertIn('all', medias._css)
self.assertTrue(len(medias._css['all']) == 1)
self.assertIn('grappelli', medias._css['all'][0])
class TabbedAdminTagsTest(TestCase):
def setUp(self):
self.site = AdminSite()
self.admin = BandAdmin(Band, self.site)
self.req = request.get('/admin/tabbed_admin/tab/')
self.req.user = request.user
self.view = self.admin.add_view(self.req)
self.context = Context(self.view)
self.context.push()
self.context['adminform'] = self.view.context_data['adminform']
self.context['request'] = self.req
self.context['inline_admin_formsets'] = self.view.context_data['inline_admin_formsets']
def test_request_not_in_context_raising_improperly_configured(self):
"""
Tests if an exception is thrown when no request is passed.
"""
from django.core.exceptions import ImproperlyConfigured
context = self.context
del context['request']
self.assertRaises(ImproperlyConfigured, render_tab_fieldsets_inlines, self.context, [])
def test_fieldset_passed_returns_fieldset_templated(self):
"""
Tests if the fieldset html is correctly generated when a fieldset is passed
"""
fieldset = self.view.context_data['tabs']['fields'][0]['entries'][0]
self.assertEqual('fieldset', fieldset['type'])
#tag = render_tab_fieldsets_inlines(self.context, fieldset)
#self.assertIn('fieldset', tag)
def test_inline_passed_returns_inline_templated(self):
"""
Tests if an inline html is correctly generated when an inline is passed.
"""
inline = self.view.context_data['tabs']['fields'][0]['entries'][1]
self.assertEqual('inline', inline['type'])
#tag = render_tab_fieldsets_inlines(self.context, inline)
#self.assertIn('inline', tag)
def test_wrong_inline_key_returns_nothing(self):
"""
Tests if a worng inline naming returns nothing.
"""
inline = self.view.context_data['tabs']['fields'][0]['entries'][1]
self.assertEqual('inline', inline['type'])
inline['name'] = 'Not exists'
tag = render_tab_fieldsets_inlines(self.context, inline)
self.assertEqual('', tag)

View File

@ -0,0 +1,6 @@
from django.conf.urls import include, url
from django.contrib import admin
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
]

View File

@ -1233,6 +1233,21 @@ select {
padding-bottom: 7px;
}
.tpage-nav ul li:hover {
font-weight: 700;
-webkit-text-decoration-line: underline;
text-decoration-line: underline;
}
.tpage-nav ul li.active {
-webkit-text-decoration-line: none;
text-decoration-line: none;
}
.tpage-nav ul li.active a {
font-weight: 600;
}
.sr-only {
position: absolute;
width: 1px;
@ -1406,6 +1421,10 @@ select {
resize: both;
}
.flex-row {
flex-direction: row;
}
.flex-col {
flex-direction: column;
}
@ -1505,6 +1524,11 @@ select {
background-color: rgb(250 250 249 / var(--tw-bg-opacity));
}
.bg-green-100 {
--tw-bg-opacity: 1;
background-color: rgb(220 252 231 / var(--tw-bg-opacity));
}
.bg-gray-50 {
--tw-bg-opacity: 1;
background-color: rgb(249 250 251 / var(--tw-bg-opacity));
@ -1795,19 +1819,6 @@ h4 {
font-size: 1.25em;
}
#atu {
transition: all 0.2s ease, visibility 0s;
border-radius: 0px;
background: repeat padding-box border-box 0% 0% / auto auto scroll
linear-gradient(180deg, rgba(8, 50, 4, 0.5) 0%, rgba(8, 50, 4, 0.5) 100%),
no-repeat padding-box border-box 78% 59%/200% scroll url("/static/atu.jpg");
display: block;
}
.target\:block:target {
display: block;
}
.active\:ring-2:active {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
@ -1818,6 +1829,10 @@ h4 {
display: block;
}
.prose-h1\:font-medium :is(:where(h1):not(:where([class~="not-prose"] *))) {
font-weight: 500;
}
.prose-h2\:mb-2 :is(:where(h2):not(:where([class~="not-prose"] *))) {
margin-bottom: 0.5rem;
}

View File

@ -48,6 +48,17 @@ h4 {
}
}
}
.tpage-nav ul li {
@apply hover:font-bold hover:underline;
&.active {
@apply no-underline;
a {
@apply font-semibold;
}
}
}
}
@layer utilities {
@ -77,12 +88,3 @@ h4 {
}
}
}
#atu {
transition: all 0.2s ease, visibility 0s;
border-radius: 0px;
background: repeat padding-box border-box 0% 0% / auto auto scroll
linear-gradient(180deg, rgba(8, 50, 4, 0.5) 0%, rgba(8, 50, 4, 0.5) 100%),
no-repeat padding-box border-box 78% 59%/200% scroll url("/static/atu.jpg");
display: block;
}

View File

View File

@ -0,0 +1,72 @@
from django.contrib import admin
from tabbed_admin import TabbedModelAdmin
from .models import TournamentPage
# Register your models here.
@admin.register(TournamentPage)
class BandAdmin(TabbedModelAdmin):
model = TournamentPage
list_display = ['name', 'link']
tab_general = (
(None, {
'fields': ('name', 'published', 'header', 'footer')
}),
)
tab_homepage = (
(None, {
'fields': ('homepage',)
}),
)
tab_schedule_and_results = (
(None, {
'fields': ('schedule_and_results', 'schedule_and_results_enabled')
}),
)
tab_registration = (
(None, {
'fields': ('registration', 'registration_enabled')
}),
)
tab_rules = (
(None, {
'fields': ('rules', 'rules_enabled')
}),
)
tab_fee_and_prizes = (
(None, {
'fields': ('fee_and_prizes', 'fee_and_prizes_enabled')
}),
)
tab_accomodation = (
(None, {
'fields': ('accomodation', 'accomodation_enabled')
}),
)
tab_contact = (
(None, {
'fields': ('contact', 'contact_enabled')
}),
)
tabs = [
('Ogólne ustawienia', tab_general),
('Strona główna', tab_homepage),
('Harmonogram i wyniki', tab_schedule_and_results),
('Rejestracja', tab_registration),
('Regulamin', tab_rules),
('Wpisowe i nagrody', tab_fee_and_prizes),
('Noclegi', tab_accomodation),
('Kontakt', tab_contact),
]

View File

@ -4,3 +4,4 @@ from django.apps import AppConfig
class TournamentpagesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'tournamentpages'
verbose_name = 'Strony turniejów'

View File

@ -0,0 +1,42 @@
# Generated by Django 4.0.5 on 2022-10-05 18:29
from django.db import migrations, models
import tinymce.models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='TournamentPage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=250, verbose_name='Nazwa')),
('published', models.BooleanField(default=False, verbose_name='Turniej opublikowany')),
('header', tinymce.models.HTMLField(blank=True, default='', verbose_name='Nagłówek')),
('footer', tinymce.models.HTMLField(blank=True, default='', verbose_name='Stopka')),
('homepage', tinymce.models.HTMLField(blank=True, default='', verbose_name='Strona główna')),
('schedule_and_results', tinymce.models.HTMLField(blank=True, default='', verbose_name='Harmonogram i wyniki')),
('schedule_and_results_enabled', models.BooleanField(default=False, verbose_name='Strona włączona')),
('registration', tinymce.models.HTMLField(blank=True, default='', verbose_name='Rejestracja')),
('registration_enabled', models.BooleanField(default=False, verbose_name='Strona włączona')),
('rules', tinymce.models.HTMLField(blank=True, default='', verbose_name='Regulamin')),
('rules_enabled', models.BooleanField(default=False, verbose_name='Strona włączona')),
('fee_and_prizes', tinymce.models.HTMLField(blank=True, default='', verbose_name='Wpisowe i nagrody')),
('fee_and_prizes_enabled', models.BooleanField(default=False, verbose_name='Strona włączona')),
('accomodation', tinymce.models.HTMLField(blank=True, default='', verbose_name='Noclegi')),
('accomodation_enabled', models.BooleanField(default=False, verbose_name='Strona włączona')),
('contact', tinymce.models.HTMLField(blank=True, default='', verbose_name='Kontakt')),
('contact_enabled', models.BooleanField(default=False, verbose_name='Strona włączona')),
],
options={
'verbose_name': 'Strona turnieju',
'verbose_name_plural': 'Strony turniejów',
},
),
]

View File

@ -0,0 +1,52 @@
from django.db import models
from django.urls.base import reverse_lazy
from django.utils.safestring import mark_safe
from tinymce.models import HTMLField
# Create your models here.
class TournamentPage(models.Model):
name = models.CharField('Nazwa', max_length=250)
published = models.BooleanField('Turniej opublikowany', default=False)
header = HTMLField('Nagłówek', default='', blank=True)
footer = HTMLField('Stopka', default='', blank=True)
homepage = HTMLField('Strona główna', default='', blank=True)
schedule_and_results = HTMLField(
'Harmonogram i wyniki', default='', blank=True)
schedule_and_results_enabled = models.BooleanField(
'Strona włączona', default=False)
registration = HTMLField('Rejestracja', default='', blank=True)
registration_enabled = models.BooleanField(
'Strona włączona', default=False)
rules = HTMLField('Regulamin', default='', blank=True)
rules_enabled = models.BooleanField('Strona włączona', default=False)
fee_and_prizes = HTMLField('Wpisowe i nagrody', default='', blank=True)
fee_and_prizes_enabled = models.BooleanField(
'Strona włączona', default=False)
accomodation = HTMLField('Noclegi', default='', blank=True)
accomodation_enabled = models.BooleanField(
'Strona włączona', default=False)
contact = HTMLField('Kontakt', default='', blank=True)
contact_enabled = models.BooleanField('Strona włączona', default=False)
class Meta:
verbose_name = 'Strona turnieju'
verbose_name_plural = 'Strony turniejów'
@property
def link(self):
href = reverse_lazy('homepage', args=[self.id])
return mark_safe(f'<a href="{href}" target="_blank">{href}</a>')
def __str__(self):
return self.name

View File

@ -0,0 +1,76 @@
<!DOCTYPE html>
{% load static tailwind_tags %}
<html lang="pl" dir="ltr">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Podlaski Związek Brydża Sportowego" />
<meta name="author" content="Mikołaj Kubiczek" />
<link rel="icon" href="{% static 'favicon.ico' %}" type="image/x-icon" />
<link
rel="icon"
type="image/png"
sizes="192x192"
href="{% static 'icon-192x192.png' %}"
/>
<link
rel="icon"
type="image/png"
sizes="512x512"
href="{% static 'icon-512x512.png' %}"
/>
<title>{% block title %}{{ t.name }}{% endblock %}</title>
<base href="/" />
{% tailwind_css %}
</head>
<body class="antialiased flex flex-col gap-4 items-center prose max-w-full">
<header>{{ t.header | safe}}</header>
<nav class="not-prose bg-green-100 p-2 tpage-nav">
<ul class="flex flex-row flex-wrap gap-4 text-xl xl:max-w-screen-xl">
<li class="{% if homepage %}active{% endif %}">
<a href="{% url 'homepage' t.id %}">Strona główna</a>
</li>
{% if t.schedule_and_results_enabled %}
<li class="{% if schedule_and_results %}active{% endif %}">
<a href="{% url 'schedule_and_results' t.id %}"
>Harmonogram i wyniki</a
>
</li>
{% endif %} {% if t.registration_enabled %}
<li class="{% if registration %}active{% endif %}">
<a href="{% url 'registration' t.id %}">Rejestracja</a>
</li>
{% endif %} {% if t.rules_enabled %}
<li class="{% if rules %}active{% endif %}">
<a href="{% url 'rules' t.id %}">Regulamin</a>
</li>
{% endif %} {% if t.fee_and_prizes_enabled %}
<li class="{% if fee_and_prizes %}active{% endif %}">
<a href="{% url 'fee_and_prizes' t.id %}">Wpisowe i nagrody</a>
</li>
{% endif %} {% if t.accomodation_enabled %}
<li class="{% if accomodation %}active{% endif %}">
<a href="{% url 'accomodation' t.id %}">Noclegi</a>
</li>
{% endif %} {% if t.contact_enabled %}
<li class="{% if contact %}active{% endif %}">
<a href="{% url 'contact' t.id %}">Kontakt</a>
</li>
{% endif %}
</ul>
</nav>
<main
class="container xl:max-w-screen-xl mx-auto p-4 prose-h1:font-medium prose-img:my-1 prose-p:my-[0.3em]"
>
{{ content | safe }}
</main>
<footer>{{ t.footer | safe}}</footer>
</body>
</html>

View File

@ -0,0 +1,14 @@
from django.urls import path
from .views import *
urlpatterns = [
path('<int:id>', HomeView.as_view(), name='homepage'),
path('<int:id>/wyniki', ScheduleAndResultsView.as_view(),
name='schedule_and_results'),
path('<int:id>/rejestracja', RegistrationView.as_view(), name='registration'),
path('<int:id>/regulamin', RulesView.as_view(), name='rules'),
path('<int:id>/nagrody', FeeAndPrizesView.as_view(), name='fee_and_prizes'),
path('<int:id>/noclegi', AccomodationView.as_view(), name='accomodation'),
path('<int:id>/kontakt', ContactView.as_view(), name='contact'),
]

View File

@ -0,0 +1,67 @@
from django.shortcuts import render
from django.views.generic import TemplateView
from .models import TournamentPage
# Create your views here.
class HomeView(TemplateView):
template_name = "tournament.html"
def get_context_data(self, id, **kwargs):
t = TournamentPage.objects.get(id=id, published=True)
return {'t': t, 'content': t.homepage, 'homepage': True}
class ScheduleAndResultsView(TemplateView):
template_name = "tournament.html"
def get_context_data(self, id, **kwargs):
t = TournamentPage.objects.get(
id=id, published=True, schedule_and_results_enabled=True)
return {'t': t, 'content': t.schedule_and_results, 'schedule_and_results': True}
class RegistrationView(TemplateView):
template_name = "tournament.html"
def get_context_data(self, id, **kwargs):
t = TournamentPage.objects.get(
id=id, published=True, registration_enabled=True)
return {'t': t, 'content': t.registration, 'registration': True}
class RulesView(TemplateView):
template_name = "tournament.html"
def get_context_data(self, id, **kwargs):
t = TournamentPage.objects.get(
id=id, published=True, rules_enabled=True)
return {'t': t, 'content': t.rules, 'rules': True}
class FeeAndPrizesView(TemplateView):
template_name = "tournament.html"
def get_context_data(self, id, **kwargs):
t = TournamentPage.objects.get(
id=id, published=True, fee_and_prizes_enabled=True)
return {'t': t, 'content': t.fee_and_prizes, 'fee_and_prizes': True}
class AccomodationView(TemplateView):
template_name = "tournament.html"
def get_context_data(self, id, **kwargs):
t = TournamentPage.objects.get(
id=id, published=True, accomodation_enabled=True)
return {'t': t, 'content': t.accomodation, 'accomodation': True}
class ContactView(TemplateView):
template_name = "tournament.html"
def get_context_data(self, id, **kwargs):
t = TournamentPage.objects.get(
id=id, published=True, contact_enabled=True)
return {'t': t, 'content': t.contact, 'contact': True}