Merge branch 'master' of github.com:tixl/tixl

This commit is contained in:
Raphael Michel
2014-09-12 16:24:27 +02:00
18 changed files with 291 additions and 24 deletions

3
.gitmodules vendored
View File

@@ -1,3 +1,6 @@
[submodule "src/tixlbase/static/bootstrap"] [submodule "src/tixlbase/static/bootstrap"]
path = src/tixlbase/static/bootstrap path = src/tixlbase/static/bootstrap
url = https://github.com/twbs/bootstrap.git 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

View File

@@ -4,9 +4,12 @@ Implementation Concepts
Basic terminology 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**
Local users do only exist inside the scope of one event. They are identified by usernames, which are only valid for exactly one event. 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. 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.

0
src/helpers/__init__.py Normal file
View File

View File

@@ -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)

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: 1\n" "Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2014-09-11 11:05+200\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n" "Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: Raphael Michel <michel@rami.io>\n" "Language-Team: Raphael Michel <michel@rami.io>\n"
@@ -18,11 +18,11 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: tixl/settings.py:92 #: tixl/settings.py:104
msgid "German" msgid "German"
msgstr "Deutsch" msgstr "Deutsch"
#: tixl/settings.py:93 #: tixl/settings.py:105
msgid "English" msgid "English"
msgstr "Englisch" msgstr "Englisch"
@@ -30,7 +30,7 @@ msgstr "Englisch"
msgid "The two password fields didn't match." msgid "The two password fields didn't match."
msgstr "Die beiden eingegebenen Passwörter stimmen nicht überein." 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" msgid "Password"
msgstr "Passwort" msgstr "Passwort"
@@ -70,15 +70,27 @@ msgstr "Nachname"
msgid "Log in" msgid "Log in"
msgstr "Anmelden" 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" msgid "E-mail address"
msgstr "E-Mail-Adresse" msgstr "E-Mail-Adresse"
#: tixlcontrol/views/auth.py:18 #: tixlcontrol/views/auth.py:19
msgid "Please enter a correct e-mail address and password." msgid "Please enter a correct e-mail address and password."
msgstr "" msgstr ""
"Bitte geben Sie eine gültige Kombination aus E-Mail-Adresse und Passwort ein." "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." msgid "This account is inactive."
msgstr "Dieses Konto ist deaktiviert." msgstr "Dieses Konto ist deaktiviert."

View File

@@ -55,6 +55,18 @@ MIDDLEWARE_CLASSES = (
'tixlcontrol.middleware.LoginRequiredMiddleware', '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' ROOT_URLCONF = 'tixl.urls'
WSGI_APPLICATION = 'tixl.wsgi.application' WSGI_APPLICATION = 'tixl.wsgi.application'
@@ -114,7 +126,7 @@ STATICFILES_FINDERS = (
) )
COMPRESS_PRECOMPILERS = ( COMPRESS_PRECOMPILERS = (
('text/less', 'lessc {infile} {outfile}'), ('text/less', 'helpers.lessabsolutefilter.LessFilter'),
) )
COMPRESS_CSS_FILTERS = ( COMPRESS_CSS_FILTERS = (
@@ -122,6 +134,10 @@ COMPRESS_CSS_FILTERS = (
'compressor.filters.cssmin.CSSMinFilter', 'compressor.filters.cssmin.CSSMinFilter',
) )
# Tixl specific settings
TIXL_INSTANCE_NAME = 'tixl.de'
try: try:
from local_settings import * from local_settings import *

View File

@@ -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),
),
]

View File

@@ -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'),
),
]

View File

@@ -124,24 +124,35 @@ class User(AbstractBaseUser, PermissionsMixin):
class Organizer(models.Model): class Organizer(models.Model):
""" """
This model represents an entity organizing events, like a company, This model represents an entity organizing events, like a company.
an organization or a person. It has one user as owner (who has Any organizer has a unique slug, which is a short name (alphanumeric,
registered it) and can have any number of users with admin all lowercase) being used in URLs.
authorization. Any organizer has a unique slug, which is a short
name (alphanumeric, all lowercase) being used in URLs.
""" """
name = models.CharField(max_length=200) name = models.CharField(max_length=200)
slug = models.CharField(max_length=50, slug = models.CharField(max_length=50,
unique=True, unique=True, db_index=True)
db_index=True) permitted = models.ManyToManyField(User, through='OrganizerPermission',
owner = models.ForeignKey(User, null=True, blank=True, related_name="organizers")
on_delete=models.PROTECT)
class Meta: class Meta:
ordering = ("name",) 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): class Event(models.Model):
""" """
This model represents an event. An event is anything you can buy 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", organizer = models.ForeignKey(Organizer, related_name="events",
on_delete=models.PROTECT) on_delete=models.PROTECT)
name = models.CharField(max_length=200) name = models.CharField(max_length=200)
slug = models.CharField(max_length=50, slug = models.CharField(max_length=50, db_index=True)
db_index=True) permitted = models.ManyToManyField(User, through='EventPermission',
related_name="events")
locale = models.CharField(max_length=10) locale = models.CharField(max_length=10)
currency = models.CharField(max_length=10) currency = models.CharField(max_length=10)
date_from = models.DateTimeField() date_from = models.DateTimeField()
@@ -187,3 +199,17 @@ class Event(models.Model):
class Meta: class Meta:
unique_together = (("organizer", "slug"),) unique_together = (("organizer", "slug"),)
ordering = ("date_from", "name") 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"),)

View File

@@ -0,0 +1,7 @@
from django.conf import settings
def contextprocessor(request):
return {
'settings': settings,
}

View File

@@ -0,0 +1,3 @@
@import "../../../../tixlbase/static/bootstrap/less/bootstrap.less";
@import "../../../../tixlbase/static/fontawesome/less/font-awesome.less";
@fa-font-path: "../../fontawesome/fonts";

View File

@@ -4,7 +4,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title></title> <title>{{ settings.TIXL_INSTANCE_NAME }}</title>
{% compress css %} {% compress css %}
<link rel="stylesheet" type="text/less" href="{% static "tixlcontrol/less/auth.less" %}" /> <link rel="stylesheet" type="text/less" href="{% static "tixlcontrol/less/auth.less" %}" />
{% endcompress %} {% endcompress %}

View File

@@ -0,0 +1,40 @@
{% load compress %}
{% load staticfiles %}
{% load i18n %}
<!DOCTYPE html>
<html>
<head>
<title>{{ settings.TIXL_INSTANCE_NAME }}</title>
{% compress css %}
<link rel="stylesheet" type="text/less" href="{% static "tixlcontrol/less/main.less" %}" />
{% endcompress %}
</head>
<body>
<div class="navbar navbar-default navbar-static-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">{% trans "Toggle navigation" %}</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">{{ settings.TIXL_INSTANCE_NAME }}</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li class="active"><a href="{% url 'control:index' %}">{% trans "Dashboard" %}</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a href="#"><i class="fa fa-user"></i> {{ request.user.get_full_name }}</a></li>
<li><a href="{% url 'control:auth.logout' %}" title="{% trans "Log out" %}"><i class="fa fa-sign-out"></i></a></li>
</ul>
</div><!--/.nav-collapse -->
</div>
</div>
<div class="container">
{% block content %}
{% endblock %}
</div>
</body>
</html>

View File

@@ -2,5 +2,6 @@ from django.conf.urls import patterns, url
urlpatterns = patterns('', urlpatterns = patterns('',
url(r'^$', 'tixlcontrol.views.main.index', name='index'), 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'), url(r'^login$', 'tixlcontrol.views.auth.login', name='auth.login'),
) )

View File

@@ -4,6 +4,7 @@ from django import forms
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from django.contrib.auth import login as auth_login from django.contrib.auth import login as auth_login
from django.contrib.auth import logout as auth_logout
class AuthenticationForm(BaseAuthenticationForm): class AuthenticationForm(BaseAuthenticationForm):
@@ -64,3 +65,8 @@ def login(request):
form = AuthenticationForm() form = AuthenticationForm()
ctx['form'] = form ctx['form'] = form
return render(request, 'tixlcontrol/auth/login.html', ctx) return render(request, 'tixlcontrol/auth/login.html', ctx)
def logout(request):
auth_logout(request)
return redirect('control:auth.login')

View File

@@ -1,5 +1,6 @@
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import render
def index(request): def index(request):
return HttpResponse('Coming soon.') return render(request, 'tixlcontrol/base.html', {})