diff --git a/.gitmodules b/.gitmodules index 1b35dd8036..3b8902a835 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "src/tixlbase/static/bootstrap"] path = src/tixlbase/static/bootstrap url = https://github.com/twbs/bootstrap.git +[submodule "src/tixlbase/static/fontawesome"] + path = src/tixlbase/static/fontawesome + url = https://github.com/FortAwesome/Font-Awesome.git diff --git a/doc/development/concepts.rst b/doc/development/concepts.rst index ad89552d7b..0b8166573b 100644 --- a/doc/development/concepts.rst +++ b/doc/development/concepts.rst @@ -4,9 +4,12 @@ Implementation Concepts Basic terminology ----------------- -Tixl is a sofware selling **Items**, an abstract thing which is related to an **Event**. Every Event is managed by the **Organizer**, who runs the event. +Users and events +^^^^^^^^^^^^^^^^ -Tixl know two types of **Users**: +Tixl is all about **events**, which are defined as something happening somewhere. Every Event is managed by the **organizer**, an abstract entity running the event. + +Tixl is used by **users**, of which it knows two types: **Local users** Local users do only exist inside the scope of one event. They are identified by usernames, which are only valid for exactly one event. @@ -16,3 +19,31 @@ Tixl know two types of **Users**: For more information about this user concept and reasons behind it, see the docstring of the ``tixlbase.models.User`` class. +Items and flavors +^^^^^^^^^^^^^^^^^ + +The purpose of tixl is to sell **items** (which belong to **events**) to **users**. An **item** is a abstract thing, popular examples being event tickets or a piece of merchandise, like 'T-Shirt'. An **item** can have multiple **properties** with multiple **values** each. For example, the **item** 'T-Shirt' could have the **property** 'Size' with **values** 'S', 'M' and 'L' and the **property** 'Color' with **values** 'black' and 'blue'. + +Any combination of those **values** is called a **flavor**. Using the examples from above, a possible **flavor** would be 'T-Shirt S blue'. + +Restrictions +^^^^^^^^^^^^ + +The probably most powerful concepts of tixl is the very abstract concept of **restricitons**. We already know that **items** can come in very different **flavors**, but a **restriction** decides whether an item is available for sale and assign **prices** to **flavors**. There are **restriction types**, which are pieces of code implementing the restrictions and **restriction instances**, which are configurations made by the **organzier**. Although **restrictions** are a very abstract concept which can be used to do nearly anything, there are a few obvious examples: + +* One easy example is the time restriction, which allows the sale of certain item flavors only within a certain time frame. As restrictions can also assign a price to a flavor, this can also be used to implement something like 'early-bird prices' for your tickets by using multiple time restrictions with different prices. +* The most obvious example is the number restriction, which limits the sale of the tickets to a maximum number. You can use this either to stop selling tickets completely when your house is full or for creating limited 'VIP tickets'. +* A more advanced example is a restriction by user, for example reduced ticket prices for members who are members of a special group. +* Arbitrary sophisticated features like coupon codes are also possible to be implemented using this feature. + +Any number of **restrictions** can be applied to the whole of a **item** or to a specific **flavor**. The processing of the restriction follows the following set of rules: + +* **Flavor**-specific rules have precedence over **item**-specific rules. +* The restrictions are being processed in random order (there may not be any assumptions about the evaluation order). +* Multiple restriction instances of **different restriction types** are linked with *and*, so if both a time frame and a number restriction are applied to an item, the item is only avaliable for sale within the given time frame *and* only as long as items are available. +* Multiple restriction instances of the **same restriction type** are linked with *or*, so if two time frames are applied to an item, the item is available for sale in both of the time frames. (This behaviour is actually a decision of the restriction type itself, so this rule is not enforced but rather a general rule of thumb). +* If multiple restrictions apply which set the price, the *cheapest* price determines the final price. + +Restriction types can be introduced by 3rd-party code and do not require changes to the tixl codebase. + +.. note:: This pluggability of restrictions is implemented using the 'signal and receiver' pattern provided by Django. Restrictions can therefore live in seperate Django apps. diff --git a/src/helpers/__init__.py b/src/helpers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/helpers/lessabsolutefilter.py b/src/helpers/lessabsolutefilter.py new file mode 100644 index 0000000000..8554419aac --- /dev/null +++ b/src/helpers/lessabsolutefilter.py @@ -0,0 +1,11 @@ +from compressor.filters.base import CompilerFilter +from compressor.filters.css_default import CssAbsoluteFilter + + +class LessFilter(CompilerFilter): + def __init__(self, content, attrs, **kwargs): + super(LessFilter, self).__init__(content, command='lessc {infile} {outfile}', **kwargs) + + def input(self, **kwargs): + content = super(LessFilter, self).input(**kwargs) + return CssAbsoluteFilter(content).input(**kwargs) diff --git a/src/locale/de/LC_MESSAGES/django.po b/src/locale/de/LC_MESSAGES/django.po index 7f3cf9626f..d47c1daeb2 100644 --- a/src/locale/de/LC_MESSAGES/django.po +++ b/src/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: 1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2014-09-11 11:16+0200\n" +"POT-Creation-Date: 2014-09-11 21:09+0200\n" "PO-Revision-Date: 2014-09-11 11:05+200\n" "Last-Translator: Raphael Michel \n" "Language-Team: Raphael Michel \n" @@ -18,11 +18,11 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: tixl/settings.py:92 +#: tixl/settings.py:104 msgid "German" msgstr "Deutsch" -#: tixl/settings.py:93 +#: tixl/settings.py:105 msgid "English" msgstr "Englisch" @@ -30,7 +30,7 @@ msgstr "Englisch" msgid "The two password fields didn't match." msgstr "Die beiden eingegebenen Passwörter stimmen nicht überein." -#: tixlbase/admin.py:18 tixlcontrol/views/auth.py:14 +#: tixlbase/admin.py:18 tixlcontrol/views/auth.py:15 msgid "Password" msgstr "Passwort" @@ -70,15 +70,27 @@ msgstr "Nachname" msgid "Log in" msgstr "Anmelden" -#: tixlcontrol/views/auth.py:13 +#: tixlcontrol/templates/tixlcontrol/base.html:17 +msgid "Toggle navigation" +msgstr "" + +#: tixlcontrol/templates/tixlcontrol/base.html:26 +msgid "Dashboard" +msgstr "Übersicht" + +#: tixlcontrol/templates/tixlcontrol/base.html:30 +msgid "Log out" +msgstr "Abmelden" + +#: tixlcontrol/views/auth.py:14 msgid "E-mail address" msgstr "E-Mail-Adresse" -#: tixlcontrol/views/auth.py:18 +#: tixlcontrol/views/auth.py:19 msgid "Please enter a correct e-mail address and password." msgstr "" "Bitte geben Sie eine gültige Kombination aus E-Mail-Adresse und Passwort ein." -#: tixlcontrol/views/auth.py:19 +#: tixlcontrol/views/auth.py:20 msgid "This account is inactive." msgstr "Dieses Konto ist deaktiviert." diff --git a/src/tixl/settings.py b/src/tixl/settings.py index 2301caf0d9..4926aa5a6d 100644 --- a/src/tixl/settings.py +++ b/src/tixl/settings.py @@ -55,6 +55,18 @@ MIDDLEWARE_CLASSES = ( 'tixlcontrol.middleware.LoginRequiredMiddleware', ) +TEMPLATE_CONTEXT_PROCESSORS = ( + "django.contrib.auth.context_processors.auth", + "django.core.context_processors.debug", + "django.core.context_processors.i18n", + "django.core.context_processors.media", + "django.core.context_processors.request", + "django.core.context_processors.static", + "django.core.context_processors.tz", + "django.contrib.messages.context_processors.messages", + 'tixlcontrol.context.contextprocessor', +) + ROOT_URLCONF = 'tixl.urls' WSGI_APPLICATION = 'tixl.wsgi.application' @@ -114,7 +126,7 @@ STATICFILES_FINDERS = ( ) COMPRESS_PRECOMPILERS = ( - ('text/less', 'lessc {infile} {outfile}'), + ('text/less', 'helpers.lessabsolutefilter.LessFilter'), ) COMPRESS_CSS_FILTERS = ( @@ -122,6 +134,10 @@ COMPRESS_CSS_FILTERS = ( 'compressor.filters.cssmin.CSSMinFilter', ) +# Tixl specific settings + +TIXL_INSTANCE_NAME = 'tixl.de' + try: from local_settings import * diff --git a/src/tixlbase/migrations/0004_auto_20140911_2037.py b/src/tixlbase/migrations/0004_auto_20140911_2037.py new file mode 100644 index 0000000000..be2571dd55 --- /dev/null +++ b/src/tixlbase/migrations/0004_auto_20140911_2037.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('tixlbase', '0003_auto_20140910_1649'), + ] + + operations = [ + migrations.CreateModel( + name='OrganizerPermission', + fields=[ + ('id', models.AutoField(auto_created=True, verbose_name='ID', serialize=False, primary_key=True)), + ('can_create_events', models.BooleanField(default=True)), + ('organizer', models.ForeignKey(to='tixlbase.Organizer', related_name='perms')), + ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='organizer_perms')), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.AlterUniqueTogether( + name='organizerpermission', + unique_together=set([('organizer', 'user')]), + ), + migrations.RemoveField( + model_name='organizer', + name='owner', + ), + migrations.AlterField( + model_name='event', + name='organizer', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='events', to='tixlbase.Organizer'), + ), + migrations.AlterField( + model_name='user', + name='email', + field=models.EmailField(null=True, blank=True, db_index=True, verbose_name='E-mail', max_length=75), + ), + migrations.AlterField( + model_name='user', + name='event', + field=models.ForeignKey(null=True, blank=True, on_delete=django.db.models.deletion.PROTECT, related_name='users', to='tixlbase.Event'), + ), + migrations.AlterField( + model_name='user', + name='familyname', + field=models.CharField(null=True, blank=True, verbose_name='Family name', max_length=255), + ), + migrations.AlterField( + model_name='user', + name='givenname', + field=models.CharField(null=True, blank=True, verbose_name='Given name', max_length=255), + ), + ] diff --git a/src/tixlbase/migrations/0005_auto_20140911_2052.py b/src/tixlbase/migrations/0005_auto_20140911_2052.py new file mode 100644 index 0000000000..e17580d85a --- /dev/null +++ b/src/tixlbase/migrations/0005_auto_20140911_2052.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('tixlbase', '0004_auto_20140911_2037'), + ] + + operations = [ + migrations.CreateModel( + name='EventPermission', + fields=[ + ('id', models.AutoField(auto_created=True, serialize=False, primary_key=True, verbose_name='ID')), + ('can_change_settings', models.BooleanField(default=True)), + ('organizer', models.ForeignKey(to='tixlbase.Event')), + ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='event_perms')), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.AlterUniqueTogether( + name='eventpermission', + unique_together=set([('organizer', 'user')]), + ), + migrations.AddField( + model_name='event', + name='permitted', + field=models.ManyToManyField(to=settings.AUTH_USER_MODEL, related_name='events', through='tixlbase.EventPermission'), + preserve_default=True, + ), + migrations.AddField( + model_name='organizer', + name='permitted', + field=models.ManyToManyField(to=settings.AUTH_USER_MODEL, related_name='organizers', through='tixlbase.OrganizerPermission'), + preserve_default=True, + ), + migrations.AlterField( + model_name='organizerpermission', + name='organizer', + field=models.ForeignKey(to='tixlbase.Organizer'), + ), + ] diff --git a/src/tixlbase/models.py b/src/tixlbase/models.py index 065b68844c..e5f770ef72 100644 --- a/src/tixlbase/models.py +++ b/src/tixlbase/models.py @@ -124,24 +124,35 @@ class User(AbstractBaseUser, PermissionsMixin): class Organizer(models.Model): """ - This model represents an entity organizing events, like a company, - an organization or a person. It has one user as owner (who has - registered it) and can have any number of users with admin - authorization. Any organizer has a unique slug, which is a short - name (alphanumeric, all lowercase) being used in URLs. + This model represents an entity organizing events, like a company. + Any organizer has a unique slug, which is a short name (alphanumeric, + all lowercase) being used in URLs. """ name = models.CharField(max_length=200) slug = models.CharField(max_length=50, - unique=True, - db_index=True) - owner = models.ForeignKey(User, null=True, blank=True, - on_delete=models.PROTECT) + unique=True, db_index=True) + permitted = models.ManyToManyField(User, through='OrganizerPermission', + related_name="organizers") class Meta: ordering = ("name",) +class OrganizerPermission(models.Model): + """ + The relation between an Organizer and an User who has permissions to + access an organizer profile. + """ + + organizer = models.ForeignKey(Organizer) + user = models.ForeignKey(User, related_name="organizer_perms") + can_create_events = models.BooleanField(default=True) + + class Meta: + unique_together = (("organizer", "user"),) + + class Event(models.Model): """ This model represents an event. An event is anything you can buy @@ -171,8 +182,9 @@ class Event(models.Model): organizer = models.ForeignKey(Organizer, related_name="events", on_delete=models.PROTECT) name = models.CharField(max_length=200) - slug = models.CharField(max_length=50, - db_index=True) + slug = models.CharField(max_length=50, db_index=True) + permitted = models.ManyToManyField(User, through='EventPermission', + related_name="events") locale = models.CharField(max_length=10) currency = models.CharField(max_length=10) date_from = models.DateTimeField() @@ -187,3 +199,17 @@ class Event(models.Model): class Meta: unique_together = (("organizer", "slug"),) ordering = ("date_from", "name") + + +class EventPermission(models.Model): + """ + The relation between an Event and an User who has permissions to + access an event. + """ + + organizer = models.ForeignKey(Event) + user = models.ForeignKey(User, related_name="event_perms") + can_change_settings = models.BooleanField(default=True) + + class Meta: + unique_together = (("organizer", "user"),) diff --git a/src/tixlbase/static/bootstrap b/src/tixlbase/static/bootstrap index 97027a2f6f..c068162161 160000 --- a/src/tixlbase/static/bootstrap +++ b/src/tixlbase/static/bootstrap @@ -1 +1 @@ -Subproject commit 97027a2f6fad00c4d74fbef5aef6cccb179f8229 +Subproject commit c068162161154a4b85110ea1e7dd3d7897ce2b72 diff --git a/src/tixlbase/static/fontawesome b/src/tixlbase/static/fontawesome new file mode 160000 index 0000000000..a65bd93d81 --- /dev/null +++ b/src/tixlbase/static/fontawesome @@ -0,0 +1 @@ +Subproject commit a65bd93d81e9e6bd5ebfa41757a4474960b973b4 diff --git a/src/tixlcontrol/context.py b/src/tixlcontrol/context.py new file mode 100644 index 0000000000..27766ad082 --- /dev/null +++ b/src/tixlcontrol/context.py @@ -0,0 +1,7 @@ +from django.conf import settings + + +def contextprocessor(request): + return { + 'settings': settings, + } diff --git a/src/tixlcontrol/static/tixlcontrol/less/main.less b/src/tixlcontrol/static/tixlcontrol/less/main.less new file mode 100644 index 0000000000..d056d1fe2f --- /dev/null +++ b/src/tixlcontrol/static/tixlcontrol/less/main.less @@ -0,0 +1,3 @@ +@import "../../../../tixlbase/static/bootstrap/less/bootstrap.less"; +@import "../../../../tixlbase/static/fontawesome/less/font-awesome.less"; +@fa-font-path: "../../fontawesome/fonts"; diff --git a/src/tixlcontrol/templates/tixlcontrol/auth/base.html b/src/tixlcontrol/templates/tixlcontrol/auth/base.html index d6eaf27765..a04f163658 100644 --- a/src/tixlcontrol/templates/tixlcontrol/auth/base.html +++ b/src/tixlcontrol/templates/tixlcontrol/auth/base.html @@ -4,7 +4,7 @@ - + {{ settings.TIXL_INSTANCE_NAME }} {% compress css %} {% endcompress %} diff --git a/src/tixlcontrol/templates/tixlcontrol/base.html b/src/tixlcontrol/templates/tixlcontrol/base.html new file mode 100644 index 0000000000..9d03b47254 --- /dev/null +++ b/src/tixlcontrol/templates/tixlcontrol/base.html @@ -0,0 +1,40 @@ +{% load compress %} +{% load staticfiles %} +{% load i18n %} + + + + {{ settings.TIXL_INSTANCE_NAME }} + {% compress css %} + + {% endcompress %} + + + +
+ {% block content %} + {% endblock %} +
+ + diff --git a/src/tixlcontrol/urls.py b/src/tixlcontrol/urls.py index 3f6fc73632..1e0432aa2d 100644 --- a/src/tixlcontrol/urls.py +++ b/src/tixlcontrol/urls.py @@ -2,5 +2,6 @@ from django.conf.urls import patterns, url urlpatterns = patterns('', url(r'^$', 'tixlcontrol.views.main.index', name='index'), + url(r'^logout$', 'tixlcontrol.views.auth.logout', name='auth.logout'), url(r'^login$', 'tixlcontrol.views.auth.login', name='auth.login'), ) diff --git a/src/tixlcontrol/views/auth.py b/src/tixlcontrol/views/auth.py index d6b812c063..f664931143 100644 --- a/src/tixlcontrol/views/auth.py +++ b/src/tixlcontrol/views/auth.py @@ -4,6 +4,7 @@ from django import forms from django.utils.translation import ugettext as _ from django.contrib.auth import authenticate from django.contrib.auth import login as auth_login +from django.contrib.auth import logout as auth_logout class AuthenticationForm(BaseAuthenticationForm): @@ -64,3 +65,8 @@ def login(request): form = AuthenticationForm() ctx['form'] = form return render(request, 'tixlcontrol/auth/login.html', ctx) + + +def logout(request): + auth_logout(request) + return redirect('control:auth.login') diff --git a/src/tixlcontrol/views/main.py b/src/tixlcontrol/views/main.py index 8090b1d99f..d52d69b01f 100644 --- a/src/tixlcontrol/views/main.py +++ b/src/tixlcontrol/views/main.py @@ -1,5 +1,6 @@ from django.http import HttpResponse +from django.shortcuts import render def index(request): - return HttpResponse('Coming soon.') + return render(request, 'tixlcontrol/base.html', {})