mirror of
https://github.com/pretix/pretix.git
synced 2026-09-14 16:24:43 +00:00
WIP
This commit is contained in:
+36
@@ -0,0 +1,36 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, inject, reactive, watchEffect } from "vue";
|
||||||
|
import { StoreKey } from "../../walletStore";
|
||||||
|
import PlaceholderFieldgroupPreview from "./placeholder-fieldgroup-preview.vue";
|
||||||
|
|
||||||
|
const store = inject(StoreKey)!
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
config: Record<string, string>;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const style_def = Object.fromEntries(store.currentPlatformStyles[store.currentPlatformLayout.style].fieldgroups.map(x => [x.identifier, x]))[props.config.fieldgroup]
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template lang="pug">
|
||||||
|
PlaceholderFieldgroupPreview(v-if="style_def.type == 'placeholder'" :config="config" :style_def="style_def")
|
||||||
|
//- // TODO: support predefined group
|
||||||
|
//- div(:style="{'flex-grow': config.relSize}")
|
||||||
|
//- div(style="background-color: gray; display: flex; flex-direction: row;")
|
||||||
|
//- div(v-for="entry in layout.entries.slice(0,style_def.max_entries)")
|
||||||
|
//- div(v-if="style_def.labels") {{ i18nstringLocalize(entry.label) }}
|
||||||
|
//- div(v-if="entry.type == 'custom'") {{ i18nstringLocalize(entry.content) }}
|
||||||
|
//- div(v-if="entry.type == 'placeholder'") {{ store.variables[style_def.content_type][entry.content] }}
|
||||||
|
//- pre
|
||||||
|
//- //- code {{ config }}
|
||||||
|
//- code {{ style_def }}
|
||||||
|
//- code {{ layout }}
|
||||||
|
//- div(style="position: absolute; background-color: red; top: 0.5cm; left: 0cm; height: 1cm; width: 2cm;") Logo
|
||||||
|
//- div(style="position: absolute; background-color: red; top: 0.5cm; left: 2.25cm; height: 1cm; width: 5.5cm;") Logo Text
|
||||||
|
//- div(style="position: absolute; background-color: red; top: 0.5cm; left: 8cm; height: 1cm; width: 2cm;") Header
|
||||||
|
//- div(style="position: absolute; background-color: red; top: 2.5cm; left: 0cm; height: 2cm; width: 10cm;") Primary
|
||||||
|
//- div(style="position: absolute; background-color: red; top: 4.75cm; left: 0cm; height: 1cm; width: 10cm;") Secondary
|
||||||
|
//- div(style="position: absolute; background-color: red; top: 7cm; left: 0cm; height: 1cm; width: 10cm;") Auxiliary
|
||||||
|
//- div(style="position: absolute; background-color: red; top: 10cm; left: 3cm; height: 4cm; width: 4cm;")
|
||||||
|
</template>
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { inject } from "vue";
|
||||||
|
import { StoreKey } from "../../walletStore.js";
|
||||||
|
import RowPreview from "./row-preview.vue";
|
||||||
|
|
||||||
|
const store = inject(StoreKey)!;
|
||||||
|
|
||||||
|
const props = defineProps<{ layout: Array<PreviewLayout> }>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template lang="pug">
|
||||||
|
div.pass-container
|
||||||
|
div.pass-content
|
||||||
|
RowPreview(v-for="row in layout" :config="row")
|
||||||
|
</template>
|
||||||
|
<style lang="css" scoped>
|
||||||
|
.pass-container {
|
||||||
|
margin: 1em;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 9 / 11;
|
||||||
|
max-width: 360px;
|
||||||
|
max-height: 440px;
|
||||||
|
vertical-align: top;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
outline: solid 1px #949494;
|
||||||
|
border-radius: 1em;
|
||||||
|
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
.pass-content {
|
||||||
|
overflow: scroll;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, inject, reactive, watchEffect } from "vue";
|
||||||
|
import { StoreKey } from "../../walletStore";
|
||||||
|
import { i18nstringLocalize } from "../../helpers";
|
||||||
|
|
||||||
|
const store = inject(StoreKey)!;
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
config: PreviewFieldgroup;
|
||||||
|
style_def: PlaceholderFieldGroupDefinition;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template lang="pug">
|
||||||
|
// TODO: support predefined group
|
||||||
|
div.fieldgroup-container(:style="{'flex-grow': config.relSize, 'flex-direction': config.direction || 'row'}")
|
||||||
|
div.fieldgroup-item(v-for="{ entry, label, content } in store.currentLayoutFieldContent[config.fieldgroup]")
|
||||||
|
div.fieldgroup-item-qrcode(v-if="style_def.display == 'code'") {{ label }}
|
||||||
|
|
||||||
|
template(v-else)
|
||||||
|
div.fieldgroup-label.nowrap(v-if="style_def.display == 'with_label'") {{ label }}
|
||||||
|
div.nowrap.content(v-if="style_def.content_type == 'text'" :class="config.display") {{ content }}
|
||||||
|
img.fieldgroup-item-image(v-else-if="style_def.content_type == 'image' && content" :src="i18nstringLocalize(content)")
|
||||||
|
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
.fieldgroup-container {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5em;
|
||||||
|
min-height: 2lh;
|
||||||
|
}
|
||||||
|
.fieldgroup-label {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
.fieldgroup-item {
|
||||||
|
flex: 0 1 100%;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.fieldgroup-item-image {
|
||||||
|
max-height: 3lh;
|
||||||
|
}
|
||||||
|
.fieldgroup-item-qrcode {
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: lightgray;
|
||||||
|
width: 50%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
margin: auto;
|
||||||
|
/* center content */
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: 1em;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nowrap {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
text-wrap: nowrap;
|
||||||
|
}
|
||||||
|
.bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.large {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
.tight {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import FieldgroupPreview from './fieldgroup-preview.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
config: Record<string, string>;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template lang="pug">
|
||||||
|
div(v-if="'children' in config" style="width: 100%; display: flex; gap: 1em")
|
||||||
|
RowPreview(v-for="child in config.children" :config="child")
|
||||||
|
FieldgroupPreview(v-else :config="config")
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
export function i18nstringLocalize(s: I18nString): string {
|
||||||
|
if (typeof s === 'string') {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
if (s === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
var locale = document.body.attributes['data-pretixlocale'].value
|
||||||
|
var short_locale = locale.split('-')[0]
|
||||||
|
if (locale in s)
|
||||||
|
return s[locale]
|
||||||
|
|
||||||
|
if (short_locale in s)
|
||||||
|
return s[short_locale]
|
||||||
|
|
||||||
|
for (const k of Object.keys(s)) {
|
||||||
|
if (k.split('-')[0] === short_locale && s[k]) {
|
||||||
|
return s[k]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s['en'])
|
||||||
|
return s['en']
|
||||||
|
|
||||||
|
for (const k of Object.keys(s)) {
|
||||||
|
if (s[k]) {
|
||||||
|
return s[k]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,10 +1,210 @@
|
|||||||
from .base import FieldGroupDisplay, PassStyle, PredefinedFieldGroup, TextFieldGroup, WalletPlatform
|
from pretix.base.models import Event, OrderPosition
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
from .base import FieldGroupDisplay, PassLayout, PassStyle, PredefinedFieldGroup, TextFieldGroup, WalletPlatform
|
||||||
|
from django.utils.translation import gettext_lazy as _, gettext
|
||||||
|
import json
|
||||||
|
|
||||||
|
from walletobjects import ButtonJWT, EventTicketClass, EventTicketObject
|
||||||
|
from walletobjects.comms import Comms
|
||||||
|
from walletobjects.constants import (
|
||||||
|
Barcode, ClassType, ConfirmationCode, DoorsOpen,
|
||||||
|
MultipleDevicesAndHoldersAllowedStatus, ObjectState, ObjectType,
|
||||||
|
ReviewStatus, Seat,
|
||||||
|
)
|
||||||
|
from pretix.base.settings import GlobalSettingsObject
|
||||||
|
import uuid
|
||||||
|
from pretix.multidomain.urlreverse import eventreverse_absolute
|
||||||
|
from django.utils import translation
|
||||||
|
|
||||||
|
def _get_instance_uuid():
|
||||||
|
gs = GlobalSettingsObject()
|
||||||
|
if not gs.settings.wallet_google_instance_uuid:
|
||||||
|
gs.settings.wallet_google_instance_uuid = str(uuid.uuid4())
|
||||||
|
return gs.settings.get("wallet_google_instance_uuid")
|
||||||
|
|
||||||
|
def get_class_id(event: Event):
|
||||||
|
instance_uuid = _get_instance_uuid()
|
||||||
|
issuer_id = event.settings.get('wallet_google_issuer_id')
|
||||||
|
return "%s.pretix-%s-%s-%s" % (issuer_id, instance_uuid, event.organizer.slug, event.slug)
|
||||||
|
|
||||||
|
|
||||||
|
def get_object_id(op: OrderPosition):
|
||||||
|
instance_uuid = _get_instance_uuid()
|
||||||
|
issuer_id = op.order.event.settings.get('wallet_google_issuer_id')
|
||||||
|
|
||||||
|
return "%s.pretix-%s-%s-%s-%s-%s" % (issuer_id, instance_uuid,
|
||||||
|
op.order.event.organizer.slug, op.order.event.slug,
|
||||||
|
op.order.code, op.positionid)
|
||||||
|
|
||||||
|
def get_translated_dict(string, locales):
|
||||||
|
translated = {}
|
||||||
|
|
||||||
|
for locale in locales:
|
||||||
|
translation.activate(locale)
|
||||||
|
translated[locale] = gettext(string)
|
||||||
|
translation.deactivate()
|
||||||
|
|
||||||
|
return translated
|
||||||
|
|
||||||
|
|
||||||
|
def get_translated_string(string, locale):
|
||||||
|
translation.activate(locale)
|
||||||
|
translated = gettext(string)
|
||||||
|
translation.deactivate()
|
||||||
|
|
||||||
|
return translated
|
||||||
|
|
||||||
class GooglePlatform(WalletPlatform):
|
class GooglePlatform(WalletPlatform):
|
||||||
identifier = "google"
|
identifier = "google"
|
||||||
name = _("Google")
|
name = _("Google")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def generate(cls, layout: PassLayout, op: OrderPosition):
|
||||||
|
class_object = cls._generate_class(op.order.event)
|
||||||
|
|
||||||
|
# ticket_object = cls._get_object(op)
|
||||||
|
|
||||||
|
# if not ticket_object:
|
||||||
|
# return False
|
||||||
|
|
||||||
|
# generated_jwt = self._comms().sign_jwt(
|
||||||
|
# ButtonJWT(
|
||||||
|
# origins=[django_settings.SITE_URL],
|
||||||
|
# issuer=self._comms().client_email,
|
||||||
|
# event_ticket_objects=[ticket_object],
|
||||||
|
# skinny=True
|
||||||
|
# )
|
||||||
|
# )
|
||||||
|
|
||||||
|
# if generated_jwt:
|
||||||
|
# return 'googlepaypass', 'text/uri-list', 'https://pay.google.com/gp/v/save/%s' % generated_jwt
|
||||||
|
# else:
|
||||||
|
# return False
|
||||||
|
|
||||||
|
data = {"class": class_object}
|
||||||
|
return 'pass.json', 'application/json', json.dumps(data)
|
||||||
|
# from ..views import get_layout_variables
|
||||||
|
|
||||||
|
# order = op.order
|
||||||
|
# event = order.event
|
||||||
|
# filename = "{}-{}.pkpass".format(order.event.slug, order.code)
|
||||||
|
|
||||||
|
# ticket = str(op.item.name)
|
||||||
|
# if op.variation:
|
||||||
|
# ticket += " - " + str(op.variation)
|
||||||
|
|
||||||
|
# serialNumber = "%s-%s-%s-%d" % (
|
||||||
|
# order.event.organizer.slug,
|
||||||
|
# order.event.slug,
|
||||||
|
# order.code,
|
||||||
|
# op.pk,
|
||||||
|
# )
|
||||||
|
|
||||||
|
# context = {
|
||||||
|
# "placeholders": get_layout_variables(op.order.event),
|
||||||
|
# "evaluation_context": [op, order, order.event],
|
||||||
|
# "ca_certificate": order.event.settings.wallet_apple_ca_certificate.read(),
|
||||||
|
# "certificate": order.event.settings.wallet_apple_certificate.read(),
|
||||||
|
# "key": order.event.settings.wallet_apple_key.read(),
|
||||||
|
# "password": order.event.settings.wallet_apple_key_password,
|
||||||
|
# "description": _("Ticket for {event} ({product})").format( # TODO: i18n
|
||||||
|
# event=event.name, product=ticket
|
||||||
|
# ),
|
||||||
|
# "organizationName": event.organizer.name,
|
||||||
|
# "passTypeIdentifier": order.event.settings.wallet_apple_pass_type_id,
|
||||||
|
# "teamIdentifier": order.event.settings.wallet_apple_team_id,
|
||||||
|
# "serialNumber": serialNumber,
|
||||||
|
# "locales": event.settings.locales,
|
||||||
|
# }
|
||||||
|
|
||||||
|
# data = layout.generate(context)
|
||||||
|
# return filename, "application/vnd.apple.pkpass", data
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _generate_class(cls, event: Event):
|
||||||
|
class_name = get_class_id(event)
|
||||||
|
|
||||||
|
output_class = EventTicketClass(
|
||||||
|
event.organizer.name,
|
||||||
|
class_name,
|
||||||
|
MultipleDevicesAndHoldersAllowedStatus.multipleHolders, # TODO: Make configurable
|
||||||
|
event.name,
|
||||||
|
ReviewStatus.draft, # TODO: ReviewStatus.underReview,
|
||||||
|
event.settings.locale
|
||||||
|
)
|
||||||
|
|
||||||
|
output_class.homepage_uri(
|
||||||
|
eventreverse_absolute(event, 'presale:event.index'),
|
||||||
|
get_translated_string('Website', event.settings.locale),
|
||||||
|
get_translated_dict('Website', event.settings.locales)
|
||||||
|
)
|
||||||
|
|
||||||
|
# output_class.callback_url(eventreverse_absolute(event.organizer, 'plugins:pretix_googlepaypasses:webhook'))
|
||||||
|
|
||||||
|
# if (event.settings.get('ticketoutput_googlepaypasses_latitude')
|
||||||
|
# and event.settings.get('ticketoutput_googlepaypasses_longitude')):
|
||||||
|
# output_class.locations(
|
||||||
|
# event.settings.get('ticketoutput_googlepaypasses_latitude'),
|
||||||
|
# event.settings.get('ticketoutput_googlepaypasses_longitude')
|
||||||
|
# )
|
||||||
|
# elif event.geo_lat and event.geo_lon:
|
||||||
|
# output_class.locations(
|
||||||
|
# event.geo_lat,
|
||||||
|
# event.geo_lon
|
||||||
|
# )
|
||||||
|
|
||||||
|
output_class.country_code(event.settings.locale)
|
||||||
|
|
||||||
|
output_class.hide_barcode(False)
|
||||||
|
|
||||||
|
# if event.settings.get('ticketoutput_googlepaypasses_hero'):
|
||||||
|
# output_class.hero_image(
|
||||||
|
# urljoin(django_settings.SITE_URL, event.settings.get('ticketoutput_googlepaypasses_hero').url),
|
||||||
|
# str(event.name),
|
||||||
|
# event.name,
|
||||||
|
# )
|
||||||
|
|
||||||
|
# output_class.hex_background_color(event.settings.get('primary_color'))
|
||||||
|
# output_class.event_id('pretix-%s-%s-%s' % (gs.settings.get('update_check_id'), event.organizer.id, event.id))
|
||||||
|
|
||||||
|
# if event.settings.get('ticketoutput_googlepaypasses_logo'):
|
||||||
|
# output_class.logo(
|
||||||
|
# urljoin(django_settings.SITE_URL, event.settings.get('ticketoutput_googlepaypasses_logo').url),
|
||||||
|
# str(event.name),
|
||||||
|
# event.name,
|
||||||
|
# )
|
||||||
|
|
||||||
|
# if event.location:
|
||||||
|
# name = {}
|
||||||
|
# address = {}
|
||||||
|
|
||||||
|
# for key, value in event.location.data.items():
|
||||||
|
# lines = value.splitlines()
|
||||||
|
# name[key] = lines[0]
|
||||||
|
# # We must provide at least one address line each for the name and address - no way around it.
|
||||||
|
# if len(lines) > 1:
|
||||||
|
# address[key] = '\n'.join(value.splitlines()[1:])
|
||||||
|
# else:
|
||||||
|
# address[key] = lines[0]
|
||||||
|
|
||||||
|
# output_class.venue(name, address)
|
||||||
|
|
||||||
|
# if event.date_from and event.date_to and event.date_admission:
|
||||||
|
# output_class.date_time(
|
||||||
|
# DoorsOpen.doorsOpen,
|
||||||
|
# event.date_admission.isoformat(),
|
||||||
|
# event.date_from.isoformat(),
|
||||||
|
# event.date_to.isoformat(),
|
||||||
|
# )
|
||||||
|
|
||||||
|
# output_class.confirmation_code_label(ConfirmationCode.orderNumber)
|
||||||
|
|
||||||
|
# if event.seating_plan_id is not None:
|
||||||
|
# output_class.seat_label(Seat.seat)
|
||||||
|
|
||||||
|
# return self._comms().put_item(ClassType.eventTicketClass, class_name, output_class)
|
||||||
|
return output_class._eventTicketClass
|
||||||
|
|
||||||
|
|
||||||
class GoogleWalletStyle(PassStyle):
|
class GoogleWalletStyle(PassStyle):
|
||||||
platform = GooglePlatform
|
platform = GooglePlatform
|
||||||
|
|||||||
@@ -71,7 +71,9 @@ class WalletOutput(BaseTicketOutput):
|
|||||||
wallet_layout = op.item.walletlayout
|
wallet_layout = op.item.walletlayout
|
||||||
else:
|
else:
|
||||||
wallet_layout = op.event.wallet_layouts.get(default=True)
|
wallet_layout = op.event.wallet_layouts.get(default=True)
|
||||||
platform_layout = get_object_or_404(wallet_layout.platform_layouts, platform=self.platform.identifier)
|
platform_layout = get_object_or_404(
|
||||||
|
wallet_layout.platform_layouts, platform=self.platform.identifier
|
||||||
|
)
|
||||||
return self.platform.generate(platform_layout.pass_layout, op)
|
return self.platform.generate(platform_layout.pass_layout, op)
|
||||||
|
|
||||||
|
|
||||||
@@ -81,6 +83,48 @@ class GoogleWalletTicketOutput(WalletOutput):
|
|||||||
download_button_text = "Add to Google Wallet"
|
download_button_text = "Add to Google Wallet"
|
||||||
platform = GooglePlatform
|
platform = GooglePlatform
|
||||||
|
|
||||||
|
def get_global_settings(sender, **kwargs):
|
||||||
|
return OrderedDict(
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"wallet_google_issuer_id",
|
||||||
|
forms.CharField(
|
||||||
|
label=_("Google Wallet Issuer/Merchant ID"),
|
||||||
|
help_text=_(
|
||||||
|
"After getting accepted by Google into the Google Pay API for Passes program, "
|
||||||
|
"your Issuer ID can be found in the Merchant center at "
|
||||||
|
"https://wallet.google.com/merchant/walletobjects/"
|
||||||
|
),
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"wallet_google_credentials",
|
||||||
|
forms.FileField(
|
||||||
|
label=_("Google Wallet Service Account Credentials"),
|
||||||
|
help_text=_(
|
||||||
|
"Please paste the contents of the JSON credentials file "
|
||||||
|
"of the service account you tied to your Google Pay API "
|
||||||
|
"for Passes Issuer ID"
|
||||||
|
),
|
||||||
|
required=False,
|
||||||
|
# validators=[validate_json_credentials]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"wallet_google_instance_uuid",
|
||||||
|
forms.CharField(
|
||||||
|
label=_("Google Wallet Pass Instance UUID"),
|
||||||
|
help_text=_(
|
||||||
|
"Instance-specific part to be added to the wallet ids"
|
||||||
|
),
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AppleWalletTicketOutput(WalletOutput):
|
class AppleWalletTicketOutput(WalletOutput):
|
||||||
identifier = "wallet_apple"
|
identifier = "wallet_apple"
|
||||||
@@ -129,7 +173,7 @@ class AppleWalletTicketOutput(WalletOutput):
|
|||||||
label=_("Apple Wallet Pass secret key"),
|
label=_("Apple Wallet Pass secret key"),
|
||||||
required=False,
|
required=False,
|
||||||
validators=[validate_rsa_privkey],
|
validators=[validate_rsa_privkey],
|
||||||
widget=ClearableBasenameFileInput
|
widget=ClearableBasenameFileInput,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
|||||||
Reference in New Issue
Block a user