示例#1
0
    def update_view(self):
        url = get_redirect_target() or self.get_url(".index_view")
        if request.method != "POST":
            return redirect(url)
        change_form = OfferChangeForm(request.form)
        if change_form.validate():
            offer_ids: List[str] = change_form.ids.data.split(",")
            criteria: List[OfferCriterion] = change_form.data["tags"]
            remove_other_tags = change_form.data["remove_other_tags"]

            if remove_other_tags:
                OfferCriterion.query.filter(
                    OfferCriterion.offerId.in_(offer_ids)).delete(
                        synchronize_session=False)

            offer_criteria: List[OfferCriterion] = []
            for criterion in criteria:
                offer_criteria.extend(
                    OfferCriterion(offerId=offer_id, criterionId=criterion.id)
                    for offer_id in offer_ids if OfferCriterion.query.filter(
                        OfferCriterion.offerId == offer_id, OfferCriterion.
                        criterionId == criterion.id).first() is None)

            db.session.bulk_save_objects(offer_criteria)
            db.session.commit()

            # synchronize with external apis that generate playlists based on tags
            search.async_index_offer_ids(offer_ids)
            return redirect(url)

        # Form didn't validate
        flash("Le formulaire est invalide: %s" % (change_form.errors), "error")
        return redirect(url, code=307)
示例#2
0
def update_pending_offer_validation(
        offer: Offer, validation_status: OfferValidationStatus) -> bool:
    offer = offer_queries.get_offer_by_id(offer.id)
    if offer.validation != OfferValidationStatus.PENDING:
        logger.info(
            "Offer validation status cannot be updated, initial validation status is not PENDING. %s",
            extra={"offer": offer.id},
        )
        return False
    offer.validation = validation_status
    if validation_status == OfferValidationStatus.APPROVED:
        offer.isActive = True

    try:
        db.session.commit()
    except Exception as exception:  # pylint: disable=broad-except
        logger.exception(
            "Could not update offer validation status: %s",
            extra={
                "offer": offer.id,
                "validation_status": validation_status,
                "exc": str(exception)
            },
        )
        return False
    search.async_index_offer_ids([offer.id])
    logger.info("Offer validation status updated", extra={"offer": offer.id})
    return True
示例#3
0
def _cancel_booking(booking: Booking, reason: BookingCancellationReasons) -> None:
    """Cancel booking and update a user's credit information on Batch"""
    with transaction():
        stock = offers_repository.get_and_lock_stock(stock_id=booking.stockId)
        db.session.refresh(booking)
        try:
            booking.cancel_booking()
        except (BookingIsAlreadyUsed, BookingIsAlreadyCancelled) as e:
            logger.info(
                str(e),
                extra={
                    "booking": booking.id,
                    "reason": str(reason),
                },
            )
            return
        booking.cancellationReason = reason
        stock.dnBookedQuantity -= booking.quantity
        repository.save(booking, stock)
    logger.info(
        "Booking has been cancelled",
        extra={
            "booking": booking.id,
            "reason": str(reason),
        },
    )

    if booking.individualBooking is not None:
        update_external_user(booking.individualBooking.user)

    search.async_index_offer_ids([booking.stock.offerId])
示例#4
0
    def after_model_change(self,
                           form: wtforms.Form,
                           offer: Offer,
                           is_created: bool = False) -> None:
        if hasattr(form, "validation"):
            previous_validation = form._fields["validation"].object_data
            new_validation = offer.validation
            if previous_validation != new_validation:
                offer.lastValidationDate = datetime.utcnow()
                if new_validation == OfferValidationStatus.APPROVED:
                    offer.isActive = True
                if new_validation == OfferValidationStatus.REJECTED:
                    offer.isActive = False
                    cancelled_bookings = cancel_bookings_from_rejected_offer(
                        offer)
                    if cancelled_bookings:
                        send_cancel_booking_notification.delay(
                            [booking.id for booking in cancelled_bookings])

                repository.save(offer)

                recipients = ([
                    offer.venue.bookingEmail
                ] if offer.venue.bookingEmail else [
                    recipient.user.email
                    for recipient in offer.venue.managingOfferer.UserOfferers
                ])
                send_offer_validation_status_update_email(
                    offer, new_validation, recipients)
                send_offer_validation_notification_to_administration(
                    new_validation, offer)

                flash("Le statut de l'offre a bien été modifié", "success")

        search.async_index_offer_ids([offer.id])
示例#5
0
def _reindex_offers(created_or_updated_objects):
    offer_ids = set()
    for obj in created_or_updated_objects:
        if isinstance(obj, Stock):
            offer_ids.add(obj.offerId)
        elif isinstance(obj, Offer):
            offer_ids.add(obj.id)
    search.async_index_offer_ids(offer_ids)
示例#6
0
def move_all_offers_from_venue_to_other_venue(
        origin_venue_id: str, destination_venue_id: str) -> None:
    origin_venue = Venue.query.filter_by(id=origin_venue_id).one()
    offers = origin_venue.offers
    for o in offers:
        o.venueId = destination_venue_id
    repository.save(*offers)
    search.async_index_offer_ids({offer.id for offer in offers})
示例#7
0
def create_mediation(
    user: User,
    offer: Offer,
    credit: str,
    image_as_bytes: bytes,
    crop_params: tuple = None,
) -> Mediation:
    # checks image type, min dimensions
    validation.check_image(image_as_bytes)

    mediation = Mediation(
        author=user,
        offer=offer,
        credit=credit,
    )
    # `create_thumb()` requires the object to have an id, so we must save now.
    repository.save(mediation)

    try:
        create_thumb(mediation,
                     image_as_bytes,
                     image_index=0,
                     crop_params=crop_params)

    except Exception as exc:
        logger.exception(
            "An unexpected error was encountered during the thumbnail creation: %s",
            exc)
        # I could not use savepoints and rollbacks with SQLA
        repository.delete(mediation)
        raise ThumbnailStorageError

    else:
        mediation.thumbCount = 1
        repository.save(mediation)
        # cleanup former thumbnails and mediations

        previous_mediations = (Mediation.query.filter(
            Mediation.offerId == offer.id).filter(
                Mediation.id != mediation.id).all())
        for previous_mediation in previous_mediations:
            try:
                for thumb_index in range(0, previous_mediation.thumbCount):
                    remove_thumb(previous_mediation, image_index=thumb_index)
            except Exception as exc:  # pylint: disable=broad-except
                logger.exception(
                    "An unexpected error was encountered during the thumbnails deletion for %s: %s",
                    mediation,
                    exc,
                )
            else:
                repository.delete(previous_mediation)

        search.async_index_offer_ids([offer.id])

        return mediation
示例#8
0
def refuse_educational_booking(
        educational_booking_id: int) -> EducationalBooking:
    educational_booking = educational_repository.find_educational_booking_by_id(
        educational_booking_id)

    if educational_booking is None:
        raise exceptions.EducationalBookingNotFound()

    if educational_booking.status == EducationalBookingStatus.REFUSED:
        return educational_booking

    with transaction():
        stock = offers_repository.get_and_lock_stock(
            stock_id=educational_booking.booking.stockId)
        booking = educational_booking.booking
        db.session.refresh(educational_booking.booking)

        try:
            educational_booking.mark_as_refused()
        except (
                exceptions.EducationalBookingNotRefusable,
                exceptions.EducationalBookingAlreadyCancelled,
        ) as exception:
            logger.error(
                "User from adage trying to refuse educational booking that cannot be refused",
                extra={
                    "educational_booking_id": educational_booking_id,
                    "exception_type": exception.__class__.__name__,
                },
            )
            raise exception

        stock.dnBookedQuantity -= booking.quantity

        repository.save(booking, educational_booking)

    logger.info(
        "Booking has been cancelled",
        extra={
            "booking": booking.id,
            "reason": str(booking.cancellationReason),
        },
    )

    search.async_index_offer_ids([stock.offerId])

    return educational_booking
示例#9
0
def batch_update_offers(query, update_fields):
    offer_ids_tuples = query.filter(
        Offer.validation == OfferValidationStatus.APPROVED).with_entities(
            Offer.id)

    offer_ids = [offer_id for offer_id, in offer_ids_tuples]
    number_of_offers_to_update = len(offer_ids)
    batch_size = 1000
    for current_start_index in range(0, number_of_offers_to_update,
                                     batch_size):
        offer_ids_batch = offer_ids[
            current_start_index:min(current_start_index +
                                    batch_size, number_of_offers_to_update)]

        query_to_update = Offer.query.filter(Offer.id.in_(offer_ids_batch))
        query_to_update.update(update_fields, synchronize_session=False)
        db.session.commit()

        search.async_index_offer_ids(offer_ids_batch)
def test_offer_indexation_on_booking_cycle(app):
    beneficiary = users_factories.BeneficiaryGrant18Factory()
    stock = offers_factories.StockFactory(quantity=1)
    offer = stock.offer
    assert search_testing.search_store["offers"] == {}

    search.async_index_offer_ids([offer.id])
    assert search_testing.search_store["offers"] == {}

    search.index_offers_in_queue()
    assert offer.id in search_testing.search_store["offers"]

    booking = bookings_api.book_offer(beneficiary, stock.id, quantity=1)
    search.index_offers_in_queue()
    assert offer.id not in search_testing.search_store["offers"]

    bookings_api.cancel_booking_by_beneficiary(beneficiary, booking)
    search.index_offers_in_queue()
    assert offer.id in search_testing.search_store["offers"]
示例#11
0
def deactivate_inappropriate_products(isbn: str) -> bool:
    products = Product.query.filter(
        Product.extraData["isbn"].astext == isbn).all()
    if not products:
        return False

    for product in products:
        product.isGcuCompatible = False
        db.session.add(product)

    offers = Offer.query.filter(Offer.productId.in_(
        p.id for p in products)).filter(Offer.isActive.is_(True))
    offer_ids = [
        offer_id for offer_id, in offers.with_entities(Offer.id).all()
    ]
    offers.update(values={"isActive": False}, synchronize_session="fetch")

    try:
        db.session.commit()
    except Exception as exception:  # pylint: disable=broad-except
        logger.exception(
            "Could not mark product and offers as inappropriate: %s",
            extra={
                "isbn": isbn,
                "products": [p.id for p in products],
                "exc": str(exception)
            },
        )
        return False
    logger.info(
        "Deactivated inappropriate products",
        extra={
            "isbn": isbn,
            "products": [p.id for p in products],
            "offers": offer_ids
        },
    )

    search.async_index_offer_ids(offer_ids)

    return True
示例#12
0
def add_criteria_to_offers(criteria: list[Criterion],
                           isbn: Optional[str] = None,
                           visa: Optional[str] = None) -> bool:
    if not isbn and not visa:
        return False

    query = Product.query
    if isbn:
        isbn = isbn.replace("-", "").replace(" ", "")
        query = query.filter(Product.extraData["isbn"].astext == isbn)
    if visa:
        query = query.filter(Product.extraData["visa"].astext == visa)

    products = query.all()
    if not products:
        return False

    offer_ids_query = Offer.query.filter(
        Offer.productId.in_(p.id for p in products),
        Offer.isActive.is_(True)).with_entities(Offer.id)
    offer_ids = [offer_id for offer_id, in offer_ids_query.all()]

    if not offer_ids:
        return False

    offer_criteria: list[OfferCriterion] = []
    for criterion in criteria:
        logger.info("Adding criterion %s to %d offers", criterion,
                    len(offer_ids))

        offer_criteria.extend(
            OfferCriterion(offerId=offer_id, criterionId=criterion.id)
            for offer_id in offer_ids)

    db.session.bulk_save_objects(offer_criteria)
    db.session.commit()

    search.async_index_offer_ids(offer_ids)

    return True
示例#13
0
def upsert_stocks(offer_id: int,
                  stock_data_list: list[Union[StockCreationBodyModel,
                                              StockEditionBodyModel]],
                  user: User) -> list[Stock]:
    activation_codes = []
    stocks = []
    edited_stocks = []
    edited_stocks_previous_beginnings = {}

    offer = offer_queries.get_offer_by_id(offer_id)

    for stock_data in stock_data_list:
        if isinstance(stock_data, StockEditionBodyModel):
            stock = (Stock.queryNotSoftDeleted().filter_by(
                id=stock_data.id).options(joinedload(
                    Stock.activationCodes)).first_or_404())
            if stock.offerId != offer_id:
                errors = ApiErrors()
                errors.add_error(
                    "global",
                    "Vous n'avez pas les droits d'accès suffisant pour accéder à cette information."
                )
                errors.status_code = 403
                raise errors
            edited_stocks_previous_beginnings[
                stock.id] = stock.beginningDatetime
            edited_stock = _edit_stock(
                stock,
                price=stock_data.price,
                quantity=stock_data.quantity,
                beginning=stock_data.beginning_datetime,
                booking_limit_datetime=stock_data.booking_limit_datetime,
            )
            edited_stocks.append(edited_stock)
            stocks.append(edited_stock)
        else:
            activation_codes_exist = stock_data.activation_codes is not None and len(
                stock_data.activation_codes) > 0  # type: ignore[arg-type]

            if activation_codes_exist:
                validation.check_offer_is_digital(offer)
                validation.check_activation_codes_expiration_datetime(
                    stock_data.activation_codes_expiration_datetime,
                    stock_data.booking_limit_datetime,
                )

            quantity = len(
                stock_data.activation_codes
            ) if activation_codes_exist else stock_data.quantity  # type: ignore[arg-type]

            created_stock = _create_stock(
                offer=offer,
                price=stock_data.price,
                quantity=quantity,
                beginning=stock_data.beginning_datetime,
                booking_limit_datetime=stock_data.booking_limit_datetime,
            )

            if activation_codes_exist:
                for activation_code in stock_data.activation_codes:  # type: ignore[union-attr]
                    activation_codes.append(
                        ActivationCode(
                            code=activation_code,
                            expirationDate=stock_data.
                            activation_codes_expiration_datetime,
                            stock=created_stock,
                        ))

            stocks.append(created_stock)

    repository.save(*stocks, *activation_codes)
    logger.info("Stock has been created or updated", extra={"offer": offer_id})

    if offer.validation == OfferValidationStatus.DRAFT:
        offer.validation = set_offer_status_based_on_fraud_criteria(offer)
        offer.author = user
        offer.lastValidationDate = datetime.datetime.utcnow()
        if offer.validation == OfferValidationStatus.PENDING or offer.validation == OfferValidationStatus.REJECTED:
            offer.isActive = False
        repository.save(offer)
        if offer.validation == OfferValidationStatus.APPROVED:
            admin_emails.send_offer_creation_notification_to_administration(
                offer)

    for stock in edited_stocks:
        previous_beginning = edited_stocks_previous_beginnings[stock.id]
        if stock.beginningDatetime != previous_beginning and not stock.offer.isEducational:
            _notify_beneficiaries_upon_stock_edit(stock)
    search.async_index_offer_ids([offer.id])

    return stocks
示例#14
0
def cancel_bookings_when_offerer_deletes_stock(stock: Stock) -> list[Booking]:
    cancelled_bookings = _cancel_bookings_from_stock(stock, BookingCancellationReasons.OFFERER)
    search.async_index_offer_ids([stock.offerId])
    return cancelled_bookings
示例#15
0
def book_educational_offer(redactor_informations: RedactorInformation,
                           stock_id: int) -> EducationalBooking:
    redactor = educational_repository.find_redactor_by_email(
        redactor_informations.email)
    if not redactor:
        redactor = _create_redactor(redactor_informations)

    educational_institution = educational_repository.find_educational_institution_by_uai_code(
        redactor_informations.uai)
    validation.check_institution_exists(educational_institution)

    # The call to transaction here ensures we free the FOR UPDATE lock
    # on the stock if validation issues an exception
    with transaction():
        stock = offers_repository.get_and_lock_stock(stock_id=stock_id)
        validation.check_stock_is_bookable(stock)

        educational_year = educational_repository.find_educational_year_by_date(
            stock.beginningDatetime)
        validation.check_educational_year_exists(educational_year)

        educational_booking = EducationalBooking(
            educationalInstitution=educational_institution,
            educationalYear=educational_year,
            educationalRedactor=redactor,
            confirmationLimitDate=stock.bookingLimitDatetime,
        )

        booking = bookings_models.Booking(
            educationalBooking=educational_booking,
            stockId=stock.id,
            amount=stock.price,
            token=bookings_repository.generate_booking_token(),
            venueId=stock.offer.venueId,
            offererId=stock.offer.venue.managingOffererId,
            status=bookings_models.BookingStatus.PENDING,
        )

        booking.dateCreated = datetime.utcnow()
        booking.cancellationLimitDate = compute_cancellation_limit_date(
            stock.beginningDatetime, booking.dateCreated)
        stock.dnBookedQuantity += EAC_DEFAULT_BOOKED_QUANTITY

        repository.save(booking)

    logger.info(
        "Redactor booked an educational offer",
        extra={
            "redactor": redactor_informations.email,
            "offerId": stock.offerId,
            "stockId": stock.id,
            "bookingId": booking.id,
        },
    )
    if stock.offer.bookingEmail:
        mails.send(recipients=[stock.offer.bookingEmail],
                   data=_build_prebooking_mail_data(booking))

    search.async_index_offer_ids([stock.offerId])

    return booking
示例#16
0
def book_offer(
    beneficiary: User,
    stock_id: int,
    quantity: int,
) -> Booking:
    """
    Return a booking or raise an exception if it's not possible.
    Update a user's credit information on Batch.
    """
    # The call to transaction here ensures we free the FOR UPDATE lock
    # on the stock if validation issues an exception
    with transaction():
        stock = offers_repository.get_and_lock_stock(stock_id=stock_id)
        validation.check_offer_is_not_educational(stock)
        validation.check_can_book_free_offer(beneficiary, stock)
        validation.check_offer_already_booked(beneficiary, stock.offer)
        validation.check_quantity(stock.offer, quantity)
        validation.check_stock_is_bookable(stock, quantity)
        total_amount = quantity * stock.price
        validation.check_expenses_limits(beneficiary, total_amount, stock.offer)

        from pcapi.core.offers.api import is_activation_code_applicable  # To avoid import loops

        if is_activation_code_applicable(stock):
            validation.check_activation_code_available(stock)

        # FIXME (dbaty, 2020-10-20): if we directly set relations (for
        # example with `booking.user = beneficiary`) instead of foreign keys,
        # the session tries to add the object when `get_user_expenses()`
        # is called because autoflush is enabled. As such, the PostgreSQL
        # exceptions (tooManyBookings and insufficientFunds) may raise at
        # this point and will bubble up. If we want them to be caught, we
        # have to set foreign keys, so that the session is NOT autoflushed
        # in `get_user_expenses` and is only committed in `repository.save()`
        # where exceptions are caught. Since we are using flask-sqlalchemy,
        # I don't think that we should use autoflush, nor should we use
        # the `pcapi.repository.repository` module.
        booking = Booking(
            userId=beneficiary.id,
            stockId=stock.id,
            amount=stock.price,
            quantity=quantity,
            token=generate_booking_token(),
            venueId=stock.offer.venueId,
            offererId=stock.offer.venue.managingOffererId,
        )

        booking.dateCreated = datetime.datetime.utcnow()
        booking.cancellationLimitDate = compute_cancellation_limit_date(stock.beginningDatetime, booking.dateCreated)

        if is_activation_code_applicable(stock):
            booking.activationCode = offers_repository.get_available_activation_code(stock)

            if FeatureToggle.AUTO_ACTIVATE_DIGITAL_BOOKINGS.is_active():
                booking.mark_as_used()

        individual_booking = IndividualBooking(
            booking=booking,
            depositId=beneficiary.deposit.id if beneficiary.has_active_deposit else None,
            userId=beneficiary.id,
        )
        stock.dnBookedQuantity += booking.quantity

        repository.save(individual_booking, stock)

    logger.info(
        "Beneficiary booked an offer",
        extra={
            "actor": beneficiary.id,
            "offer": stock.offerId,
            "stock": stock.id,
            "booking": booking.id,
            "used": booking.isUsed,
        },
    )

    try:
        user_emails.send_individual_booking_confirmation_email_to_offerer(individual_booking)
    except MailServiceException as error:
        logger.exception("Could not send booking=%s confirmation email to offerer: %s", booking.id, error)
    try:
        user_emails.send_individual_booking_confirmation_email_to_beneficiary(individual_booking)
    except MailServiceException as error:
        logger.exception("Could not send booking=%s confirmation email to beneficiary: %s", booking.id, error)

    search.async_index_offer_ids([stock.offerId])

    update_external_user(individual_booking.user)

    return individual_booking.booking
示例#17
0
def update_offer(
    offer: Offer,
    bookingEmail: str = UNCHANGED,
    description: str = UNCHANGED,
    isNational: bool = UNCHANGED,
    name: str = UNCHANGED,
    extraData: dict = UNCHANGED,
    externalTicketOfficeUrl: str = UNCHANGED,
    url: str = UNCHANGED,
    withdrawalDetails: str = UNCHANGED,
    isActive: bool = UNCHANGED,
    isDuo: bool = UNCHANGED,
    durationMinutes: int = UNCHANGED,
    mediaUrls: list[str] = UNCHANGED,
    ageMin: int = UNCHANGED,
    ageMax: int = UNCHANGED,
    conditions: str = UNCHANGED,
    venueId: str = UNCHANGED,
    productId: str = UNCHANGED,
    audioDisabilityCompliant: bool = UNCHANGED,
    mentalDisabilityCompliant: bool = UNCHANGED,
    motorDisabilityCompliant: bool = UNCHANGED,
    visualDisabilityCompliant: bool = UNCHANGED,
) -> Offer:
    validation.check_validation_status(offer)
    # fmt: off
    modifications = {
        field: new_value
        for field, new_value in locals().items()
        if field != 'offer' and new_value is
        not UNCHANGED  # has the user provided a value for this field
        and getattr(offer, field) !=
        new_value  # is the value different from what we have on database?
    }
    # fmt: on
    if not modifications:
        return offer

    if offer.isFromProvider:
        validation.check_update_only_allowed_fields_for_offer_from_provider(
            set(modifications), offer.lastProvider)

    offer.populate_from_dict(modifications)
    if offer.product.owningOfferer and offer.product.owningOfferer == offer.venue.managingOfferer:
        offer.product.populate_from_dict(modifications)
        product_has_been_updated = True
    else:
        product_has_been_updated = False

    if offer.isFromAllocine:
        offer.fieldsUpdated = list(
            set(offer.fieldsUpdated) | set(modifications))

    repository.save(offer)
    logger.info("Offer has been updated", extra={"offer": offer.id})
    if product_has_been_updated:
        repository.save(offer.product)
        logger.info("Product has been updated",
                    extra={"product": offer.product.id})

    search.async_index_offer_ids([offer.id])

    return offer