Ejemplo n.º 1
0
class DataImportAttempt(models.Model):
    # {{{
    SOURCE_CHOICES = (
        ('rs', "Ohloh"),
        ('ou', "Ohloh"),
        ('lp', "Launchpad"),
        ('gh', "Github"),
        ('ga', "Github"),
        ('db', "Debian"),
        ('bb', "Bitbucket")
    )
    completed = models.BooleanField(default=False)
    failed = models.BooleanField(default=False)
    source = models.CharField(max_length=2,
                              choices=SOURCE_CHOICES)
    person = models.ForeignKey(Person)
    query = models.CharField(max_length=200)
    date_created = models.DateTimeField(default=datetime.datetime.utcnow)
    web_response = models.ForeignKey(mysite.customs.models.WebResponse,
                                     null=True)  # null=True for
    # now, so the migration doesn't cause data validation errors

    def get_formatted_source_description(self):
        return self.get_source_display() % self.query

    def do_what_it_says_on_the_tin(self):
        """Attempt to import data by enqueuing a job in celery."""
        # We need to import here to avoid vicious cyclical imports.
        import mysite.profile.tasks
        mysite.profile.tasks.FetchPersonDataFromOhloh.delay(self.id)

    def __unicode__(self):
        return "Attempt to import data, source = %s, person = <%s>, query = %s" % (self.get_source_display(), self.person, self.query)
Ejemplo n.º 2
0
class Citation(models.Model):
    portfolio_entry = models.ForeignKey(PortfolioEntry)  # [0]
    url = models.URLField(null=True, verbose_name="URL")
    contributor_role = models.CharField(max_length=200, null=True)
    languages = models.CharField(max_length=200, null=True)
    first_commit_time = models.DateTimeField(null=True)
    date_created = models.DateTimeField(default=datetime.datetime.utcnow)
    is_published = models.BooleanField(default=False)  # unpublished == Unread
    is_deleted = models.BooleanField(default=False)
    ignored_due_to_duplicate = models.BooleanField(default=False)
    old_summary = models.TextField(null=True, default=None)

    objects = models.Manager()
    untrashed = UntrashedCitationManager()

    @property
    def summary(self):
        # FIXME: Pluralize correctly.
        # FIXME: Use "since year_started"

        if self.url is not None:
            return url2printably_short(self.url, CUTOFF=38)
        elif self.languages is not None:
            return "Coded in %s." % (self.languages, )

    def get_languages_as_list(self):
        if self.languages is None:
            return []
        return [
            lang.strip() for lang in self.languages.split(",") if lang.strip()
        ]

    def get_url_or_guess(self):
        if self.url:
            return self.url

    def save_and_check_for_duplicates(self):
        # FIXME: Cache summaries in the DB so this query is faster.
        duplicates = [
            citation for citation in Citation.objects.filter(
                portfolio_entry=self.portfolio_entry)
            if (citation.pk != self.pk) and (citation.summary == self.summary)
        ]

        if duplicates:
            self.ignored_due_to_duplicate = True
        return self.save()

    # [0]: FIXME: Let's learn how to use Django's ManyToManyField etc.

    def __unicode__(self):
        if self.pk is not None:
            pk = self.pk
        else:
            pk = 'unassigned'
        return "pk=%s, summary=%s" % (pk, self.summary)
Ejemplo n.º 3
0
class Link_Person_Tag(models.Model):

    "Many-to-many relation between Person and Tags."
    # {{{
    tag = models.ForeignKey(Tag)
    person = models.ForeignKey(Person)
    source = models.CharField(max_length=200)
Ejemplo n.º 4
0
class Link_Project_Tag(models.Model):

    "Many-to-many relation between Projects and Tags."
    # {{{
    tag = models.ForeignKey(Tag)
    project = models.ForeignKey(Project)
    source = models.CharField(max_length=200)
Ejemplo n.º 5
0
class Tag(models.Model):
    # {{{
    text = models.CharField(null=False, max_length=255)
    tag_type = models.ForeignKey(TagType)

    @property
    def name(self):
        return self.text

    def save(self, *args, **kwargs):
        if self.text:
            return super(Tag, self).save(*args, **kwargs)
        raise ValueError

    @staticmethod
    def get_people_by_tag_name(tag_name):
        peeps = []
        for tag_type in TagType.objects.all():
            peeps.extend(
                mysite.profile.controllers.people_matching(
                    tag_type.name, tag_name))
        return peeps

    def __unicode__(self):
        return "%s: %s" % (self.tag_type.name, self.text)
Ejemplo n.º 6
0
class TagType(models.Model):
    # {{{
    short_name2long_name = {'understands': 'understands',
                            'can_mentor': 'can mentor in',
                            'can_pitch_in': 'can pitch in with',
                            'understands_not': 'will never understand',
                            'studying': 'currently studying'}
    name = models.CharField(max_length=100)

    def __unicode__(self):
        return self.name
Ejemplo n.º 7
0
class DataImportAttempt(models.Model):
    # {{{
    SOURCE_CHOICES = (('rs', "Ohloh"), ('ou', "Ohloh"), ('lp', "Launchpad"),
                      ('gh', "Github"), ('ga', "Github"), ('db', "Debian"),
                      ('bb', "Bitbucket"))
    completed = models.BooleanField(default=False)
    failed = models.BooleanField(default=False)
    source = models.CharField(max_length=2, choices=SOURCE_CHOICES)
    person = models.ForeignKey(Person)
    query = models.CharField(max_length=200)
    date_created = models.DateTimeField(default=datetime.datetime.utcnow)
    web_response = models.ForeignKey(mysite.customs.models.WebResponse,
                                     null=True)  # null=True for

    # now, so the migration doesn't cause data validation errors

    def get_formatted_source_description(self):
        return self.get_source_display() % self.query

    def __unicode__(self):
        return "Attempt to import data, source = %s, person = <%s>, query = %s" % (
            self.get_source_display(), self.person, self.query)
Ejemplo n.º 8
0
class UnsubscribeToken(mysite.search.models.OpenHatchModel):
    string = models.CharField(
        null=False, blank=False, unique=True, max_length=255)
    owner = models.ForeignKey(Person)

    @staticmethod
    def whose_token_string_is_this(string):
        try:
            expiry_date = datetime.datetime.utcnow() - \
                datetime.timedelta(days=90)
            return UnsubscribeToken.objects.get(string=string, created_date__gte=expiry_date).owner
        except UnsubscribeToken.DoesNotExist:
            return None
Ejemplo n.º 9
0
class Tag(models.Model):
    # {{{
    text = models.CharField(null=False, max_length=255)
    tag_type = models.ForeignKey(TagType)

    @property
    def name(self):
        return self.text

    def save(self, *args, **kwargs):
        if self.text:
            return super(Tag, self).save(*args, **kwargs)
        raise ValueError

    def __unicode__(self):
        return "%s: %s" % (self.tag_type.name, self.text)
Ejemplo n.º 10
0
class Link_SF_Proj_Dude_FM(models.Model):
    '''Link from SourceForge Project to Person, via FlossMOLE'''
    person = models.ForeignKey(SourceForgePerson)
    project = models.ForeignKey(SourceForgeProject)
    is_admin = models.BooleanField(default=False)
    position = models.CharField(max_length=200)
    date_collected = models.DateTimeField()

    class Meta:
        unique_together = [
            ('person', 'project'),
        ]

    @staticmethod
    def create_from_flossmole_row_data(dev_loginname, proj_unixname, is_admin,
                                       position, date_collected):
        """Given:
        {'dev_loginname': x, 'proj_unixname': y, is_admin: z,
        'position': a, 'date_collected': b}, return a
        SourceForgeProjectMembershipFromFlossMole instance."""
        person, _ = SourceForgePerson.objects.get_or_create(
            username=dev_loginname)
        project, _ = SourceForgeProject.objects.get_or_create(
            unixname=proj_unixname)
        is_admin = bool(int(is_admin))
        date_collected = datetime.datetime.strptime(
            date_collected, '%Y-%m-%d %H:%M:%S')  # no time zone
        return Link_SF_Proj_Dude_FM.objects.get_or_create(
            person=person,
            project=project,
            is_admin=is_admin,
            position=position,
            date_collected=date_collected)

    @staticmethod
    def create_from_flossmole_row_string(row):
        row = row.strip()
        if row.startswith('#'):
            return None
        if row.startswith('dev_loginname'):
            return None
        person, proj_unixname, is_admin, position, date_collected = row.split(
            '\t')
        return Link_SF_Proj_Dude_FM.create_from_flossmole_row_data(
            person, proj_unixname, is_admin, position, date_collected)
Ejemplo n.º 11
0
class Person(models.Model):
    """ A human bean. """
    # {{{
    homepage_url = models.URLField(default="", blank=True)
    user = models.ForeignKey(User, unique=True)
    gotten_name_from_ohloh = models.BooleanField(default=False)
    last_polled = models.DateTimeField(default=datetime.datetime(1970, 1, 1))
    show_email = models.BooleanField(default=False)
    bio = models.TextField(blank=True)
    contact_blurb = models.TextField(blank=True)
    expand_next_steps = models.BooleanField(default=True)
    photo = models.ImageField(
        upload_to=lambda a, b: 'static/photos/profile-photos/' +
        generate_person_photo_path(a, b),
        default='')
    photo_thumbnail = models.ImageField(
        upload_to=lambda a, b: 'static/photos/profile-photos/' +
        generate_person_photo_path(a, b, suffix="-thumbnail"),
        default='',
        null=True)

    photo_thumbnail_30px_wide = models.ImageField(
        upload_to=lambda a, b: 'static/photos/profile-photos/' +
        generate_person_photo_path(a, b, suffix="-thumbnail-30px-wide"),
        default='',
        null=True)

    photo_thumbnail_20px_wide = models.ImageField(
        upload_to=lambda a, b: 'static/photos/profile-photos/' +
        generate_person_photo_path(a, b, suffix="-thumbnail-20px-wide"),
        default='',
        null=True)

    blacklisted_repository_committers = models.ManyToManyField(
        RepositoryCommitter)
    dont_guess_my_location = models.BooleanField(default=False)
    location_confirmed = models.BooleanField(default=False)
    location_display_name = models.CharField(max_length=255,
                                             blank=True,
                                             default=DEFAULT_LOCATION,
                                             verbose_name='Location')
    latitude = models.FloatField(null=False, default=-37.3049962)
    longitude = models.FloatField(null=False, default=-12.6790445)
    email_me_re_projects = models.BooleanField(
        default=True,
        verbose_name='Email me periodically about activity in my projects')

    irc_nick = models.CharField(max_length=30, blank=True, null=True)

    @staticmethod
    def create_dummy(first_name="", email=None, **kwargs):

        # Generate a random string to use as a username. Keep it short
        # so that it doesn't overflow the username field!
        username = uuid.uuid4().hex[:16]

        if email is None:
            email = "*****@*****.**" % username
        user = User(username=username, first_name=first_name, email=email)
        data = {'user': user}

        # If the caller of create_dummy passes in a user, then we won't use the
        # user defined above
        data.update(kwargs)

        # Save the user after the update, so we don't save a new user if one
        # was never needed
        user = data['user']
        user.save()
        person = user.get_profile()

        for key, value in data.items():
            setattr(person, key, value)
        person.save()

        return person

    def location_is_public(self):
        # If you change this method, change the method immediately below this
        # one (Person.inaccessible_islanders)
        return self.location_confirmed and self.location_display_name

    @staticmethod
    def inaccessible_islanders():
        # If you change this method, change the method immediately above this
        # one (location_is_public)
        return Person.objects.filter(
            Q(location_confirmed=False) | Q(location_display_name=''))

    def get_public_location_or_default(self):
        if self.location_is_public():
            return self.location_display_name
        else:
            return DEFAULT_LOCATION

    def get_public_latitude_or_default(self):
        if self.location_is_public():
            return self.latitude
        else:
            return DEFAULT_LATITUDE

    def get_public_longitude_or_default(self):
        if self.location_is_public():
            return self.longitude
        else:
            return DEFAULT_LONGITUDE

    def __unicode__(self):
        return "username: %s, name: %s %s" % (
            self.user.username, self.user.first_name, self.user.last_name)

    def get_photo_url_or_default(self):
        try:
            return self.photo.url
        except (IOError, ValueError):
            return '/static/images/profile-photos/penguin.png'

    @staticmethod
    def get_from_session_key(session_key):
        '''Based almost entirely on
        http://www.djangosnippets.org/snippets/1276/
        Thanks jdunck!'''

        session_engine = __import__(settings.SESSION_ENGINE, {}, {}, [''])
        session_wrapper = session_engine.SessionStore(session_key)
        user_id = session_wrapper.get(SESSION_KEY)
        auth_backend = load_backend(session_wrapper.get(BACKEND_SESSION_KEY))

        if user_id and auth_backend:
            return Person.objects.get(user=auth_backend.get_user(user_id))
        else:
            return None

    def get_photo_thumbnail_url_or_default(self):
        try:
            return self.photo_thumbnail.url
        except (IOError, ValueError):
            return '/static/images/profile-photos/penguin-40px.png'

    def get_photo_thumbnail_width(self):
        try:
            return self.photo_thumbnail.width
        except (IOError, ValueError):
            return 40

    def get_photo_thumbnail_height(self):
        try:
            return self.photo_thumbnail.height
        except (IOError, ValueError):
            return 51

    def get_photo_thumbnail_30px_wide_url_or_default(self):
        try:
            return self.photo_thumbnail_30px_wide.url
        except (IOError, ValueError):
            return '/static/images/profile-photos/penguin-30px.png'

    def get_photo_thumbnail_20px_wide_url_or_default(self):
        try:
            return self.photo_thumbnail_20px_wide.url
        except (IOError, ValueError):
            return '/static/images/profile-photos/penguin-20px.png'

    def get_photo_thumbnail_width_20px(self):
        try:
            return self.photo_thumbnail_20px_wide.width
        except (IOError, ValueError):
            return 20

    def get_photo_thumbnail_height_20px(self):
        try:
            return self.photo_thumbnail_20px_wide.height
        except (IOError, ValueError):
            return 20

    def get_published_portfolio_entries(self):
        return PortfolioEntry.published_ones.filter(person=self)

    def get_nonarchived_published_portfolio_entries(self):
        return PortfolioEntry.published_ones.filter(person=self,
                                                    is_archived=False)

    def get_maintainer_portfolio_entries(self):
        """
        Return the PortfolioEntries for which this person wants to receive
        maintainer updates.
        """
        return PortfolioEntry.published_ones.filter(
            person=self, receive_maintainer_updates=True)

    def get_list_of_all_published_projects(self):
        # This method looks familiar but testing -- jl

        return self.get_published_portfolio_entries()

    def get_list_of_all_project_names(self):
        # if you change this method, be sure to increment the version number in
        # the cache key above
        return list(self.get_published_portfolio_entries().values_list(
            'project__name', flat=True).distinct())

    def get_display_names_of_nonarchived_projects(self):
        return list(
            self.get_nonarchived_published_portfolio_entries().values_list(
                'project__display_name', flat=True).distinct())

    @staticmethod
    def only_terms_with_results(terms):
        # Remove terms whose hit counts are zero.
        terms_with_results = []
        for term in terms:
            query = mysite.search.view_helpers.Query(terms=[term])
            hit_count = query.get_or_create_cached_hit_count()
            if hit_count != 0:
                terms_with_results.append(term)
        return terms_with_results

    def get_recommended_search_terms(self):
        if settings.RECOMMEND_BUGS:
            return self._get_recommended_search_terms()
        return []

    def _get_recommended_search_terms(self):
        # {{{
        terms = []

        # Add terms based on languages in citations
        citations = self.get_published_citations_flat()
        for c in citations:
            terms.extend(c.get_languages_as_list())

        # Add terms based on projects in citations
        terms.extend([
            pfe.project.name for pfe in self.get_published_portfolio_entries()
            if pfe.project.name and pfe.project.name.strip()
        ])

        # Add terms based on tags
        terms.extend([tag.text for tag in self.get_tags_for_recommendations()])

        # Remove duplicates
        terms = sorted(set(terms), key=lambda s: s.lower())

        return Person.only_terms_with_results(terms)

        # FIXME: Add support for recommended projects.
        # FIXME: Add support for recommended project tags.

        # }}}

    def get_published_citations_flat(self):
        return sum([
            list(pfe.get_published_citations())
            for pfe in self.get_published_portfolio_entries()
        ], [])

    def get_tag_texts_for_map(self):
        """Return a list of Tags linked to this Person.  Tags that would be useful from the map view of the people list"""
        my_tag_texts = Tag.objects.filter(link_person_tag__person=self).extra(
            select={'lowername': 'LOWER(text)'})
        without_irrelevant_tags = my_tag_texts.exclude(
            tag_type__name__in=['understands_not', 'studying'])
        just_distinct_lowername = without_irrelevant_tags.values(
            'lowername').distinct().order_by('lowername')
        text_and_lower = just_distinct_lowername.values_list(
            'lowername', 'text')
        lower_set_so_far = set()
        ret = []
        for (lower, text) in text_and_lower:
            if lower in lower_set_so_far:
                continue
            ret.append(text)
            lower_set_so_far.add(lower)
        return ret

    def get_tags_as_dict(self):
        ret = collections.defaultdict(set)
        for link in self.link_person_tag_set.all():
            ret[link.tag.tag_type.name].add(link.tag.text.lower())
        return ret

    def get_tag_descriptions_for_keyword(self, keyword):
        keyword = keyword.lower()
        d = self.get_tags_as_dict()
        return sorted([
            TagType.short_name2long_name[short]
            for short in [key for key in d if (keyword in d[key])]
        ])

    def get_tags_for_recommendations(self):
        """Return a list of Tags linked to this Person.  For use with bug recommendations."""
        exclude_me = TagType.objects.filter(name='understands_not')
        return [
            link.tag for link in self.link_person_tag_set.all()
            if link.tag.tag_type not in exclude_me
        ]

    def get_full_name(self):
        # {{{
        name = self.user.first_name
        if self.user.first_name and self.user.last_name:
            name += " "
        name += self.user.last_name
        return name
        # }}}

    def get_full_name_with_nbsps(self):
        full_name = self.get_full_name()
        full_name_escaped = cgi.escape(full_name)
        full_name_escaped_with_nbsps = re.sub("\s+", "&nbsp;",
                                              full_name_escaped)
        return full_name_escaped_with_nbsps

    def get_full_name_or_username(self):
        return self.get_full_name() or self.user.username

    def get_full_name_and_username(self):
        full_name_start = ("%s (" % self.get_full_name() if
                           self.user.first_name or self.user.last_name else "")
        full_name_end = (")" if self.user.first_name or self.user.last_name
                         else "")
        return "%s%s%s" % (full_name_start, self.user.username, full_name_end)

    def generate_thumbnail_from_photo(self):
        if self.photo:
            width = 40
            self.photo.file.seek(0)
            scaled_down = get_image_data_scaled(self.photo.file.read(), width)
            self.photo_thumbnail.save('', ContentFile(scaled_down))

            width = 30
            self.photo.file.seek(0)
            scaled_down = get_image_data_scaled(self.photo.file.read(), width)
            self.photo_thumbnail_30px_wide.save('', ContentFile(scaled_down))

            width = 20
            self.photo.file.seek(0)
            scaled_down = get_image_data_scaled(self.photo.file.read(), width)
            self.photo_thumbnail_20px_wide.save('', ContentFile(scaled_down))

    def get_collaborators_for_landing_page(self, n=9):
        projects = set(
            [e.project for e in self.get_published_portfolio_entries()])
        infinity = 10000
        collaborator_lists = []
        for project in projects:
            people = project.get_n_other_contributors_than(n=infinity,
                                                           person=self)
            people = random.sample(people, min(n, len(people)))
            collaborator_lists.append(people)
        round_robin = mysite.profile.view_helpers.roundrobin(
            *collaborator_lists)
        collaborators = set()
        while len(collaborators) < n:
            try:
                collaborators.add(round_robin.next())
            except StopIteration:
                break
        collaborators = list(collaborators)
        # don't forget, this has a side effect and returns None
        random.shuffle(collaborators)
        return collaborators

    @property
    def profile_url(self):
        return reverse(
            mysite.profile.views.display_person_web,
            kwargs={'user_to_display__username': self.user.username})

    @staticmethod
    def get_by_username(username):
        return Person.objects.get(user__username=username)

    def should_be_nudged_about_location(self):
        return not self.location_confirmed and not self.dont_guess_my_location

    def get_coolness_factor(self, unhashed_tiebreaker):
        '''This function's output is used as the sort order in (at least) the periodic emails.
        You can be more cool if you:
           * Have projects
           * Have a picture
           * Have user tags set
           * Are a wannahelper of something
        and finally we break ties by get_full_name_or_username(), just so that
        we have a predictable sort.
        .'''
        hashed_tiebreaker = hashlib.sha1(unhashed_tiebreaker).hexdigest()
        factor = (bool(self.get_list_of_all_project_names()),
                  bool(self.get_tags_as_dict()), bool(self.photo),
                  bool(self.projects_i_wanna_help), hashed_tiebreaker)
        return factor

    def generate_new_unsubscribe_token(self):
        token = UnsubscribeToken(string=uuid.uuid4().hex, owner=self)
        token.save()
        return token
Ejemplo n.º 12
0
class Citation(models.Model):
    portfolio_entry = models.ForeignKey(PortfolioEntry)  # [0]
    url = models.URLField(null=True, verbose_name="URL")
    contributor_role = models.CharField(max_length=200, null=True)
    data_import_attempt = models.ForeignKey(DataImportAttempt, null=True)
    languages = models.CharField(max_length=200, null=True)
    first_commit_time = models.DateTimeField(null=True)
    date_created = models.DateTimeField(default=datetime.datetime.utcnow)
    is_published = models.BooleanField(default=False)  # unpublished == Unread
    is_deleted = models.BooleanField(default=False)
    ignored_due_to_duplicate = models.BooleanField(default=False)
    old_summary = models.TextField(null=True, default=None)

    objects = models.Manager()
    untrashed = UntrashedCitationManager()

    @property
    def summary(self):
        # FIXME: Pluralize correctly.
        # FIXME: Use "since year_started"

        if self.data_import_attempt:
            if self.data_import_attempt.source == 'db' and (
                    self.contributor_role == 'Maintainer'):
                return 'Maintain a package in Debian.'
            if self.data_import_attempt.source == 'db' and (
                    self.contributor_role.startswith('Maintainer of')):
                return self.contributor_role
            if self.data_import_attempt.source in ('gh', 'ga'):
                return '%s a repository on Github.' % self.contributor_role
            if self.data_import_attempt.source in ['rs', 'ou']:
                if not self.languages:
                    return "Committed to codebase (%s)" % (
                        self.data_import_attempt.get_source_display(), )
                return "Coded in %s (%s)" % (
                    self.languages,
                    self.data_import_attempt.get_source_display(),
                )
            elif self.data_import_attempt.source == 'lp':
                return "Participated in %s (%s)" % (
                    self.contributor_role,
                    self.data_import_attempt.get_source_display())
            elif self.data_import_attempt.source == 'bb':
                return "Created a repository on Bitbucket."
            else:
                raise ValueError, "There's a DIA of a kind I don't know how to summarize."
        elif self.url is not None:
            return url2printably_short(self.url, CUTOFF=38)
        elif self.languages is not None:
            return "Coded in %s." % (self.languages, )

        raise ValueError(
            "There's no DIA and I don't know how to summarize this.")

    def get_languages_as_list(self):
        if self.languages is None:
            return []
        return [
            lang.strip() for lang in self.languages.split(",") if lang.strip()
        ]

    def get_url_or_guess(self):
        if self.url:
            return self.url
        else:
            if self.data_import_attempt:
                if self.data_import_attempt.source in ['rs', 'ou']:
                    try:
                        project_name = unicode(
                            self.portfolio_entry.project.name, 'utf-8')
                        return "http://www.ohloh.net/search?%s" % http.urlencode(
                            {u'q': project_name})
                    except:
                        logger.warning(
                            "During Citation.get_url_or_guess, we failed to encode the project name correctly."
                        )
                        # This is just a temporary workaround since the above
                        # line was causing errors.
                        return "http://www.ohloh.net/search?q=%s" % self.portfolio_entry.project.name
                elif self.data_import_attempt.source == 'lp':
                    return "https://launchpad.net/~%s" % urllib.quote(
                        self.data_import_attempt.query)

    def save_and_check_for_duplicates(self):
        # FIXME: Cache summaries in the DB so this query is faster.
        duplicates = [
            citation for citation in Citation.objects.filter(
                portfolio_entry=self.portfolio_entry)
            if (citation.pk != self.pk) and (citation.summary == self.summary)
        ]

        if duplicates:
            self.ignored_due_to_duplicate = True
        return self.save()

    # [0]: FIXME: Let's learn how to use Django's ManyToManyField etc.

    def __unicode__(self):
        if self.pk is not None:
            pk = self.pk
        else:
            pk = 'unassigned'
        return "pk=%s, summary=%s" % (pk, self.summary)
Ejemplo n.º 13
0
class SourceForgeProject(models.Model):
    '''A project in SourceForge.'''
    # FIXME: Make this unique
    unixname = models.CharField(max_length=200)
Ejemplo n.º 14
0
class SourceForgePerson(models.Model):
    '''A person in SourceForge.'''
    # FIXME: Make this unique
    username = models.CharField(max_length=200)