• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rdmorganiser / rdmo / 20428555173

22 Dec 2025 10:02AM UTC coverage: 94.693% (-0.1%) from 94.814%
20428555173

Pull #1436

github

web-flow
Merge 0ffb48a5d into 57a75b09e
Pull Request #1436: Draft: add `rdmo.config` app for `Plugin` model (plugin managament)

2191 of 2304 branches covered (95.1%)

23411 of 24723 relevant lines covered (94.69%)

3.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.28
rdmo/projects/views/project.py
1
import logging
4✔
2

3
from django.conf import settings
4✔
4
from django.contrib.auth.mixins import LoginRequiredMixin
4✔
5
from django.db.models import F, OuterRef, Subquery
4✔
6
from django.forms import Form
4✔
7
from django.http import Http404
4✔
8
from django.shortcuts import get_object_or_404, redirect
4✔
9
from django.urls import reverse_lazy
4✔
10
from django.utils.decorators import method_decorator
4✔
11
from django.utils.translation import gettext_lazy as _
4✔
12
from django.views.decorators.csrf import ensure_csrf_cookie
4✔
13
from django.views.generic import DeleteView, DetailView, TemplateView
4✔
14
from django.views.generic.edit import FormMixin
4✔
15

16
from rdmo.config.constants import PluginType
4✔
17
from rdmo.config.models import Plugin
4✔
18
from rdmo.core.views import CSRFViewMixin, ObjectPermissionMixin, RedirectViewMixin, StoreIdViewMixin
4✔
19
from rdmo.questions.models import Catalog
4✔
20

21
from ...tasks.models import Task
4✔
22
from ...views.models import View
4✔
23
from ..models import Integration, Invite, Membership, Project
4✔
24
from ..utils import filter_tasks_or_views_for_project, get_upload_accept
4✔
25

26
logger = logging.getLogger(__name__)
4✔
27

28

29
class ProjectsView(LoginRequiredMixin, CSRFViewMixin, StoreIdViewMixin, TemplateView):
4✔
30
    template_name = 'projects/projects.html'
4✔
31

32

33
class ProjectDetailView(ObjectPermissionMixin, DetailView):
4✔
34
    model = Project
4✔
35
    queryset = Project.objects.prefetch_related(
4✔
36
        'issues',
37
        'issues__task',
38
        'issues__task__conditions',
39
        'issues__task__conditions__source',
40
        'issues__task__conditions__target_option',
41
        'tasks',
42
        'views',
43
        'values'
44
    )
45
    permission_required = 'projects.view_project_object'
4✔
46

47
    def get_context_data(self, **kwargs):
4✔
48
        context = super().get_context_data(**kwargs)
4✔
49
        project = context['project']
4✔
50
        ancestors = project.get_ancestors(include_self=True)
4✔
51
        values = project.values.filter(snapshot=None).select_related('attribute', 'option')
4✔
52
        highest = Membership.objects.filter(project__in=ancestors, user_id=OuterRef('user_id')) \
4✔
53
                                    .order_by('-project__level')
54
        memberships = Membership.objects.filter(project__in=ancestors) \
4✔
55
                                        .annotate(highest=Subquery(highest.values('project__level')[:1])) \
56
                                        .filter(highest=F('project__level')) \
57
                                        .select_related('user')
58

59
        if settings.SOCIALACCOUNT:
4✔
60
            # prefetch the users social account, if that relation exists
61
            memberships = memberships.prefetch_related('user__socialaccount_set')
×
62

63
        integrations = Integration.objects.filter(project__in=ancestors)
4✔
64
        context['catalogs'] = Catalog.objects.filter_for_user(self.request.user)
4✔
65

66
        if settings.PROJECT_TASKS_SYNC:
4✔
67
            # tasks should be synced, the user can not change them
68
            context['tasks_available'] = project.tasks.exists()
×
69
        else:
70
            context['tasks_available'] = (
4✔
71
                filter_tasks_or_views_for_project(Task, project).filter_availability(self.request.user).exists()
72
            )
73

74
        if settings.PROJECT_VIEWS_SYNC:
4✔
75
            # views should be synced, the user can not change them
76
            context['views_available'] = project.views.exists()
×
77
        else:
78
            context['views_available'] = (
4✔
79
                filter_tasks_or_views_for_project(View, project).filter_availability(self.request.user).exists()
80
            )
81

82
        ancestors_import = []
4✔
83
        for instance in ancestors.exclude(id=project.id):
4✔
84
            if self.request.user.has_perm('projects.view_project_object', instance):
4✔
85
                ancestors_import.append(instance)
4✔
86
        context['ancestors_import'] = ancestors_import
4✔
87
        context['memberships'] = memberships.order_by('user__last_name', '-project__level')
4✔
88
        context['integrations'] = integrations.order_by('provider_key', '-project__level')
4✔
89
        if settings.PLUGINS:
4✔
90
            plugins = Plugin.objects.for_context(
4✔
91
                plugin_type=PluginType.PROJECT_ISSUE_PROVIDER,
92
                project=project,
93
                user=self.request.user
94
            )
95
            providers = {i.url_name: i.initialize_class() for i in plugins}
4✔
96
            context['providers'] = providers
4✔
97
        context['issues'] = [
4✔
98
            issue for issue in project.issues.order_by('-status', 'task__order', 'task__uri') if issue.resolve(values)
99
        ]
100
        context['views'] = project.views.order_by('order', 'uri')
4✔
101
        context['snapshots'] = project.snapshots.all()
4✔
102
        context['invites'] = project.invites.all()
4✔
103
        context['membership'] = Membership.objects.filter(project=project, user=self.request.user).first()
4✔
104
        context['upload_accept'] = ','.join([
4✔
105
            suffix for suffixes in get_upload_accept().values() for suffix in suffixes
106
        ])
107
        return context
4✔
108

109

110
class ProjectDeleteView(ObjectPermissionMixin, RedirectViewMixin, DeleteView):
4✔
111
    model = Project
4✔
112
    queryset = Project.objects.all()
4✔
113
    success_url = reverse_lazy('projects')
4✔
114
    permission_required = 'projects.delete_project_object'
4✔
115

116

117
class ProjectJoinView(LoginRequiredMixin, RedirectViewMixin, TemplateView):
4✔
118
    template_name = 'core/error.html'
4✔
119

120
    def get(self, request, token):
4✔
121
        try:
4✔
122
            invite = Invite.objects.get(token=token)
4✔
123

124
            if invite.is_expired:
4✔
125
                error = _('Sorry, your invitation has been expired.')
4✔
126
                invite.delete()
4✔
127
            elif invite.user and invite.user != request.user:
4✔
128
                error = _('Sorry, but this invitation is for the user "%s".') % invite.user
4✔
129
            elif Membership.objects.filter(project=invite.project, user=request.user).exists():
4✔
130
                invite.delete()
4✔
131
                return redirect(invite.project.get_absolute_url())
4✔
132
            else:
133
                Membership.objects.create(
4✔
134
                    project=invite.project,
135
                    user=request.user,
136
                    role=invite.role
137
                )
138
                invite.delete()
4✔
139
                return redirect(invite.project.get_absolute_url())
4✔
140

141
        except Invite.DoesNotExist:
4✔
142
            error = _('Sorry, the invitation link is not valid.')
4✔
143

144
        return self.render_to_response({
4✔
145
            'title': _('Error'),
146
            'errors': [error]
147
        })
148

149

150
class ProjectCancelView(LoginRequiredMixin, RedirectViewMixin, TemplateView):
4✔
151
    template_name = 'core/error.html'
4✔
152
    success_url = reverse_lazy('projects')
4✔
153

154
    def get(self, request, token=None):
4✔
155
        invite = get_object_or_404(Invite, token=token)
×
156
        if invite.user in [None, request.user]:
×
157
            invite.delete()
×
158

159
        return redirect(self.success_url)
×
160

161

162
class ProjectLeaveView(ObjectPermissionMixin, RedirectViewMixin, FormMixin, DetailView):
4✔
163
    model = Project
4✔
164
    form_class = Form
4✔
165
    queryset = Project.objects.all()
4✔
166
    success_url = reverse_lazy('projects')
4✔
167
    permission_required = 'projects.leave_project_object'
4✔
168
    template_name = 'projects/project_confirm_leave.html'
4✔
169

170
    def post(self, request, *args, **kwargs):
4✔
171
        form = self.get_form()
4✔
172
        if form.is_valid() and 'cancel' not in request.POST:
4✔
173
            membership = Membership.objects.filter(project=self.get_object()).get(user=request.user)
4✔
174
            if not membership.is_last_owner:
4✔
175
                membership.delete()
4✔
176

177
        return redirect(self.success_url)
4✔
178

179

180
class ProjectExportView(ObjectPermissionMixin, DetailView):
4✔
181
    model = Project
4✔
182
    queryset = Project.objects.all()
4✔
183
    permission_required = 'projects.export_project_object'
4✔
184

185
    def get_export_plugin(self):
4✔
186
        export_plugins = Plugin.objects.for_context(
4✔
187
            project=self.object, plugin_type=PluginType.PROJECT_EXPORT,
188
            user=self.request.user, format=self.kwargs.get('format')
189
        )
190
        if not export_plugins.exists():
4✔
191
            raise Http404
×
192

193
        export_plugin_instance = export_plugins.first()
4✔
194

195
        export_plugin = export_plugin_instance.initialize_class()
4✔
196
        export_plugin.request = self.request
4✔
197
        export_plugin.project = self.object
4✔
198

199
        return export_plugin
4✔
200

201
    def get(self, request, *args, **kwargs):
4✔
202
        self.object = self.get_object()
4✔
203
        return self.get_export_plugin().render()
4✔
204

205
    def post(self, request, *args, **kwargs):
4✔
206
        self.object = self.get_object()
×
207
        return self.get_export_plugin().submit()
×
208

209

210
class ProjectInterviewView(ObjectPermissionMixin, DetailView):
4✔
211
    model = Project
4✔
212
    queryset = Project.objects.all()
4✔
213
    permission_required = 'projects.view_project_object'
4✔
214
    template_name = 'projects/project_interview.html'
4✔
215

216
    @method_decorator(ensure_csrf_cookie)
4✔
217
    def get(self, request, *args, **kwargs):
4✔
218
        self.object = self.get_object()
×
219

220
        if self.object.catalog is None:
×
221
            return redirect('project_error', pk=self.object.pk)
×
222
        else:
223
            return super().get(request, *args, **kwargs)
×
224

225

226
class ProjectErrorView(ObjectPermissionMixin, DetailView):
4✔
227
    model = Project
4✔
228
    queryset = Project.objects.all()
4✔
229
    permission_required = 'projects.view_project_object'
4✔
230
    template_name = 'projects/project_error.html'
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc