Ticket #6095: 6095-nfadmin.2.diff

File 6095-nfadmin.2.diff, 25.9 KB (added by floguy, 16 years ago)

Made sure that create is not allowed. This was an oversight in the original patch, but now there are tests which verify that create should not be allowed on a ManyRelatedManager. Also removed new.instancemethod craziness because it was not necessary. All tests pass.

  • django/db/models/options.py

     
    22from django.db.models.related import RelatedObject
    33from django.db.models.fields.related import ManyToManyRel
    44from django.db.models.fields import AutoField, FieldDoesNotExist
    5 from django.db.models.loading import get_models, app_cache_ready
     5from django.db.models.loading import get_models, get_model, app_cache_ready
    66from django.db.models.query import orderlist2sql
    77from django.utils.translation import activate, deactivate_all, get_language, string_concat
    88from django.utils.encoding import force_unicode, smart_str
     
    161161            follow = self.get_follow()
    162162        return [f for f in self.get_all_related_objects() if follow.get(f.name, None)]
    163163
     164    def get_related_object(self, from_model):
     165        "Gets the RelatedObject which links from from_model to this model."
     166        if isinstance(from_model, str):
     167            from_model = get_model(self.app_label, from_model)
     168        for related_object in self.get_all_related_objects():
     169            if related_object.model == from_model:
     170                return related_object
     171        return None
     172
    164173    def get_data_holders(self, follow=None):
    165174        if follow == None:
    166175            follow = self.get_follow()
  • django/db/models/fields/related.py

     
    298298            manager.clear()
    299299        manager.add(*value)
    300300
    301 def create_many_related_manager(superclass):
     301def create_many_related_manager(superclass, through=False):
    302302    """Creates a manager that subclasses 'superclass' (which is a Manager)
    303303    and adds behavior for many-to-many related objects."""
    304304    class ManyRelatedManager(superclass):
     
    312312            self.join_table = join_table
    313313            self.source_col_name = source_col_name
    314314            self.target_col_name = target_col_name
     315            self.through = through
    315316            self._pk_val = self.instance._get_pk_val()
    316317            if self._pk_val is None:
    317318                raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % model)
     
    321322
    322323        def add(self, *objs):
    323324            self._add_items(self.source_col_name, self.target_col_name, *objs)
    324 
    325325            # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table
    326326            if self.symmetrical:
    327327                self._add_items(self.target_col_name, self.source_col_name, *objs)
    328328        add.alters_data = True
    329 
     329   
    330330        def remove(self, *objs):
    331331            self._remove_items(self.source_col_name, self.target_col_name, *objs)
    332 
    333332            # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table
    334333            if self.symmetrical:
    335334                self._remove_items(self.target_col_name, self.source_col_name, *objs)
     
    344343        clear.alters_data = True
    345344
    346345        def create(self, **kwargs):
     346            if through:
     347                raise AttributeError, "'ManyRelatedManager' object has no attribute 'create'"
    347348            new_obj = self.model(**kwargs)
    348349            new_obj.save()
    349350            self.add(new_obj)
     
    411412                [self._pk_val])
    412413            transaction.commit_unless_managed()
    413414
     415    # If it's an intermediary model, detach the add, remove, and create methods
     416    # from this ManyRelatedManager.
     417    if through:
     418        del ManyRelatedManager.add
     419        del ManyRelatedManager.remove
     420
    414421    return ManyRelatedManager
    415422
    416423class ManyRelatedObjectsDescriptor(object):
     
    431438        # model's default manager.
    432439        rel_model = self.related.model
    433440        superclass = rel_model._default_manager.__class__
    434         RelatedManager = create_many_related_manager(superclass)
     441        RelatedManager = create_many_related_manager(superclass, self.related.field.rel.through)
    435442
    436443        qn = connection.ops.quote_name
    437444        manager = RelatedManager(
     
    450457        if instance is None:
    451458            raise AttributeError, "Manager must be accessed via instance"
    452459
     460        through = getattr(self.related.field.rel, 'through', None)
     461        if through:
     462            raise AttributeError, "Cannot set values on a ManyToManyField which specifies a through model.  Use %s's Manager instead." % through
     463       
    453464        manager = self.__get__(instance)
    454465        manager.clear()
    455466        manager.add(*value)
     
    472483        # model's default manager.
    473484        rel_model=self.field.rel.to
    474485        superclass = rel_model._default_manager.__class__
    475         RelatedManager = create_many_related_manager(superclass)
     486        RelatedManager = create_many_related_manager(superclass, self.field.rel.through)
    476487
    477488        qn = connection.ops.quote_name
    478489        manager = RelatedManager(
     
    491502        if instance is None:
    492503            raise AttributeError, "Manager must be accessed via instance"
    493504
     505        through = getattr(self.field.rel, 'through', None)
     506        if through:
     507            raise AttributeError, "Cannot set values on a ManyToManyField which specifies a through model.  Use %s's Manager instead." % through
     508
    494509        manager = self.__get__(instance)
    495510        manager.clear()
    496511        manager.add(*value)
     
    669684            num_in_admin=kwargs.pop('num_in_admin', 0),
    670685            related_name=kwargs.pop('related_name', None),
    671686            limit_choices_to=kwargs.pop('limit_choices_to', None),
    672             symmetrical=kwargs.pop('symmetrical', True))
     687            symmetrical=kwargs.pop('symmetrical', True),
     688            through=kwargs.pop('through', None))
    673689        self.db_table = kwargs.pop('db_table', None)
     690        if kwargs['rel'].through:
     691            self.creates_table = False
     692            assert not self.db_table, "Cannot specify a db_table if an intermediary model is used."
     693        else:
     694            self.creates_table = True
    674695        Field.__init__(self, **kwargs)
    675696
    676697        msg = ugettext_lazy('Hold down "Control", or "Command" on a Mac, to select more than one.')
     
    685706
    686707    def _get_m2m_db_table(self, opts):
    687708        "Function that can be curried to provide the m2m table name for this relation"
    688         if self.db_table:
     709        if self.rel.through != None:
     710            return get_model(opts.app_label, self.rel.through)._meta.db_table
     711        elif self.db_table:
    689712            return self.db_table
    690713        else:
    691714            return '%s_%s' % (opts.db_table, self.name)
     
    693716    def _get_m2m_column_name(self, related):
    694717        "Function that can be curried to provide the source column name for the m2m table"
    695718        # If this is an m2m relation to self, avoid the inevitable name clash
    696         if related.model == related.parent_model:
     719        if self.rel.through != None:
     720            field = related.model._meta.get_related_object(self.rel.through).field
     721            attname, column = field.get_attname_column()
     722            return column
     723        elif related.model == related.parent_model:
    697724            return 'from_' + related.model._meta.object_name.lower() + '_id'
    698725        else:
    699726            return related.model._meta.object_name.lower() + '_id'
     
    701728    def _get_m2m_reverse_name(self, related):
    702729        "Function that can be curried to provide the related column name for the m2m table"
    703730        # If this is an m2m relation to self, avoid the inevitable name clash
    704         if related.model == related.parent_model:
     731        if self.rel.through != None:
     732            field = related.parent_model._meta.get_related_object(self.rel.through).field
     733            attname, column = field.get_attname_column()
     734            return column
     735        elif related.model == related.parent_model:
    705736            return 'to_' + related.parent_model._meta.object_name.lower() + '_id'
    706737        else:
    707738            return related.parent_model._meta.object_name.lower() + '_id'
     
    816847
    817848class ManyToManyRel(object):
    818849    def __init__(self, to, num_in_admin=0, related_name=None,
    819         limit_choices_to=None, symmetrical=True):
     850        limit_choices_to=None, symmetrical=True, through=None):
    820851        self.to = to
    821852        self.num_in_admin = num_in_admin
    822853        self.related_name = related_name
     
    826857        self.edit_inline = False
    827858        self.symmetrical = symmetrical
    828859        self.multiple = True
     860        self.through = through
  • django/core/management/validation.py

     
    102102                        if r.get_accessor_name() == rel_query_name:
    103103                            e.add(opts, "Reverse query name for field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name))
    104104
     105        seen_intermediary_signatures = []
     106
    105107        for i, f in enumerate(opts.many_to_many):
    106108            # Check to see if the related m2m field will clash with any
    107109            # existing fields, m2m fields, m2m related objects or related objects
     
    111113                # so skip the next section
    112114                if isinstance(f.rel.to, (str, unicode)):
    113115                    continue
     116            if hasattr(f.rel, 'through') and f.rel.through != None:
     117                intermediary_model = None
     118                for model in models.get_models():
     119                    if model._meta.module_name == f.rel.through.lower():
     120                        intermediary_model = model
     121                if intermediary_model == None:
     122                    e.add(opts, "%s has a manually-defined m2m relationship through a model (%s) which does not exist." % (f.name, f.rel.through))
     123                else:
     124                    signature = (f.rel.to, cls, intermediary_model)
     125                    if signature in seen_intermediary_signatures:
     126                        e.add(opts, "%s has two manually-defined m2m relationships through the same model (%s), which is not possible.  Please use a field on your intermediary model instead." % (cls._meta.object_name, intermediary_model._meta.object_name))
     127                    else:
     128                        seen_intermediary_signatures.append(signature)
     129                    seen_related_fk, seen_this_fk = False, False
     130                    for field in intermediary_model._meta.fields:
     131                        if field.rel:
     132                            if field.rel.to == f.rel.to:
     133                                seen_related_fk = True
     134                            elif field.rel.to == cls:
     135                                seen_this_fk = True
     136                    if not seen_related_fk or not seen_this_fk:
     137                        e.add(opts, "%s has a manually-defined m2m relationship through a model (%s) which does not have foreign keys to %s and %s" % (f.name, f.rel.through, f.rel.to._meta.object_name, cls._meta.object_name))
    114138
    115139            rel_opts = f.rel.to._meta
    116140            rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name()
  • django/core/management/sql.py

     
    354354    qn = connection.ops.quote_name
    355355    inline_references = connection.features.inline_fk_references
    356356    for f in opts.many_to_many:
    357         if not isinstance(f.rel, generic.GenericRel):
     357        if not isinstance(f.rel, generic.GenericRel) and f.creates_table:
    358358            tablespace = f.db_tablespace or opts.db_tablespace
    359359            if tablespace and connection.features.supports_tablespaces and connection.features.autoindexes_primary_keys:
    360360                tablespace_sql = ' ' + connection.ops.tablespace_sql(tablespace, inline=True)
  • django/contrib/admin/options.py

     
    172172                kwargs['widget'] = widgets.ForeignKeyRawIdWidget(db_field.rel)
    173173            else:
    174174                if isinstance(db_field, models.ManyToManyField):
    175                     if db_field.name in self.raw_id_fields:
     175                    # If it uses an intermediary model, don't show field in admin.
     176                    if db_field.rel.through != None:
     177                        return None
     178                    elif db_field.name in self.raw_id_fields:
    176179                        kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel)
    177180                        kwargs['help_text'] = ''
    178181                    elif db_field.name in (self.filter_vertical + self.filter_horizontal):
  • tests/modeltests/invalid_models/models.py

     
    110110class MissingRelations(models.Model):
    111111    rel1 = models.ForeignKey("Rel1")
    112112    rel2 = models.ManyToManyField("Rel2")
     113   
     114class MissingManualM2MModel(models.Model):
     115    name = models.CharField(max_length=5)
     116    missing_m2m = models.ManyToManyField(Model, through="MissingM2MModel")
     117   
     118class Person(models.Model):
     119    name = models.CharField(max_length=5)
    113120
     121class Group(models.Model):
     122    name = models.CharField(max_length=5)
     123    primary = models.ManyToManyField(Person, through="Membership", related_name="primary")
     124    secondary = models.ManyToManyField(Person, through="Membership", related_name="secondary")
     125
     126class GroupTwo(models.Model):
     127    name = models.CharField(max_length=5)
     128    primary = models.ManyToManyField(Person, through="Membership")
     129    secondary = models.ManyToManyField(Group, through="MembershipMissingFK")
     130
     131class Membership(models.Model):
     132    person = models.ForeignKey(Person)
     133    group = models.ForeignKey(Group)
     134    not_default_or_null = models.CharField(max_length=5)
     135
     136class MembershipMissingFK(models.Model):
     137    person = models.ForeignKey(Person)
     138
    114139model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute.
    115140invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute.
    116141invalid_models.fielderrors: "decimalfield": DecimalFields require a "max_digits" attribute.
     
    195220invalid_models.selfclashm2m: Reverse query name for m2m field 'm2m_4' clashes with field 'SelfClashM2M.selfclashm2m'. Add a related_name argument to the definition for 'm2m_4'.
    196221invalid_models.missingrelations: 'rel2' has m2m relation with model Rel2, which has not been installed
    197222invalid_models.missingrelations: 'rel1' has relation with model Rel1, which has not been installed
     223invalid_models.group: Group has two manually-defined m2m relationships through the same model (Membership), which is not possible.  Please use a field on your intermediary model instead.
     224invalid_models.missingmanualm2mmodel: missing_m2m has a manually-defined m2m relationship through a model (MissingM2MModel) which does not exist.
     225invalid_models.grouptwo: primary has a manually-defined m2m relationship through a model (Membership) which does not have foreign keys to Person and GroupTwo
     226invalid_models.grouptwo: secondary has a manually-defined m2m relationship through a model (MembershipMissingFK) which does not have foreign keys to Group and GroupTwo
    198227"""
  • tests/modeltests/m2m_manual/__init__.py

     
     1
  • tests/modeltests/m2m_manual/models.py

     
     1from django.db import models
     2from datetime import datetime
     3
     4# M2M described on one of the models
     5class Person(models.Model):
     6    name = models.CharField(max_length=128)
     7
     8    def __unicode__(self):
     9        return self.name
     10
     11class Group(models.Model):
     12    name = models.CharField(max_length=128)
     13    members = models.ManyToManyField(Person, through='Membership')
     14    custom_members = models.ManyToManyField(Person, through='CustomMembership', related_name="custom")
     15    nodefaultsnonulls = models.ManyToManyField(Person, through='TestNoDefaultsOrNulls', related_name="testnodefaultsnonulls")
     16   
     17    def __unicode__(self):
     18        return self.name
     19
     20class Membership(models.Model):
     21    person = models.ForeignKey(Person)
     22    group = models.ForeignKey(Group)
     23    date_joined = models.DateTimeField(default=datetime.now)
     24    invite_reason = models.CharField(max_length=64, null=True)
     25   
     26    def __unicode__(self):
     27        return "%s is a member of %s" % (self.person.name, self.group.name)
     28
     29class CustomMembership(models.Model):
     30    person = models.ForeignKey(Person, db_column="custom_person_column", related_name="custom_person_related_name")
     31    group = models.ForeignKey(Group)
     32    weird_fk = models.ForeignKey(Membership, null=True)
     33    date_joined = models.DateTimeField(default=datetime.now)
     34   
     35    def __unicode__(self):
     36        return "%s is a member of %s" % (self.person.name, self.group.name)
     37   
     38    class Meta:
     39        db_table = "test_table"
     40
     41class TestNoDefaultsOrNulls(models.Model):
     42    person = models.ForeignKey(Person)
     43    group = models.ForeignKey(Group)
     44    nodefaultnonull = models.CharField(max_length=5)
     45
     46__test__ = {'API_TESTS':"""
     47>>> from datetime import datetime
     48
     49### Creation and Saving Tests ###
     50
     51>>> bob = Person.objects.create(name = 'Bob')
     52>>> jim = Person.objects.create(name = 'Jim')
     53>>> jane = Person.objects.create(name = 'Jane')
     54>>> rock = Group.objects.create(name = 'Rock')
     55>>> roll = Group.objects.create(name = 'Roll')
     56
     57# We start out by making sure that the Group 'rock' has no members.
     58>>> rock.members.all()
     59[]
     60
     61# To make Jim a member of Group Rock, simply create a Membership object.
     62>>> m1 = Membership.objects.create(person = jim, group = rock)
     63
     64# We can do the same for Jane and Rock.
     65>>> m2 = Membership.objects.create(person = jane, group = rock)
     66
     67# Let's check to make sure that it worked.  Jane and Jim should be members of Rock.
     68>>> rock.members.all()
     69[<Person: Jim>, <Person: Jane>]
     70
     71# Now we can add a bunch more Membership objects to test with.
     72>>> m3 = Membership.objects.create(person = bob, group = roll)
     73>>> m4 = Membership.objects.create(person = jim, group = roll)
     74>>> m5 = Membership.objects.create(person = jane, group = roll)
     75
     76# We can get Jim's Group membership as with any ForeignKey.
     77>>> jim.group_set.all()
     78[<Group: Rock>, <Group: Roll>]
     79
     80# Querying the intermediary model works like normal. 
     81# In this case we get Jane's membership to Rock.
     82>>> m = Membership.objects.get(person = jane, group = rock)
     83>>> m
     84<Membership: Jane is a member of Rock>
     85
     86# Now we set some date_joined dates for further testing.
     87>>> m2.invite_reason = "She was just awesome."
     88>>> m2.date_joined = datetime(2006, 1, 1)
     89>>> m2.save()
     90
     91>>> m5.date_joined = datetime(2004, 1, 1)
     92>>> m5.save()
     93
     94>>> m3.date_joined = datetime(2004, 1, 1)
     95>>> m3.save()
     96
     97# It's not only get that works.  Filter works like normal as well.
     98>>> Membership.objects.filter(person = jim)
     99[<Membership: Jim is a member of Rock>, <Membership: Jim is a member of Roll>]
     100
     101
     102### Forward Descriptors Tests ###
     103
     104# Due to complications with adding via an intermediary model, the add method is
     105# not provided.
     106>>> rock.members.add(bob)
     107Traceback (most recent call last):
     108...
     109AttributeError: 'ManyRelatedManager' object has no attribute 'add'
     110
     111>>> rock.members.create(name = 'Anne')
     112Traceback (most recent call last):
     113...
     114AttributeError: 'ManyRelatedManager' object has no attribute 'create'
     115
     116# Remove has similar complications, and is not provided either.
     117>>> rock.members.remove(jim)
     118Traceback (most recent call last):
     119...
     120AttributeError: 'ManyRelatedManager' object has no attribute 'remove'
     121
     122# Here we back up the list of all members of Rock.
     123>>> backup = list(rock.members.all())
     124
     125# ...and we verify that it has worked.
     126>>> backup
     127[<Person: Jim>, <Person: Jane>]
     128
     129# The clear function should still work.
     130>>> rock.members.clear()
     131
     132# Now there will be no members of Rock.
     133>>> rock.members.all()
     134[]
     135
     136# Assignment should not work with models specifying a through model for many of
     137# the same reasons as adding.
     138>>> rock.members = backup
     139Traceback (most recent call last):
     140...
     141AttributeError: Cannot set values on a ManyToManyField which specifies a through model.  Use Membership's Manager instead.
     142
     143# Let's re-save those instances that we've cleared.
     144>>> m1.save()
     145>>> m2.save()
     146
     147# Verifying that those instances were re-saved successfully.
     148>>> rock.members.all()
     149[<Person: Jim>, <Person: Jane>]
     150
     151
     152### Reverse Descriptors Tests ###
     153
     154# Due to complications with adding via an intermediary model, the add method is
     155# not provided.
     156>>> bob.group_set.add(rock)
     157Traceback (most recent call last):
     158...
     159AttributeError: 'ManyRelatedManager' object has no attribute 'add'
     160
     161# Create is another method that should not work correctly, as it suffers from
     162# the same problems as add.
     163>>> bob.group_set.create(name = 'Funk')
     164Traceback (most recent call last):
     165...
     166AttributeError: 'ManyRelatedManager' object has no attribute 'create'
     167
     168# Remove has similar complications, and is not provided either.
     169>>> jim.group_set.remove(rock)
     170Traceback (most recent call last):
     171...
     172AttributeError: 'ManyRelatedManager' object has no attribute 'remove'
     173
     174# Here we back up the list of all of Jim's groups.
     175>>> backup = list(jim.group_set.all())
     176>>> backup
     177[<Group: Rock>, <Group: Roll>]
     178
     179# The clear function should still work.
     180>>> jim.group_set.clear()
     181
     182# Now Jim will be in no groups.
     183>>> jim.group_set.all()
     184[]
     185
     186# Assignment should not work with models specifying a through model for many of
     187# the same reasons as adding.
     188>>> jim.group_set = backup
     189Traceback (most recent call last):
     190...
     191AttributeError: Cannot set values on a ManyToManyField which specifies a through model.  Use Membership's Manager instead.
     192
     193# Let's re-save those instances that we've cleared.
     194>>> m1.save()
     195>>> m4.save()
     196
     197# Verifying that those instances were re-saved successfully.
     198>>> jim.group_set.all()
     199[<Group: Rock>, <Group: Roll>]
     200
     201### Custom Tests ###
     202
     203# Let's see if we can query through our second relationship.
     204>>> rock.custom_members.all()
     205[]
     206
     207# We can query in the opposite direction as well.
     208>>> bob.custom.all()
     209[]
     210
     211# Let's create some membership objects in this custom relationship.
     212>>> cm1 = CustomMembership.objects.create(person = bob, group = rock)
     213>>> cm2 = CustomMembership.objects.create(person = jim, group = rock)
     214
     215# If we get the number of people in Rock, it should be both Bob and Jim.
     216>>> rock.custom_members.all()
     217[<Person: Bob>, <Person: Jim>]
     218
     219# Bob should only be in one custom group.
     220>>> bob.custom.all()
     221[<Group: Rock>]
     222
     223# Let's make sure our new descriptors don't conflict with the FK related_name.
     224>>> bob.custom_person_related_name.all()
     225[<CustomMembership: Bob is a member of Rock>]
     226
     227### QUERY TESTS ###
     228
     229# We can query for the related model by using its attribute name (members, in
     230# this case).
     231>>> Group.objects.filter(members__name='Bob')
     232[<Group: Roll>]
     233
     234# To query through the intermediary model, we specify its model name.
     235# In this case, membership.
     236>>> Group.objects.filter(membership__invite_reason = "She was just awesome.")
     237[<Group: Rock>]
     238
     239# If we want to query in the reverse direction by the related model, use its
     240# model name (group, in this case).
     241>>> Person.objects.filter(group__name="Rock")
     242[<Person: Jim>, <Person: Jane>]
     243
     244# If the m2m field has specified a related_name, using that will work.
     245>>> Person.objects.filter(custom__name="Rock")
     246[<Person: Bob>, <Person: Jim>]
     247
     248# To query through the intermediary model in the reverse direction, we again
     249# specify its model name (membership, in this case).
     250>>> Person.objects.filter(membership__invite_reason = "She was just awesome.")
     251[<Person: Jane>]
     252
     253# Let's see all of the groups that Jane joined after 1 Jan 2005:
     254>>> Group.objects.filter(membership__date_joined__gt = datetime(2005, 1, 1),
     255... membership__person = jane)
     256[<Group: Rock>]
     257
     258# Queries also work in the reverse direction: Now let's see all of the people
     259# that have joined Rock since 1 Jan 2005:
     260>>> Person.objects.filter(membership__date_joined__gt = datetime(2005, 1, 1),
     261... membership__group = rock)
     262[<Person: Jim>, <Person: Jane>]
     263
     264# Conceivably, queries through membership could return correct, but non-unique
     265# querysets.  To demonstrate this, we query for all people who have joined a
     266# group after 2004:
     267>>> Person.objects.filter(membership__date_joined__gt = datetime(2004, 1, 1))
     268[<Person: Jim>, <Person: Jim>, <Person: Jane>]
     269
     270# Jim showed up twice, because he joined two groups ('Rock', and 'Roll'):
     271>>> [(m.person.name, m.group.name) for m in
     272... Membership.objects.filter(date_joined__gt = datetime(2004, 1, 1))]
     273[(u'Jim', u'Rock'), (u'Jane', u'Rock'), (u'Jim', u'Roll')]
     274
     275# QuerySet's distinct() method can correct this problem.
     276>>> Person.objects.filter(membership__date_joined__gt = datetime(2004, 1, 1)).distinct()
     277[<Person: Jim>, <Person: Jane>]
     278"""}
     279 No newline at end of file
  • AUTHORS

     
    137137    Afonso Fernández Nogueira <fonzzo.django@gmail.com>
    138138    Matthew Flanagan <http://wadofstuff.blogspot.com>
    139139    Eric Floehr <eric@intellovations.com>
     140    Eric Florenzano <floguy@gmail.com>
    140141    Vincent Foley <vfoleybourgon@yahoo.ca>
    141142    Rudolph Froger <rfroger@estrate.nl>
    142143    Jorge Gajon <gajon@gajon.org>
Back to Top