├── exhibitors
├── migrations
│ ├── __init__.py
│ ├── 0012_alter_lead_booth_id.py
│ ├── 0005_exhibitorinfo_booth_id_exhibitorinfo_booth_name.py
│ ├── 0008_alter_exhibitorinfo_booth_id.py
│ ├── 0007_populate_booth_ids.py
│ ├── 0006_exhibitorinfo_allow_lead_access_and_more.py
│ ├── 0003_lead.py
│ ├── 0004_exhibitortag.py
│ ├── 0011_alter_exhibitorinfo_booth_id_alter_lead_booth_id.py
│ ├── 0002_alter_exhibitorinfo_lead_scanning_enabled_and_more.py
│ ├── 0009_lead_booth_id_lead_booth_name_lead_exhibitor_name_and_more.py
│ ├── 0001_initial.py
│ └── 0010_alter_lead_booth_id_exhibitorsettings.py
├── locale
│ ├── de_Informal
│ │ ├── .gitkeep
│ │ └── LC_MESSAGES
│ │ │ └── django.po
│ └── de
│ │ └── LC_MESSAGES
│ │ └── django.po
├── static
│ └── exhibitors
│ │ └── .gitkeep
├── templates
│ └── exhibitors
│ │ ├── .gitkeep
│ │ ├── delete.html
│ │ ├── exhibitor_info.html
│ │ ├── settings.html
│ │ └── add.html
├── __init__.py
├── apps.py
├── forms.py
├── signals.py
├── urls.py
├── models.py
├── views.py
└── api.py
├── setup.py
├── MANIFEST.in
├── Makefile
├── tests
├── conftest.py
└── test_main.py
├── .install-hooks.sh
├── setup.cfg
├── .update-locales.sh
├── .gitignore
├── pyproject.toml
├── .github
└── workflows
│ ├── release.yml
│ ├── tests.yml
│ └── style.yml
├── README.rst
└── LICENSE
/exhibitors/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/exhibitors/locale/de_Informal/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/exhibitors/static/exhibitors/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/exhibitors/templates/exhibitors/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/exhibitors/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = "1.0.0"
2 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 |
4 | setup()
5 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | recursive-include exhibitors/static *
2 | recursive-include exhibitors/templates *
3 | recursive-include exhibitors/locale *
4 | include LICENSE
5 | exclude .gitlab-ci.yml
6 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | all: localecompile
2 | LNGS:=`find exhibitors/locale/ -mindepth 1 -maxdepth 1 -type d -printf "-l %f "`
3 |
4 | localecompile:
5 | django-admin compilemessages
6 |
7 | localegen:
8 | django-admin makemessages --keep-pot -i build -i dist -i "*egg*" $(LNGS)
9 |
10 | .PHONY: all localecompile localegen
11 |
--------------------------------------------------------------------------------
/exhibitors/locale/de/LC_MESSAGES/django.po:
--------------------------------------------------------------------------------
1 | msgid ""
2 | msgstr ""
3 | "Project-Id-Version: \n"
4 | "Report-Msgid-Bugs-To: \n"
5 | "POT-Creation-Date: 2017-03-07 19:01+0100\n"
6 | "PO-Revision-Date: \n"
7 | "Last-Translator: Srivatsav Auswin\n"
8 | "Language-Team: \n"
9 | "Language: de\n"
10 | "MIME-Version: 1.0\n"
11 | "Content-Type: text/plain; charset=UTF-8\n"
12 | "Content-Transfer-Encoding: 8bit\n"
13 |
--------------------------------------------------------------------------------
/exhibitors/locale/de_Informal/LC_MESSAGES/django.po:
--------------------------------------------------------------------------------
1 | msgid ""
2 | msgstr ""
3 | "Project-Id-Version: \n"
4 | "Report-Msgid-Bugs-To: \n"
5 | "POT-Creation-Date: 2017-03-07 19:01+0100\n"
6 | "PO-Revision-Date: \n"
7 | "Last-Translator: Srivatsav Auswin\n"
8 | "Language-Team: \n"
9 | "Language: de\n"
10 | "MIME-Version: 1.0\n"
11 | "Content-Type: text/plain; charset=UTF-8\n"
12 | "Content-Transfer-Encoding: 8bit\n"
13 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from django.utils.timezone import now
4 | from pretix.base.models import Event, Organizer
5 |
6 |
7 | @pytest.fixture
8 | def event(db):
9 | organizer = Organizer.objects.create(name="Test Organizer", slug="test-organizer")
10 | event = Event.objects.create(
11 | organizer=organizer,
12 | name="Test Event",
13 | slug="test-event",
14 | live=True,
15 | date_from=now(),
16 | )
17 | return event
18 |
--------------------------------------------------------------------------------
/exhibitors/migrations/0012_alter_lead_booth_id.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.1.4 on 2025-02-11 10:45
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('exhibitors', '0011_alter_exhibitorinfo_booth_id_alter_lead_booth_id'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='lead',
15 | name='booth_id',
16 | field=models.CharField(max_length=100),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/.install-hooks.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | REPO_DIR=$(git rev-parse --show-toplevel)
3 | GIT_DIR=$REPO_DIR/.git
4 | VENV_ACTIVATE=$VIRTUAL_ENV/bin/activate
5 | if [[ ! -f $VENV_ACTIVATE ]]
6 | then
7 | echo "Could not find your virtual environment"
8 | fi
9 |
10 | echo "#!/bin/sh" >> $GIT_DIR/hooks/pre-commit
11 | echo "set -e" >> $GIT_DIR/hooks/pre-commit
12 | echo "source $VENV_ACTIVATE" >> $GIT_DIR/hooks/pre-commit
13 | echo "black --check ." >> $GIT_DIR/hooks/pre-commit
14 | echo "isort -c ." >> $GIT_DIR/hooks/pre-commit
15 | echo "flake8 ." >> $GIT_DIR/hooks/pre-commit
16 | chmod +x $GIT_DIR/hooks/pre-commit
17 |
--------------------------------------------------------------------------------
/exhibitors/templates/exhibitors/delete.html:
--------------------------------------------------------------------------------
1 | {% extends "pretixcontrol/event/base.html" %}
2 | {% load i18n %}
3 | {% block content %}
4 |
{% trans "Delete Exhibitor" %}
5 |
6 |
7 | {% trans "Are you sure you want to delete this exhibitor? This action cannot be undone." %}
8 |
9 |
10 |
22 | {% endblock %}
23 |
--------------------------------------------------------------------------------
/exhibitors/migrations/0005_exhibitorinfo_booth_id_exhibitorinfo_booth_name.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.1.2 on 2024-10-30 16:21
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('exhibitors', '0004_exhibitortag'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='exhibitorinfo',
15 | name='booth_id',
16 | field=models.IntegerField(auto_created=True, null=True),
17 | ),
18 | migrations.AddField(
19 | model_name='exhibitorinfo',
20 | name='booth_name',
21 | field=models.CharField(default='Unnamed Booth', max_length=100),
22 | preserve_default=False,
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/exhibitors/apps.py:
--------------------------------------------------------------------------------
1 | from django.utils.translation import gettext_lazy as _
2 |
3 | from . import __version__
4 |
5 | try:
6 | from pretix.base.plugins import PluginConfig
7 | except ImportError:
8 | raise RuntimeError("Please use a later version of eventyay-tickets")
9 |
10 |
11 | class ExhibitorApp(PluginConfig):
12 | default = True
13 | name = "exhibitors"
14 | verbose_name = _("Exhibitors")
15 |
16 | class PretixPluginMeta:
17 | name = _("Exhibitors")
18 | author = "FOSSASIA"
19 | description = _("This plugin enables to add and control exhibitors in eventyay")
20 | visible = True
21 | featured = True
22 | version = __version__
23 | category = "FEATURE"
24 |
25 | def ready(self):
26 | from . import signals # NOQA
27 |
--------------------------------------------------------------------------------
/exhibitors/migrations/0008_alter_exhibitorinfo_booth_id.py:
--------------------------------------------------------------------------------
1 | from django.db import migrations, models
2 |
3 |
4 | def generate_booth_id():
5 | from django.db.models import Max
6 |
7 | from exhibitors.models import ExhibitorInfo
8 | max_id = ExhibitorInfo.objects.all().aggregate(Max('booth_id'))['booth_id__max']
9 | return 1000 if max_id is None else max_id + 1
10 |
11 | class Migration(migrations.Migration):
12 | dependencies = [
13 | ('exhibitors', '0007_populate_booth_ids'), # Reference the previous migration
14 | ]
15 |
16 | operations = [
17 | migrations.AlterField(
18 | model_name='exhibitorinfo',
19 | name='booth_id',
20 | field=models.IntegerField(default=generate_booth_id, unique=True, editable=False),
21 | ),
22 | ]
23 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [flake8]
2 | ignore = N802,W503,E402
3 | max-line-length = 160
4 | exclude = migrations,.ropeproject,static,_static,build
5 |
6 | [isort]
7 | combine_as_imports = true
8 | default_section = THIRDPARTY
9 | include_trailing_comma = true
10 | known_third_party = pretix
11 | known_standard_library = typing
12 | multi_line_output = 5
13 | not_skip = __init__.py
14 | skip = setup.py
15 |
16 | [tool:pytest]
17 | DJANGO_SETTINGS_MODULE = pretix.testutils.settings
18 |
19 | [coverage:run]
20 | source = exhibitors
21 | omit = */migrations/*,*/urls.py,*/tests/*
22 |
23 | [coverage:report]
24 | exclude_lines =
25 | pragma: no cover
26 | def __str__
27 | der __repr__
28 | if settings.DEBUG
29 | NOQA
30 | NotImplementedError
31 |
32 | [check-manifest]
33 | ignore =
34 | .update-locales
35 | .update-locales.sh
36 | .install-hooks.sh
37 | Makefile
38 | pytest.ini
39 | manage.py
40 | tests/*
41 |
--------------------------------------------------------------------------------
/.update-locales.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # Check if 'wlc' command is available
4 | if ! command -v wlc >/dev/null 2>&1; then
5 | echo "Error: 'wlc' command not found. Please install it before running this script."
6 | exit 1
7 | fi
8 |
9 | COMPONENTS=pretix/pretix-plugin-exhibitors
10 | DIR=exhibitors/locale
11 | # Renerates .po files used for translating the plugin
12 | set -e
13 | set -x
14 |
15 | # Lock Weblate
16 | for c in $COMPONENTS; do
17 | wlc lock $c;
18 | done
19 |
20 | # Push changes from Weblate to GitHub
21 | for c in $COMPONENTS; do
22 | wlc commit $c;
23 | done
24 |
25 | # Pull changes from GitHub
26 | git pull --rebase
27 |
28 | # Update po files itself
29 | make localegen
30 |
31 | # Commit changes
32 | git add $DIR/*/*/*.po
33 | git add $DIR/*.pot
34 |
35 | git commit -s -m "Update po files
36 | [CI skip]"
37 |
38 | # Push changes
39 | git push
40 |
41 | # Unlock Weblate
42 | for c in $COMPONENTS; do
43 | wlc unlock $c;
44 | done
45 |
--------------------------------------------------------------------------------
/exhibitors/migrations/0007_populate_booth_ids.py:
--------------------------------------------------------------------------------
1 | from django.db import migrations
2 |
3 |
4 | def populate_booth_ids(apps, schema_editor):
5 | ExhibitorInfo = apps.get_model('exhibitors', 'ExhibitorInfo')
6 | starting_id = 1000
7 |
8 | # Update all exhibitors that don't have a booth_id
9 | for exhibitor in ExhibitorInfo.objects.all().order_by('id'):
10 | exhibitor.booth_id = starting_id
11 | exhibitor.save()
12 | starting_id += 1
13 |
14 | def reverse_populate_booth_ids(apps, schema_editor):
15 | ExhibitorInfo = apps.get_model('exhibitors', 'ExhibitorInfo')
16 | ExhibitorInfo.objects.all().update(booth_id=None)
17 |
18 | class Migration(migrations.Migration):
19 | dependencies = [
20 | ('exhibitors', '0006_exhibitorinfo_allow_lead_access_and_more'), # Note: removed .py
21 | ]
22 |
23 | operations = [
24 | migrations.RunPython(populate_booth_ids, reverse_populate_booth_ids),
25 | ]
26 |
--------------------------------------------------------------------------------
/exhibitors/migrations/0006_exhibitorinfo_allow_lead_access_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.1.2 on 2024-10-30 19:47
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('exhibitors', '0005_exhibitorinfo_booth_id_exhibitorinfo_booth_name'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='exhibitorinfo',
15 | name='allow_lead_access',
16 | field=models.BooleanField(default=False),
17 | ),
18 | migrations.AddField(
19 | model_name='exhibitorinfo',
20 | name='allow_voucher_access',
21 | field=models.BooleanField(default=False),
22 | ),
23 | migrations.AddField(
24 | model_name='exhibitorinfo',
25 | name='lead_scanning_scope_by_device',
26 | field=models.BooleanField(default=False),
27 | ),
28 | ]
29 |
--------------------------------------------------------------------------------
/exhibitors/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.utils.translation import gettext, gettext_lazy as _
3 | from pretix.base.forms import SettingsForm
4 |
5 | from .models import ExhibitorInfo
6 |
7 |
8 | class ExhibitorInfoForm(forms.ModelForm):
9 | allow_voucher_access = forms.BooleanField(required=False)
10 | allow_lead_access = forms.BooleanField(required=False)
11 | lead_scanning_scope_by_device = forms.BooleanField(required=False)
12 | comment = forms.CharField(
13 | widget=forms.Textarea(attrs={'rows': 10}),
14 | required=False
15 | )
16 | booth_id = forms.CharField(required=False)
17 |
18 | class Meta:
19 | model = ExhibitorInfo
20 | fields = [
21 | 'name',
22 | 'description',
23 | 'url',
24 | 'email',
25 | 'logo',
26 | 'booth_id',
27 | 'booth_name',
28 | 'lead_scanning_enabled'
29 | ]
30 | widgets = {
31 | 'description': forms.Textarea(attrs={'rows': 4}),
32 | }
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 | .ropeproject/
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *,cover
47 | .hypothesis/
48 |
49 | # Translations
50 | *.mo
51 |
52 | # Django stuff:
53 | *.log
54 | data/
55 |
56 | # Sphinx documentation
57 | docs/_build/
58 |
59 | # PyBuilder
60 | target/
61 |
62 | #Ipython Notebook
63 | .ipynb_checkpoints
64 |
--------------------------------------------------------------------------------
/exhibitors/migrations/0003_lead.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.14 on 2024-10-14 10:02
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('exhibitors', '0002_alter_exhibitorinfo_lead_scanning_enabled_and_more'),
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='Lead',
16 | fields=[
17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
18 | ('pseudonymization_id', models.CharField(max_length=190)),
19 | ('scanned', models.DateTimeField()),
20 | ('scan_type', models.CharField(max_length=50)),
21 | ('device_name', models.CharField(max_length=50)),
22 | ('attendee', models.JSONField(null=True)),
23 | ('exhibitor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='exhibitors.exhibitorinfo')),
24 | ],
25 | ),
26 | ]
27 |
--------------------------------------------------------------------------------
/exhibitors/migrations/0004_exhibitortag.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.1.2 on 2024-10-28 20:43
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('exhibitors', '0003_lead'),
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='ExhibitorTag',
16 | fields=[
17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
18 | ('name', models.CharField(max_length=50)),
19 | ('use_count', models.IntegerField(default=0)),
20 | ('created_at', models.DateTimeField(auto_now_add=True)),
21 | ('exhibitor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tags', to='exhibitors.exhibitorinfo')),
22 | ],
23 | options={
24 | 'ordering': ['-use_count', 'name'],
25 | 'unique_together': {('exhibitor', 'name')},
26 | },
27 | ),
28 | ]
29 |
--------------------------------------------------------------------------------
/exhibitors/migrations/0011_alter_exhibitorinfo_booth_id_alter_lead_booth_id.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.1.2 on 2024-11-04 09:04
2 |
3 | from django.db import migrations
4 |
5 | def convert_booth_ids_to_string(apps, schema_editor):
6 | ExhibitorInfo = apps.get_model('exhibitors', 'ExhibitorInfo')
7 | Lead = apps.get_model('exhibitors', 'Lead')
8 |
9 | # Convert ExhibitorInfo booth_ids
10 | for exhibitor in ExhibitorInfo.objects.all():
11 | if exhibitor.booth_id is not None:
12 | exhibitor.booth_id = str(exhibitor.booth_id)
13 | exhibitor.save()
14 |
15 | # Convert Lead booth_ids
16 | for lead in Lead.objects.all():
17 | if lead.booth_id is not None:
18 | lead.booth_id = str(lead.booth_id)
19 | lead.save()
20 |
21 | class Migration(migrations.Migration):
22 |
23 | dependencies = [
24 | ('exhibitors', '0010_alter_lead_booth_id_exhibitorsettings'),
25 | ]
26 |
27 | operations = [
28 | migrations.RunPython(convert_booth_ids_to_string, reverse_code=migrations.RunPython.noop),
29 | ]
30 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "exhibitors"
3 | dynamic = ["version"]
4 | description = "Eventyay-tickets plugin to add and manage exhibitors"
5 | readme = "README.rst"
6 | requires-python = ">=3.11"
7 | license = {file = "LICENSE"}
8 | keywords = ["Eventyay-tickets"]
9 | authors = [
10 | {name = "eventyay team", email = "support@eventyay.com"},
11 | ]
12 | maintainers = [
13 | {name = "eventyay team", email = "support@eventyay.com"},
14 | ]
15 |
16 | dependencies = [
17 |
18 | ]
19 |
20 | [project.entry-points."pretix.plugin"]
21 | exhibitors = "exhibitors:PretixPluginMeta"
22 |
23 | [project.entry-points."distutils.commands"]
24 | build = "pretix_plugin_build.build:CustomBuild"
25 |
26 | [build-system]
27 | requires = [
28 | "setuptools",
29 | "pretix-plugin-build",
30 | ]
31 |
32 | [project.urls]
33 | homepage = "https://github.com/fossasia/eventyay-tickets-exhibitors"
34 |
35 | [tool.setuptools]
36 | include-package-data = true
37 |
38 | [tool.setuptools.dynamic]
39 | version = {attr = "exhibitors.__version__"}
40 |
41 | [tool.setuptools.packages.find]
42 | include = ["exhibitors*"]
43 | namespaces = false
44 |
--------------------------------------------------------------------------------
/exhibitors/migrations/0002_alter_exhibitorinfo_lead_scanning_enabled_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.14 on 2024-09-16 05:42
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('pretixbase', '0001_initial'),
11 | ('exhibitors', '0001_initial'),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterField(
16 | model_name='exhibitorinfo',
17 | name='lead_scanning_enabled',
18 | field=models.BooleanField(default=False),
19 | ),
20 | migrations.CreateModel(
21 | name='ExhibitorItem',
22 | fields=[
23 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
24 | ('exhibitor', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='item_assignments', to='exhibitors.exhibitorinfo')),
25 | ('item', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='exhibitor_assignment', to='pretixbase.item')),
26 | ],
27 | options={
28 | 'ordering': ('id',),
29 | },
30 | ),
31 | ]
32 |
--------------------------------------------------------------------------------
/exhibitors/migrations/0009_lead_booth_id_lead_booth_name_lead_exhibitor_name_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.1.2 on 2024-10-30 20:56
2 |
3 | from django.db import migrations, models
4 |
5 | import exhibitors.models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('exhibitors', '0008_alter_exhibitorinfo_booth_id'),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name='lead',
17 | name='booth_id',
18 | field=models.IntegerField(default=0),
19 | preserve_default=False,
20 | ),
21 | migrations.AddField(
22 | model_name='lead',
23 | name='booth_name',
24 | field=models.CharField(default='', max_length=100),
25 | preserve_default=False,
26 | ),
27 | migrations.AddField(
28 | model_name='lead',
29 | name='exhibitor_name',
30 | field=models.CharField(default='', max_length=190),
31 | preserve_default=False,
32 | ),
33 | migrations.AlterField(
34 | model_name='exhibitorinfo',
35 | name='booth_id',
36 | field=models.IntegerField(default=exhibitors.models.generate_booth_id, unique=True),
37 | ),
38 | ]
39 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release to PyPI
2 |
3 | on:
4 | push:
5 | tags:
6 | - "v*.*.*"
7 |
8 | jobs:
9 | release:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v3
13 |
14 | - name: Set up Python 3.11
15 | uses: actions/setup-python@v4
16 | with:
17 | python-version: "3.11"
18 |
19 | - name: Cache pip
20 | uses: actions/cache@v3
21 | with:
22 | path: ~/.cache/pip
23 | key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
24 | restore-keys: |
25 | ${{ runner.os }}-pip-
26 |
27 | - name: Install dependencies
28 | run: |
29 | python -m pip install --upgrade pip
30 | pip install wheel setuptools twine check-manifest
31 | XDG_CACHE_HOME=/cache pip install -e ".[dev]"
32 |
33 | - name: Check manifest
34 | run: check-manifest
35 |
36 | - name: Build package
37 | run: |
38 | python setup.py sdist bdist_wheel
39 | ls -l dist
40 |
41 | - name: Upload to PyPI
42 | env:
43 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
44 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
45 | run: |
46 | twine check dist/*
47 | twine upload dist/*
48 |
49 | artifacts:
50 | paths:
51 | - dist/
52 |
--------------------------------------------------------------------------------
/exhibitors/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.14 on 2024-09-02 09:37
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 | import exhibitors.models
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | initial = True
12 |
13 | dependencies = [
14 | ('pretixbase', '0001_initial'),
15 | ]
16 |
17 | operations = [
18 | migrations.CreateModel(
19 | name='ExhibitorInfo',
20 | fields=[
21 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
22 | ('name', models.CharField(max_length=190)),
23 | ('description', models.TextField(null=True)),
24 | ('url', models.URLField(null=True)),
25 | ('email', models.EmailField(max_length=254, null=True)),
26 | ('logo', models.ImageField(null=True, upload_to=exhibitors.models.exhibitor_logo_path)),
27 | ('key', models.CharField(default=exhibitors.models.generate_key, max_length=8)),
28 | ('lead_scanning_enabled', models.BooleanField(default=True)),
29 | ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.event')),
30 | ],
31 | options={
32 | 'ordering': ('name',),
33 | },
34 | ),
35 | ]
36 |
--------------------------------------------------------------------------------
/exhibitors/migrations/0010_alter_lead_booth_id_exhibitorsettings.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.1.2 on 2024-11-02 10:09
2 |
3 | import django.db.models.deletion
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('exhibitors', '0009_lead_booth_id_lead_booth_name_lead_exhibitor_name_and_more'),
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='ExhibitorSettings',
16 | fields=[
17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
18 | ('exhibitors_access_mail_subject', models.CharField(max_length=255)),
19 | ('exhibitors_access_mail_body', models.TextField()),
20 | ('allowed_fields', models.JSONField(default=list)),
21 | ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.event')),
22 | ],
23 | options={
24 | 'unique_together': {('event',)},
25 | },
26 | ),
27 | migrations.AlterField(
28 | model_name='exhibitorinfo',
29 | name='booth_id',
30 | field=models.CharField(max_length=100, null=True, unique=True),
31 | ),
32 | migrations.AlterField(
33 | model_name='lead',
34 | name='booth_id',
35 | field=models.CharField(max_length=100, unique=True), # Changed to CharField
36 | ),
37 | ]
38 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Exhibitors
2 | ==========================
3 |
4 | This is a plugin for `eventyay-tickets`_.
5 |
6 | This plugin enables to add and control exhibitors in eventyay
7 |
8 | Development setup
9 | -----------------
10 |
11 | 1. Make sure that you have a working `eventyay-tickets development setup`_.
12 |
13 | 2. Clone this repository, eg to ``local/exhibitors``.
14 |
15 | 3. Activate the virtual environment you use for eventyay-tickets development.
16 |
17 | 4. Execute ``pip install -e .`` within this directory to register this application with eventyay-tickets plugin registry.
18 |
19 | 5. Execute ``make`` within this directory to compile translations.
20 |
21 | 6. Restart your local eventyay-tickets server. You can now use the plugin from this repository for your events by enabling it in
22 | the 'plugins' tab in the settings.
23 |
24 |
25 | This plugin has CI set up to enforce a few code style rules. To check locally, you need these packages installed::
26 |
27 | pip install flake8 isort black
28 |
29 | To check your plugin for rule violations, run::
30 |
31 | black --check .
32 | isort -c .
33 | flake8 .
34 |
35 | You can auto-fix some of these issues by running::
36 |
37 | isort .
38 | black .
39 |
40 | To automatically check for these issues before you commit, you can run ``.install-hooks``.
41 |
42 |
43 | License
44 | -------
45 |
46 |
47 | Copyright 2024 FOSSASIA
48 |
49 | Released under the terms of the Apache License 2.0
50 |
51 |
52 |
53 | .. _eventyay-tickets: https://github.com/fossasia/eventyay-tickets
54 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on:
4 | push:
5 | branches: [main, master]
6 | paths-ignore:
7 | - "exhibitors/locale/**"
8 | pull_request:
9 | branches: [main, master]
10 | paths-ignore:
11 | - "exhibitors/locale/**"
12 |
13 | jobs:
14 | test:
15 | runs-on: ubuntu-latest
16 | name: Exhibitors Plugin Tests
17 |
18 | steps:
19 | - name: Checkout exhibitors plugin
20 | uses: actions/checkout@v3
21 |
22 | - name: Clone eventyay-tickets
23 | run: |
24 | git clone https://github.com/fossasia/eventyay-tickets.git ../eventyay-tickets
25 |
26 | - name: Set up Python
27 | uses: actions/setup-python@v4
28 | with:
29 | python-version: "3.11"
30 |
31 | - name: Install system dependencies
32 | run: sudo apt-get update && sudo apt-get install -y gettext libjpeg-dev zlib1g-dev libpq-dev
33 |
34 | - name: Set up virtual environment
35 | run: |
36 | python -m venv venv
37 | source venv/bin/activate
38 | pip install --upgrade pip
39 |
40 | # Install eventyay-tickets with dev dependencies
41 | cd ../eventyay-tickets
42 | pip install -e ".[dev]"
43 |
44 | # Go back to plugin directory and install it
45 | cd ../eventyay-tickets-exhibitors
46 | pip install -e .
47 |
48 | - name: Compile translations
49 | run: |
50 | source venv/bin/activate
51 | make
52 |
53 | - name: Run tests
54 | run: |
55 | source venv/bin/activate
56 | pytest tests --reruns 3
57 |
--------------------------------------------------------------------------------
/.github/workflows/style.yml:
--------------------------------------------------------------------------------
1 | name: Code Style
2 |
3 | on:
4 | push:
5 | branches: [main, master]
6 | paths-ignore:
7 | - "exhibitors/locale/**"
8 | - "exhibitors/static/**"
9 | pull_request:
10 | branches: [main, master]
11 | paths-ignore:
12 | - "exhibitors/locale/**"
13 | - "exhibitors/static/**"
14 |
15 | jobs:
16 | isort:
17 | name: isort
18 | runs-on: ubuntu-latest
19 | steps:
20 | - uses: actions/checkout@v3
21 |
22 | - name: Set up Python 3.11
23 | uses: actions/setup-python@v4
24 | with:
25 | python-version: "3.11"
26 |
27 | - name: Cache pip
28 | uses: actions/cache@v3
29 | with:
30 | path: ~/.cache/pip
31 | key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
32 | restore-keys: |
33 | ${{ runner.os }}-pip-
34 |
35 | - name: Install Dependencies
36 | run: |
37 | python -m pip install --upgrade pip
38 | pip install -e ".[dev]" psycopg2-binary
39 | pip install isort
40 |
41 | - name: Run isort
42 | run: isort --check-only .
43 |
44 | flake:
45 | name: flake8
46 | runs-on: ubuntu-latest
47 | steps:
48 | - uses: actions/checkout@v3
49 |
50 | - name: Set up Python 3.11
51 | uses: actions/setup-python@v4
52 | with:
53 | python-version: "3.11"
54 |
55 | - name: Cache pip
56 | uses: actions/cache@v3
57 | with:
58 | path: ~/.cache/pip
59 | key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
60 | restore-keys: |
61 | ${{ runner.os }}-pip-
62 |
63 | - name: Install Dependencies
64 | run: |
65 | python -m pip install --upgrade pip
66 | pip install -e ".[dev]" psycopg2-binary
67 | pip install flake8
68 |
69 | - name: Run flake8
70 | run: flake8 .
71 |
--------------------------------------------------------------------------------
/exhibitors/signals.py:
--------------------------------------------------------------------------------
1 | from django.dispatch import receiver
2 | from django.template.loader import get_template
3 | from django.urls import resolve, reverse
4 | from django.utils.timezone import now
5 | from django.utils.translation import gettext_lazy as _
6 | from i18nfield.strings import LazyI18nString
7 | from pretix.base.models import Event, Order
8 | from pretix.base.reldate import RelativeDateWrapper
9 | from pretix.base.settings import settings_hierarkey
10 | from pretix.base.signals import event_copy_data, item_copy_data
11 | from pretix.control.signals import nav_event, nav_event_settings
12 | from pretix.presale.signals import order_info_top, position_info_top
13 |
14 |
15 | @receiver(nav_event, dispatch_uid="exhibitors_nav")
16 | def control_nav_import(sender, request=None, **kwargs):
17 | url = resolve(request.path_info)
18 | return [
19 | {
20 | 'label': _('Exhibitors'),
21 | 'url': reverse(
22 | 'plugins:exhibitors:info',
23 | kwargs={
24 | 'event': request.event.slug,
25 | 'organizer': request.event.organizer.slug,
26 | }
27 | ),
28 | 'active': url.namespace == 'plugins:exhibitors',
29 | 'icon': 'map-pin',
30 | }
31 | ]
32 |
33 |
34 | @receiver(nav_event_settings, dispatch_uid='exhibitors_nav')
35 | def navbar_info(sender, request, **kwargs):
36 | url = resolve(request.path_info)
37 | if not request.user.has_event_permission(
38 | request.organizer, request.event, 'can_change_event_settings', request=request):
39 | return []
40 | return [{
41 | 'label': 'Exhibitors',
42 | 'url': reverse(
43 | 'plugins:exhibitors:settings',
44 | kwargs={
45 | 'event': request.event.slug,
46 | 'organizer': request.organizer.slug,
47 | }
48 | ),
49 | 'active': url.namespace == 'plugins:exhibitors',
50 | }]
51 |
--------------------------------------------------------------------------------
/exhibitors/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from pretix.api.urls import event_router
3 |
4 | from .api import (
5 | ExhibitorAuthView, ExhibitorInfoViewSet, ExhibitorItemViewSet,
6 | LeadCreateView, LeadRetrieveView, LeadUpdateView, TagListView,
7 | )
8 | from .views import (
9 | ExhibitorCopyKeyView, ExhibitorCreateView, ExhibitorDeleteView,
10 | ExhibitorEditView, ExhibitorListView, SettingsView,
11 | )
12 |
13 | urlpatterns = [
14 | path(
15 | 'control/event///settings/exhibitors',
16 | SettingsView.as_view(),
17 | name='settings'
18 | ),
19 | path(
20 | 'control/event///exhibitors',
21 | ExhibitorListView.as_view(),
22 | name='info'
23 | ),
24 | path(
25 | 'control/event///exhibitors/add',
26 | ExhibitorCreateView.as_view(),
27 | name='add'
28 | ),
29 | path(
30 | 'control/event///exhibitors/edit/',
31 | ExhibitorEditView.as_view(),
32 | name='edit'
33 | ),
34 | path(
35 | 'control/event///exhibitors/delete/',
36 | ExhibitorDeleteView.as_view(),
37 | name='delete'
38 | ),
39 | path(
40 | 'control/event///exhibitors/copy_key/',
41 | ExhibitorCopyKeyView.as_view(),
42 | name='copy_key'
43 | ),
44 | path(
45 | 'api/v1/event///exhibitors/auth',
46 | ExhibitorAuthView.as_view(),
47 | name='exhibitor-auth'
48 | ),
49 | path(
50 | 'api/v1/event///exhibitors/lead/create',
51 | LeadCreateView.as_view(),
52 | name='lead-create'
53 | ),
54 | path(
55 | 'api/v1/event///exhibitors/lead/retrieve',
56 | LeadRetrieveView.as_view(),
57 | name='lead-retrieve'
58 | ),
59 | path(
60 | 'api/v1/event///exhibitors/tags',
61 | TagListView.as_view(),
62 | name='exhibitor-tags'
63 | ),
64 | path(
65 | 'api/v1/event///exhibitors/lead//update',
66 | LeadUpdateView.as_view(),
67 | name='lead-update'
68 | ),
69 | ]
70 |
71 | event_router.register('exhibitors', ExhibitorInfoViewSet, basename='exhibitorinfo')
72 | event_router.register('exhibitoritems', ExhibitorItemViewSet, basename='exhibitoritem')
73 |
--------------------------------------------------------------------------------
/exhibitors/templates/exhibitors/exhibitor_info.html:
--------------------------------------------------------------------------------
1 | {% extends "pretixcontrol/event/base.html" %}
2 | {% load i18n %}
3 | {% block title %}{% trans "Exhibitors" %}{% endblock %}
4 | {% block content %}
5 | {% trans "Exhibitors" %}
6 | {% if exhibitors|length == 0 %}
7 |
8 |
9 | {% blocktrans trimmed %}
10 | You don't have any exhibitors yet.
11 | {% endblocktrans %}
12 |
13 |
14 | {% if "can_change_event_settings" in request.eventpermset %}
15 |
{% trans "Add Exhibitor" %}
17 |
18 | {% endif %}
19 |
20 | {% else %}
21 |
22 | {% if "can_change_event_settings" in request.eventpermset %}
23 | {% trans "Add Exhibitor" %}
24 |
25 | {% endif %}
26 |
27 |
28 |
29 |
30 |
31 | | {% trans "Name" %} |
32 | |
33 |
34 |
35 |
36 | {% for e in exhibitors %}
37 |
38 | |
39 | {% if "can_change_event_settings" in request.eventpermset %}
40 |
41 | {{ e.name }}
42 |
43 | {% else %}
44 | {{ e.name }}
45 | {% endif %}
46 | |
47 |
48 | {% if "can_change_event_settings" in request.eventpermset %}
49 |
50 |
51 |
52 | {% endif %}
53 | |
54 |
55 | {% endfor %}
56 |
57 |
58 |
59 | {% endif %}
60 | {% include "pretixcontrol/pagination.html" %}
61 | {% endblock %}
62 |
--------------------------------------------------------------------------------
/tests/test_main.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from django.core.files.uploadedfile import SimpleUploadedFile
4 |
5 | from exhibitors.models import ExhibitorInfo
6 |
7 |
8 | @pytest.mark.django_db
9 | def test_create_exhibitor_info(event):
10 | # CREATE: Simulate an image upload and create an exhibitor
11 | logo = SimpleUploadedFile("test_logo.jpg", b"file_content", content_type="image/jpeg")
12 |
13 | exhibitor = ExhibitorInfo.objects.create(
14 | event=event,
15 | name="Test Exhibitor",
16 | description="This is a test exhibitor",
17 | url="http://testexhibitor.com",
18 | email="test@example.com",
19 | logo=logo,
20 | lead_scanning_enabled=True
21 | )
22 |
23 | # Verify the exhibitor was created and the fields are correct
24 | assert exhibitor.name == "Test Exhibitor"
25 | assert exhibitor.description == "This is a test exhibitor"
26 | assert exhibitor.url == "http://testexhibitor.com"
27 | assert exhibitor.email == "test@example.com"
28 | assert exhibitor.logo.name == "exhibitors/logos/Test Exhibitor/test_logo.jpg"
29 | assert exhibitor.lead_scanning_enabled is True
30 |
31 | @pytest.mark.django_db
32 | def test_read_exhibitor_info(event):
33 | # CREATE an exhibitor first to test reading
34 | logo = SimpleUploadedFile("test_logo.jpg", b"file_content", content_type="image/jpeg")
35 | exhibitor = ExhibitorInfo.objects.create(
36 | event=event,
37 | name="Test Exhibitor",
38 | description="This is a test exhibitor",
39 | url="http://testexhibitor.com",
40 | email="test@example.com",
41 | logo=logo,
42 | lead_scanning_enabled=True
43 | )
44 |
45 | # READ: Fetch the exhibitor from the database and verify fields
46 | exhibitor_from_db = ExhibitorInfo.objects.get(id=exhibitor.id)
47 | assert exhibitor_from_db.name == "Test Exhibitor"
48 | assert exhibitor_from_db.description == "This is a test exhibitor"
49 | assert exhibitor_from_db.url == "http://testexhibitor.com"
50 | assert exhibitor_from_db.email == "test@example.com"
51 | assert exhibitor_from_db.lead_scanning_enabled is True
52 |
53 | @pytest.mark.django_db
54 | def test_update_exhibitor_info(event):
55 | # CREATE an exhibitor first to test updating
56 | logo = SimpleUploadedFile("test_logo.jpg", b"file_content", content_type="image/jpeg")
57 | exhibitor = ExhibitorInfo.objects.create(
58 | event=event,
59 | name="Test Exhibitor",
60 | description="This is a test exhibitor",
61 | url="http://testexhibitor.com",
62 | email="test@example.com",
63 | logo=logo,
64 | lead_scanning_enabled=True
65 | )
66 |
67 | # UPDATE: Modify some fields and save the changes
68 | exhibitor.name = "Updated Exhibitor"
69 | exhibitor.description = "This is an updated description"
70 | exhibitor.lead_scanning_enabled = False
71 | exhibitor.save()
72 |
73 | # Verify the updated fields
74 | updated_exhibitor = ExhibitorInfo.objects.get(id=exhibitor.id)
75 | assert updated_exhibitor.name == "Updated Exhibitor"
76 | assert updated_exhibitor.description == "This is an updated description"
77 | assert updated_exhibitor.lead_scanning_enabled is False
78 |
79 | @pytest.mark.django_db
80 | def test_delete_exhibitor_info(event):
81 | # CREATE an exhibitor first to test deleting
82 | logo = SimpleUploadedFile("test_logo.jpg", b"file_content", content_type="image/jpeg")
83 | exhibitor = ExhibitorInfo.objects.create(
84 | event=event,
85 | name="Test Exhibitor",
86 | description="This is a test exhibitor",
87 | url="http://testexhibitor.com",
88 | email="test@example.com",
89 | logo=logo,
90 | lead_scanning_enabled=True
91 | )
92 |
93 | # DELETE: Delete the exhibitor and verify it no longer exists
94 | exhibitor_id = exhibitor.id
95 | exhibitor.delete()
96 |
97 | with pytest.raises(ExhibitorInfo.DoesNotExist):
98 | ExhibitorInfo.objects.get(id=exhibitor_id)
99 |
--------------------------------------------------------------------------------
/exhibitors/templates/exhibitors/settings.html:
--------------------------------------------------------------------------------
1 | {% extends "pretixcontrol/event/base.html" %}
2 | {% load i18n %}
3 | {% block title %}{% trans "Exhibitor Settings" %}{% endblock %}
4 |
5 | {% block content %}
6 |
7 |
{% trans "Exhibitor Settings" %}
8 |
9 |
92 |
93 | {% endblock %}
94 |
--------------------------------------------------------------------------------
/exhibitors/models.py:
--------------------------------------------------------------------------------
1 | import os
2 | import secrets
3 | import string
4 | from django.db import models
5 | from django.conf import settings
6 | from django.utils.translation import gettext_lazy as _
7 | from pretix.base.models import Event
8 |
9 |
10 | def generate_key():
11 | alphabet = string.ascii_lowercase + string.digits
12 | return ''.join(secrets.choice(alphabet) for _ in range(8))
13 |
14 | def generate_booth_id():
15 | import string
16 | import random
17 |
18 | # Generate a random booth_id if none exists
19 | characters = string.ascii_letters + string.digits
20 | while True:
21 | booth_id = ''.join(random.choices(characters, k=8)) # 8-character random string
22 | if not ExhibitorInfo.objects.filter(booth_id=booth_id).exists():
23 | return booth_id
24 |
25 |
26 | def exhibitor_logo_path(instance, filename):
27 | return os.path.join('exhibitors', 'logos', instance.name, filename)
28 |
29 | class ExhibitorSettings(models.Model):
30 | event = models.ForeignKey('pretixbase.Event', on_delete=models.CASCADE)
31 | exhibitors_access_mail_subject = models.CharField(max_length=255)
32 | exhibitors_access_mail_body = models.TextField()
33 | allowed_fields = models.JSONField(default=list)
34 |
35 | @property
36 | def all_allowed_fields(self):
37 | """Return all allowed fields, including required default fields"""
38 | default_fields = ['attendee_name', 'attendee_email']
39 | return list(set(default_fields + self.allowed_fields))
40 |
41 | class Meta:
42 | unique_together = ('event',)
43 |
44 | class ExhibitorInfo(models.Model):
45 | event = models.ForeignKey(Event, on_delete=models.CASCADE)
46 | name = models.CharField(
47 | max_length=190,
48 | verbose_name=_('Name')
49 | )
50 | description = models.TextField(
51 | verbose_name=_('Description'),
52 | null=True,
53 | blank=True
54 | )
55 | url = models.URLField(
56 | verbose_name=_('URL'),
57 | null=True,
58 | blank=True
59 | )
60 | email = models.EmailField(
61 | verbose_name=_('Email'),
62 | null=True,
63 | blank=True
64 | )
65 | logo = models.ImageField(
66 | upload_to=exhibitor_logo_path,
67 | null=True,
68 | blank=True
69 | )
70 | key = models.CharField(
71 | max_length=8,
72 | default=generate_key,
73 | )
74 | booth_id = models.CharField(
75 | max_length=100,
76 | unique=True,
77 | null=True,
78 | blank=True,
79 | )
80 | booth_name = models.CharField(
81 | max_length=100,
82 | verbose_name=_('BoothName'),
83 | )
84 | lead_scanning_enabled = models.BooleanField(
85 | default=False
86 | )
87 | allow_voucher_access = models.BooleanField(default=False)
88 | allow_lead_access = models.BooleanField(default=False)
89 | lead_scanning_scope_by_device = models.BooleanField(default=False)
90 |
91 | class Meta:
92 | ordering = ("name",)
93 |
94 | def __str__(self):
95 | return self.name
96 |
97 | class ExhibitorItem(models.Model):
98 | # If no ExhibitorItem exists => use default
99 | # If ExhibitorItem exists with layout=None => don't print
100 | item = models.OneToOneField('pretixbase.Item', null=True, blank=True, related_name='exhibitor_assignment',
101 | on_delete=models.CASCADE)
102 | exhibitor = models.ForeignKey('ExhibitorInfo', on_delete=models.CASCADE, related_name='item_assignments',
103 | null=True, blank=True)
104 |
105 | class Meta:
106 | ordering = ('id',)
107 |
108 |
109 | class Lead(models.Model):
110 | exhibitor = models.ForeignKey(
111 | ExhibitorInfo,
112 | on_delete=models.CASCADE
113 | )
114 | exhibitor_name = models.CharField(
115 | max_length=190
116 | )
117 | pseudonymization_id = models.CharField(
118 | max_length=190
119 | )
120 | scanned = models.DateTimeField()
121 | scan_type = models.CharField(
122 | max_length=50
123 | )
124 | device_name = models.CharField(
125 | max_length=50
126 | )
127 | attendee = models.JSONField(
128 | null=True,
129 | blank=True
130 | )
131 | booth_id = models.CharField(
132 | max_length=100,
133 | editable=True
134 | )
135 | booth_name = models.CharField(
136 | max_length=100,
137 | verbose_name=_('BoothName'),
138 | )
139 |
140 | def __str__(self):
141 | return f"Lead scanned by {self.exhibitor.name}"
142 |
143 | class ExhibitorTag(models.Model):
144 | exhibitor = models.ForeignKey(
145 | ExhibitorInfo,
146 | on_delete=models.CASCADE,
147 | related_name='tags'
148 | )
149 | name = models.CharField(max_length=50)
150 | use_count = models.IntegerField(default=0)
151 | created_at = models.DateTimeField(auto_now_add=True)
152 |
153 | class Meta:
154 | unique_together = ('exhibitor', 'name')
155 | ordering = ['-use_count', 'name']
156 |
157 | def __str__(self):
158 | return f"{self.name} ({self.exhibitor.name})"
159 |
--------------------------------------------------------------------------------
/exhibitors/templates/exhibitors/add.html:
--------------------------------------------------------------------------------
1 | {% extends "pretixcontrol/event/base.html" %}
2 | {% load i18n %}
3 | {% block title %}
4 | {% if action == 'edit' %}
5 | {% trans "Edit Exhibitor" %}
6 | {% else %}
7 | {% trans "Add Exhibitor" %}
8 | {% endif %}
9 | {% endblock %}
10 |
11 | {% block content %}
12 |
13 | {% if action == 'edit' %}
14 | {% trans "Edit Exhibitor" %}
15 | {% else %}
16 | {% trans "Add Exhibitor" %}
17 | {% endif %}
18 |
19 |
20 |
151 | {% endblock %}
152 |
--------------------------------------------------------------------------------
/exhibitors/views.py:
--------------------------------------------------------------------------------
1 | from django.core.exceptions import PermissionDenied
2 | from django.http import Http404, HttpResponse, JsonResponse
3 | from django.shortcuts import get_object_or_404, redirect
4 | from django.urls import reverse
5 | from django.contrib import messages
6 | from django.utils.translation import gettext, gettext_lazy as _
7 | from django.views import View
8 | from django.views.generic import CreateView, DeleteView, ListView, UpdateView
9 | from pretix.base.forms import SettingsForm
10 | from pretix.base.models import Event
11 | from pretix.control.permissions import EventPermissionRequiredMixin
12 | from pretix.control.views.event import (
13 | EventSettingsFormView, EventSettingsViewMixin,
14 | )
15 | from pretix.helpers.models import modelcopy
16 |
17 | from .forms import ExhibitorInfoForm
18 | from .models import ExhibitorInfo, ExhibitorSettings, generate_booth_id
19 |
20 |
21 | class SettingsView(EventPermissionRequiredMixin, ListView):
22 | model = ExhibitorInfo
23 | template_name = 'exhibitors/settings.html'
24 | context_object_name = 'exhibitors'
25 | permission = 'can_change_settings'
26 |
27 | def get_context_data(self, **kwargs):
28 | ctx = super().get_context_data(**kwargs)
29 | settings, _ = ExhibitorSettings.objects.get_or_create(event=self.request.event)
30 | ctx['settings'] = settings
31 | ctx['default_fields'] = ['attendee_name', 'attendee_email']
32 | return ctx
33 |
34 | def post(self, request, *args, **kwargs):
35 | settings, created = ExhibitorSettings.objects.get_or_create(event=self.request.event)
36 |
37 | # Get selected fields, excluding default fields
38 | allowed_fields = request.POST.getlist('exhibitors_access_voucher')
39 |
40 | # Update settings
41 | settings.allowed_fields = allowed_fields
42 | settings.exhibitors_access_mail_subject = request.POST.get('exhibitors_access_mail_subject', '')
43 | settings.exhibitors_access_mail_body = request.POST.get('exhibitors_access_mail_body', '')
44 | settings.save()
45 |
46 | messages.success(self.request, _('Settings have been saved.'))
47 | return redirect(request.path)
48 |
49 |
50 | class ExhibitorListView(EventPermissionRequiredMixin, ListView):
51 | model = ExhibitorInfo
52 | permission = ('can_change_event_settings', 'can_view_orders')
53 | template_name = 'exhibitors/exhibitor_info.html'
54 | context_object_name = 'exhibitors'
55 |
56 | def get_queryset(self):
57 | return ExhibitorInfo.objects.filter(event=self.request.event)
58 |
59 | def get_success_url(self) -> str:
60 | return reverse('plugins:exhibitors:index', kwargs={
61 | 'organizer': self.request.event.organizer.slug,
62 | 'event': self.request.event.slug
63 | })
64 |
65 |
66 | class ExhibitorCreateView(EventPermissionRequiredMixin, CreateView):
67 | model = ExhibitorInfo
68 | form_class = ExhibitorInfoForm
69 | template_name = 'exhibitors/add.html'
70 | permission = 'can_change_event_settings'
71 |
72 | def form_valid(self, form):
73 | form.instance.event = self.request.event
74 | form.instance.lead_scanning_enabled = (
75 | self.request.POST.get('lead_scanning_enabled') == 'on'
76 | )
77 |
78 | # Only generate booth_id if none was provided
79 | if not form.cleaned_data.get('booth_id'):
80 | form.instance.booth_id = generate_booth_id()
81 |
82 | return super().form_valid(form)
83 |
84 | def get_context_data(self, **kwargs):
85 | context = super().get_context_data(**kwargs)
86 | context['action'] = 'create'
87 | return context
88 |
89 | def get_success_url(self):
90 | return reverse('plugins:exhibitors:info', kwargs={
91 | 'organizer': self.request.event.organizer.slug,
92 | 'event': self.request.event.slug
93 | })
94 |
95 |
96 | class ExhibitorEditView(EventPermissionRequiredMixin, UpdateView):
97 | model = ExhibitorInfo
98 | form_class = ExhibitorInfoForm
99 | template_name = 'exhibitors/add.html'
100 | permission = 'can_change_event_settings'
101 |
102 | def get_initial(self):
103 | initial = super().get_initial()
104 | obj = self.get_object()
105 | initial['lead_scanning_enabled'] = obj.lead_scanning_enabled
106 | return initial
107 |
108 | def form_valid(self, form):
109 | exhibitor = form.save(commit=False)
110 | exhibitor.lead_scanning_enabled = self.request.POST.get('lead_scanning_enabled') == 'on'
111 |
112 | # generate booth_id if none provided and there isn't an existing one
113 | if not form.cleaned_data.get('booth_id') and not exhibitor.booth_id:
114 | exhibitor.booth_id = generate_booth_id()
115 |
116 | exhibitor.save()
117 | return super().form_valid(form)
118 |
119 | def get_context_data(self, **kwargs):
120 | context = super().get_context_data(**kwargs)
121 | context['action'] = 'edit'
122 | return context
123 |
124 | def get_success_url(self):
125 | return reverse('plugins:exhibitors:info', kwargs={
126 | 'organizer': self.request.event.organizer.slug,
127 | 'event': self.request.event.slug
128 | })
129 |
130 |
131 | class ExhibitorDeleteView(EventPermissionRequiredMixin, DeleteView):
132 | model = ExhibitorInfo
133 | template_name = 'exhibitors/delete.html'
134 | permission = ('can_change_event_settings',)
135 |
136 | def get_success_url(self) -> str:
137 | return reverse('plugins:exhibitors:info', kwargs={
138 | 'organizer': self.request.event.organizer.slug,
139 | 'event': self.request.event.slug
140 | })
141 |
142 |
143 | class ExhibitorCopyKeyView(EventPermissionRequiredMixin, View):
144 | permission = ('can_change_event_settings',)
145 |
146 | def get(self, request, *args, **kwargs):
147 | exhibitor = get_object_or_404(ExhibitorInfo, pk=kwargs['pk'])
148 | response = HttpResponse(exhibitor.key)
149 | response['Content-Disposition'] = (
150 | 'attachment; filename="password.txt"'
151 | )
152 | return response
153 |
--------------------------------------------------------------------------------
/exhibitors/api.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import get_object_or_404
2 | from django.utils import timezone
3 | from pretix.api.serializers.i18n import I18nAwareModelSerializer
4 | from pretix.api.serializers.order import CompatibleJSONField
5 | from pretix.base.models import OrderPosition
6 | from rest_framework import status, views, viewsets
7 | from rest_framework.response import Response
8 | from django.core.exceptions import ObjectDoesNotExist
9 | from .models import ExhibitorInfo, ExhibitorItem,ExhibitorSettings , ExhibitorTag, Lead
10 |
11 |
12 | class ExhibitorAuthView(views.APIView):
13 | def post(self, request, *args, **kwargs):
14 | key = request.data.get('key')
15 |
16 | if not key:
17 | return Response(
18 | {'detail': 'Missing parameters'},
19 | status=status.HTTP_400_BAD_REQUEST
20 | )
21 |
22 | try:
23 | exhibitor = ExhibitorInfo.objects.get(key=key)
24 | return Response(
25 | {
26 | 'success': True,
27 | 'exhibitor_id': exhibitor.id,
28 | 'exhibitor_name': exhibitor.name,
29 | 'booth_id': exhibitor.booth_id,
30 | 'booth_name': exhibitor.booth_name,
31 | },
32 | status=status.HTTP_200_OK
33 | )
34 | except ExhibitorInfo.DoesNotExist:
35 | return Response(
36 | {'success': False, 'error': 'Invalid credentials'},
37 | status=status.HTTP_401_UNAUTHORIZED
38 | )
39 |
40 |
41 | class ExhibitorItemAssignmentSerializer(I18nAwareModelSerializer):
42 | class Meta:
43 | model = ExhibitorItem
44 | fields = ('id', 'item', 'exhibitor')
45 |
46 |
47 | class NestedItemAssignmentSerializer(I18nAwareModelSerializer):
48 | class Meta:
49 | model = ExhibitorItem
50 | fields = ('item',)
51 |
52 |
53 | class ExhibitorInfoSerializer(I18nAwareModelSerializer):
54 | class Meta:
55 | model = ExhibitorInfo
56 | fields = ('id', 'name', 'description', 'url', 'email', 'logo', 'key', 'lead_scanning_enabled')
57 |
58 |
59 | class ExhibitorInfoViewSet(viewsets.ReadOnlyModelViewSet):
60 | serializer_class = ExhibitorInfoSerializer
61 | lookup_field = 'id'
62 |
63 | def get_queryset(self):
64 | return ExhibitorInfo.objects.filter(event=self.request.event)
65 |
66 |
67 | class ExhibitorItemViewSet(viewsets.ReadOnlyModelViewSet):
68 | serializer_class = ExhibitorItemAssignmentSerializer
69 | queryset = ExhibitorItem.objects.none()
70 | lookup_field = 'id'
71 |
72 | def get_queryset(self):
73 | return ExhibitorItem.objects.filter(item__event=self.request.event)
74 |
75 |
76 | class LeadCreateView(views.APIView):
77 | def get_allowed_attendee_data(self, order_position, settings, exhibitor):
78 | """Helper method to get allowed attendee data based on settings"""
79 | # Get all allowed fields including defaults
80 | allowed_fields = settings.all_allowed_fields
81 | attendee_data = {
82 | 'name': order_position.attendee_name, # Always included
83 | 'email': order_position.attendee_email, # Always included
84 | 'company': order_position.company, # Always included
85 | 'city': order_position.city if 'attendee_city' in allowed_fields else None,
86 | 'country': str(order_position.country) if 'attendee_country' in allowed_fields else None,
87 | 'note': '',
88 | 'tags': []
89 | }
90 |
91 | return {k: v for k, v in attendee_data.items() if v is not None}
92 |
93 | def post(self, request, *args, **kwargs):
94 | # Extract parameters from the request
95 | pseudonymization_id = request.data.get('lead')
96 | scanned = request.data.get('scanned')
97 | scan_type = request.data.get('scan_type')
98 | device_name = request.data.get('device_name')
99 | open_event = request.data.get('open_event')
100 | key = request.headers.get('Exhibitor')
101 |
102 | if not all([pseudonymization_id, scanned, scan_type, device_name]):
103 | return Response(
104 | {'detail': 'Missing parameters'},
105 | status=status.HTTP_400_BAD_REQUEST
106 | )
107 |
108 | # Authenticate the exhibitor
109 | try:
110 | exhibitor = ExhibitorInfo.objects.get(key=key)
111 | settings = ExhibitorSettings.objects.get(event=exhibitor.event)
112 | except (ExhibitorInfo.DoesNotExist, ExhibitorSettings.DoesNotExist):
113 | return Response(
114 | {'success': False, 'error': 'Invalid exhibitor key'},
115 | status=status.HTTP_401_UNAUTHORIZED
116 | )
117 |
118 | # Get attendee details
119 | try:
120 | if open_event:
121 | order_position = OrderPosition.objects.get(
122 | secret = pseudonymization_id
123 | )
124 | else:
125 | order_position = OrderPosition.objects.get(
126 | pseudonymization_id=pseudonymization_id
127 | )
128 | except OrderPosition.DoesNotExist:
129 | return Response(
130 | {'success': False, 'error': 'Attendee not found'},
131 | status=status.HTTP_404_NOT_FOUND
132 | )
133 |
134 | # Check for duplicate scan
135 | if Lead.objects.filter(
136 | exhibitor=exhibitor,
137 | pseudonymization_id=pseudonymization_id
138 | ).exists():
139 | attendee_data = self.get_allowed_attendee_data(
140 | order_position,
141 | settings,
142 | exhibitor
143 | )
144 | return Response(
145 | {
146 | 'success': False,
147 | 'error': 'Lead already scanned',
148 | 'attendee': attendee_data
149 | },
150 | status=status.HTTP_409_CONFLICT
151 | )
152 |
153 | # Get allowed attendee data based on settings
154 | attendee_data = self.get_allowed_attendee_data(
155 | order_position,
156 | settings,
157 | exhibitor
158 | )
159 | # Create the lead entry
160 | lead = Lead.objects.create(
161 | exhibitor=exhibitor,
162 | exhibitor_name=exhibitor.name,
163 | pseudonymization_id=pseudonymization_id,
164 | scanned=timezone.now(),
165 | scan_type=scan_type,
166 | device_name=device_name,
167 | booth_id=exhibitor.booth_id,
168 | booth_name=exhibitor.booth_name,
169 | attendee=attendee_data
170 | )
171 |
172 | return Response(
173 | {
174 | 'success': True,
175 | 'lead_id': lead.id,
176 | 'attendee': attendee_data
177 | },
178 | status=status.HTTP_201_CREATED
179 | )
180 |
181 |
182 | class LeadRetrieveView(views.APIView):
183 | def get(self, request, *args, **kwargs):
184 | # Authenticate the exhibitor using the key
185 | key = request.headers.get('Exhibitor')
186 | try:
187 | exhibitor = ExhibitorInfo.objects.get(key=key)
188 | except ExhibitorInfo.DoesNotExist:
189 | return Response(
190 | {
191 | 'success': False,
192 | 'error': 'Invalid exhibitor key'
193 | },
194 | status=status.HTTP_401_UNAUTHORIZED
195 | )
196 |
197 | # Fetch all leads associated with the exhibitor
198 | leads = Lead.objects.filter(exhibitor=exhibitor).values(
199 | 'id',
200 | 'pseudonymization_id',
201 | 'exhibitor_name',
202 | 'scanned',
203 | 'scan_type',
204 | 'device_name',
205 | 'booth_id',
206 | 'booth_name',
207 | 'attendee'
208 | )
209 |
210 | return Response(
211 | {
212 | 'success': True,
213 | 'leads': list(leads)
214 | },
215 | status=status.HTTP_200_OK
216 | )
217 |
218 |
219 | class TagListView(views.APIView):
220 | def get(self, request, organizer, event, *args, **kwargs):
221 | key = request.headers.get('Exhibitor')
222 | try:
223 | exhibitor = ExhibitorInfo.objects.get(key=key)
224 | tags = ExhibitorTag.objects.filter(exhibitor=exhibitor)
225 | return Response({
226 | 'success': True,
227 | 'tags': [tag.name for tag in tags]
228 | })
229 | except ExhibitorInfo.DoesNotExist:
230 | return Response(
231 | {
232 | 'success': False,
233 | 'error': 'Invalid exhibitor key'
234 | },
235 | status=status.HTTP_401_UNAUTHORIZED
236 | )
237 |
238 | class LeadUpdateView(views.APIView):
239 | def post(self, request, organizer, event, lead_id, *args, **kwargs):
240 | key = request.headers.get('Exhibitor')
241 | note = request.data.get('note')
242 | tags = request.data.get('tags', [])
243 |
244 | try:
245 | exhibitor = ExhibitorInfo.objects.get(key=key)
246 | except ExhibitorInfo.DoesNotExist:
247 | return Response(
248 | {
249 | 'success': False,
250 | 'error': 'Invalid exhibitor key'
251 | },
252 | status=status.HTTP_401_UNAUTHORIZED
253 | )
254 |
255 | try:
256 | lead = Lead.objects.get(pseudonymization_id=lead_id, exhibitor=exhibitor)
257 | except Lead.DoesNotExist:
258 | return Response(
259 | {
260 | 'success': False,
261 | 'error': 'Lead not found'
262 | },
263 | status=status.HTTP_404_NOT_FOUND
264 | )
265 |
266 | # Update lead's attendee info
267 | attendee_data = lead.attendee or {}
268 | if note is not None:
269 | attendee_data['note'] = note
270 | if tags is not None:
271 | attendee_data['tags'] = tags
272 |
273 | # Update tag usage counts and create new tags
274 | for tag_name in tags:
275 | tag, created = ExhibitorTag.objects.get_or_create(
276 | exhibitor=exhibitor,
277 | name=tag_name
278 | )
279 | if not created:
280 | tag.use_count += 1
281 | tag.save()
282 |
283 | lead.attendee = attendee_data
284 | lead.save()
285 |
286 | return Response(
287 | {
288 | 'success': True,
289 | 'lead_id': lead.id,
290 | 'attendee': lead.attendee
291 | },
292 | status=status.HTTP_200_OK
293 | )
294 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------