Esempio n. 1
0
def create_mediation(
    user: User,
    offer: Offer,
    credit: str,
    image_as_bytes: bytes,
    crop_params=None,
) -> Mediation:
    validation.check_mediation_thumb_quality(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)

    create_thumb(mediation, image_as_bytes, image_index=0, crop_params=crop_params)
    mediation.thumbCount = 1
    repository.save(mediation)

    if feature_queries.is_active(FeatureToggle.SYNCHRONIZE_ALGOLIA):
        redis.add_offer_id(client=app.redis_client, offer_id=offer.id)

    return mediation
Esempio n. 2
0
def add_criterion_to_offers(criterion_name: str,
                            isbns: Iterable[str],
                            batch_size: int = 100) -> None:
    isbns = [isbn.replace("-", "").replace(" ", "") for isbn in isbns]

    criterion = Criterion.query.filter_by(name=criterion_name).one()

    offers = Offer.query.filter(
        Offer.extraData["isbn"].astext.in_(isbns),
        Offer.isActive.is_(True),
    ).all()

    if not offers:
        logger.info("Did not match any offer: double-check the ISBN's")
        return

    logger.info("Adding criterion %s to %d offers", criterion, len(offers))

    offer_criteria = []
    for offer in offers:
        offer_criteria.append(OfferCriterion(offer=offer, criterion=criterion))

        if len(offer_criteria) > batch_size:
            repository.save(*offer_criteria)
            offer_criteria = []

    repository.save(*offer_criteria)

    logger.info("Reindexing %d offers after addition of criterion %s",
                len(offers), criterion_name)
    for offer in offers:
        redis.add_offer_id(client=app.redis_client, offer_id=offer.id)
Esempio n. 3
0
def cancel_booking_by_beneficiary(user: User, booking: Booking) -> None:
    if not user.isBeneficiary:
        raise RuntimeError(
            "Unexpected call to cancel_booking_by_beneficiary with non-beneficiary user %s"
            % user)
    validation.check_beneficiary_can_cancel_booking(user, booking)

    booking.isCancelled = True
    booking.cancellationReason = BookingCancellationReasons.BENEFICIARY
    repository.save(booking)

    notifier = MailjetNotificationService()
    notifier.send_booking_cancellation_emails_to_user_and_offerer(
        booking=booking,
        reason=booking.cancellationReason,
        # FIXME: we should not have to pass this argument.
        # Notification-related code should be reorganized.
        send_email=send_raw_email,
    )

    # FIXME: why do we do that when the booking is cancelled by the
    # *beneficiary*, but not when it's cancelled by the *offerer* (see
    # cancel_booking_by_offerer)?
    if feature_queries.is_active(FeatureToggle.SYNCHRONIZE_ALGOLIA):
        redis.add_offer_id(client=app.redis_client,
                           offer_id=booking.stock.offerId)
Esempio n. 4
0
def create_stock(
    offer: models.Offer,
    price: float,
    quantity: int = None,
    beginning: datetime.datetime = None,
    booking_limit_datetime: datetime.datetime = None,
) -> Stock:
    """Return the new stock or raise an exception if it's not possible."""
    validation.check_required_dates_for_stock(offer, beginning, booking_limit_datetime)
    validation.check_offer_is_editable(offer)
    validation.check_stocks_are_editable_for_offer(offer)

    stock = models.Stock(
        offer=offer,
        price=price,
        quantity=quantity,
        beginningDatetime=beginning,
        bookingLimitDatetime=booking_limit_datetime,
    )

    repository.save(stock)

    if feature_queries.is_active(FeatureToggle.SYNCHRONIZE_ALGOLIA):
        redis.add_offer_id(client=app.redis_client, offer_id=offer.id)

    return stock
Esempio n. 5
0
def delete_stock(stock: Stock) -> None:
    validation.check_stock_is_deletable(stock)

    stock.isSoftDeleted = True

    cancelled_bookings = []
    for booking in stock.bookings:
        if not booking.isCancelled and not booking.isUsed:
            booking.isCancelled = True
            cancelled_bookings.append(booking)

    repository.save(stock, *cancelled_bookings)

    if cancelled_bookings:
        try:
            user_emails.send_batch_cancellation_emails_to_users(cancelled_bookings, mailing.send_raw_email)
        except mailing.MailServiceException as exc:
            app.logger.exception("Could not notify beneficiaries about deletion of stock=%s: %s", stock.id, exc)
        try:
            user_emails.send_offerer_bookings_recap_email_after_offerer_cancellation(
                cancelled_bookings, mailing.send_raw_email
            )
        except mailing.MailServiceException as exc:
            app.logger.exception("Could not notify offerer about deletion of stock=%s: %s", stock.id, exc)

    if feature_queries.is_active(FeatureToggle.SYNCHRONIZE_ALGOLIA):
        redis.add_offer_id(client=app.redis_client, offer_id=stock.offerId)
def deactivate_inappropriate_offers(offer_ids: List[int]):
    offers = Offer.query.filter(Offer.id.in_(offer_ids)).all()
    for o in offers:
        o.isActive = False
        o.product.isGcuCompatible = False
    repository.save(*offers)
    for o in offers:
        redis.add_offer_id(client=app.redis_client, offer_id=o.id)
Esempio n. 7
0
def update_mediation(mediation: Mediation, is_active: bool) -> Mediation:
    mediation.isActive = is_active
    repository.save(mediation)

    if feature_queries.is_active(FeatureToggle.SYNCHRONIZE_ALGOLIA):
        redis.add_offer_id(client=app.redis_client, offer_id=mediation.offerId)

    return mediation
Esempio n. 8
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)
    for o in offers:
        redis.add_offer_id(client=app.redis_client, offer_id=o.id)
Esempio n. 9
0
def create_mediation_v2(
    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)

    existing_mediations = mediation_queries.get_mediations_for_offers(
        [offer.id])

    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:
        # TODO(fseguin): cleanup after image upload v2 launch
        create_thumb(mediation,
                     image_as_bytes,
                     image_index=0,
                     crop_params=crop_params,
                     use_v2=True)

    except Exception as exc:
        app.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
        for previous_mediation in existing_mediations:
            try:
                for thumb_index in range(0, mediation.thumbCount):
                    remove_thumb(previous_mediation, image_index=thumb_index)
            except Exception as exc:  # pylint: disable=broad-except
                app.logger.exception(
                    "An unexpected error was encountered during the thumbnails deletion for %s: %s",
                    mediation,
                    exc,
                )
            else:
                repository.delete(previous_mediation)

        if feature_queries.is_active(FeatureToggle.SYNCHRONIZE_ALGOLIA):
            redis.add_offer_id(client=app.redis_client, offer_id=offer.id)

        return mediation
Esempio n. 10
0
def cancel_booking_by_offerer(booking: Booking) -> None:
    validation.check_offerer_can_cancel_booking(booking)
    booking.isCancelled = True
    booking.cancellationReason = BookingCancellationReasons.OFFERER
    repository.save(booking)

    if feature_queries.is_active(FeatureToggle.SYNCHRONIZE_ALGOLIA):
        redis.add_offer_id(client=app.redis_client,
                           offer_id=booking.stock.offerId)
Esempio n. 11
0
    def test_should_add_offer_id(self):
        # Given
        client = MagicMock()
        client.rpush = MagicMock()

        # When
        add_offer_id(client=client, offer_id=1)

        # Then
        client.rpush.assert_called_once_with("offer_ids", 1)
Esempio n. 12
0
def update_offer(  # pylint: disable=redefined-builtin
    offer: Offer,
    bookingEmail: str = UNCHANGED,
    description: str = UNCHANGED,
    isNational: bool = UNCHANGED,
    name: str = UNCHANGED,
    extraData: dict = UNCHANGED,
    type: 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,
) -> 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

    validation.check_offer_is_editable(offer)

    if offer.isFromAllocine:
        validation.check_update_only_allowed_offer_fields_for_allocine_offer(set(modifications))

    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)
    if product_has_been_updated:
        repository.save(offer.product)

    if feature_queries.is_active(FeatureToggle.SYNCHRONIZE_ALGOLIA):
        redis.add_offer_id(client=app.redis_client, offer_id=offer.id)

    return offer
Esempio n. 13
0
def book_offer(
    beneficiary: User,
    stock: Stock,
    quantity: int,
) -> Booking:
    """Return a booking or raise an exception if it's not possible."""
    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)
    total_amount = quantity * stock.price
    validation.check_expenses_limits(beneficiary, total_amount, stock.offer)

    # 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(),
    )

    booking.dateCreated = datetime.datetime.utcnow()
    booking.confirmationDate = compute_confirmation_date(
        stock.beginningDatetime, booking.dateCreated)

    repository.save(booking)

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

    if feature_queries.is_active(FeatureToggle.SYNCHRONIZE_ALGOLIA):
        redis.add_offer_id(client=app.redis_client, offer_id=stock.offerId)

    return booking
Esempio n. 14
0
def update_offers_active_status(query, is_active):
    # We cannot just call `query.update()` because `distinct()` may
    # already have been called on `query`.
    query_to_update = Offer.query.filter(Offer.id.in_(query.with_entities(Offer.id)))
    query_to_update.update({"isActive": is_active}, synchronize_session=False)
    db.session.commit()

    if feature_queries.is_active(FeatureToggle.SYNCHRONIZE_ALGOLIA):
        offer_ids = {offer_id for offer_id, in query.with_entities(Offer.id)}
        for offer_id in offer_ids:
            redis.add_offer_id(client=app.redis_client, offer_id=offer_id)
Esempio n. 15
0
def _reindex_offers(created_or_updated_objects):
    if not feature_queries.is_active(FeatureToggle.SYNCHRONIZE_ALGOLIA):
        return
    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)
    for offer_id in offer_ids:
        redis.add_offer_id(client=app.redis_client, offer_id=offer_id)
Esempio n. 16
0
def upsert_stocks(
    offer_id: int, stock_data_list: List[Union[StockCreationBodyModel,
                                               StockEditionBodyModel]]
) -> List[Stock]:
    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).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:
            created_stock = _create_stock(
                offer=offer,
                price=stock_data.price,
                quantity=stock_data.quantity,
                beginning=stock_data.beginning_datetime,
                booking_limit_datetime=stock_data.booking_limit_datetime,
            )
            stocks.append(created_stock)

    repository.save(*stocks)

    for stock in edited_stocks:
        previous_beginning = edited_stocks_previous_beginnings[stock.id]
        if stock.beginningDatetime != previous_beginning:
            _notify_beneficiaries_upon_stock_edit(stock)
    if feature_queries.is_active(FeatureToggle.SYNCHRONIZE_ALGOLIA):
        redis.add_offer_id(client=app.redis_client, offer_id=offer.id)

    return stocks
Esempio n. 17
0
def cancel_booking_by_beneficiary(user: User, booking: Booking) -> None:
    if not user.isBeneficiary:
        raise RuntimeError(
            "Unexpected call to cancel_booking_by_beneficiary with non-beneficiary user %s"
            % user)
    validation.check_beneficiary_can_cancel_booking(user, booking)

    booking.isCancelled = True
    booking.cancellationReason = BookingCancellationReasons.BENEFICIARY
    repository.save(booking)

    try:
        user_emails.send_booking_cancellation_emails_to_user_and_offerer(
            booking, booking.cancellationReason)
    except MailServiceException as error:
        logger.exception(
            "Could not send booking=%s cancellation emails to user and offerer: %s",
            booking.id, error)

    if feature_queries.is_active(FeatureToggle.SYNCHRONIZE_ALGOLIA):
        redis.add_offer_id(client=app.redis_client,
                           offer_id=booking.stock.offerId)
def create_booking_for_user_on_specific_stock_bypassing_capping_limits(
        user_id: int, stock_id: int) -> None:
    stock = Stock.query.get(stock_id)
    user = User.query.get(user_id)
    quantity = 1

    validation.check_offer_already_booked(user, stock.offer)
    validation.check_quantity(stock.offer, quantity)
    validation.check_can_book_free_offer(user, stock)
    validation.check_stock_is_bookable(stock)

    booking = models.Booking()
    # FIXME: this is not right. PcObject's constructor should allow
    # `Booking(stock=stock, ...)`
    booking.userId = user.id
    booking.stockId = stock.id
    booking.amount = stock.price
    booking.quantity = quantity
    booking.token = random_token()
    repository.save(booking)

    redis.add_offer_id(client=app.redis_client, offer_id=stock.offerId)
Esempio n. 19
0
 def on_model_change(self,
                     form: Form,
                     offer: Offer,
                     is_created: bool = False) -> None:
     redis.add_offer_id(client=app.redis_client, offer_id=offer.id)
Esempio n. 20
0
def edit_stock(
    stock: Stock,
    price: int = None,
    quantity: int = None,
    beginning: datetime.datetime = None,
    booking_limit_datetime: datetime.datetime = None,
) -> Stock:
    validation.check_stock_is_updatable(stock)
    validation.check_required_dates_for_stock(stock.offer, beginning, booking_limit_datetime)

    # FIXME (dbaty, 2020-11-25): We need this ugly workaround because
    # the frontend sends us datetimes like "2020-12-03T14:00:00Z"
    # (note the "Z" suffix). Pydantic deserializes it as a datetime
    # *with* a timezone. However, datetimes are stored in the database
    # as UTC datetimes *without* any timezone. Thus, we wrongly detect
    # a change for the "beginningDatetime" field for Allocine stocks:
    # because we do not allow it to be changed, we raise an error when
    # we should not.
    def as_utc_without_timezone(d: datetime.datetime) -> datetime.datetime:
        return d.astimezone(pytz.utc).replace(tzinfo=None)

    if beginning:
        beginning = as_utc_without_timezone(beginning)
    if booking_limit_datetime:
        booking_limit_datetime = as_utc_without_timezone(booking_limit_datetime)

    updates = {
        "price": price,
        "quantity": quantity,
        "beginningDatetime": beginning,
        "bookingLimitDatetime": booking_limit_datetime,
    }

    if stock.offer.isFromAllocine:
        # fmt: off
        updated_fields = {
            attr
            for attr, new_value in updates.items()
            if new_value != getattr(stock, attr)
        }
        # fmt: on
        validation.check_update_only_allowed_stock_fields_for_allocine_offer(updated_fields)
        stock.fieldsUpdated = list(updated_fields)

    previous_beginning = stock.beginningDatetime

    for model_attr, value in updates.items():
        setattr(stock, model_attr, value)
    repository.save(stock)

    if beginning != previous_beginning:
        bookings = bookings_repository.find_not_cancelled_bookings_by_stock(stock)
        if bookings:
            bookings = update_confirmation_dates(bookings, beginning)
            date_in_two_days = datetime.datetime.utcnow() + datetime.timedelta(days=2)
            check_event_is_in_more_than_48_hours = beginning > date_in_two_days
            if check_event_is_in_more_than_48_hours:
                bookings = _invalidate_bookings(bookings)
            try:
                user_emails.send_batch_stock_postponement_emails_to_users(bookings, send_email=mailing.send_raw_email)
            except mailing.MailServiceException as exc:
                # fmt: off
                app.logger.exception(
                    "Could not notify beneficiaries about update of stock=%s: %s",
                    stock.id,
                    exc,
                )
                # fmt: on

    if feature_queries.is_active(FeatureToggle.SYNCHRONIZE_ALGOLIA):
        redis.add_offer_id(client=app.redis_client, offer_id=stock.offerId)

    return stock