Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • konstantin/akplanning
  • matedealer/akplanning
  • kif/akplanning
  • mirco/akplanning
  • lordofthevoid/akplanning
  • voidptr/akplanning
  • xayomer/akplanning-fork
  • mollux/akplanning
  • neumantm/akplanning
  • mmarx/akplanning
  • nerf/akplanning
  • felix_bonn/akplanning
  • sebastian.uschmann/akplanning
13 results
Select Git revision
Show changes
Commits on Source (11)
Showing with 219 additions and 30 deletions
......@@ -20,6 +20,10 @@ before_script:
- apt-get -qq install -y python3-virtualenv python3 python3-dev python3-pip gettext default-mysql-client default-libmysqlclient-dev
- export DJANGO_SETTINGS_MODULE=AKPlanning.settings_ci
- ./Utils/setup.sh --prod
- mkdir -p public/badges public/lint
- echo undefined > public/badges/$CI_JOB_NAME.score
- source venv/bin/activate
- pip install pylint-gitlab pylint-django
- mysql --version
check:
......@@ -48,3 +52,32 @@ test:
coverage_format: cobertura
path: coverage.xml
junit: unit.xml
lint:
stage: test
script:
- pylint --load-plugins pylint_django --exit-zero --output-format=text $(find -type f -name "*.py" ! -path "**/venv/**") | tee /tmp/pylint.txt
- sed -n 's/^Your code has been rated at \([-0-9.]*\)\/.*/\1/p' /tmp/pylint.txt > public/badges/$CI_JOB_NAME.score
- pylint --load-plugins pylint_django --exit-zero --output-format=pylint_gitlab.GitlabCodeClimateReporter $(find -type f -name "*.py" ! -path "**/venv/**") > codeclimate.json
- pylint --load-plugins pylint_django --exit-zero --output-format=pylint_gitlab.GitlabPagesHtmlReporter $(find -type f -name "*.py" ! -path "**/venv/**") > public/lint/index.html
after_script:
- |
echo "Linting score: $(cat public/badges/$CI_JOB_NAME.score)"
artifacts:
paths:
- public
reports:
codequality: codeclimate.json
when: always
pages:
stage: deploy
image: alpine:latest
script:
- echo
artifacts:
paths:
- public
only:
refs:
- main
......@@ -4,6 +4,9 @@ from AKDashboard.models import DashboardButton
@admin.register(DashboardButton)
class DashboardButtonAdmin(admin.ModelAdmin):
"""
Admin interface for dashboard buttons
"""
list_display = ['text', 'url', 'event']
list_filter = ['event']
search_fields = ['text', 'url']
......
......@@ -2,4 +2,7 @@ from django.apps import AppConfig
class AkdashboardConfig(AppConfig):
"""
App configuration for dashboard (default)
"""
name = 'AKDashboard'
......@@ -6,6 +6,17 @@ from AKModel.models import Event
class DashboardButton(models.Model):
"""
Model for a single dashboard button
Allows to specify
* a text (currently without possibility to translate),
* a color (based on predefined design colors)
* a url the button should point to (internal or external)
* an icon (from the collection of fontawesome)
Each button is associated with a single event and will be deleted when the event is deleted.
"""
class Meta:
verbose_name = _("Dashboard Button")
verbose_name_plural = _("Dashboard Buttons")
......
from django.apps import apps
from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.generic import TemplateView, DetailView
......@@ -10,6 +9,11 @@ from AKPlanning import settings
class DashboardView(TemplateView):
"""
Index view of dashboard and therefore the main entry point for AKPlanning
Displays information and buttons for all public events
"""
template_name = 'AKDashboard/dashboard.html'
@method_decorator(ensure_csrf_cookie)
......@@ -23,6 +27,14 @@ class DashboardView(TemplateView):
class DashboardEventView(DetailView):
"""
Dashboard view for a single event
In addition to the basic information and the buttons,
an overview over recent events (new and changed AKs, moved AKSlots) for the given event is shown.
The event dashboard also exists for non-public events (one only needs to know the URL/slug of the event).
"""
template_name = 'AKDashboard/dashboard_event.html'
context_object_name = 'event'
model = Event
......@@ -32,11 +44,16 @@ class DashboardEventView(DetailView):
# Show feed of recent changes (if activated)
if settings.DASHBOARD_SHOW_RECENT:
# Create a list of chronically sorted events (both AK and plan changes):
recent_changes = []
# Newest AKs
# Newest AKs (if AKSubmission is used)
if apps.is_installed("AKSubmission"):
submission_changes = AK.history.filter(event=context['event'])[:int(settings.DASHBOARD_RECENT_MAX)]
# Get the latest x changes (if there are that many),
# where x corresponds to the entry threshold configured in the settings
# (such that the list will be completely filled even if there are no (newer) plan changes)
submission_changes = AK.history.filter(event=context['event'])[:int(settings.DASHBOARD_RECENT_MAX)] # pylint: disable=no-member, line-too-long
# Create textual representation including icons
for s in submission_changes:
if s.history_type == '+':
text = _('New AK: %(ak)s.') % {'ak': s.name}
......@@ -48,18 +65,21 @@ class DashboardEventView(DetailView):
text = _('AK "%(ak)s" deleted.') % {'ak': s.name}
icon = ('times', 'fas')
recent_changes.append({'icon': icon, 'text': text, 'link': s.instance.detail_url, 'timestamp': s.history_date})
# Changes in plan
if apps.is_installed("AKPlan"):
if not context['event'].plan_hidden:
last_changed_slots = AKSlot.objects.select_related('ak').filter(event=context['event'], start__isnull=False).order_by('-updated')[
:int(settings.DASHBOARD_RECENT_MAX)]
for changed_slot in last_changed_slots:
recent_changes.append({'icon': ('clock', 'far'),
'text': _('AK "%(ak)s" (re-)scheduled.') % {'ak': changed_slot.ak.name},
'link': changed_slot.ak.detail_url,
'timestamp': changed_slot.updated})
# Store representation in change list (still unsorted)
recent_changes.append(
{'icon': icon, 'text': text, 'link': s.instance.detail_url, 'timestamp': s.history_date}
)
# Changes in plan (if AKPlan is used and plan is publicly visible)
if apps.is_installed("AKPlan") and not context['event'].plan_hidden:
# Get the latest plan changes (again using a threshold, see above)
last_changed_slots = AKSlot.objects.select_related('ak').filter(event=context['event'], start__isnull=False).order_by('-updated')[:int(settings.DASHBOARD_RECENT_MAX)] #pylint: disable=line-too-long
for changed_slot in last_changed_slots:
# Create textual representation including icons and links and store in list (still unsorted)
recent_changes.append({'icon': ('clock', 'far'),
'text': _('AK "%(ak)s" (re-)scheduled.') % {'ak': changed_slot.ak.name},
'link': changed_slot.ak.detail_url,
'timestamp': changed_slot.updated})
# Sort by change date...
recent_changes.sort(key=lambda x: x['timestamp'], reverse=True)
......
......@@ -5,6 +5,9 @@ from AKOnline.models import VirtualRoom
@admin.register(VirtualRoom)
class VirtualRoomAdmin(admin.ModelAdmin):
"""
Admin interface for virtual room model
"""
model = VirtualRoom
list_display = ['room', 'event', 'url']
list_filter = ['room__event']
......
......@@ -2,4 +2,7 @@ from django.apps import AppConfig
class AkonlineConfig(AppConfig):
"""
App configuration (default -- only to set the app name)
"""
name = 'AKOnline'
......@@ -6,23 +6,38 @@ from AKOnline.models import VirtualRoom
class VirtualRoomForm(ModelForm):
"""
Form to create a virtual room
Should be used as part of a multi form (see :class:`RoomWithVirtualForm` below)
"""
class Meta:
model = VirtualRoom
exclude = ['room']
# Show all fields except for room
exclude = ['room'] #pylint: disable=modelform-uses-exclude
def __init__(self, *args, **kwargs):
super(VirtualRoomForm, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
# Make the URL field optional to allow submitting the multi form without creating a virtual room
self.fields['url'].required = False
class RoomWithVirtualForm(MultiModelForm):
"""
Combined form to create rooms and optionally virtual rooms
Multi-Form that combines a :class:`RoomForm` (from AKModel) and a :class:`VirtualRoomForm` (see above).
The form will always create a room on valid input
and may additionally create a virtual room if the url field of the virtual room form part is set.
"""
form_classes = {
'room': RoomForm,
'virtual': VirtualRoomForm
}
def save(self, commit=True):
objects = super(RoomWithVirtualForm, self).save(commit=False)
objects = super().save(commit=False)
if commit:
room = objects['room']
......
from django.db import models
from django.utils.translation import gettext_lazy as _
from AKModel.models import Event, Room
from AKModel.models import Room
class VirtualRoom(models.Model):
......@@ -18,6 +18,12 @@ class VirtualRoom(models.Model):
@property
def event(self):
"""
Property: Event this virtual room belongs to.
:return: Event this virtual room belongs to
:rtype: Event
"""
return self.room.event
def __str__(self):
......
......@@ -9,20 +9,31 @@ from AKOnline.forms import RoomWithVirtualForm
class RoomCreationWithVirtualView(RoomCreationView):
"""
View to create both rooms and optionally virtual rooms by filling one form
"""
form_class = RoomWithVirtualForm
template_name = 'admin/AKOnline/room_create_with_virtual.html'
room = None
def form_valid(self, form):
# This will create the room and additionally a virtual room if the url field is not blank
# objects['room'] will always a room instance afterwards, objects['virtual'] may be empty
objects = form.save()
self.room = objects['room']
messages.success(self.request, _("Created Room '%(room)s'" % {'room': objects['room']}))
# Create a (translated) success message containing information about the created room
messages.success(self.request, _("Created Room '%(room)s'" % {'room': objects['room']})) #pylint: disable=consider-using-f-string, line-too-long
if objects['virtual'] is not None:
messages.success(self.request, _("Created related Virtual Room '%(vroom)s'" % {'vroom': objects['virtual']}))
# Create a (translated) success message containing information about the created virtual room
messages.success(self.request, _("Created related Virtual Room '%(vroom)s'" % {'vroom': objects['virtual']})) #pylint: disable=consider-using-f-string, line-too-long
return HttpResponseRedirect(self.get_success_url())
@status_manager.register(name="event_virtual_rooms")
class EventVirtualRoomsWidget(TemplateStatusWidget):
"""
Status page widget to contain information about all virtual rooms belonging to the given event
"""
required_context_type = "event"
title = _("Virtual Rooms")
template_name = "admin/AKOnline/status/event_virtual_rooms.html"
......@@ -2,4 +2,7 @@ from django.apps import AppConfig
class AkplanConfig(AppConfig):
"""
App configuration (default, only specifies name of the app)
"""
name = 'AKPlan'
# gradients based on http://bsou.io/posts/color-gradients-with-python
def hex_to_rgb(hex):
def hex_to_rgb(hex): #pylint: disable=redefined-builtin
"""
Convert hex color to RGB color code
:param hex: hex encoded color
......@@ -23,8 +23,7 @@ def rgb_to_hex(rgb):
"""
# Components need to be integers for hex to make sense
rgb = [int(x) for x in rgb]
return "#"+"".join(["0{0:x}".format(v) if v < 16 else
"{0:x}".format(v) for v in rgb])
return "#"+"".join([f"0{v:x}" if v < 16 else f"{v:x}" for v in rgb])
def linear_blend(start_hex, end_hex, position):
......
......@@ -11,6 +11,14 @@ register = template.Library()
@register.filter
def highlight_change_colors(akslot):
"""
Adjust color to highlight recent changes if needed
:param akslot: akslot to determine color for
:type akslot: AKSlot
:return: color that should be used (either default color of the category or some kind of red)
:rtype: str
"""
# Do not highlight in preview mode or when changes occurred before the plan was published
if akslot.event.plan_hidden or (akslot.event.plan_published_at is not None
and akslot.event.plan_published_at > akslot.updated):
......@@ -25,9 +33,14 @@ def highlight_change_colors(akslot):
# Recent change? Calculate gradient blend between red and
recentness = seconds_since_update / settings.PLAN_MAX_HIGHLIGHT_UPDATE_SECONDS
return darken("#b71540", recentness)
# return linear_blend("#b71540", "#000000", recentness)
@register.simple_tag
def timestamp_now(tz):
"""
Get the current timestamp for the given timezone
:param tz: timezone to be used for the timestamp
:return: current timestamp in given timezone
"""
return date_format(datetime.now().astimezone(tz), "c")
from datetime import timedelta
from django.conf import settings
from django.shortcuts import redirect
from django.urls import reverse_lazy
......@@ -11,6 +9,11 @@ from AKModel.metaviews.admin import FilterByEventSlugMixin
class PlanIndexView(FilterByEventSlugMixin, ListView):
"""
Default plan view
Shows two lists of current and upcoming AKs and a graphical full plan below
"""
model = AKSlot
template_name = "AKPlan/plan_index.html"
context_object_name = "akslots"
......@@ -60,6 +63,15 @@ class PlanIndexView(FilterByEventSlugMixin, ListView):
class PlanScreenView(PlanIndexView):
"""
Plan view optimized for screens and projectors
This again shows current and upcoming AKs as well as a graphical plan,
but no navigation elements and trys to use the available space as best as possible
such that no scrolling is needed.
The view contains a frontend functionality for auto-reload.
"""
template_name = "AKPlan/plan_wall.html"
def get(self, request, *args, **kwargs):
......@@ -92,22 +104,34 @@ class PlanScreenView(PlanIndexView):
class PlanRoomView(FilterByEventSlugMixin, DetailView):
"""
Plan view for a single room
"""
template_name = "AKPlan/plan_room.html"
model = Room
context_object_name = "room"
def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(object_list=object_list, **kwargs)
# Restrict AKSlot list to the given room
# while joining AK, room and category information to reduce the amount of necessary SQL queries
context["slots"] = AKSlot.objects.filter(room=context['room']).select_related('ak', 'ak__category', 'ak__track')
return context
class PlanTrackView(FilterByEventSlugMixin, DetailView):
"""
Plan view for a single track
"""
template_name = "AKPlan/plan_track.html"
model = AKTrack
context_object_name = "track"
def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(object_list=object_list, **kwargs)
context["slots"] = AKSlot.objects.filter(event=self.event, ak__track=context['track']).select_related('ak', 'room', 'ak__category')
# Restrict AKSlot list to given track
# while joining AK, room and category information to reduce the amount of necessary SQL queries
context["slots"] = AKSlot.objects.\
filter(event=self.event, ak__track=context['track']).\
select_related('ak', 'room', 'ak__category')
return context
......@@ -55,6 +55,7 @@ INSTALLED_APPS = [
'bootstrap_datepicker_plus',
'django_tex',
'compressor',
'docs',
]
MIDDLEWARE = [
......@@ -234,4 +235,9 @@ CSP_FONT_SRC = ("'self'", "data:", "fonts.gstatic.com")
SEND_MAILS = True
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# Documentation
DOCS_ROOT = os.path.join(BASE_DIR, 'docs/_build/html')
DOCS_ACCESS = 'public'
include(optional("settings/*.py"))
"""AKPlanning URL Configuration
"""
AKPlanning URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/2.2/topics/http/urls/
......@@ -13,13 +14,15 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
import debug_toolbar
from django.apps import apps
from django.contrib import admin
from django.urls import path, include
from django.urls import path, include, re_path
urlpatterns = [
path('admin/', admin.site.urls),
re_path(r'^docs/', include('docs.urls')),
path('accounts/', include('django.contrib.auth.urls')),
path('accounts/', include('registration.backends.simple.urls')),
path('', include('AKModel.urls', namespace='model')),
......
......@@ -26,4 +26,9 @@ python manage.py compilemessages -l de_DE
# Credentials are entered interactively on CLI
python manage.py createsuperuser
# Generate documentation
cd docs
./make html
cd ..
deactivate
......@@ -27,4 +27,10 @@ pip install --upgrade -r requirements.txt
./manage.py collectstatic --noinput
./manage.py compilemessages -l de_DE
# Update documentation
cd docs
./make html
cd ..
touch AKPlanning/wsgi.py
_build/
code/
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)