@@ -16,7 +16,6 @@ PATH_PREFETCH_MAP = { 'payment_plan__plan', ), 'prefetch': ( - Prefetch('payment_plan__plan__accessed_models', queryset=NeuronModel.objects.only('slug')), Prefetch( 'business_account__parent_company__user__payment_plan__plan', queryset=NeuronModel.objects.only('slug'), @@ -86,7 +85,6 @@ PATH_PREFETCH_MAP = { 'payment_plan__plan' ), 'prefetch': ( - Prefetch('payment_plan__plan__accessed_models', queryset=NeuronModel.objects.only('slug')), Prefetch( 'business_account__parent_company__user__payment_plan__plan', queryset=NeuronModel.objects.only('slug'), @@ -804,6 +804,15 @@ msgstr "ZIP архив" msgid "Audio" msgstr "Аудио" +msgid "Chat-bots" +msgstr "Чат-боты" + +msgid "Images" +msgstr "Изображения" + +msgid "Video" +msgstr "Видео" + #: ml_model/models.py:234 ml_model/models.py:272 #: payments/models/promocode.py:41 msgid "Type" @@ -10,10 +10,13 @@ def move_points_to_features(apps, schema_editor): ModelCategory = apps.get_model('ml_model', 'ModelCategory') measurement_units = {'images': 'file', 'chat-bots': 'text_page'} - categories = { - 'images': ModelCategory.objects.get(slug='images'), - 'chat-bots': ModelCategory.objects.get(slug='chat-bots') - } + try: + categories = { + 'images': ModelCategory.objects.get(slug='images'), + 'chat-bots': ModelCategory.objects.get(slug='chat-bots') + } + except: + pass for plan in PaymentPlan.objects.all(): order_counter = 1 payment_plan_features_data = [] @@ -0,0 +1,72 @@ +# Generated by Django 5.0.11 on 2026-01-19 09:11 + +import django.db.models.deletion +from django.db import migrations, models +from django.db.models import Q + + +def convert_features_title_to_model(apps, schema_editor): + PaymentPlanFeature = apps.get_model('payments', 'PaymentPlanFeature') + NeuronModel = apps.get_model('ml_model', 'NeuronModel') + payment_plan_features = [] + + for payment_plan_feature in PaymentPlanFeature.objects.all(): + try: + payment_plan_feature.model = NeuronModel.objects.get( + Q(title=payment_plan_feature.name) | Q(slug=payment_plan_feature.name) + ) + except NeuronModel.DoesNotExist: + continue + payment_plan_features.append(payment_plan_feature) + + PaymentPlanFeature.objects.bulk_update(payment_plan_features, ['model']) + + +class Migration(migrations.Migration): + + dependencies = [ + ('ml_model', '0052_alter_modelinput_unique_together'), + ('payments', '0021_update_plans_features_quantity'), + ] + + operations = [ + migrations.AddField( + model_name='paymentplanfeature', + name='model', + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='feature', + to='ml_model.neuronmodel', + verbose_name='Neuron Model', + null=True, + blank=True, + ), + ), + migrations.RunPython(convert_features_title_to_model, migrations.RunPython.noop), + migrations.AlterField( + model_name='paymentplanfeature', + name='model', + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='feature', + to='ml_model.neuronmodel', + verbose_name='Neuron Model', + ), + ), + migrations.AlterUniqueTogether( + name='paymentplanfeature', + unique_together={('plan', 'model')}, + ), + migrations.RemoveField( + model_name='paymentplanfeature', + name='name', + ), + migrations.RemoveField( + model_name='paymentplan', + name='accessed_models', + ), + migrations.RemoveField( + model_name='paymentplanfeature', + name='category', + ), + ] @@ -0,0 +1,42 @@ +# Generated by Django 5.0.11 on 2026-01-19 11:42 + +from collections import defaultdict +from django.db import migrations + + +def reordering_payment_plan_features(apps, schema_editor): + NeuronModel = apps.get_model('ml_model', 'NeuronModel') + PaymentPlan = apps.get_model('payments', 'PaymentPlan') + PaymentPlanFeature = apps.get_model('payments', 'PaymentPlanFeature') + + category_model_order = { + category: {model.slug: model.order for model in NeuronModel.objects.filter(category__slug=category)} + for category in {'chat-bots', 'images', 'videos', 'audio'} + } + + features_to_update = [] + + for plan in PaymentPlan.objects.prefetch_related('features__model__category', 'features__model').all(): + by_category = defaultdict(list) + for feature in plan.features.all(): + by_category[feature.model.category.slug].append(feature) + + for category_slug, category_features in by_category.items(): + category_features.sort(key=lambda f: category_model_order[category_slug][f.model.slug]) + for index, feature in enumerate(category_features, start=1): + feature.order = index + features_to_update.append(feature) + + if features_to_update: + PaymentPlanFeature.objects.bulk_update(features_to_update, ['order']) + + +class Migration(migrations.Migration): + + dependencies = [ + ('payments', '0022_remove_paymentplan_accessed_models_and_more'), + ] + + operations = [ + migrations.RunPython(reordering_payment_plan_features, migrations.RunPython.noop) + ] @@ -39,10 +39,11 @@ class PaymentPlan(BaseModel): verbose_name='Поинты', help_text='Перечислять через запятую', ) - accessed_models = models.ManyToManyField( - NeuronModel, - verbose_name='Доступные модели', - ) + + @property + def accessed_models(self): + ids = self.features.values_list('model__pk', flat=True) + return NeuronModel.objects.filter(pk__in=ids) def __str__(self) -> str: return f'{self.title or "Ошибка"}' @@ -2,7 +2,7 @@ from django.db import models from django.utils.translation import gettext_lazy as _ from ordered_model.models import OrderedModel -from ml_model.models import ModelCategory +from ml_model.models import NeuronModel from payments.models import PaymentPlan @@ -15,14 +15,8 @@ class PaymentPlanFeature(OrderedModel): plan = models.ForeignKey( PaymentPlan, on_delete=models.CASCADE, related_name='features', verbose_name=_('Payment Plan') ) - name = models.CharField(max_length=30, verbose_name=_('Name')) + model = models.ForeignKey(NeuronModel, on_delete=models.CASCADE, related_name='feature', verbose_name=_('Neuron Model')) quantity = models.PositiveIntegerField(default=1, verbose_name=_('Quantity')) - category = models.ForeignKey( - ModelCategory, - on_delete=models.CASCADE, - related_name='payment_plan_feature', - verbose_name=_('Category'), - ) measurement_unit = models.CharField( max_length=15, choices=MeasurementUnitChoices.choices, @@ -30,9 +24,12 @@ class PaymentPlanFeature(OrderedModel): verbose_name=_('Measurement unit'), ) + order_with_respect_to = ('plan', 'model__category') + def __str__(self) -> str: - return self.name + return f'{self.plan.title} ({self.model.title})' class Meta(OrderedModel.Meta): verbose_name = _('Payment plan feature') verbose_name_plural = _('Payment plan features') + unique_together = ('plan', 'model') @@ -2,7 +2,6 @@ import calendar from datetime import timedelta, date from decimal import Decimal -from collections import defaultdict from dateutil.relativedelta import relativedelta from django.db.models import Sum, F, Value, Func, CharField @@ -91,15 +90,15 @@ async def list_payment_plans(request): plans = PaymentPlan.objects.filter( price__gt=0, is_corporate=is_corporate, is_visible=True - ).prefetch_related('features__category', 'accessed_models') + ).prefetch_related('features__model', 'features__model__category') result = [] async for plan in plans: - grouped = defaultdict(list) + grouped = {_('Chat-bots'): [], _('Images'): [], _('Video'): [], _('Audio'): []} for feature in plan.features.all(): - grouped[feature.category.title].append( + grouped[feature.model.category.title].append( { - 'name': feature.name, + 'name': feature.model.title, 'quantity': feature.quantity, 'measurement_unit': feature.measurement_unit, } @@ -113,7 +112,6 @@ async def list_payment_plans(request): duration=plan.duration, points=plan.points, grouped_features=[{'name': cat, 'features': feats} for cat, feats in grouped.items()], - accessed_models=plan.accessed_models.all(), ) ) return result @@ -5,7 +5,7 @@ from django.db import models from django.db.models import Count, Sum from django.db.models.functions import Coalesce from django.utils.translation import gettext_lazy as _ -from ordered_model.admin import OrderedInlineModelAdminMixin, OrderedTabularInline +from ordered_model.admin import OrderedInlineModelAdminMixin, OrderedModelAdmin from authentication.admin import CustomUserModelAdmin from payments.models import ( @@ -41,12 +41,6 @@ class PaymentAdmin(admin.ModelAdmin): return str(obj.user) -class PaymentPlanFeatureInline(OrderedTabularInline): - model = PaymentPlanFeature - extra = 1 - ordering = ('order',) - - @admin.register(PaymentPlan) class PaymentPlanAdmin(OrderedInlineModelAdminMixin, admin.ModelAdmin): list_display = [ @@ -59,8 +53,6 @@ class PaymentPlanAdmin(OrderedInlineModelAdminMixin, admin.ModelAdmin): 'is_visible', ] search_fields = ['price', 'tokens_per_plan'] - inlines = (PaymentPlanFeatureInline,) - filter_horizontal = ('accessed_models',) @admin.register(PaymentPlanUserInfo) @@ -76,6 +68,12 @@ class PaymentPlanUserInfoAdmin(admin.ModelAdmin): search_help_text = _('You can search by user email, exacted company name') +@admin.register(PaymentPlanFeature) +class PaymentPlanFeatureAdmin(OrderedModelAdmin): + list_display = ('plan', 'model', 'move_up_down_links') + list_filter = ('plan', 'model__category') + + @admin.register(UserPaymentMethod) class UserPaymentMethodAdmin(admin.ModelAdmin): raw_id_fields = ['user'] @@ -1,7 +1,6 @@ from django.contrib.auth import get_user_model from rest_framework import serializers -from ml_model.models import NeuronModel from payments.models import Invoice, Payment, PromoCode from payments.models.referral_account import ReferralAccount @@ -12,9 +11,10 @@ class PaymentPlanSerializer(serializers.Serializer): price = serializers.DecimalField(max_digits=10, decimal_places=2) tokens_per_plan = serializers.DecimalField(max_digits=50, decimal_places=2) duration = serializers.CharField(read_only=True) - accessed_models = serializers.SlugRelatedField( - slug_field='slug', queryset=NeuronModel.objects.all(), many=True - ) + accessed_models = serializers.SerializerMethodField() + + def get_accessed_models(self, obj): + return [model.slug for model in obj.accessed_models] class ReferralSerializer(serializers.ModelSerializer):