Django-rest-framework: توثيق كيفية التعامل مع الأذونات والتصفية للحقول ذات الصلة.

تم إنشاؤها على ٢٣ أكتوبر ٢٠١٤  ·  26تعليقات  ·  مصدر: encode/django-rest-framework

لا تقوم العلاقات حاليًا بتطبيق نفس مجموعة الأذونات والتصفية التي يتم تطبيقها على طرق العرض تلقائيًا. إذا كنت بحاجة إلى أذونات أو تصفية العلاقات ، فأنت بحاجة إلى التعامل معها بشكل صريح.

أنا شخصياً لا أرى أي طرق جيدة للتعامل مع هذا تلقائيًا ، لكن من الواضح أنه شيء يمكننا فعله على الأقل بتوثيق أفضل.

رأيي الآن هو أننا يجب أن نحاول التوصل إلى مثال بسيط لحالة وتوثيق كيفية التعامل مع ذلك بشكل صريح. يجب ترك أي رمز تلقائي للتعامل مع هذا لمؤلفي الحزم الخارجيين للتعامل معه. يتيح ذلك للمساهمين الآخرين استكشاف المشكلة ومعرفة ما إذا كان بإمكانهم التوصل إلى أي حلول جيدة يمكن تضمينها في الأساس.

في المستقبل ، قد يتم ترقية هذه المشكلة من "التوثيق" إلى "التحسين" ، ولكن ما لم يكن هناك أي مقترحات ملموسة مدعومة بحزمة جزء ثالث ، فستبقى في هذه الحالة.

Documentation

التعليق الأكثر فائدة

لقد استخدمت مزيج Serializer البسيط هذا لتصفية مجموعات الاستعلام في الحقول ذات الصلة:

class FilterRelatedMixin(object):
    def __init__(self, *args, **kwargs):
        super(FilterRelatedMixin, self).__init__(*args, **kwargs)
        for name, field in self.fields.iteritems():
            if isinstance(field, serializers.RelatedField):
                method_name = 'filter_%s' % name
                try:
                    func = getattr(self, method_name)
                except AttributeError:
                    pass
                else:
                    field.queryset = func(field.queryset)

الاستخدام بسيط أيضًا:

class SocialPageSerializer(FilterRelatedMixin, serializers.ModelSerializer):
    account = serializers.PrimaryKeyRelatedField()

    class Meta:
        model = models.SocialPage

    def filter_account(self, queryset):
        request = self.context['request']
        return queryset.filter(user=request.user)

كيف هذا؟ هل ترى أي مشاكل في ذلك؟

ال 26 كومينتر

حاولت البحث في الكود ، لكنني لم أجد طريقة سهلة للقيام بذلك. من الناحية المثالية ، يجب أن تكون قادرًا على استدعاء الأذونات "has_object_permission" لكل كائن ذي صلة. الآن ، المسلسل ليس لديه حق الوصول إلى كائن الإذن.

الآن ، المسلسل ليس لديه حق الوصول إلى كائن الإذن.

إلا أن هذا ليس بهذه البساطة.

_ أي كائن إذن؟ هذه علاقات مع كائنات أخرى ، لذلك لن تكون بالضرورة فئات الأذونات والتصفية في طريقة العرض الحالية هي نفس القواعد التي تريد تطبيقها على علاقات الكائنات.

بالنسبة للعلاقات ذات الروابط التشعبية ، يمكنك من الناحية النظرية تحديد وجهة النظر التي أشاروا إليها (+) وتحديد التصفية / الأذونات بناءً على ذلك ، ولكن سينتهي بها الأمر بالتأكيد كتصميم فظيع مرتبط بإحكام. بالنسبة للعلاقات التي لا تحتوي على روابط تشعبية ، لا يمكنك حتى القيام بذلك. ليس هناك ما يضمن أن يتم عرض كل نموذج مرة واحدة على طريقة عرض أساسية واحدة ، لذلك لا يمكنك محاولة تحديد الأذونات التي تريد استخدامها تلقائيًا للعلاقات غير المرتبطة بالارتباط التشعبي.

(+) في الواقع ربما ليس من الممكن فعلاً فعل ذلك بأي طريقة _ معقولة_ ، لكن لنتظاهر في الوقت الحالي.

ربما يكون لديك "has__permission "؟ سيكون كل كائن إذن قادرًا بعد ذلك على تحديد الكائنات ذات الصلة التي يمكن عرضها أم لا.

كيف يستخدم الناس التصفية؟ هل يستخدمونه فقط لإخفاء كائنات لا يملك المستخدم أذونات؟ لأنه إذا كانت هذه هي حالة الاستخدام ، فربما لا تكون هناك حاجة إلى المرشحات.

تتعامل إحدى المشكلات المشار إليها # 1646 مع تقييد الخيارات المعروضة على صفحات واجهة برمجة التطبيقات القابلة للتصفح للحقول ذات الصلة.

أنا أحب واجهة برمجة التطبيقات القابلة للتصفح وأعتقد أنها أداة رائعة ليس فقط بالنسبة لي كمطور للواجهة الخلفية ولكن أيضًا لمطوري / مستخدمي الواجهة الأمامية لـ REST API. أرغب في شحن المنتج مع تشغيل واجهة برمجة التطبيقات القابلة للتصفح (أي .. تعمل حتى عندما لا يكون الموقع وحيدًا في وضع DEBUG). لكي أكون قادرًا على القيام بذلك ، لا يمكنني حدوث تسرب للمعلومات من خلال صفحات واجهة برمجة التطبيقات القابلة للتصفح. (هذا بالطبع بالإضافة إلى اشتراط أن تكون هذه الصفحات بشكل عام جاهزة وآمنة للإنتاج).

ما يعنيه ذلك هو أنه لا يمكن التعرف على مزيد من المعلومات حول وجود الحقول ذات الصلة من خلال صفحات HTML أكثر مما يمكن التعرف عليه من خلال النشر.

انتهى بي الأمر بإنشاء فئة mixin للمسلسلات الخاصة بي والتي تستخدم عرض الحقل ذي الصلة لتوفير التصفية.

class RelatedFieldPermissionsSerializerMixin(object):
    """
    Limit related fields based on the permissions in the related object's view.

    To use, mixin the class, and add a dictionary to the Serializer's Meta class
    named "related_queryset_filters" mapping the field name to the string name 
    of the appropriate view class.  Example:

    class MySerializer(serializers.ModelSerializer):
        class Meta:
            related_queryset_filters = {
                'user': 'UserViewSet',
            }

    """
    def __init__(self, *args, **kwargs):
        super(RelatedFieldPermissionsSerializerMixin, self).__init__(*args, **kwargs)
        self._filter_related_fields_for_html()

    def _filter_related_fields_for_html(self):
        """
        Ensure thatk related fields are ownership filtered for
        the browseable HTML views.
        """
        import views
        try:
            # related_queryset_filters is a map of the fieldname and the viewset name (str)
            related_queryset_filters = self.Meta.related_queryset_filters
        except AttributeError:
            related_queryset_filters = {}
        for field, viewset in related_queryset_filters.items():
            try:
                self.fields[field].queryset = self._filter_related_qs(self.context['request'], getattr(views, viewset))
            except KeyError:
                pass

    def _filter_related_qs(self, request, ViewSet):
        """
        Helper function to filter related fields using
        existing filtering logic in ViewSets.
        """
        view = ViewSet()
        view.request = request
        view.action = 'retrieve'
        queryset =  view.get_queryset()
        try:
            return view.queryset_ownership_filter(queryset)
        except AttributeError:
            return queryset

لقد قمت بحل هذا باستخدام طريقة عرض mixin: # 1935 بدلاً من خلط Serializers و Views. بدلاً من الحاجة إلى قاموس ، استخدمت للتو قائمة secured_fields في العرض.

لقد استخدمت مزيج Serializer البسيط هذا لتصفية مجموعات الاستعلام في الحقول ذات الصلة:

class FilterRelatedMixin(object):
    def __init__(self, *args, **kwargs):
        super(FilterRelatedMixin, self).__init__(*args, **kwargs)
        for name, field in self.fields.iteritems():
            if isinstance(field, serializers.RelatedField):
                method_name = 'filter_%s' % name
                try:
                    func = getattr(self, method_name)
                except AttributeError:
                    pass
                else:
                    field.queryset = func(field.queryset)

الاستخدام بسيط أيضًا:

class SocialPageSerializer(FilterRelatedMixin, serializers.ModelSerializer):
    account = serializers.PrimaryKeyRelatedField()

    class Meta:
        model = models.SocialPage

    def filter_account(self, queryset):
        request = self.context['request']
        return queryset.filter(user=request.user)

كيف هذا؟ هل ترى أي مشاكل في ذلك؟

بالنسبة لي ، أود الاحتفاظ بالمنطق حول المستخدمين والطلبات خارج المسلسل وترك ذلك في العرض.

المشكلة في المجالات ذات الصلة. يمكن للمستخدم الوصول إلى العرض ولكن
ليس لجميع الكائنات ذات الصلة.

يوم الأربعاء ، 5 تشرين الثاني (نوفمبر) 2014 الساعة 6:16 مساءً ، أليكس روتبيرج [email protected]
كتب:

بالنسبة لي ، أود الاحتفاظ بمنطق حول المستخدمين والطلبات خارج نطاق
المسلسل وترك ذلك في العرض.

-
قم بالرد على هذا البريد الإلكتروني مباشرة أو قم بعرضه على GitHub
https://github.com/tomchristie/django-rest-framework/issues/1985#issuecomment -61873766
.

من فضلك ، اقرأ هذا ، يبدو أنه موضوع متصل. كيف يمكننا فصل كائنات الحقول ذات الصلة بالفلترة (ModelSerializer) لطريقة OPTIONS وطريقة POST أو PUT؟

https://groups.google.com/forum/#!topic/django -rest-framework / jMePw1vS66A

إذا قمنا بتعيين النموذج = serializers.PrimaryKeyRelatedField (queryset = Model.objects.none ()) ، فلا يمكننا حفظ الكائن الحالي مع مثيل النموذج ذي الصلة ، لأن مجموعة استعلام sealizer PrimaryKeyRelatedField "تستخدم للبحث عن مثيل النموذج عند التحقق من صحة إدخال الحقل".

إذا كان النموذج = serializers.PrimaryKeyRelatedField (queryset = Model.objects.all ()) (كإعداد افتراضي لـ ModelSerializer يمكننا التعليق على ذلك) ، ثم جميع الكائنات "ذات الصلة" (ليست مرتبطة حقًا ، لأن OPTIONS تعرض الإجراءات (POST ، PUT) خصائص فئة النموذج الرئيسية ، وليس المثيل مع الكائنات ذات الصلة) المعروضة في اختيارات حقل "النموذج" (طريقة OPTIONS).

تحديث. @ cancan101 +1. ولكن ليس فقط "المستخدم". أعتقد أن هذه فكرة سيئة لدمج المنطق والمسلسلات ، كما أرى مجموعة الاستعلام في المسلسلات: "serializers.PrimaryKeyRelatedField (queryset =".

طبعا هذا جيد:

فئة الموديل
فئة ميتا:
النموذج = الموديل

لأن المسلسل يجب أن يعرف كيف وأي الحقول يتم إنشاؤها تلقائيًا من النموذج.

ومع ذلك ، قد أكون مخطئا.

يبدو أن هذا يعمل:

class BlogSerializer(serializers.ModelSerializer):

    entries = serializers.SerializerMethodField()

    class Meta:
        model = Blog

    def get_entries(self, obj):
        queryset = obj.entries.all()
        if 'request' in self.context:
            queryset = queryset.filter(author=self.context['request'].user)
        serializer = EntrySerializer(queryset, many=True, context=self.context)
        return serializer.data

@ dustinfarris هذا يجعله

ركض في مشكلة يبدو أنها مرتبطة بهذا الموضوع. عند تمكين خلفية التصفية (مرشح Django في حالتي) ، تضيف واجهة برمجة التطبيقات القابلة للتصفح زر Filters إلى الواجهة وبقدر ما أستطيع أن أقول أن القائمة المنسدلة لا تحترم مجموعة الاستعلام المحددة في الحقل. يبدو لي كما ينبغي.

مثال:

class Item(models.Model):
    project = models.ForeignKey(Project)

class ItemSerializer(serializers.ModelSerializer):
    def __init__(self, *args, **kwargs):
        request = kwargs.get('context', {}).get('request')
        self.fields['project'].queryset = request.user.project_set.all()
        super(ItemSerializer, self).__init__(*args, **kwargs)

يحد المثال أعلاه من القائمة المنسدلة لمشروع إضافة / تحرير العنصر إلى المشاريع الصحيحة ولكن لا يزال كل شيء يظهر في القائمة المنسدلة Filters .

لقد نجح نهج Nailgun بشكل جيد بالنسبة لي ، ولكن فقط بالنسبة للعلاقات الفردية. الآن ، لدي نموذج واحد حيث تكون علاقتي في ManyToManyField. في هذه الحالة ، لا يعمل نهج Mixin. أي فكرة عن كيفية حلها لهذه؟

fibbs يغير نهج Nailgun عن طريق إضافة ما يلي:

            if isinstance(field, serializers.ManyRelatedField):
                method_name = 'filter_%s' % name
                try:
                    func = getattr(self, method_name)
                except AttributeError:
                    pass
                else:
                    field.child_relation.queryset = func(field.child_relation.queryset) 

من الواضح أن شخصًا ما ساهم في حل نظيف لهذا الأمر وأصبح ممكنًا الآن دون اختراق طرق init: https://medium.com/django-rest-framework/limit-related-data-choices-with-django-rest-framework-c54e96f5815e

أتساءل لماذا لم يتم تحديث / إغلاق هذا الموضوع؟

أن يكون هذا الشخص أنا ؛)
نقطة جيدة ، سأغلق هذا لأن رقم 3605 يضيف بالفعل شيئًا عن ذلك في الوثائق.
سنظل نفكر في إجراء مزيد من التحسينات على هذا الجزء إذا كان بإمكان شخص ما أن يأتي بشيء ما.

إنه لأمر رائع أن تكون طريقة get_queryset() موجودة الآن لـ RelatedFields. سيكون من الرائع الحصول عليه مع Serializers المتداخلة أيضًا!

المحتمل. تريد المضي قدما في ذلك؟ :)

واو ... مذهل ، إذا تم توثيق هذا ، هل يمكنك أن تدلني على الاتجاه الصحيح؟ استغرق مني وقتا طويلا لمعرفة هذا واحد!

فيما يلي ملخص لمشكلتي من أجل التحرير:

في هذا المثال ، أسماء النماذج الخاصة بي هي "publishEnvs" و "Host".
تحتوي عمليات النشر على مفتاح خارجي لنموذج المضيف (على سبيل المثال ، يمكن للعديد من البرامج التي تم نشرها أن تشير إلى نفس المضيف). كنت بحاجة إلى جهاز التسلسل لعرض حقل fqdn الخاص بـ HOST بدلاً من PK للمضيف (لقد استخدمت حقلًا مرتبطًا بميزة slug من أجل ذلك الذي كان بسيطًا جدًا). كنت بحاجة أيضًا عند إنشاء إدخال منشور (POST) ، لأتمكن من تحديد المضيف من خلال البحث عن قيمة FK لحقل HOST بواسطة FQDN ذي الصلة. على سبيل المثال: أنشئ منشورًا مع مضيف حقل (تم ضبطه على مطابقة fqdn لكائن مضيف ذي صلة) من خلال البحث عن PK لكائن المضيف باستخدام حقل match host.fqdn.

لسوء الحظ ، لم أتمكن من تقييد النتائج التي تم إرجاعها في الشريط المنسدل إلى مجرد اختيارات للكائنات المضيفة التي يملكها المستخدم الحالي.

إليك رمز الإصلاح الخاص بي الذي تم تكييفه لاستخدام slugRelatedField

class UserHostsOnly(serializers.SlugRelatedField):
    def get_queryset(self):
        user = self.context['request'].user
        queryset = Host.objects.filter(owner=user)
        return queryset

class deployEnvSerializer(serializers.ModelSerializer):
    host = UserHostsOnly(slug_field='fqdn')

لدي حوالي 5 كتب في Django (يمكنني سردها جميعًا إذا كنت ترغب في ذلك) ، ولا يُظهر أي من النصوص المرجعية كيفية العمل مع هذه المنطقة / وظيفة الإطار المحددة. في البداية ظننت أنني أفعل شيئًا خاطئًا ، أليس كذلك؟ هل هناك طريقة أفضل للقيام بما أحاول القيام به؟ لا تتردد في الاتصال بي خارج النطاق الترددي حتى لا ينتهي بي الأمر بالتلاعب بالتعليقات الخاصة بهذه المشكلة. شكرًا للجميع لأخذ الوقت الكافي لقراءة تعليقي (بصفتي مبتدئًا في django ، كان من الصعب حقًا فهم ذلك).

تضمين التغريدة

يحتوي كل Field في DRF (بما في ذلك Serializers أنفسهم) على طريقتين أساسيتين لتسلسل البيانات داخل وخارج (أي بين أنواع JSON و Python):

  1. to_representation - خروج البيانات
  2. to_internal_value - تأتي البيانات "في"

الخروج من المخطط التقريبي للنماذج التي قدمتها ، فيما يلي مخطط تفصيلي لكيفية عمل RelatedFields ، مع كون SlugRelatedField إصدارًا متخصصًا:

class UserHostsRelatedField(serializers.RelatedField):
    def get_queryset(self):
        # do any permission checks and filtering here
        return Host.objects.filter(user=self.context['request'].user)

    def to_representation(self, obj):
        # this is the data that "goes out"
        # convert a Python ORM object into a string value, that will in turn be shown in the JSON
        return str(obj.fqdn)

    def to_internal_value(self, data):
        # turn an INCOMING JSON value into a Python value, in this case a Django ORM object
        # lets say the value 'ADSF-1234'  comes into the serializer, you want to grab it from the ORM
        return self.get_queryset().get(fqdn=data)

في الواقع ، تريد عادةً وضع مجموعة من الشيكات إما بالطريقة get_queryset أو to_internal_value ، لأشياء مثل الأمان (في حالة استخدام شيء مثل django-guardian أو rules ) وكذلك للتأكد من وجود كائن ORM الفعلي.

قد يبدو هذا المثال الأكثر اكتمالا مثل هذا

from rest_framework.exceptions import (
    ValidationError,
    PermissionError,
)
class UserHostsRelatedField(serializers.RelatedField):
    def get_queryset(self):
        return Host.objects.filter(user=self.context['request'].user)

    def to_representation(self, obj):
        return str(obj.fqdn)

    def to_internal_value(self, data):
        if not isinstance(data, str):
            raise ValidationError({'error': 'Host fields must be strings, you passed in type %s' % type(data)})
        try:
            return self.get_queryset().get(fqdn=data)
        except Host.DoesNotExist:
            raise PermissionError({'error': 'You do not have access to this resource'})

فيما يتعلق بما كتبه @ cancan101 منذ بعض الوقت:

ركض في مشكلة يبدو أنها مرتبطة بهذا الموضوع. عند تمكين خلفية التصفية (مرشح Django في حالتي) ، تضيف واجهة برمجة التطبيقات القابلة للتصفح زر عوامل التصفية إلى الواجهة وبقدر ما أستطيع أن أقول أن القائمة المنسدلة لا تحترم مجموعة الاستعلام المحددة في الحقل. يبدو لي كما ينبغي.

لا يزال هذا صحيحًا بقدر ما أستطيع أن أرى. يمكن معالجة ذلك عبر حقل Filterset مخصص لحقل المفتاح الأجنبي الذي يسرب البيانات ، لكن tomchristie ما زلت أعتقد أنه يجب حل هذا "تلقائيًا" ويجب أن يحترم اختيار نموذج المرشح طريقة get_queryset من إعلان الحقل المخصص في المسلسل.

في أي حال سوف تحتاج إلى وثائق إضافية.

أقوم بتوثيق أدناه كيفية حل هذه المشكلة عبر مجموعة فلاتر مخصصة:

نموذج نموذج العمل:

class WorkEntry(models.Model):
   date = models.DateField(blank=False, null=True, default=date.today)
   who = models.ForeignKey(User, on_delete=models.CASCADE)
   ...

مجموعة عرض النموذج الأساسي:

class WorkEntryViewSet(viewsets.ModelViewSet):
   queryset = WorkEntry.objects.all().order_by('-date')
   # only work entries that are owned by request.user are returned
   filter_backends = (OnlyShowWorkEntriesThatAreOwnedByRequestUserFilterBackend, ...)
   # 
   filter_fields = (
      # this shows a filter dropdown that contains User.objects.all() - data leakage!
      'who',
   )
   # Solution: this overrides filter_fields above
   filter_class = WorkentryFilter

Custom FilterSet (يتجاوز filter_fields عبر filter_class في مجموعة عرض النموذج الأساسي)

class WorkentryFilter(FilterSet):
    """
    This sets the available filters and filter types
    """
    # foreignkey fields need to be overridden otherwise the browseable API will show User.objects.all()
    # data leakage!
    who = ModelChoiceFilter(queryset=who_filter_function)

    class Meta:
        model = WorkEntry
        fields = {
            'who': ('exact',),
        }

مجموعة الاستعلام قابلة للاستدعاء كما هو موثق هنا: http://django-filter.readthedocs.io/en/latest/ref/filters.html#modelchoicefilter

def who_filter_function(request):
    if request is None:
        return User.objects.none()
   # this solves the data leakage via the filter dropdown
   return User.objects.filter(pk=request.user.pk)

تضمين التغريدة

ألق نظرة على هذا الكود:

ألا يصلح هذا مشكلة تسرب البيانات التي تشير إليها؟ حقول البحث الخاصة بي موجودة في واجهة برمجة التطبيقات القابلة للتصفح ، ولكن النتائج لا تزال مقتصرة على مجموعة طلبات البحث التي تمت تصفيتها من قبل المالك.

class HostsViewSet(DefaultsMixin, viewsets.ModelViewSet):
    search_fields = ('hostname','fqdn')
    def get_queryset(self):
        owner = self.request.user
        queryset = Host.objects.filter(owner=owner)
        return queryset

Lcstyle لا أحاول تصفية المضيفين ، فأنا أحاول تصفية مثيلات حقل ذي صلة (على سبيل المثال ، مالكو مضيف)

أنا أبحث في هذه المشكلة بالذات التي أود حلها في REST ... عادةً تستند الأمثلة على request.user . أود التعامل مع حالة أكثر تعقيدًا بعض الشيء.

لنفترض أن لدي Company لديه Employees و Company له سمة موظف الشهر:

class Company(Model):
   employee_of_the_month = ForeignKey(Employee)
   ...

class Employee(Model):
    company = ForeignKey(Company)

أرغب في أن تحد واجهة REST من employee_of_the_month بمقدار Employee بنفس company.id مثل Company .

هذا ما توصلت إليه حتى الآن ،

class CompanySerializer(ModelSerializer):
   employee_of_the_month_id = PrimaryKeyRelatedField(
     source='employee_of_the_month',
     queryset=Employee.objects.all())

   def __init__(self, *args, **kwargs):                                        
        super(CompanySerializer, self).__init__(*args, **kwargs)              
        view = self.context.get('view', None)                                   
        company_id = None                                                     
        if view and isinstance(view, mixins.RetrieveModelMixin):                
            obj = view.get_object()                                             
            if isinstance(obj, Company):   #  We could get the model from the queryset.                                     
                company_id = obj.id                                           
        q = self.fields['employee_of_the_month_id'].queryset
        self.fields['employee_of_the_month_id'].queryset = q.filter(company_id=company_id)

... هل هذه الطريقة يمكن استخلاصها؟ أنها تقوم قليلا علىnailgun الصورة https://github.com/encode/django-rest-framework/issues/1985#issuecomment -61871134

أفكر أيضًا أنه يمكنني أيضًا validate() أن employee_of_the_month يفي بمجموعة طلبات البحث المبنية أعلاه من خلال محاولة إجراء get() مقابل مجموعة طلبات البحث باستخدام employee_of_the_month.id

بالنظر إلى # 3605 ، أرى أنه يمكن أيضًا القيام بذلك باستخدام مُسلسل مخصص للمجال - دعنا نستخدم الرئيس التنفيذي بدلاً من موظف الشهر:

 class CEOField(serializers.PrimaryKeyRelatedField):                 

      def get_queryset(self):                                                     
          company_id = None                                                     
          view = self.context.get('view', None)                                   
          if view and isinstance(view, mixins.RetrieveModelMixin):                
              obj = view.get_object()                                             
              if isinstance(obj, Company):                                      
                  dashboard_id = obj.id                                           
          return Employee.objects.filter(company_id=company_id)           

تم تصميم هذا خصيصًا لعدم إرجاع أي كائنات للاختيار إلا إذا كنا نبحث عن شركة معينة. بعبارة أخرى ، لا يمكن أن يكون لشركة جديدة مدير تنفيذي إلا إذا كان لديها موظفين ، وهو ما لا يمكنك الحصول عليه حتى يتم إنشاء الشركة.

أسفي الوحيد لهذا النهج هو أنه يبدو أنه يمكن جعله جافًا / عام.

هل كانت هذه الصفحة مفيدة؟
0 / 5 - 0 التقييمات