Files
pretix_original/src/tests/base/test_event_clone.py
Phin Wolkwitz fd9d03786b Add program times for items (Z#23178639)
* Add program times for items

* Fix frontend date validation

* Add ical data for program times [wip]

* Improve ical data for program times

* Remove duplicate code and add comments

* Adjust migration

* Remove program times form for event series

* Add pdf placeholder [wip]

* Improve explanation text with suggestion

Co-authored-by: Raphael Michel <michel@pretix.eu>

* Fix import sorting

* Improve ical generation

* Improve ical entry description

* Fix migration

* Add copyability for program times fot items and events

* Update migration

* Add API endpoints/functions, fix isort

* Improve variable name

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Remove todo comment

* Add documentation, Change endpoint name

* Change related name

* Remove unnecessary code block

* Add program times to item API

* Fix imports

* Add log text

* Use daterange helper

* Add and update API tests

* Add another API test

* Add program times to cloning tests

* Update query count because of program times query

* Invalidate cached tickets on program time changes

* Reduce invalidation calls

* Update migration after rebase

* Apply improvements to invalidation from review

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* remove unneccessary attr=item param

* remove unnecessary kwargs for formset_factory

* fix local var name being overwritten in for-loop

* fix empty formset being saved

* Use subevent if available

* make code less verbose

* remove double event-label in ical desc

* fix unnecessary var re-assign

* fix ev vs p.subevent

---------

Co-authored-by: Raphael Michel <michel@pretix.eu>
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2025-11-06 12:24:47 +01:00

287 lines
13 KiB
Python

#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
#
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
#
# This file contains Apache-licensed contributions copyrighted by: Christopher Dambamuromo, Enrique Saez, Tobias Kunze
#
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
import datetime
from datetime import timedelta
import pytest
from django.utils.timezone import now
from django_scopes import scopes_disabled
from pretix.base.models import (
Event, ItemProgramTime, Organizer, Question, SeatingPlan,
)
from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue
@pytest.mark.django_db
@scopes_disabled()
def test_full_clone_same_organizer():
organizer = Organizer.objects.create(name='Dummy', slug='dummy')
membership_type = organizer.membership_types.create(name="Membership")
plan = SeatingPlan.objects.create(name="Plan", organizer=organizer, layout="{}")
sc = organizer.sales_channels.get(identifier="web")
event = Event.objects.create(
organizer=organizer, name='Dummy', slug='dummy',
date_from=now(),
date_admission=now() - timedelta(hours=1),
date_to=now() + timedelta(hours=1),
testmode=True,
seating_plan=plan,
all_sales_channels=False,
)
event.limit_sales_channels.add(sc)
item_meta = event.item_meta_properties.create(name="Bla")
tax_rule = event.tax_rules.create(name="VAT", rate=19)
category = event.categories.create(name="Tickets")
cross_sell_category = event.categories.create(name="Recommendations", cross_selling_mode="only",
cross_selling_condition="products")
q1 = event.quotas.create(name="Quota 1", size=5)
q2 = event.quotas.create(name="Quota 2", size=0, closed=True)
item1 = event.items.create(category=category, tax_rule=tax_rule, name="Ticket", default_price=23,
grant_membership_type=membership_type, hidden_if_available=q2,
all_sales_channels=False)
item1.limit_sales_channels.add(sc)
# todo: test that item pictures are copied, not linked
ItemMetaValue.objects.create(item=item1, property=item_meta, value="Foo")
assert item1.meta_data
ItemProgramTime.objects.create(item=item1,
start=datetime.datetime(2017, 12, 27, 0, 0, 0, tzinfo=datetime.timezone.utc),
end=datetime.datetime(2017, 12, 28, 0, 0, 0, tzinfo=datetime.timezone.utc))
assert item1.program_times
item2 = event.items.create(category=category, tax_rule=tax_rule, name="T-shirt", default_price=15,
hidden_if_item_available=item1)
item2v = item2.variations.create(value="red", default_price=15, all_sales_channels=False)
item2v.limit_sales_channels.add(sc)
item2v.meta_values.create(property=item_meta, value="Bar")
item2.require_membership_types.add(membership_type)
ItemAddOn.objects.create(base_item=item1, addon_category=category)
ItemBundle.objects.create(base_item=item1, bundled_item=item2, bundled_variation=item2v)
q1.items.add(item1)
q2.items.add(item2)
q2.variations.add(item2v)
cross_sell_category.cross_selling_match_products.add(item1)
event.discounts.create(internal_name="Fake discount")
question1 = event.questions.create(question="Yes or no", type=Question.TYPE_BOOLEAN)
question2 = event.questions.create(question="Size", type=Question.TYPE_CHOICE_MULTIPLE,
dependency_question=question1)
question2.options.create(answer="Foobar")
event.seat_category_mappings.create(
layout_category='Stalls', product=item1
)
event.seats.create(seat_number="A1", product=item1, seat_guid="A1")
clist = event.checkin_lists.create(name="Default", rules={
"or": [
{
"inList": [
{"var": "product"}, {
"objectList": [
{"lookup": ["product", str(item1.pk), "Ticket"]},
]
}
]
},
{
"inList": [
{"var": "variation"}, {
"objectList": [
{"lookup": ["variation", str(item2v.pk), "T-shirt - red"]},
]
}
]
}
],
})
clist.limit_products.add(item1)
copied_event = Event.objects.create(
organizer=organizer, name='Dummy2', slug='dummy2',
date_from=datetime.datetime(2022, 4, 15, 9, 0, 0, tzinfo=datetime.timezone.utc),
)
copied_event.copy_data_from(event)
copied_event.refresh_from_db()
event.refresh_from_db()
# Verify event properties
assert abs(copied_event.date_admission - (copied_event.date_from - timedelta(hours=1))) < timedelta(minutes=1)
assert copied_event.testmode
assert copied_event.limit_sales_channels.get() == sc
# Verify that we actually *copied*, not just moved objects over
assert event.tax_rules.count() == copied_event.tax_rules.count() == 1
assert event.checkin_lists.count() == copied_event.checkin_lists.count() == 1
assert event.quotas.count() == copied_event.quotas.count() == 2
assert event.items.count() == copied_event.items.count() == 2
assert event.discounts.count() == copied_event.discounts.count() == 1
assert event.questions.count() == copied_event.questions.count() == 2
assert event.seat_category_mappings.count() == copied_event.seat_category_mappings.count() == 1
assert event.seats.count() == copied_event.seats.count() == 1
assert event.limit_sales_channels.get() == sc
# Verify relationship integrity
copied_q1 = copied_event.quotas.get(name=q1.name)
copied_q2 = copied_event.quotas.get(name=q2.name)
copied_item1 = copied_event.items.get(name=item1.name)
copied_item2 = copied_event.items.get(name=item2.name)
assert copied_item1.tax_rule == copied_event.tax_rules.get()
assert copied_item1.category == copied_event.categories.get(name='Tickets')
assert copied_item1.limit_sales_channels.get() == sc
assert copied_item1.meta_data == item1.meta_data
assert copied_item1.program_times.first().start == item1.program_times.first().start
assert copied_item1.program_times.first().end == item1.program_times.first().end
assert copied_item2.variations.get().meta_data == item2v.meta_data
assert copied_item1.hidden_if_available == copied_q2
assert copied_item1.grant_membership_type == membership_type
assert copied_item2.variations.count() == 1
assert copied_item2.variations.get().limit_sales_channels.get() == sc
assert copied_item2.require_membership_types.count() == 1
assert copied_item2.require_membership_types.get() == membership_type
assert copied_item1.addons.get().addon_category == copied_event.categories.get(name='Tickets')
assert copied_item1.bundles.get().bundled_item == copied_item2
assert copied_item1.bundles.get().bundled_variation == copied_item2.variations.get()
assert copied_item2.hidden_if_item_available == copied_item1
assert copied_q1.items.get() == copied_item1
assert copied_q2.items.get() == copied_item2
assert copied_q2.variations.get() == copied_item2.variations.get()
copied_cross_sell_category = copied_event.categories.get(name=cross_sell_category.name)
assert copied_cross_sell_category.cross_selling_match_products.get() == copied_item1
copied_question1 = copied_event.questions.get(type=question1.type)
copied_question2 = copied_event.questions.get(type=question2.type)
assert copied_question2.dependency_question == copied_question1
assert copied_question2.dependency_question == copied_question1
assert copied_event.seat_category_mappings.get().product == copied_item1
assert copied_event.seats.get().product == copied_item1
copied_clist = copied_event.checkin_lists.get()
assert copied_clist.rules == {
"or": [
{
"inList": [
{"var": "product"}, {
"objectList": [
{"lookup": ["product", str(copied_item1.pk), "Ticket"]},
]
}
]
},
{
"inList": [
{"var": "variation"}, {
"objectList": [
{"lookup": ["variation", str(copied_item2.variations.get().pk), "T-shirt - red"]},
]
}
]
}
],
}
assert copied_clist.limit_products.get() == copied_item1
# todo: test that the plugin hook is called
# todo: test custom style
# todo: test that files in settings are copied not linked
# todo: test that references to questions in ticket layouts are updated
@pytest.mark.django_db
@scopes_disabled()
def test_full_clone_cross_organizer_differences():
organizer = Organizer.objects.create(name='Dummy', slug='dummy')
organizer2 = Organizer.objects.create(name='Dummy2', slug='dummy2')
membership_type = organizer.membership_types.create(name="Membership")
plan = SeatingPlan.objects.create(name="Plan", organizer=organizer, layout="{}")
sc1_a = organizer.sales_channels.get(identifier="web")
sc1_b = organizer.sales_channels.create(identifier="b")
sc1_c = organizer.sales_channels.create(identifier="c")
sc2_a = organizer2.sales_channels.get(identifier="web")
sc2_c = organizer2.sales_channels.create(identifier="c")
event = Event.objects.create(
organizer=organizer, name='Dummy', slug='dummy',
date_from=now(),
date_admission=now() - timedelta(hours=1),
date_to=now() + timedelta(hours=1),
testmode=True,
seating_plan=plan,
all_sales_channels=False,
)
event.limit_sales_channels.add(sc1_a)
event.limit_sales_channels.add(sc1_b)
event.limit_sales_channels.add(sc1_c)
item1 = event.items.create(name="Ticket", default_price=23,
grant_membership_type=membership_type,
all_sales_channels=False)
item1.limit_sales_channels.add(sc1_a)
item2 = event.items.create(name="T-shirt", default_price=15)
item2.require_membership_types.add(membership_type)
event.settings.payment_giftcard__enabled = True
event.settings.payment_giftcard__restrict_to_sales_channels = ['web', 'b', 'c']
copied_event = Event.objects.create(
organizer=organizer2, name='Dummy2', slug='dummy2',
date_from=datetime.datetime(2022, 4, 15, 9, 0, 0, tzinfo=datetime.timezone.utc),
)
copied_event.copy_data_from(event)
copied_event.refresh_from_db()
event.refresh_from_db()
assert organizer2.seating_plans.count() == 1
assert organizer2.seating_plans.get().layout == plan.layout
assert copied_event.seating_plan.organizer == organizer2
assert set(copied_event.limit_sales_channels.all()) == {sc2_a, sc2_c}
assert event.seating_plan.organizer == organizer
copied_item1 = copied_event.items.get(name=item1.name)
copied_item2 = copied_event.items.get(name=item2.name)
assert copied_item1.grant_membership_type is None
assert copied_item2.require_membership_types.count() == 0
assert copied_item1.limit_sales_channels.get() == sc2_a
assert event.settings.get('payment_giftcard__restrict_to_sales_channels', as_type=list) == ['web', 'b', 'c']
assert copied_event.settings.get('payment_giftcard__restrict_to_sales_channels', as_type=list) == ['web', 'c']