Browse Source

Haz reverse relations!

Marcos Dumay de Medeiros 7 years ago
parent
commit
770e53981a

+ 6 - 0
TODO

@@ -1 +1,7 @@
 Many2many does not appear in list
+Export data to csv
+Go to page number
+Filter for all basic types
+Form fields for reverse relations
+Union and disjunction of permissions
+Multiple registering of actions

+ 18 - 0
src/rapid/migrations/0004_auto_20160607_1957.py

@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('rapid', '0003_documenttemplate'),
+    ]
+
+    operations = [
+        migrations.AlterUniqueTogether(
+            name='profile',
+            unique_together=set([('application', 'name')]),
+        ),
+    ]

+ 10 - 0
src/rapid/rapidfields.py

@@ -24,6 +24,16 @@ class AlternativeData(GenericForeignKey):
 
     Create like a Django GenericForeignKey, but use an AlternativeDataTables instead of a
     ForeignKey to ContentTypes.
+
+    That is, a model becomes something like this:
+    class MyModel(models.Model):
+        alt_data_type = rapidfields.AlternativeDataTables((Model1, Model2))
+        alt_data_id = models.PositiveIntegerField()
+        alt_data = rapidfields.AlternativeData('alt_data_type', 'alt_data_id', verbose_name='fill either a model1 or
+        a model2 object')
+
+    In forms, Rapid will display a select widget with the options "model1" and "model2",
+    and show the appropriate form after selected.
     """
     is_rapid_alternatives = True
 

+ 111 - 4
src/rapid/rapidforms.py

@@ -13,7 +13,7 @@ from django.core.exceptions import ValidationError
 from django.db import transaction
 from django.utils.encoding import force_text
 
-from rapid.widgets import RapidSelector, RapidRelationReadOnly, rapid_alternatives_widget
+from rapid.widgets import RapidSelector, RapidRelationReadOnly, rapid_alternatives_widget, rapid_dependent_widget
 from rapid.wrappers import FieldData, ModelData
 
 import gettext
@@ -21,6 +21,9 @@ _ = gettext.gettext
 
 
 class RapidAlternativesField(forms.Field):
+    """
+    Form field for AlternativeData.
+    """
     def __init__(self, field_name, alternatives, selector_name, form, request, instance=None, *args, **kwargs):
         model = None
         if instance:
@@ -54,6 +57,62 @@ class RapidAlternativesField(forms.Field):
         raise ValidationError(_("Invalid value"), code='invalid')
 
 
+class RapidDependentField(forms.Field):
+    """
+    Form fields for dependent instances.
+
+    This field will be displayed for reverse relations where the
+    foreign key of this model is required. That is, for related models
+    that can not exist unless when related to this one.
+    """
+    def __init__(self, field, originator, model, request, instance=None, multiple=False, *args, **kwargs):
+        self.multiple = multiple
+        md = ModelData(model)
+        prefix = field.bare_name() + '_' + str(md.content_type().pk)
+        if multiple:
+            ftt = for_model(request, model, [(field.related_field().bare_name(), originator)])
+            ft = forms.modelformset_factory(model, form=ftt)
+        else:
+            ft = for_model(request, model, [(field.related_field().bare_name(), originator)])
+        if request.method == 'POST':
+            if multiple:
+                fm = ft(request.POST, request.FILES, prefix=prefix, queryset=instance)
+            else:
+                fm = ft(request.POST, request.FILES, prefix=prefix, instance=instance)
+        else:
+            if multiple:
+                fm = ft(prefix=prefix, queryset=instance)
+            else:
+                fm = ft(instance=instance, prefix=prefix)
+        kwargs['widget'] = rapid_dependent_widget(md, fm, bool(instance), multiple)
+        # noinspection PyArgumentList
+        super(RapidDependentField, self).__init__(field, *args, **kwargs)
+        self.required = False
+
+    def to_python(self, value):
+        if self.multiple:
+            (f, ii) = value
+            if f is None: return None
+            if f.is_valid():
+                class ValList:
+                    def __init__(self, vals):
+                        self.vals = vals
+                    def save_m2m(self):
+                        for v in self.vals:
+                            v.save_m2m()
+                    def __iter__(self):
+                        return self.vals.__iter__()
+                objs = [x.save(commit=False) for x in f.forms]
+                return ValList([objs[i] for i in ii])
+        else:
+            if value is None: return None
+            if value.is_valid():
+                obj = value.save(commit=False)
+                obj.save_m2m = value.save_m2m
+                return obj
+            raise ValidationError(_("Invalid value"), code='invalid')
+
+
 def for_model(request, model, default_relations=()):
     default_relations = list(default_relations)
     default_relations_request = request.GET.get('default')
@@ -63,9 +122,10 @@ def for_model(request, model, default_relations=()):
         default_relations_fields = default_relations_request.split(",")
         default_relations += ((x, int(y)) for (x, y) in (f.split(":") for f in default_relations_fields))
         default_relations_fields = [x for x, y in default_relations]
-        for (x, y) in default_relations:
-            f = FieldData(getattr(model, x).field, request)
-            widgets.append((x, RapidRelationReadOnly(f.related_model())))
+    for (x, y) in default_relations:
+        f = FieldData(getattr(model, x).field, request)
+        widgets.append((x, RapidRelationReadOnly(f.related_model().model)))
+        default_relations_fields.append(x)
     for f in ModelData(model).local_fields():
         if f.is_relation() and force_text(f.bare_name()) not in default_relations_fields:
             if f.related_model().has_permission(request, 'select'):
@@ -77,6 +137,8 @@ def for_model(request, model, default_relations=()):
     # noinspection PyTypeChecker
     class CForm(forms.ModelForm):
         def __init__(self, *args, **kwargs):
+
+            # Sets alternative data:
             initial = kwargs.get('initial', {})
             for (k, v) in default_relations:
                 initial[k] = v
@@ -97,6 +159,21 @@ def for_model(request, model, default_relations=()):
                     else:
                         nd[k] = v
                 self.__class__.base_fields = nd
+
+            # Sets fields for reverse relations:
+            for f in ModelData(model).related_fields():
+                if f.is_existential_dependency():
+                    md = f.related_model().model
+                    ft = {f.related_field().bare_name(): instance}
+                    dt = md.objects.filter(**ft)
+                    if f.is_multiple():
+                        fl = RapidDependentField(f, instance, md, request, dt, True)
+                    else:
+                        o = dt[0] if dt else None
+                        fl = RapidDependentField(f, instance, md, request,
+                                                 o, False)
+                    self.__class__.base_fields[f.bare_name()] = fl
+
             super(CForm, self).__init__(*args, **kwargs)
 
         @transaction.atomic
@@ -105,6 +182,8 @@ def for_model(request, model, default_relations=()):
                 return super(CForm, self).save(commit)
             else:
                 obj = super(CForm, self).save(commit=False)
+
+                # Saving alternative data:
                 for n, fd in ModelData(model).rapid_alternative_data():
                     if self.instance:
                         old_t = getattr(self.instance, fd.ct_field)
@@ -116,7 +195,34 @@ def for_model(request, model, default_relations=()):
                     if hasattr(fob, 'save_m2m'):
                         fob.save_m2m()
                     setattr(obj, fd.fk_field, fob.pk)
+
                 obj.save()
+
+                # Saving reverse relations:
+                for f in ModelData(model).related_fields():
+                    if f.is_existential_dependency():
+                        fob = self.cleaned_data[f.bare_name()]
+                        ft = {f.related_field().bare_name(): obj}
+                        if fob:
+                            if f.is_multiple():
+                                ex = {'pk__in': [ob.pk for ob in fob if ob.pk]}
+                                f.related_model().model.objects.filter(**ft).exclude(**ex).delete()
+                                for ob in fob:
+                                    setattr(ob, f.related_field().bare_name(), obj)
+                                    ob.save()
+                                    if hasattr(ob, 'save_m2m'):
+                                        ob.save_m2m()
+                            else:
+                                ex = {'pk__exact': fob.pk}
+                                f.related_model().model.objects.filter(**ft).exclude(**ex).delete()
+                                setattr(fob, f.related_field().bare_name(), obj)
+                                fob.save()
+                                if hasattr(fob, 'save_m2m'):
+                                    fob.save_m2m()
+                        else:
+                            f.related_model().model.objects.filter(**ft).delete()
+
+
                 self.save_m2m()
                 return obj
 
@@ -126,3 +232,4 @@ def for_model(request, model, default_relations=()):
             widgets = form_widgets
 
     return CForm
+

+ 7 - 5
src/rapid/templates/rapid/widgets/multiple-selector.html

@@ -1,3 +1,5 @@
+{% load rapid_crud %}
+
 <div class="rapid-select {{ id }} data reload-here">
     <input type="hidden" id="{{ id }}" name="{{ name }}" value="{{ value }}" {% for k, v in attrs %} {{ k }} = "{{ v }}" {% endfor %}>
     <ul class="rapid-select-selected">
@@ -8,7 +10,7 @@
     <a class="rapid-select-add interaction"><span class="fa fa-plus">Adicionar</span></a>
 </div>
 <script>
-    function add_ids_to_{{ id }}(source){
+    function add_ids_to_{{ id|jsid }}(source){
         var sel = source.find("table.rapid-object-selector");
         var markers = sel.children("tbody").children("tr.selected").children("td").children("input.rapid-select-id-marker");
         var old_ids = [];
@@ -32,8 +34,8 @@
             widget_root.append(li);
         }
     };
-    function add_elements_to_{{ id }}(){
-        show_overlay("{{ select_url }}", $("div.rapid-select.{{ id }}").find("a.rapid-select-add"), add_ids_to_{{ id }}, false);
+    function add_elements_to_{{ id|jsid }}(){
+        show_overlay("{{ select_url }}", $("div.rapid-select.{{ id }}").find("a.rapid-select-add"), add_ids_to_{{ id|jsid }}, false);
     };
     $(document).ready(function(){
         var d = $("div.rapid-select.{{ id }}")
@@ -49,6 +51,6 @@
             $("input#{{ id }}").val(old_ids.join(","));
             $(this).closest("li").remove();
         });
-        d.find(".rapid-select-add").click(add_elements_to_{{ id }});
+        d.find(".rapid-select-add").click(add_elements_to_{{ id|jsid }});
     });
-</script>
+</script>

+ 36 - 0
src/rapid/templates/rapid/widgets/multipleDependent.html

@@ -0,0 +1,36 @@
+{% load rapid_crud %}
+<div id="formset_{{ formset.prefix }}">
+{{ formset.management_form }}
+<style scoped>
+    .rapid-singledependent{
+        border-width: 1px;
+        border-radius: 10px;
+        border-style: solid;
+        padding: 1em;
+    }
+    .deleted{
+        text-decoration: line-through;
+    }
+</style>
+<div id="forms-for-{{ formset.prefix }}">
+    {% for form in formset %}
+        <div class="rapid-singledependent {{ formset.prefix }} form">
+            <input type="checkbox" class="includes_{{ formset.prefix }}"
+                   name="includes_{{ form.prefix }}" checked="true">
+            {{ form.as_p }}
+        </div>
+    {% endfor %}
+</div>
+<input type="button" value="Adicionar" id="add-form-in-{{ formset.prefix }}">
+<script>
+    $("#formset_{{ formset.prefix }}").on("change", "input.includes{{ formset.prefix }}", function(){
+        $(this).parent().toggleClass("deleted");
+    });
+    $("#add-form-in-{{ formset.prefix }}").click(function(){
+        var formcount = $("#forms-for-{{ formset.prefix }}").children("div").length;
+        $("#id_{{ formset.prefix }}-TOTAL_FORMS").val(formcount + 1);
+        var empty = "<div class=\"rapid-singledependent {{ formset.prefix }} form\">\n<input type=\"checkbox\" class=\"includes_{{ formset.prefix }}\" name=\"includes_{{ formset.prefix }}-__prefix__\" checked=\"true\">\n{{ formset.empty_form.as_p|jsstr }}\n<\div>";
+        $("#forms-for-{{ formset.prefix }}").append(empty.replace(/__prefix__/g, formcount));
+    });
+</script>
+</div>

+ 6 - 4
src/rapid/templates/rapid/widgets/single-selector.html

@@ -1,10 +1,12 @@
+{% load rapid_crud %}
+
 <div class="rapid-select {{ id }} data reload-here">
     <input type="hidden" id="{{ id }}" name="{{ name }}" value="{{ value }}" {% for k, v in attrs %} {{ k }} = "{{ v }}" {% endfor %}>
     <span class="value-{{ id }}">{{ selected }}</span>
     <a class="rapid-select-search interaction"><span class="fa {{ icon }}"></span></a>
 </div>
 <script>
-    function select_id_in_{{ id }}(source){
+    function select_id_in_{{ id|jsid }}(source){
         var sel = source.find("table.rapid-object-selector");
         var marker = sel.children("tbody").children("tr.selected").children("td").children("input.rapid-select-id-marker");
         var new_id = marker.val();
@@ -12,11 +14,11 @@
         $("input#{{ id }}").val(new_id);
         $("span.value-{{ id }}").text(new_name);
     };
-    function search_{{ id }}(){
-        show_overlay("{{ select_url }}", $("div.rapid-select.{{ id }}").find("a.rapid-select-search"), select_id_in_{{ id }});
+    function search_{{ id|jsid }}(){
+        show_overlay("{{ select_url }}", $("div.rapid-select.{{ id }}").find("a.rapid-select-search"), select_id_in_{{ id|jsid }});
     };
     $(document).ready(function(){
         var d = $("div.rapid-select.{{ id }}");
-        d.find(".rapid-select-search").click(search_{{ id }});
+        d.find(".rapid-select-search").click(search_{{ id|jsid }});
     });
 </script>

+ 22 - 0
src/rapid/templates/rapid/widgets/singleDependent.html

@@ -0,0 +1,22 @@
+<div>
+<style scoped>
+    .rapid-dependent{
+        border-width: 1px;
+        border-radius: 10px;
+        border-style: solid;
+        padding: 1em;
+    }
+    .hidden{
+        display: none;
+    }
+</style>
+<input type="checkbox" id="includes_{{ name }}" name="includes_{{ name }}"{% if present %} checked{% endif %}>
+<div class="rapid-dependent {{ name }} form{% if not present %} hidden{% endif %}">
+    {{ form.as_p }}
+</div>
+<script>
+    $("#includes_{{ name }}").change(function(){
+        $("div.{{ name }}.form").toggleClass("hidden");
+    });
+</script>
+</div>

+ 12 - 0
src/rapid/templatetags/rapid_crud.py

@@ -55,6 +55,18 @@ def render_to_javascript_string(template_name, context=None):
     return mark_safe(txt)
 
 
+@register.filter(name="jsid")
+def jsid(value):
+    v = value.replace('"', "").replace("-", "_").replace("'", "")
+    return v
+
+
+@register.filter(name="jsstr")
+def jsstr(txt):
+    txt = txt.replace("\"", "\\\"")
+    txt = txt.replace("\n", "\\n")
+    return mark_safe(txt)
+
 @register.inclusion_tag('rapid/overlay/register.html')
 def register_overlay():
     overlay_text = render_to_javascript_string('rapid/overlay/text.html')

+ 44 - 1
src/rapid/widgets.py

@@ -64,7 +64,7 @@ class RapidSelector(widgets.Select):
         super(RapidSelector, self).__init__(*args, **kwargs)
         self.relation = relation
         self.allow_multiple_selected = relation.is_multiple()
-        self.remove_deselected = relation.is_weak()
+        self.remove_deselected = relation.is_existential_dependency()
 
     def render(self, name, value, attrs=None, choices=()):
         tp_id = attrs.get('id', name)
@@ -130,3 +130,46 @@ def rapid_alternatives_widget(alternatives, selector):
             return alternatives.get(int(ct_id))[1]
 
     return RapidAlternativeFormsWidget
+
+
+#TODO: Finish it!
+def rapid_dependent_widget(model_data, form, has_instance, is_mutiple):
+    class RapidDependentWidget(widgets.Widget):
+        # What to ask:
+        # Is it optional? Yep, if it's dependent, it must be optional
+        # Is it multiple? In is_multiple
+        def render(self, name, value, attrs=None):
+            attrs = attrs if attrs else []
+            c = Context({'name': name,
+                         'value': value,
+                         'attrs': attrs,
+                         'model': model_data,
+                         'formset': form,
+                         }) if is_mutiple else Context (
+                {'name': name,
+                 'value': value,
+                 'attrs': attrs,
+                 'model': model_data,
+                 'form': form,
+                 'present': has_instance,
+                 })
+            # import pdb; pdb.set_trace()
+            t = loader.get_template(_templates_root + ('multipleDependent.html' if
+                                    is_mutiple else 'singleDependent.html'))
+            return t.render(c)
+
+        def value_from_datadict(self, data, files, name):
+            # Might return:
+            # None of model object if not is_multiple
+            # [] ot list of model objects if is_multiple
+            if is_mutiple:
+                list_includes = [x for x in range(0, len(form)) if data.get("includes_" + form.prefix + "-" + str(x))]
+                return (form, list_includes)
+            else:
+                s = data.get("includes_" + name)
+                if s is None:
+                    return None
+                else:
+                    return form
+
+    return RapidDependentWidget

+ 40 - 11
src/rapid/wrappers.py

@@ -195,6 +195,9 @@ class ModelData(object):
         return self._fields
 
     def local_fields(self):
+        """
+        :return: FieldData for each local (not related) field of this model.
+        """
         r = []
         # noinspection PyProtectedMember
         for f in self.model._meta.local_fields:
@@ -218,10 +221,16 @@ class ModelData(object):
                 yield (k, v)
 
     def related_fields(self):
+        """
+        :return: A FieldData for each related manager for this model.
+        """
         # noinspection PyProtectedMember
         return [FieldData(f, self.request) for f in self.model._meta.get_all_related_objects()]
 
     def is_controlled(self):
+        """
+        :return: True if this model is already controlled by the rapid registry.
+        """
         return registry.is_controlled(self.model)
 
     def can_read(self):
@@ -239,15 +248,27 @@ class ModelData(object):
         return False
 
     def create_url(self):
+        """
+        :return: URL of the "add" action.
+        """
         return registry.get_url_of_action(self.model, "add")
 
     def list_url(self):
+        """
+        :return: URL of the "list" action.
+        """
         return registry.get_url_of_action(self.model, "list")
 
     def select_url(self):
+        """
+        :return: URL of the "select" action.
+        """
         return registry.get_url_of_action(self.model, "select")
 
     def actions(self):
+        """
+        :return: All actions available for this model (with the object's request).
+        """
         r = []
         acts = registry.model_entry(self.model)
         if self.request and acts:
@@ -267,10 +288,16 @@ class ModelData(object):
         return False
 
     def field_by_name(self, field_name):
+        """
+        :return: A FieldData with the field of this model that has the given name.
+        """
         # noinspection PyProtectedMember
         return FieldData(self.model._meta.get_field(field_name), self.request)
 
     def content_type(self):
+        """
+        :return: The Django registry ContentType for this model.
+        """
         return ContentType.objects.get_for_model(self.model)
 
     def __unicode__(self):
@@ -342,7 +369,16 @@ class FieldData(object):
     def is_auto(self):
         return self.field.auto_created
 
-    def is_weak(self):
+    def is_existential_dependency(self):
+        """
+        :return: True if this field determine an existential dependency relation between this model
+        and another one.
+
+        That is, returns true if, because of this field, one element of this model can only exist if
+        there is one element of one other model.
+
+        Example: A required ForeignKey.
+        """
         if not self.is_relation():
             return False
         f = self.field
@@ -350,16 +386,9 @@ class FieldData(object):
             return False
         if hasattr(f, "many_to_one") and self.field.many_to_one:
             return False
-        if hasattr(self.field, "get_related_field"):
-            o = self.field.get_related_field
-            if self.field.one_to_one or self.field.one_to_many:
-                if hasattr(o, "required"):
-                    return o.required
-                return True
-        if isinstance(f, models.ForeignKey):
-            # noinspection PyUnresolvedReferences,PyProtectedMember
-            return self.related_model()._meta.pk.name
-        return False
+        if hasattr(self.field, "required"):
+            return self.field.required
+        return True
 
     def filter_html(self):
         return filters.Filter.selection_type_html(self, self.request)

+ 8 - 0
testproject/testproject/models.py

@@ -29,3 +29,11 @@ class Test1(models.Model):
     alt_data_type = rapidfields.AlternativeDataTables((AltData1, AltData2))
     alt_data_id = models.PositiveIntegerField()
     alt_data = rapidfields.AlternativeData('alt_data_type', 'alt_data_id', verbose_name='test alt data')
+
+class SingleDepOnTest1(models.Model):
+    test = models.OneToOneField(Test1)
+    some_text = models.CharField(max_length=30)
+
+class MultpleDepOnTest1(models.Model):
+    test = models.ForeignKey(Test1)
+    some_text = models.CharField(max_length=30)

+ 1 - 1
testproject/testproject/urls.py

@@ -17,7 +17,7 @@ urlpatterns = [
     url(r'^applications/', include('rapid.urls')),
 ]
 
-for m in (models.Test1, models.AltData1, models.AltData2):
+for m in (models.Test1, models.AltData1, models.AltData2, models.SingleDepOnTest1, models.MultpleDepOnTest1):
     urlpatterns += register.crud(m, permissions.to_all(), permissions.to_all())
 
 urlpatterns += register.document_templates(models.Test1, permissions.to_all(), permissions.to_all())