|
26 | 26 | from readthedocs.projects.querysets import (
|
27 | 27 | ProjectQuerySet,
|
28 | 28 | RelatedProjectQuerySet,
|
29 |
| - ChildRelatedProjectQuerySet |
| 29 | + ChildRelatedProjectQuerySet, |
| 30 | + FeatureQuerySet, |
30 | 31 | )
|
31 | 32 | from readthedocs.projects.templatetags.projects_tags import sort_version_aware
|
32 |
| -from readthedocs.projects.utils import make_api_version |
33 | 33 | from readthedocs.projects.version_handling import determine_stable_version, version_windows
|
34 | 34 | from readthedocs.restapi.client import api
|
35 | 35 | from readthedocs.vcs_support.backends import backend_cls
|
@@ -639,9 +639,10 @@ def get_latest_build(self, finished=True):
|
639 | 639 | return self.builds.filter(**kwargs).first()
|
640 | 640 |
|
641 | 641 | def api_versions(self):
|
| 642 | + from readthedocs.builds.models import APIVersion |
642 | 643 | ret = []
|
643 | 644 | for version_data in api.project(self.pk).active_versions.get()['versions']:
|
644 |
| - version = make_api_version(version_data) |
| 645 | + version = APIVersion(**version_data) |
645 | 646 | ret.append(version)
|
646 | 647 | return sort_version_aware(ret)
|
647 | 648 |
|
@@ -821,6 +822,57 @@ def add_comment(self, version_slug, page, content_hash, commit, user, text):
|
821 | 822 | hash=content_hash, commit=commit)
|
822 | 823 | return node.comments.create(user=user, text=text)
|
823 | 824 |
|
| 825 | + @property |
| 826 | + def features(self): |
| 827 | + return Feature.objects.for_project(self) |
| 828 | + |
| 829 | + def has_feature(self, feature_id): |
| 830 | + """Does project have existing feature flag |
| 831 | +
|
| 832 | + If the feature has a historical True value before the feature was added, |
| 833 | + we consider the project to have the flag. This is used for deprecating a |
| 834 | + feature or changing behavior for new projects |
| 835 | + """ |
| 836 | + return self.features.filter(feature_id=feature_id).exists() |
| 837 | + |
| 838 | + |
| 839 | +class APIProject(Project): |
| 840 | + |
| 841 | + """Project proxy model for API data deserialization |
| 842 | +
|
| 843 | + This replaces the pattern where API data was deserialized into a mocked |
| 844 | + :py:cls:`Project` object. This pattern was confusing, as it was not explicit |
| 845 | + as to what form of object you were working with -- API backed or database |
| 846 | + backed. |
| 847 | +
|
| 848 | + This model preserves the Project model methods, allowing for overrides on |
| 849 | + model field differences. This model pattern will generally only be used on |
| 850 | + builder instances, where we are interacting solely with API data. |
| 851 | + """ |
| 852 | + |
| 853 | + features = [] |
| 854 | + |
| 855 | + class Meta: |
| 856 | + proxy = True |
| 857 | + |
| 858 | + def __init__(self, *args, **kwargs): |
| 859 | + self.features = kwargs.pop('features', []) |
| 860 | + # These fields only exist on the API return, not on the model, so we'll |
| 861 | + # remove them to avoid throwing exceptions due to unexpected fields |
| 862 | + for key in ['users', 'resource_uri', 'absolute_url', 'downloads', |
| 863 | + 'main_language_project', 'related_projects']: |
| 864 | + try: |
| 865 | + del kwargs[key] |
| 866 | + except KeyError: |
| 867 | + pass |
| 868 | + super(APIProject, self).__init__(*args, **kwargs) |
| 869 | + |
| 870 | + def save(self, *args, **kwargs): |
| 871 | + return 0 |
| 872 | + |
| 873 | + def has_feature(self, feature_id): |
| 874 | + return feature_id in self.features |
| 875 | + |
824 | 876 |
|
825 | 877 | @python_2_unicode_compatible
|
826 | 878 | class ImportedFile(models.Model):
|
@@ -921,3 +973,63 @@ def delete(self, *args, **kwargs): # pylint: disable=arguments-differ
|
921 | 973 | from readthedocs.projects import tasks
|
922 | 974 | broadcast(type='app', task=tasks.symlink_domain, args=[self.project.pk, self.pk, True])
|
923 | 975 | super(Domain, self).delete(*args, **kwargs)
|
| 976 | + |
| 977 | + |
| 978 | +@python_2_unicode_compatible |
| 979 | +class Feature(models.Model): |
| 980 | + |
| 981 | + """Project feature flags |
| 982 | +
|
| 983 | + Features should generally be added here as choices, however features may |
| 984 | + also be added dynamically from a signal in other packages. Features can be |
| 985 | + added by external packages with the use of signals:: |
| 986 | +
|
| 987 | + @receiver(pre_init, sender=Feature) |
| 988 | + def add_features(sender, **kwargs): |
| 989 | + sender.FEATURES += (('blah', 'BLAH'),) |
| 990 | +
|
| 991 | + The FeatureForm will grab the updated list on instantiation. |
| 992 | + """ |
| 993 | + |
| 994 | + # Feature constants - this is not a exhaustive list of features, features |
| 995 | + # may be added by other packages |
| 996 | + USE_SPHINX_LATEST = 'use_sphinx_latest' |
| 997 | + |
| 998 | + FEATURES = ( |
| 999 | + (USE_SPHINX_LATEST, _('Use latest version of Sphinx')), |
| 1000 | + ) |
| 1001 | + |
| 1002 | + projects = models.ManyToManyField( |
| 1003 | + Project, |
| 1004 | + blank=True, |
| 1005 | + ) |
| 1006 | + # Feature is not implemented as a ChoiceField, as we don't want validation |
| 1007 | + # at the database level on this field. Arbitrary values are allowed here. |
| 1008 | + feature_id = models.CharField( |
| 1009 | + _('Feature identifier'), |
| 1010 | + max_length=32, |
| 1011 | + unique=True, |
| 1012 | + ) |
| 1013 | + add_date = models.DateTimeField( |
| 1014 | + _('Date feature was added'), |
| 1015 | + auto_now_add=True, |
| 1016 | + ) |
| 1017 | + default_true = models.BooleanField( |
| 1018 | + _('Historical default is True'), |
| 1019 | + default=False, |
| 1020 | + ) |
| 1021 | + |
| 1022 | + objects = FeatureQuerySet.as_manager() |
| 1023 | + |
| 1024 | + def __str__(self): |
| 1025 | + return "{0} feature".format( |
| 1026 | + self.get_feature_display(), |
| 1027 | + ) |
| 1028 | + |
| 1029 | + def get_feature_display(self): |
| 1030 | + """Implement display name field for fake ChoiceField |
| 1031 | +
|
| 1032 | + Because the field is not a ChoiceField here, we need to manually |
| 1033 | + implement this behavior. |
| 1034 | + """ |
| 1035 | + return dict(self.FEATURES).get(self.feature_id, self.feature_id) |
0 commit comments