Code

Ticket #10720: patch_for_django_rev-10131.diff

File patch_for_django_rev-10131.diff, 25.3 KB (added by Aryeh Leib Taurog <vim@…>, 5 years ago)

Provides OrderedManyToManyField with convenient API

Line 
1Ordered Many-to-Many Relation Fields for Django
2
3Version: 1.0
4Date: Tue Mar 31 21:47:05 JDT 2009
5
6For more information, see http://www.aryehleib.com/MutableLists.html
7
8    Todo
9    ----
10
11    1. List-interface API to ordered ForeignKey relations
12    2. Doubly-ordered many-to-many relation fields
13    3. Symmetric, ordered self-referential many-to-many relations
14    4. Allow multiple occurences, i.e. (to, from, order) is unique,
15       not just (to, from)
16
17Index: django/db/models/fields/ordered.py
18===================================================================
19--- django/db/models/fields/ordered.py  (revision 0)
20+++ django/db/models/fields/ordered.py  (revision 0)
21@@ -0,0 +1,292 @@
22+from django.db import connection, transaction
23+from django.db.models.fields import related as relfields
24+from django.utils.functional import curry
25+from django.utils.mutable_list import ListMixin
26+from django.core.exceptions import ObjectDoesNotExist
27+"""
28+Provides Singly Ordered Many To Many Relation Field
29+"""
30+
31+class OrderedManyIndexError(ObjectDoesNotExist):
32+    "Out of range index on ordered many-to-many relation list"
33+
34+def create_ordered_many_related_manager(manager_base, through=None):
35+    """
36+    Creates a manager that subclasses manager_base
37+    to provide a set-style interface for the non-ordered side
38+    for partially ordered many-to-many related objects
39+    """
40+    ManyRelatedManager = relfields.create_many_related_manager(manager_base, through)
41+
42+    class OrderedManyRelatedManager(ManyRelatedManager):
43+        def __init__(self, model, *args, **kwargs):
44+            self.order_col_name = kwargs.pop('order_col_name')
45+            super(OrderedManyRelatedManager, self).__init__(model, *args, **kwargs)
46+
47+        def _add_one_item(self, target_pk, cursor=None):
48+            if cursor is None:
49+                cursor = connection.cursor()
50+
51+            # get the ordering for object to add and append this rel
52+            cursor.execute("SELECT MAX(%s) FROM %s WHERE %s = %%s" % \
53+                            (self.order_col_name, self.join_table, self.target_col_name),
54+                            [ target_pk ])
55+            result  = cursor.fetchone()[0]
56+            if result is None:
57+                next_order = 0
58+            else:
59+                next_order = result + 1
60+
61+            # Add the ones that aren't there already
62+            cursor.execute("INSERT INTO %s (%s, %s, %s) VALUES (%%s, %%s, %%s)" % \
63+                (self.join_table, self.source_col_name, self.target_col_name,
64+                    self.order_col_name),
65+                [self._pk_val, target_pk, next_order])
66+
67+        def _add_items(self, source_col_name, target_col_name, *objs):
68+            # join_table: name of the m2m link table
69+            # source_col_name: the PK colname in join_table for the source object
70+            # target_col_name: the PK colname in join_table for the target object
71+            # *objs - objects to add. Either object instances, or primary keys of object instances.
72+            assert source_col_name == self.source_col_name, 'source is "%s", should be "%s"' % \
73+                    (source_col_name, self.source_col_name)
74+            assert target_col_name == self.target_col_name, 'target is "%s", should be "%s"' % \
75+                    (target_col_name, self.target_col_name)
76+
77+            # If there aren't any objects, there is nothing to do.
78+            if objs:
79+                # Check that all the objects are of the right type
80+                new_ids = set()
81+                for obj in objs:
82+                    if isinstance(obj, self.model):
83+                        new_ids.add(obj._get_pk_val())
84+                    else:
85+                        new_ids.add(obj)
86+                # Add the newly created or already existing objects to the join table.
87+                # First find out which items are already added, to avoid adding them twice
88+                cursor = connection.cursor()
89+                cursor.execute("SELECT %s FROM %s WHERE %s = %%s AND %s IN (%s)" % \
90+                    (target_col_name, self.join_table, source_col_name,
91+                    target_col_name, ",".join(['%s'] * len(new_ids))),
92+                    [self._pk_val] + list(new_ids))
93+                existing_ids = set([row[0] for row in cursor.fetchall()])
94+
95+                # Add the ones that aren't there already
96+                for obj_id in (new_ids - existing_ids):
97+                    self._add_one_item(obj_id, cursor)
98+                transaction.commit_unless_managed()
99+
100+    return OrderedManyRelatedManager
101+
102+def create_reverse_ordered_many_related_manager(manager_base, through=None):
103+    """
104+    Creates a manager that subclasses manager_base
105+    as well as ListMixin to provide list-style interface
106+    to ordered many-to-many related objects
107+    """
108+    ManyRelatedManager = relfields.create_many_related_manager(manager_base, through)
109+
110+    class ReverseOrderedManyRelatedManager(ManyRelatedManager, ListMixin):
111+        _IndexError = OrderedManyIndexError
112+
113+        def __init__(self, model, *args, **kwargs):
114+            self.order_col_name = kwargs.pop('order_col_name')
115+            super(ReverseOrderedManyRelatedManager, self).__init__(model, *args, **kwargs)
116+            # TODO: dynamically get the pk type for related field,
117+            # instead of assuming it's an int/long
118+            self._allowed = (model, int, long)
119+
120+        def get_query_set(self):
121+            extras = { 'order_by'    : ['%s.%s' % (self.join_table, self.order_col_name)], }
122+            return manager_base.get_query_set(self)._next_is_sticky().filter(**(self.core_filters)).extra(**extras)
123+
124+        def __len__(self):
125+            return self.count()
126+
127+        def __getitem__(self, index):
128+            self._begin()
129+            result = super(ReverseOrderedManyRelatedManager, self).__getitem__(index)
130+            self._end(commit=False)
131+            return result
132+
133+        def __setitem__(self, index, val):
134+            self._begin()
135+            super(ReverseOrderedManyRelatedManager, self).__setitem__(index, val)
136+            self._end(commit=True)
137+        __setitem__.alters_data = True
138+
139+        def __delitem__(self, index):
140+            self._begin()
141+            super(ReverseOrderedManyRelatedManager, self).__delitem__(index)
142+            self._end(commit=True)
143+        __delitem__.alters_data = True
144+
145+        def _begin(self):
146+            query   = 'SELECT "id", %s FROM %s WHERE %s = %%s ORDER BY %s' % \
147+                                (self.source_col_name, self.join_table,
148+                                    self.target_col_name, self.order_col_name)
149+            self._cursor  = connection.cursor()
150+            self._cursor.execute(query, [self._pk_val])
151+            self._cache = self._cursor.fetchall()
152+
153+        def _end(self, commit=False):
154+            del self._cursor
155+            del self._cache
156+            if commit:
157+                transaction.commit_unless_managed()
158+
159+        def add(self, *objs):
160+            self.extend(objs)
161+        add.alters_data = True
162+
163+        def remove(self, *objs):
164+            for o in objs:
165+                ListMixin.remove(self, o)
166+        remove.alters_data = True
167+
168+        def clear(self):
169+            self[:] = []
170+        clear.alters_data = True
171+
172+        def _get_single_external(self, i):
173+            target_pk  = self._cache[i][1]
174+            return super(ReverseOrderedManyRelatedManager, self).get(pk=target_pk)
175+
176+        def _set_single(self, i, obj):
177+            join_id = self._cache[i][0]
178+            query   = 'UPDATE %s SET %s = %%s WHERE "id" = %%s' % \
179+                        (self.join_table, self.source_col_name)
180+            self._cursor.execute(query, [obj.pk, join_id])
181+
182+        # need to use int as pk of related object for serialization
183+        # so we use a special class to distinguish indices into the cache
184+        class _PK(object):
185+            def __init__(self, pk):
186+                self.pk = pk
187+
188+        def _get_single_internal(self, i):
189+            return self._PK(i)
190+
191+        def _set_list(self, length, items):
192+            # Optimize this by re-using existing records?
193+            query   = 'DELETE FROM %s WHERE %s = %%s' % \
194+                        (self.join_table, self.target_col_name)
195+            self._cursor.execute(query, [self._pk_val])
196+           
197+            vals_list   = []
198+            my_pk       = self._pk_val
199+            for i, item in enumerate(items):
200+                if isinstance(item, self._PK):
201+                    vals_list.append((my_pk, self._cache[item.pk][1], i))
202+                elif isinstance(item, (int, long)):
203+                    vals_list.append((my_pk, item, i))
204+                else:
205+                    vals_list.append((my_pk, item.pk, i))
206+
207+            query = 'INSERT INTO %s (%s, %s, %s) VALUES (%%s, %%s, %%s)' % \
208+                    (self.join_table, self.target_col_name,
209+                        self.source_col_name, self.order_col_name)
210+            self._cursor.executemany(query, vals_list)
211+
212+    return ReverseOrderedManyRelatedManager
213+
214+class OrderedManyRelatedObjectsDescriptor(relfields.ManyRelatedObjectsDescriptor):
215+    def __get__(self, instance, instance_type=None):
216+        if instance is None:
217+            return self
218+
219+        # Dynamically create a class that subclasses the related
220+        # model's default manager.
221+        rel_model = self.related.model
222+        superclass = rel_model._default_manager.__class__
223+        OrderedRelatedManager = create_ordered_many_related_manager(superclass, self.related.field.rel.through)
224+
225+        qn = connection.ops.quote_name
226+        manager = OrderedRelatedManager(
227+            model=rel_model,
228+            core_filters={'%s__pk' % self.related.field.name: instance._get_pk_val()},
229+            instance=instance,
230+            symmetrical=False,
231+            join_table=qn(self.related.field.m2m_db_table()),
232+            source_col_name=qn(self.related.field.m2m_reverse_name()),
233+            target_col_name=qn(self.related.field.m2m_column_name()),
234+            order_col_name=qn(self.related.field.m2m_order_name()),
235+        )
236+
237+        return manager
238+
239+class ReverseOrderedManyRelatedObjectsDescriptor(relfields.ReverseManyRelatedObjectsDescriptor):
240+    def __get__(self, instance, instance_type=None):
241+        # probably should not allow this
242+        if instance is None:
243+            return self
244+
245+        # Dynamically create a class that subclasses the related
246+        # model's default manager.
247+        rel_model = self.field.rel.to
248+        superclass = rel_model._default_manager.__class__
249+        ReverseOrderedRelatedManager = create_reverse_ordered_many_related_manager(superclass, self.field.rel.through)
250+
251+        qn = connection.ops.quote_name
252+        manager = ReverseOrderedRelatedManager(
253+            model=rel_model,
254+            core_filters={'%s__pk' % self.field.related_query_name(): instance._get_pk_val()},
255+            instance=instance,
256+            symmetrical=False,
257+            join_table=qn(self.field.m2m_db_table()),
258+            source_col_name=qn(self.field.m2m_reverse_name()),
259+            target_col_name=qn(self.field.m2m_column_name()),
260+            order_col_name=qn(self.field.m2m_order_name()),
261+        )
262+
263+        return manager
264+
265+    def __set__(self, instance, val):
266+        if instance is None:
267+            raise AttributeError, "Manager must be accessed via instance"
268+        manager = self.__get__(instance)
269+        manager[:] = list(val)
270+
271+
272+class OrderedManyToManyField(relfields.ManyToManyField):
273+    ordered = 1
274+    def __init__(self, *args, **kwargs):
275+        kwargs['symmetrical'] = False
276+        if 'through' in kwargs:
277+            raise ValueError, "Use of 'through' model not allowed for OrderedManyToManyField"
278+   
279+        super(OrderedManyToManyField, self).__init__(*args, **kwargs)
280+
281+    def contribute_to_class(self, cls, name):
282+        super(OrderedManyToManyField, self).contribute_to_class(cls, name)
283+        # Add the descriptor for the m2m relation
284+        setattr(cls, self.name, ReverseOrderedManyRelatedObjectsDescriptor(self))
285+
286+        # Set up the accessor for the m2m table name for the relation
287+        self.m2m_db_table = curry(self._get_m2m_db_table, cls._meta)
288+
289+        if isinstance(self.rel.to, basestring):
290+            target = self.rel.to
291+        else:
292+            target = self.rel.to._meta.db_table
293+        cls._meta.duplicate_targets[self.column] = (target, "m2m")
294+
295+    def contribute_to_related_class(self, cls, related):
296+        # Set up the accessors for the column names on the m2m table
297+        self.m2m_column_name = curry(self._get_m2m_column_name, related)
298+        self.m2m_reverse_name = curry(self._get_m2m_reverse_name, related)
299+        self.m2m_order_name = curry(self._get_m2m_order_name, related)
300+
301+        setattr(cls, related.get_accessor_name(), OrderedManyRelatedObjectsDescriptor(related))
302+
303+    def _get_m2m_order_name(self, related):
304+        "Function that can be curried to provide the related column name for the m2m table"
305+        try:
306+            return self._m2m_order_name_cache
307+        except:
308+            self._m2m_order_name_cache = related.parent_model._meta.object_name.lower() + '_order'
309+            return self._m2m_order_name_cache
310+
311
312Property changes on: django/db/models/fields/ordered.py
313___________________________________________________________________
314Name: svn:executable
315   + *
316
317Index: django/db/models/__init__.py
318===================================================================
319--- django/db/models/__init__.py        (revision 10131)
320+++ django/db/models/__init__.py        (working copy)
321@@ -11,6 +11,7 @@
322 from django.db.models.fields.subclassing import SubfieldBase
323 from django.db.models.fields.files import FileField, ImageField
324 from django.db.models.fields.related import ForeignKey, OneToOneField, ManyToManyField, ManyToOneRel, ManyToManyRel, OneToOneRel
325+from django.db.models.fields.ordered import OrderedManyToManyField
326 from django.db.models import signals
327 
328 # Admin stages.
329Index: django/db/backends/creation.py
330===================================================================
331--- django/db/backends/creation.py      (revision 10131)
332+++ django/db/backends/creation.py      (working copy)
333@@ -223,6 +223,13 @@
334                 style.SQL_FIELD(qn(field.rel.to._meta.pk.column)),
335                 self.connection.ops.deferrable_sql())
336         ]
337+        if hasattr(field, 'ordered'):
338+            table_output.append(
339+            '    %s %s %s,' %
340+                (style.SQL_FIELD(qn(field.m2m_order_name())),
341+                style.SQL_COLTYPE(models.IntegerField().db_type()),
342+                style.SQL_KEYWORD('NOT NULL'),)
343+                )
344         deferred = []
345 
346         return table_output, deferred
347Index: django/utils/mutable_list.py
348===================================================================
349--- django/utils/mutable_list.py        (revision 0)
350+++ django/utils/mutable_list.py        (revision 0)
351@@ -0,0 +1,309 @@
352+# Copyright (c) 2008-2009 Aryeh Leib Taurog, all rights reserved.
353+# Released under the New BSD license.
354+"""
355+This module contains a base type which provides list-style mutations
356+without specific data storage methods.
357+
358+See also http://www.aryehleib.com/MutableLists.html
359+
360+Author: Aryeh Leib Taurog.
361+"""
362+class ListMixin(object):
363+    """
364+    A base class which provides complete list interface.
365+    Derived classes must call ListMixin's __init__() function
366+    and implement the following:
367+
368+    function _get_single_external(self, i):
369+        Return single item with index i for general use.
370+        The index i will always satisfy 0 <= i < len(self).
371+
372+    function _get_single_internal(self, i):
373+        Same as above, but for use within the class [Optional]
374+        Note that if _get_single_internal and _get_single_internal return
375+        different types of objects, _set_list must distinguish
376+        between the two and handle each appropriately.
377+
378+    function _set_list(self, length, items):
379+        Recreate the entire object.
380+
381+        NOTE: items may be a generator which calls _get_single_internal.
382+        Therefore, it is necessary to cache the values in a temporary:
383+            temp = list(items)
384+        before clobbering the original storage.
385+
386+    function _set_single(self, i, value):
387+        Set the single item at index i to value [Optional]
388+        If left undefined, all mutations will result in rebuilding
389+        the object using _set_list.
390+
391+    function __len__(self):
392+        Return the length
393+
394+    int _minlength:
395+        The minimum legal length [Optional]
396+
397+    int _maxlength:
398+        The maximum legal length [Optional]
399+
400+    type or tuple _allowed:
401+        A type or tuple of allowed item types [Optional]
402+
403+    class _IndexError:
404+        The type of exception to be raise on invalid index [Optional]
405+    """
406+
407+    _minlength = 0
408+    _maxlength = None
409+    _IndexError = IndexError
410+
411+    ### Python initialization and special list interface methods ###
412+
413+    def __init__(self, *args, **kwargs):
414+        if not hasattr(self, '_get_single_internal'):
415+            self._get_single_internal = self._get_single_external
416+
417+        if not hasattr(self, '_set_single'):
418+            self._set_single = self._set_single_rebuild
419+            self._assign_extended_slice = self._assign_extended_slice_rebuild
420+
421+        super(ListMixin, self).__init__(*args, **kwargs)
422+
423+    def __getitem__(self, index):
424+        "Get the item(s) at the specified index/slice."
425+        if isinstance(index, slice):
426+            return [self._get_single_external(i) for i in xrange(*index.indices(len(self)))]
427+        else:
428+            index = self._checkindex(index)
429+            return self._get_single_external(index)
430+
431+    def __delitem__(self, index):
432+        "Delete the item(s) at the specified index/slice."
433+        if not isinstance(index, (int, long, slice)):
434+            raise TypeError("%s is not a legal index" % index)
435+
436+        # calculate new length and dimensions
437+        origLen     = len(self)
438+        if isinstance(index, (int, long)):
439+            index = self._checkindex(index)
440+            indexRange  = [index]
441+        else:
442+            indexRange  = range(*index.indices(origLen))
443+
444+        newLen      = origLen - len(indexRange)
445+        newItems    = ( self._get_single_internal(i)
446+                        for i in xrange(origLen)
447+                        if i not in indexRange )
448+
449+        self._rebuild(newLen, newItems)
450+
451+    def __setitem__(self, index, val):
452+        "Set the item(s) at the specified index/slice."
453+        if isinstance(index, slice):
454+            self._set_slice(index, val)
455+        else:
456+            index = self._checkindex(index)
457+            self._check_allowed((val,))
458+            self._set_single(index, val)
459+
460+    def __iter__(self):
461+        "Iterate over the items in the list"
462+        for i in xrange(len(self)):
463+            yield self[i]
464+
465+    ### Special methods for arithmetic operations ###
466+    def __add__(self, other):
467+        'add another list-like object'
468+        return self.__class__(list(self) + list(other))
469+
470+    def __radd__(self, other):
471+        'add to another list-like object'
472+        return other.__class__(list(other) + list(self))
473+
474+    def __iadd__(self, other):
475+        'add another list-like object to self'
476+        self.extend(list(other))
477+        return self
478+
479+    def __mul__(self, n):
480+        'multiply'
481+        return self.__class__(list(self) * n)
482+
483+    def __rmul__(self, n):
484+        'multiply'
485+        return self.__class__(list(self) * n)
486+
487+    def __imul__(self, n):
488+        'multiply'
489+        if n <= 0:
490+            del self[:]
491+        else:
492+            cache = list(self)
493+            for i in range(n-1):
494+                self.extend(cache)
495+        return self
496+
497+    def __cmp__(self, other):
498+        'cmp'
499+        slen = len(self)
500+        for i in range(slen):
501+            try:
502+                c = cmp(self[i], other[i])
503+            except IndexError:
504+                # must be other is shorter
505+                return 1
506+            else:
507+                # elements not equal
508+                if c: return c
509+
510+        return cmp(slen, len(other))
511+
512+    ### Public list interface Methods ###
513+    ## Non-mutating ##
514+    def count(self, val):
515+        "Standard list count method"
516+        count = 0
517+        for i in self:
518+            if val == i: count += 1
519+        return count
520+
521+    def index(self, val):
522+        "Standard list index method"
523+        for i in xrange(0, len(self)):
524+            if self[i] == val: return i
525+        raise ValueError('%s not found in object' % str(val))
526+
527+    ## Mutating ##
528+    def append(self, val):
529+        "Standard list append method"
530+        self[len(self):] = [val]
531+
532+    def extend(self, vals):
533+        "Standard list extend method"
534+        self[len(self):] = vals
535+
536+    def insert(self, index, val):
537+        "Standard list insert method"
538+        if not isinstance(index, (int, long)):
539+            raise TypeError("%s is not a legal index" % index)
540+        self[index:index] = [val]
541+
542+    def pop(self, index=-1):
543+        "Standard list pop method"
544+        result = self[index]
545+        del self[index]
546+        return result
547+
548+    def remove(self, val):
549+        "Standard list remove method"
550+        del self[self.index(val)]
551+
552+    def reverse(self):
553+        "Standard list reverse method"
554+        self[:] = self[-1::-1]
555+
556+    def sort(self, cmp=cmp, key=None, reverse=False):
557+        "Standard list sort method"
558+        if key:
559+            temp = [(key(v),v) for v in self]
560+            temp.sort(cmp=cmp, key=lambda x: x[0], reverse=reverse)
561+            self[:] = [v[1] for v in temp]
562+        else:
563+            temp = list(self)
564+            temp.sort(cmp=cmp, reverse=reverse)
565+            self[:] = temp
566+
567+    ### Private routines ###
568+    def _rebuild(self, newLen, newItems):
569+        if newLen < self._minlength:
570+            raise ValueError('Must have at least %d items' % self._minlength)
571+        if self._maxlength is not None and newLen > self._maxlength:
572+            raise ValueError('Cannot have more than %d items' % self._maxlength)
573+
574+        self._set_list(newLen, newItems)
575+
576+    def _set_single_rebuild(self, index, value):
577+        self._set_slice(slice(index, index + 1, 1), [value])
578+
579+    def _checkindex(self, index, correct=True):
580+        length = len(self)
581+        if 0 <= index < length:
582+            return index
583+        if correct and -length <= index < 0:
584+            return index + length
585+        raise self._IndexError('invalid index: %s' % str(index))
586+
587+    def _check_allowed(self, items):
588+        if hasattr(self, '_allowed'):
589+            if False in [isinstance(val, self._allowed) for val in items]:
590+                raise TypeError('Invalid type encountered in the arguments.')
591+
592+    def _set_slice(self, index, values):
593+        "Assign values to a slice of the object"
594+        try:
595+            iter(values)
596+        except TypeError:
597+            raise TypeError('can only assign an iterable to a slice')
598+
599+        self._check_allowed(values)
600+
601+        origLen     = len(self)
602+        valueList   = list(values)
603+        start, stop, step = index.indices(origLen)
604+
605+        # CAREFUL: index.step and step are not the same!
606+        # step will never be None
607+        if index.step is None:
608+            self._assign_simple_slice(start, stop, valueList)
609+        else:
610+            self._assign_extended_slice(start, stop, step, valueList)
611+
612+    def _assign_extended_slice_rebuild(self, start, stop, step, valueList):
613+        'Assign an extended slice by rebuilding entire list'
614+        indexList   = range(start, stop, step)
615+        # extended slice, only allow assigning slice of same size
616+        if len(valueList) != len(indexList):
617+            raise ValueError('attempt to assign sequence of size %d '
618+                             'to extended slice of size %d'
619+                             % (len(valueList), len(indexList)))
620+
621+        # we're not changing the length of the sequence
622+        newLen  = len(self)
623+        newVals = dict(zip(indexList, valueList))
624+        def newItems():
625+            for i in xrange(newLen):
626+                if i in newVals:
627+                    yield newVals[i]
628+                else:
629+                    yield self._get_single_internal(i)
630+
631+        self._rebuild(newLen, newItems())
632+
633+    def _assign_extended_slice(self, start, stop, step, valueList):
634+        'Assign an extended slice by re-assigning individual items'
635+        indexList   = range(start, stop, step)
636+        # extended slice, only allow assigning slice of same size
637+        if len(valueList) != len(indexList):
638+            raise ValueError('attempt to assign sequence of size %d '
639+                             'to extended slice of size %d'
640+                             % (len(valueList), len(indexList)))
641+
642+        for i, val in zip(indexList, valueList):
643+            self._set_single(i, val)
644+
645+    def _assign_simple_slice(self, start, stop, valueList):
646+        'Assign a simple slice; Can assign slice of any length'
647+        origLen = len(self)
648+        stop = max(start, stop)
649+        newLen  = origLen - stop + start + len(valueList)
650+        def newItems():
651+            for i in xrange(origLen + 1):
652+                if i == start:
653+                    for val in valueList:
654+                        yield val
655+
656+                if i < origLen:
657+                    if i < start or i >= stop:
658+                        yield self._get_single_internal(i)
659+
660+        self._rebuild(newLen, newItems())
661
662Property changes on: django/utils/mutable_list.py
663___________________________________________________________________
664Name: svn:executable
665   + *
666