Example #1
0
class Project(models.Model):
    # Auto fields
    pub_date = models.DateTimeField(_('Publication date'), auto_now_add=True)
    modified_date = models.DateTimeField(_('Modified date'), auto_now=True)

    # Generally from conf.py
    users = models.ManyToManyField(User,
                                   verbose_name=_('User'),
                                   related_name='projects')
    name = models.CharField(_('Name'), max_length=255)
    slug = models.SlugField(_('Slug'), max_length=255, unique=True)
    description = models.TextField(_('Description'),
                                   blank=True,
                                   help_text=_('The reStructuredText '
                                               'description of the project'))
    repo = models.CharField(_('Repository URL'),
                            max_length=255,
                            help_text=_('Hosted documentation repository URL'))
    repo_type = models.CharField(_('Repository type'),
                                 max_length=10,
                                 choices=constants.REPO_CHOICES,
                                 default='git')
    project_url = models.URLField(_('Project homepage'),
                                  blank=True,
                                  help_text=_('The project\'s homepage'))
    canonical_url = models.URLField(
        _('Canonical URL'),
        blank=True,
        help_text=_('URL that documentation is expected to serve from'))
    version = models.CharField(_('Version'),
                               max_length=100,
                               blank=True,
                               help_text=_('Project version these docs apply '
                                           'to, i.e. 1.0a'))
    copyright = models.CharField(_('Copyright'),
                                 max_length=255,
                                 blank=True,
                                 help_text=_('Project copyright information'))
    theme = models.CharField(
        _('Theme'),
        max_length=20,
        choices=constants.DEFAULT_THEME_CHOICES,
        default=constants.THEME_DEFAULT,
        help_text=(u'<a href="http://sphinx.pocoo.org/theming.html#builtin-'
                   'themes" target="_blank">%s</a>') % _('Examples'))
    suffix = models.CharField(_('Suffix'),
                              max_length=10,
                              editable=False,
                              default='.rst')
    single_version = models.BooleanField(
        _('Single version'),
        default=False,
        help_text=
        _('A single version site has no translations and only your "latest" version, served at the root of the domain. Use this with caution, only turn it on if you will <b>never</b> have multiple versions of your docs.'
          ))
    default_version = models.CharField(
        _('Default version'),
        max_length=255,
        default='latest',
        help_text=_('The version of your project that / redirects to'))
    # In default_branch, None max_lengtheans the backend should choose the
    # appropraite branch. Eg 'master' for git
    default_branch = models.CharField(
        _('Default branch'),
        max_length=255,
        default=None,
        null=True,
        blank=True,
        help_text=_('What branch "latest" points to. Leave empty '
                    'to use the default value for your VCS (eg. '
                    'trunk or master).'))
    requirements_file = models.CharField(
        _('Requirements file'),
        max_length=255,
        default=None,
        null=True,
        blank=True,
        help_text=_(
            'Requires Virtualenv. A <a '
            'href="https://pip.pypa.io/en/latest/user_guide.html#requirements-files">'
            'pip requirements file</a> needed to build your documentation. '
            'Path from the root of your project.'))
    documentation_type = models.CharField(
        _('Documentation type'),
        max_length=20,
        choices=constants.DOCUMENTATION_CHOICES,
        default='auto',
        help_text=_('Type of documentation you are building. <a href="http://'
                    'sphinx-doc.org/builders.html#sphinx.builders.html.'
                    'DirectoryHTMLBuilder">More info</a>.'))
    allow_comments = models.BooleanField(_('Allow Comments'), default=False)
    comment_moderation = models.BooleanField(_('Comment Moderation)'),
                                             default=False)
    analytics_code = models.CharField(
        _('Analytics code'),
        max_length=50,
        null=True,
        blank=True,
        help_text=_("Google Analytics Tracking ID (ex. UA-22345342-1). "
                    "This may slow down your page loads."))

    # Other model data.
    path = models.CharField(_('Path'),
                            max_length=255,
                            editable=False,
                            help_text=_("The directory where conf.py lives"))
    conf_py_file = models.CharField(
        _('Python configuration file'),
        max_length=255,
        default='',
        blank=True,
        help_text=_('Path from project root to conf.py file (ex. docs/conf.py)'
                    '. Leave blank if you want us to find it for you.'))

    featured = models.BooleanField(_('Featured'), default=False)
    skip = models.BooleanField(_('Skip'), default=False)
    mirror = models.BooleanField(_('Mirror'), default=False)
    use_virtualenv = models.BooleanField(
        _('Use virtualenv'),
        help_text=_("Install your project inside a virtualenv using setup.py "
                    "install"),
        default=False)

    # This model attribute holds the python interpreter used to create the
    # virtual environment
    python_interpreter = models.CharField(
        _('Python Interpreter'),
        max_length=20,
        choices=constants.PYTHON_CHOICES,
        default='python',
        help_text=_("(Beta) The Python interpreter used to create the virtual "
                    "environment."))

    use_system_packages = models.BooleanField(
        _('Use system packages'),
        help_text=_("Give the virtual environment access to the global "
                    "site-packages dir."),
        default=False)
    django_packages_url = models.CharField(_('Django Packages URL'),
                                           max_length=255,
                                           blank=True)
    privacy_level = models.CharField(
        _('Privacy Level'),
        max_length=20,
        choices=constants.PRIVACY_CHOICES,
        default=getattr(settings, 'DEFAULT_PRIVACY_LEVEL', 'public'),
        help_text=_("(Beta) Level of privacy that you want on the repository. "
                    "Protected means public but not in listings."))
    version_privacy_level = models.CharField(
        _('Version Privacy Level'),
        max_length=20,
        choices=constants.PRIVACY_CHOICES,
        default=getattr(settings, 'DEFAULT_PRIVACY_LEVEL', 'public'),
        help_text=_("(Beta) Default level of privacy you want on built "
                    "versions of documentation."))

    # Subprojects
    related_projects = models.ManyToManyField(
        'self',
        verbose_name=_('Related projects'),
        blank=True,
        null=True,
        symmetrical=False,
        through=ProjectRelationship)

    # Language bits
    language = models.CharField(_('Language'),
                                max_length=20,
                                default='en',
                                help_text=_(
                                    "The language the project "
                                    "documentation is rendered in. "
                                    "Note: this affects your project's URL."),
                                choices=constants.LANGUAGES)

    programming_language = models.CharField(
        _('Programming Language'),
        max_length=20,
        default='words',
        help_text=_(
            "The primary programming language the project is written in."),
        choices=constants.PROGRAMMING_LANGUAGES,
        blank=True)
    # A subproject pointed at it's main language, so it can be tracked
    main_language_project = models.ForeignKey('self',
                                              related_name='translations',
                                              blank=True,
                                              null=True)

    # Version State
    num_major = models.IntegerField(
        _('Number of Major versions'),
        max_length=3,
        default=2,
        null=True,
        blank=True,
        help_text=_("2 means supporting 3.X.X and 2.X.X, but not 1.X.X"))
    num_minor = models.IntegerField(
        _('Number of Minor versions'),
        max_length=3,
        default=2,
        null=True,
        blank=True,
        help_text=_("2 means supporting 2.2.X and 2.1.X, but not 2.0.X"))
    num_point = models.IntegerField(
        _('Number of Point versions'),
        max_length=3,
        default=2,
        null=True,
        blank=True,
        help_text=_("2 means supporting 2.2.2 and 2.2.1, but not 2.2.0"))

    tags = TaggableManager(blank=True)
    objects = ProjectManager()
    all_objects = models.Manager()

    class Meta:
        ordering = ('slug', )
        permissions = (
            # Translators: Permission around whether a user can view the
            # project
            ('view_project', _('View Project')), )

    def __unicode__(self):
        return self.name

    @property
    def subdomain(self):
        prod_domain = getattr(settings, 'PRODUCTION_DOMAIN')
        # if self.canonical_domain:
        #     return self.canonical_domain
        # else:
        subdomain_slug = self.slug.replace('_', '-')
        return "%s.%s" % (subdomain_slug, prod_domain)

    def sync_supported_versions(self):
        supported = self.supported_versions(flat=True)
        if supported:
            self.versions.filter(verbose_name__in=supported).update(
                supported=True)
            self.versions.exclude(verbose_name__in=supported).update(
                supported=False)
            self.versions.filter(verbose_name='latest').update(supported=True)

    def save(self, *args, **kwargs):
        first_save = self.pk is None
        if not self.slug:
            # Subdomains can't have underscores in them.
            self.slug = slugify(self.name).replace('_', '-')
            if self.slug == '':
                raise Exception(_("Model must have slug"))
        super(Project, self).save(*args, **kwargs)
        for owner in self.users.all():
            assign('view_project', owner, self)
        try:
            if self.default_branch:
                latest = self.versions.get(slug='latest')
                if latest.identifier != self.default_branch:
                    latest.identifier = self.default_branch
                    latest.save()
        except Exception:
            log.error('Failed to update latest identifier', exc_info=True)

        # Add exceptions here for safety
        try:
            self.sync_supported_versions()
        except Exception:
            log.error('failed to sync supported versions', exc_info=True)
        try:
            if not first_save:
                symlink(project=self.slug)
        except Exception:
            log.error('failed to symlink project', exc_info=True)
        try:
            update_static_metadata(project_pk=self.pk)
        except Exception:
            log.error('failed to update static metadata', exc_info=True)
        try:
            branch = self.default_branch or self.vcs_repo().fallback_branch
            if not self.versions.filter(slug='latest').exists():
                self.versions.create(slug='latest',
                                     verbose_name='latest',
                                     machine=True,
                                     type='branch',
                                     active=True,
                                     identifier=branch)
            # if not self.versions.filter(slug='stable').exists():
            #     self.versions.create(slug='stable', verbose_name='stable', type='branch', active=True, identifier=branch)
        except Exception:
            log.error('Error creating default branches', exc_info=True)

    def get_absolute_url(self):
        return reverse('projects_detail', args=[self.slug])

    def get_docs_url(self, version_slug=None, lang_slug=None):
        """
        Return a url for the docs. Always use http for now,
        to avoid content warnings.
        """
        protocol = "http"
        version = version_slug or self.get_default_version()
        lang = lang_slug or self.language
        use_subdomain = getattr(settings, 'USE_SUBDOMAIN', False)
        if use_subdomain:
            if self.single_version:
                return "%s://%s/" % (
                    protocol,
                    self.subdomain,
                )
            else:
                return "%s://%s/%s/%s/" % (
                    protocol,
                    self.subdomain,
                    lang,
                    version,
                )
        else:
            if self.single_version:
                return reverse('docs_detail',
                               kwargs={
                                   'project_slug': self.slug,
                                   'filename': ''
                               })
            else:
                return reverse('docs_detail',
                               kwargs={
                                   'project_slug': self.slug,
                                   'lang_slug': lang,
                                   'version_slug': version,
                                   'filename': ''
                               })

    def get_translation_url(self, version_slug=None):
        parent = self.main_language_project
        lang_slug = self.language
        protocol = "http"
        version = version_slug or parent.get_default_version()
        use_subdomain = getattr(settings, 'USE_SUBDOMAIN', False)
        if use_subdomain:
            return "%s://%s/%s/%s/" % (
                protocol,
                parent.subdomain,
                lang_slug,
                version,
            )
        else:
            return reverse('docs_detail',
                           kwargs={
                               'project_slug': parent.slug,
                               'lang_slug': lang_slug,
                               'version_slug': version,
                               'filename': ''
                           })

    def get_builds_url(self):
        return reverse('builds_project_list',
                       kwargs={
                           'project_slug': self.slug,
                       })

    def get_production_media_path(self, type, version_slug, include_file=True):
        """
        Get file path for media files in production.
        This is used to see if these files exist so we can offer them for download.
        """
        if getattr(settings, 'DEFAULT_PRIVACY_LEVEL', 'public') == 'public':
            path = os.path.join(settings.MEDIA_ROOT, type, self.slug,
                                version_slug)
        else:
            path = os.path.join(settings.PRODUCTION_MEDIA_ARTIFACTS, type,
                                self.slug, version_slug)
        if include_file:
            path = os.path.join(
                path, '%s.%s' % (self.slug, type.replace('htmlzip', 'zip')))
        return path

    def get_production_media_url(self, type, version_slug, full_path=True):
        """
        Get the URL for downloading a specific media file.
        """
        path = reverse('project_download_media',
                       kwargs={
                           'project_slug': self.slug,
                           'type': type,
                           'version_slug': version_slug,
                       })
        if full_path:
            path = '//%s%s' % (settings.PRODUCTION_DOMAIN, path)
        return path

    def get_downloads(self):
        downloads = {}
        downloads['htmlzip'] = self.get_production_media_url(
            'htmlzip', self.get_default_version())
        downloads['epub'] = self.get_production_media_url(
            'epub', self.get_default_version())
        downloads['pdf'] = self.get_production_media_url(
            'pdf', self.get_default_version())
        return downloads

    @property
    def canonical_domain(self):
        if not self.clean_canonical_url:
            return ""
        return urlparse(self.clean_canonical_url).netloc

    @property
    def clean_canonical_url(self):
        if not self.canonical_url:
            return ""
        parsed = urlparse(self.canonical_url)
        if parsed.scheme:
            scheme, netloc = parsed.scheme, parsed.netloc
        elif parsed.netloc:
            scheme, netloc = "http", parsed.netloc
        else:
            scheme, netloc = "http", parsed.path
        return "%s://%s/" % (scheme, netloc)

    @property
    def clean_repo(self):
        if self.repo.startswith('http://github.com'):
            return self.repo.replace('http://github.com', 'https://github.com')
        return self.repo

    # Doc PATH:
    # MEDIA_ROOT/slug/checkouts/version/<repo>

    def doc_builder(self):
        return loading.get(self.documentation_type)

    @property
    def doc_path(self):
        return os.path.join(settings.DOCROOT, self.slug.replace('_', '-'))

    def checkout_path(self, version='latest'):
        return os.path.join(self.doc_path, 'checkouts', version)

    def venv_path(self, version='latest'):
        return os.path.join(self.doc_path, 'envs', version)

    #
    # Paths for symlinks in project doc_path.
    #
    def cnames_symlink_path(self, domain):
        """
        Path in the doc_path that we symlink cnames

        This has to be at the top-level because Nginx doesn't know the projects slug.
        """
        return os.path.join(settings.CNAME_ROOT, domain)

    def translations_symlink_path(self, language=None):
        """
        Path in the doc_path that we symlink translations
        """
        if not language:
            language = self.language
        return os.path.join(self.doc_path, 'translations', language)

    def subprojects_symlink_path(self, project):
        """
        Path in the doc_path that we symlink subprojects
        """
        return os.path.join(self.doc_path, 'subprojects', project)

    def single_version_symlink_path(self):
        """
        Path in the doc_path for the single_version symlink.
        """
        return os.path.join(self.doc_path, 'single_version')

    #
    # End symlink paths
    #

    def venv_bin(self, version='latest', bin='python'):
        return os.path.join(self.venv_path(version), 'bin', bin)

    def full_doc_path(self, version='latest'):
        """
        The path to the documentation root in the project.
        """
        doc_base = self.checkout_path(version)
        for possible_path in ['docs', 'doc', 'Doc']:
            if os.path.exists(os.path.join(doc_base, '%s' % possible_path)):
                return os.path.join(doc_base, '%s' % possible_path)
        # No docs directory, docs are at top-level.
        return doc_base

    def artifact_path(self, type, version='latest'):
        """
        The path to the build html docs in the project.
        """
        return os.path.join(self.doc_path, "artifacts", version, type)

    def full_build_path(self, version='latest'):
        """
        The path to the build html docs in the project.
        """
        return os.path.join(self.conf_dir(version), "_build", "html")

    def full_latex_path(self, version='latest'):
        """
        The path to the build LaTeX docs in the project.
        """
        return os.path.join(self.conf_dir(version), "_build", "latex")

    def full_man_path(self, version='latest'):
        """
        The path to the build man docs in the project.
        """
        return os.path.join(self.conf_dir(version), "_build", "man")

    def full_epub_path(self, version='latest'):
        """
        The path to the build epub docs in the project.
        """
        return os.path.join(self.conf_dir(version), "_build", "epub")

    def full_dash_path(self, version='latest'):
        """
        The path to the build dash docs in the project.
        """
        return os.path.join(self.conf_dir(version), "_build", "dash")

    def full_json_path(self, version='latest'):
        """
        The path to the build json docs in the project.
        """
        if 'sphinx' in self.documentation_type:
            return os.path.join(self.conf_dir(version), "_build", "json")
        elif 'mkdocs' in self.documentation_type:
            return os.path.join(self.checkout_path(version), "_build", "json")

    def full_singlehtml_path(self, version='latest'):
        """
        The path to the build singlehtml docs in the project.
        """
        return os.path.join(self.conf_dir(version), "_build", "singlehtml")

    def rtd_build_path(self, version="latest"):
        """
        The destination path where the built docs are copied.
        """
        return os.path.join(self.doc_path, 'rtd-builds', version)

    def static_metadata_path(self):
        """
        The path to the static metadata JSON settings file
        """
        return os.path.join(self.doc_path, 'metadata.json')

    def conf_file(self, version='latest'):
        if self.conf_py_file:
            log.debug('Inserting conf.py file path from model')
            return os.path.join(self.checkout_path(version), self.conf_py_file)
        files = self.find('conf.py', version)
        if not files:
            files = self.full_find('conf.py', version)
        if len(files) == 1:
            return files[0]
        for file in files:
            if file.find('doc', 70) != -1:
                return file
        # Having this be translatable causes this odd error:
        # ProjectImportError(<django.utils.functional.__proxy__ object at
        # 0x1090cded0>,)
        raise ProjectImportError(
            u"Conf File Missing. Please make sure you have a conf.py in your project."
        )

    def conf_dir(self, version='latest'):
        conf_file = self.conf_file(version)
        if conf_file:
            return conf_file.replace('/conf.py', '')

    @property
    def highest_version(self):
        return _highest(self.api_versions())

    @property
    def is_imported(self):
        return bool(self.repo)

    @property
    def has_good_build(self):
        return self.builds.filter(success=True).exists()

    @property
    def has_versions(self):
        return self.versions.exists()

    @property
    def has_aliases(self):
        return self.aliases.exists()

    def has_pdf(self, version_slug='latest'):
        return os.path.exists(
            self.get_production_media_path(type='pdf',
                                           version_slug=version_slug))

    def has_epub(self, version_slug='latest'):
        return os.path.exists(
            self.get_production_media_path(type='epub',
                                           version_slug=version_slug))

    def has_htmlzip(self, version_slug='latest'):
        return os.path.exists(
            self.get_production_media_path(type='htmlzip',
                                           version_slug=version_slug))

    @property
    def sponsored(self):
        return False

    def vcs_repo(self, version='latest'):
        backend = backend_cls.get(self.repo_type)
        if not backend:
            repo = None
        else:
            proj = VCSProject(self.name, self.default_branch,
                              self.checkout_path(version), self.clean_repo)
            repo = backend(proj, version)
        return repo

    @property
    def contribution_backend(self):
        if hasattr(self, '_contribution_backend'):
            return self._contribution_backend
        if not self.vcs_repo:
            cb = None
        else:
            cb = self.vcs_repo.get_contribution_backend()
        self._contribution_backend = cb
        return cb

    def repo_nonblockinglock(self, version, max_lock_age=5):
        return NonBlockingLock(project=self,
                               version=version,
                               max_lock_age=max_lock_age)

    def repo_lock(self, version, timeout=5, polling_interval=5):
        return Lock(self, version, timeout, polling_interval)

    def find(self, file, version):
        """
        A balla API to find files inside of a projects dir.
        """
        matches = []
        for root, dirnames, filenames in os.walk(self.full_doc_path(version)):
            for filename in fnmatch.filter(filenames, file):
                matches.append(os.path.join(root, filename))
        return matches

    def full_find(self, file, version):
        """
        A balla API to find files inside of a projects dir.
        """
        matches = []
        for root, dirnames, filenames in os.walk(self.checkout_path(version)):
            for filename in fnmatch.filter(filenames, file):
                matches.append(os.path.join(root, filename))
        return matches

    def get_latest_build(self, finished=True):
        """
        Get latest build for project

        finished
            Return only builds that are in a finished state
        """
        kwargs = {'type': 'html'}
        if finished:
            kwargs['state'] = 'finished'
        return self.builds.filter(**kwargs).first()

    def api_versions(self):
        ret = []
        for version_data in api.version.get(project=self.pk,
                                            active=True)['objects']:
            version = make_api_version(version_data)
            ret.append(version)
        return sort_version_aware(ret)

    def active_versions(self):
        from builds.models import Version
        versions = Version.objects.public(project=self, only_active=True)
        return (versions.filter(built=True, active=True)
                | versions.filter(active=True, uploaded=True))

    def ordered_active_versions(self):
        from builds.models import Version
        versions = Version.objects.public(project=self, only_active=True)
        return sort_version_aware(versions)

    def all_active_versions(self):
        """A temporary workaround for active_versions filtering out things
        that were active, but failed to build

        """
        return self.versions.filter(active=True)

    def supported_versions(self, flat=True):
        """
        Get the list of supported versions.
        Returns a list of version strings.
        """
        if not self.num_major or not self.num_minor or not self.num_point:
            return None
        versions = []
        for ver in self.versions.all():
            try:
                versions.append(BetterVersion(ver.verbose_name))
            except UnsupportedVersionError:
                # Probably a branch
                pass
        active_versions = version_windows(
            versions,
            major=self.num_major,
            minor=self.num_minor,
            point=self.num_point,
            flat=flat,
        )
        version_strings = [v._string for v in active_versions]
        return version_strings

    def version_from_branch_name(self, branch):
        try:
            return (
                self.versions.filter(identifier=branch) |
                self.versions.filter(identifier=('remotes/origin/%s' % branch))
                | self.versions.filter(identifier=('origin/%s' % branch)))[0]
        except IndexError:
            return None

    def versions_from_branch_name(self, branch):
        return (self.versions.filter(identifier=branch)
                | self.versions.filter(identifier='remotes/origin/%s' % branch)
                | self.versions.filter(identifier='origin/%s' % branch))

    def get_default_version(self):
        """
        Get the default version (slug).

        Returns self.default_version if the version with that slug actually
        exists (is built and published). Otherwise returns 'latest'.
        """
        # latest is a special case where we don't have to check if it exists
        if self.default_version == 'latest':
            return self.default_version
        # check if the default_version exists
        version_qs = self.versions.filter(slug=self.default_version,
                                          active=True)
        if version_qs.exists():
            return self.default_version
        return 'latest'

    def get_default_branch(self):
        """
        Get the version representing "latest"
        """
        if self.default_branch:
            return self.default_branch
        else:
            return self.vcs_repo().fallback_branch

    def add_subproject(self, child):
        subproject, created = ProjectRelationship.objects.get_or_create(
            parent=self,
            child=child,
        )
        return subproject

    def remove_subproject(self, child):
        ProjectRelationship.objects.filter(parent=self, child=child).delete()
        return

    def moderation_queue(self):
        # non-optimal SQL warning.
        from comments.models import DocumentComment
        queue = []
        comments = DocumentComment.objects.filter(node__project=self)
        for comment in comments:
            if not comment.has_been_approved_since_most_recent_node_change():
                queue.append(comment)

        return queue

    def add_node(self, node_hash, page, version, commit):
        from comments.models import NodeSnapshot, DocumentNode
        project_obj = Project.objects.get(slug=self.slug)
        version_obj = project_obj.versions.get(slug=version)
        try:
            NodeSnapshot.objects.get(hash=node_hash,
                                     node__project=project_obj,
                                     node__version=version_obj,
                                     node__page=page,
                                     commit=commit)
            return False  # ie, no new node was created.
        except NodeSnapshot.DoesNotExist:
            DocumentNode.objects.create(hash=node_hash,
                                        page=page,
                                        project=project_obj,
                                        version=version_obj,
                                        commit=commit)
        return True  # ie, it's True that a new node was created.

    def add_comment(self, version_slug, page, hash, commit, user, text):
        from comments.models import DocumentNode
        try:
            node = self.nodes.from_hash(version_slug, page, hash)
        except DocumentNode.DoesNotExist:
            version = self.versions.get(slug=version_slug)
            node = self.nodes.create(version=version,
                                     page=page,
                                     hash=hash,
                                     commit=commit)
        return node.comments.create(user=user, text=text)
Example #2
0
class Project(models.Model):
    #Auto fields
    pub_date = models.DateTimeField(_('Publication date'), auto_now_add=True)
    modified_date = models.DateTimeField(_('Modified date'), auto_now=True)

    #Generally from conf.py
    users = models.ManyToManyField(User,
                                   verbose_name=_('User'),
                                   related_name='projects')
    name = models.CharField(_('Name'), max_length=255)
    slug = models.SlugField(_('Slug'), max_length=255, unique=True)
    description = models.TextField(_('Description'),
                                   blank=True,
                                   help_text=_('The reStructuredText '
                                               'description of the project'))
    repo = models.CharField(_('Repository URL'),
                            max_length=100,
                            blank=True,
                            help_text=_('Hosted documentation repository URL'))
    repo_type = models.CharField(_('Repository type'),
                                 max_length=10,
                                 choices=constants.REPO_CHOICES,
                                 default='git')
    project_url = models.URLField(_('Project homepage'),
                                  blank=True,
                                  help_text=_('The project\'s homepage'))
    canonical_url = models.URLField(
        _('Canonical URL'),
        blank=True,
        help_text=_('URL that documentation is expected to serve from'))
    version = models.CharField(_('Version'),
                               max_length=100,
                               blank=True,
                               help_text=_('Project version these docs apply '
                                           'to, i.e. 1.0a'))
    copyright = models.CharField(_('Copyright'),
                                 max_length=255,
                                 blank=True,
                                 help_text=_('Project copyright information'))
    theme = models.CharField(
        _('Theme'),
        max_length=20,
        choices=constants.DEFAULT_THEME_CHOICES,
        default=constants.THEME_DEFAULT,
        help_text=(u'<a href="http://sphinx.pocoo.org/theming.html#builtin-'
                   'themes" target="_blank">%s</a>') % _('Examples'))
    suffix = models.CharField(_('Suffix'),
                              max_length=10,
                              editable=False,
                              default='.rst')
    single_version = models.BooleanField(
        _('Single version'),
        default=False,
        help_text=
        _('A single version site has no translations and only your "latest" version, served at the root of the domain. Use this with caution, only turn it on if you will <b>never</b> have multiple versions of your docs.'
          ))
    default_version = models.CharField(
        _('Default version'),
        max_length=255,
        default='latest',
        help_text=_('The version of your project that / redirects to'))
    # In default_branch, None max_lengtheans the backend should choose the
    # appropraite branch. Eg 'master' for git
    default_branch = models.CharField(
        _('Default branch'),
        max_length=255,
        default=None,
        null=True,
        blank=True,
        help_text=_('What branch "latest" points to. Leave empty '
                    'to use the default value for your VCS (eg. '
                    'trunk or master).'))
    requirements_file = models.CharField(
        _('Requirements file'),
        max_length=255,
        default=None,
        null=True,
        blank=True,
        help_text=_(
            'Requires Virtualenv. A <a '
            'href="https://pip.pypa.io/en/latest/user_guide.html#requirements-files">'
            'pip requirements file</a> needed to build your documentation. '
            'Path from the root of your project.'))
    documentation_type = models.CharField(
        _('Documentation type'),
        max_length=20,
        choices=constants.DOCUMENTATION_CHOICES,
        default='sphinx',
        help_text=_('Type of documentation you are building. <a href="http://'
                    'sphinx-doc.org/builders.html#sphinx.builders.html.'
                    'DirectoryHTMLBuilder">More info</a>.'))
    analytics_code = models.CharField(
        _('Analytics code'),
        max_length=50,
        null=True,
        blank=True,
        help_text=_("Google Analytics Tracking ID (ex. UA-22345342-1). "
                    "This may slow down your page loads."))

    # Other model data.
    path = models.CharField(_('Path'),
                            max_length=255,
                            editable=False,
                            help_text=_("The directory where conf.py lives"))
    conf_py_file = models.CharField(
        _('Python configuration file'),
        max_length=255,
        default='',
        blank=True,
        help_text=_('Path from project root to conf.py file (ex. docs/conf.py)'
                    '. Leave blank if you want us to find it for you.'))

    featured = models.BooleanField(_('Featured'), default=False)
    skip = models.BooleanField(_('Skip'), default=False)
    mirror = models.BooleanField(_('Mirror'), default=False)
    use_virtualenv = models.BooleanField(
        _('Use virtualenv'),
        help_text=_("Install your project inside a virtualenv using setup.py "
                    "install"),
        default=False)

    # This model attribute holds the python interpreter used to create the
    # virtual environment
    python_interpreter = models.CharField(
        _('Python Interpreter'),
        max_length=20,
        choices=constants.PYTHON_CHOICES,
        default='python',
        help_text=_("(Beta) The Python interpreter used to create the virtual "
                    "environment."))

    use_system_packages = models.BooleanField(
        _('Use system packages'),
        help_text=_("Give the virtual environment access to the global "
                    "site-packages dir."),
        default=False)
    django_packages_url = models.CharField(_('Django Packages URL'),
                                           max_length=255,
                                           blank=True)
    privacy_level = models.CharField(
        _('Privacy Level'),
        max_length=20,
        choices=constants.PRIVACY_CHOICES,
        default=getattr(settings, 'DEFAULT_PRIVACY_LEVEL', 'public'),
        help_text=_("(Beta) Level of privacy that you want on the repository. "
                    "Protected means public but not in listings."))
    version_privacy_level = models.CharField(
        _('Version Privacy Level'),
        max_length=20,
        choices=constants.PRIVACY_CHOICES,
        default=getattr(settings, 'DEFAULT_PRIVACY_LEVEL', 'public'),
        help_text=_("(Beta) Default level of privacy you want on built "
                    "versions of documentation."))

    # Subprojects
    related_projects = models.ManyToManyField(
        'self',
        verbose_name=_('Related projects'),
        blank=True,
        null=True,
        symmetrical=False,
        through=ProjectRelationship)

    # Language bits
    language = models.CharField(_('Language'),
                                max_length=20,
                                default='en',
                                help_text=_(
                                    "The language the project "
                                    "documentation is rendered in. "
                                    "Note: this affects your project's URL."),
                                choices=constants.LANGUAGES)
    # A subproject pointed at it's main language, so it can be tracked
    main_language_project = models.ForeignKey('self',
                                              related_name='translations',
                                              blank=True,
                                              null=True)

    # Version State
    num_major = models.IntegerField(
        _('Number of Major versions'),
        max_length=3,
        default=2,
        null=True,
        blank=True,
        help_text=_("2 means supporting 3.X.X and 2.X.X, but not 1.X.X"))
    num_minor = models.IntegerField(
        _('Number of Minor versions'),
        max_length=3,
        default=2,
        null=True,
        blank=True,
        help_text=_("2 means supporting 2.2.X and 2.1.X, but not 2.0.X"))
    num_point = models.IntegerField(
        _('Number of Point versions'),
        max_length=3,
        default=2,
        null=True,
        blank=True,
        help_text=_("2 means supporting 2.2.2 and 2.2.1, but not 2.2.0"))

    tags = TaggableManager(blank=True)
    objects = ProjectManager()

    class Meta:
        ordering = ('slug', )
        permissions = (
            # Translators: Permission around whether a user can view the
            # project
            ('view_project', _('View Project')), )

    def __unicode__(self):
        return self.name

    @property
    def subdomain(self):
        prod_domain = getattr(settings, 'PRODUCTION_DOMAIN')
        # if self.canonical_domain:
        #     return self.canonical_domain
        # else:
        subdomain_slug = self.slug.replace('_', '-')
        return "%s.%s" % (subdomain_slug, prod_domain)

    def sync_supported_versions(self):
        supported = self.supported_versions(flat=True)
        if supported:
            self.versions.filter(verbose_name__in=supported).update(
                supported=True)
            self.versions.exclude(verbose_name__in=supported).update(
                supported=False)
            self.versions.filter(verbose_name='latest').update(supported=True)

    def save(self, *args, **kwargs):
        if not self.slug:
            # Subdomains can't have underscores in them.
            self.slug = slugify(self.name).replace('_', '-')
            if self.slug == '':
                raise Exception(_("Model must have slug"))
        obj = super(Project, self).save(*args, **kwargs)
        for owner in self.users.all():
            assign('view_project', owner, self)

        # Add exceptions here for safety
        try:
            self.sync_supported_versions()
        except Exception, e:
            log.error('failed to sync supported versions', exc_info=True)
        try:
            symlink(project=self.slug)
        except Exception, e:
            log.error('failed to symlink project', exc_info=True)