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

uw-it-aca / canvas-sis-provisioner / 4571350728

pending completion
4571350728

Pull #831

github

GitHub
Merge e0b4c1d0e into 8cacbeb40
Pull Request #831: Develop

38 of 38 new or added lines in 10 files covered. (100.0%)

4514 of 8004 relevant lines covered (56.4%)

0.56 hits per line

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

40.12
/sis_provisioner/events/group/dispatch.py
1
# Copyright 2023 UW-IT, University of Washington
2
# SPDX-License-Identifier: Apache-2.0
3

4

5
from django.conf import settings
1✔
6
from sis_provisioner.dao.user import valid_net_id
1✔
7
from sis_provisioner.exceptions import UserPolicyException
1✔
8
from sis_provisioner.models.group import Group, GroupMemberGroup
1✔
9
from sis_provisioner.models.user import User
1✔
10
from restclients_core.exceptions import DataFailureException
1✔
11
import xml.etree.ElementTree as ET
1✔
12
from logging import getLogger
1✔
13
import re
1✔
14

15
log_prefix = 'GROUP:'
1✔
16
re_parser = re.compile(r'^.*(<group.*/group>).*$', re.MULTILINE | re.DOTALL)
1✔
17

18

19
class Dispatch(object):
1✔
20
    """
21
    Base class for dispatching on actions within a UW GWS Event
22
    """
23
    def __init__(self, config, message=None):
1✔
24
        self._log = getLogger(__name__)
1✔
25
        self._settings = config
1✔
26
        self._message = message
1✔
27

28
    def mine(self, group):
1✔
29
        return False
×
30

31
    def run(self, action, group, message):
1✔
32
        try:
×
33
            return {
×
34
                'update-members': self.update_members,
35
                'put-group': self.put_group,
36
                'delete-group': self.delete_group,
37
                'put-members': self.put_members,
38
                'change-subject-name': self.change_subject_name,
39
                'no-action': self.no_action
40
            }[action](group, self._parse(message))
41
        except KeyError:
×
42
            self._log.info('{} UNKNOWN {} for {}'.format(
×
43
                log_prefix, action, group))
44
            return 0
×
45

46
    def update_members(self, group_id, message):
1✔
47
        self._log.info('{} IGNORE update-members for {}'.format(
×
48
            log_prefix, group_id))
49
        return 0
×
50

51
    def put_group(self, group_id, message):
1✔
52
        self._log.info('{} IGNORE put-group {}'.format(log_prefix, group_id))
×
53
        return 0
×
54

55
    def delete_group(self, group_id, message):
1✔
56
        self._log.info('{} IGNORE delete-group {}'.format(
×
57
            log_prefix, group_id))
58
        return 0
×
59

60
    def put_members(self, group_id, message):
1✔
61
        self._log.info('{} IGNORE put-members for {}'.format(
×
62
            log_prefix, group_id))
63
        return 0
×
64

65
    def change_subject_name(self, group_id, message):
1✔
66
        self._log.info('{} IGNORE change-subject-name for {}'.format(
×
67
            log_prefix, group_id))
68
        return 0
×
69

70
    def no_action(self, group_id, message):
1✔
71
        return 0
×
72

73
    @staticmethod
1✔
74
    def _parse(message):
1✔
75
        message = re_parser.sub(r'\g<1>', message.decode('utf-8'))
1✔
76
        return ET.fromstring(message)
1✔
77

78

79
class UWGroupDispatch(Dispatch):
1✔
80
    """
81
    Canvas Enrollment Group Event Dispatcher
82
    """
83
    def mine(self, group_id):
1✔
84
        return (
×
85
            Group.objects.get_active_by_group(group_id).exists() or
86
            GroupMemberGroup.objects.get_active_by_group(group_id).exists())
87

88
    @staticmethod
1✔
89
    def _valid_member(login_id):
1✔
90
        try:
×
91
            valid_net_id(login_id)
×
92
            return 1
×
93
        except UserPolicyException:
×
94
            pass
×
95
        return 0
×
96

97
    def update_members(self, group_id, message):
1✔
98
        # body contains list of members to be added or removed
99
        group_id = message.findall('./name')[0].text
×
100
        member_count = 0
×
101

102
        for el in message.findall('./add-members/add-member'):
×
103
            member_count += self._valid_member(el.text)
×
104

105
        for el in message.findall('./delete-members/delete-member'):
×
106
            member_count += self._valid_member(el.text)
×
107

108
        if member_count > 0:
×
109
            for group in Group.objects.get_active_by_group(group_id):
×
110
                group.update_priority(group.PRIORITY_HIGH)
×
111

112
            for mgroup in GroupMemberGroup.objects.get_active_by_group(
×
113
                    group_id):
114
                for group in Group.objects.get_active_by_group(
×
115
                        mgroup.root_group_id):
116
                    group.update_priority(group.PRIORITY_HIGH)
×
117

118
            self._log.info('{} UPDATE membership for {}'.format(
×
119
                log_prefix, group_id))
120

121
        return member_count
×
122

123
    def delete_group(self, group_id, message):
1✔
124
        group_id = message.findall('./name')[0].text
×
125

126
        # mark group as deleted and ready for import
127
        Group.objects.delete_group_not_found(group_id)
×
128

129
        # mark associated root groups for import
130
        for mgroup in GroupMemberGroup.objects.get_active_by_group(group_id):
×
131
            mgroup.deactivate()
×
132

133
            for group in Group.objects.get_active_by_group(
×
134
                    mgroup.root_group_id):
135
                group.update_priority(group.PRIORITY_IMMEDIATE)
×
136

137
        self._log.info('{} DELETE {}'.format(log_prefix, group_id))
×
138

139
        return 1
×
140

141
    def change_subject_name(self, group_id, message):
1✔
142
        # body contains old and new subject names (id)
143
        # normalize 'change-subject-name' event
144
        old_name = message.findall('./subject/old-name')[0].text
×
145
        new_name = message.findall('./subject/new-name')[0].text
×
146

147
        Group.objects.update_group_id(old_name, new_name)
×
148
        GroupMemberGroup.objects.update_group_id(old_name, new_name)
×
149
        GroupMemberGroup.objects.update_root_group_id(old_name, new_name)
×
150

151
        self._log.info('{} UPDATE change-subject-name {} to {}'.format(
×
152
            log_prefix, old_name, new_name))
153

154
        return 1
×
155

156

157
class LoginGroupDispatch(Dispatch):
1✔
158
    def group(self):
1✔
159
        raise NotImplementedError
1✔
160

161
    def mine(self, group_id):
1✔
162
        return group_id == self.group()
1✔
163

164
    def _log_update(self):
1✔
165
        self._log.info('{} UPDATE membership for {}'.format(
×
166
            log_prefix, self.group()))
167

168
    def _add_user(self, net_id, flag_user=False):
1✔
169
        try:
×
170
            user = User.objects.add_user_by_netid(net_id)
×
171
            if flag_user:
×
172
                user.invalid_enrollment_check_required = True
×
173
                user.save()
×
174
                self._log.info(
×
175
                    '{} FLAG {} in {} for invalid enrollment check'.format(
176
                        log_prefix, net_id, self.group()))
177
        except UserPolicyException as ex:
×
178
            self._log.info('{} IGNORE member {}: {}'.format(
×
179
                log_prefix, net_id, ex))
180
        except DataFailureException as ex:
×
181
            self._log.info('{} ERROR adding member {}: {}'.format(
×
182
                log_prefix, net_id, ex))
183

184
    @staticmethod
1✔
185
    def _valid_member(net_id):
1✔
186
        try:
1✔
187
            valid_net_id(net_id)
1✔
188
            return True
×
189
        except UserPolicyException:
1✔
190
            return False
1✔
191

192

193
class AffiliateLoginGroupDispatch(LoginGroupDispatch):
1✔
194
    def group(self):
1✔
195
        return getattr(settings, 'ALLOWED_CANVAS_AFFILIATE_USERS')
1✔
196

197
    def update_members(self, group_id, message):
1✔
198
        member_count = 0
×
199
        for el in message.findall('./add-members/add-member'):
×
200
            if self._valid_member(el.text):
×
201
                user_exists = User.objects.filter(net_id=el.text).exists()
×
202
                self._add_user(el.text, flag_user=user_exists)
×
203
                member_count += 1
×
204

205
        for el in message.findall('./delete-members/delete-member'):
×
206
            if self._valid_member(el.text):
×
207
                self._add_user(el.text, flag_user=True)
×
208
                member_count += 1
×
209

210
        if member_count:
×
211
            self._log_update()
×
212

213
        return member_count
×
214

215

216
class SponsoredLoginGroupDispatch(LoginGroupDispatch):
1✔
217
    def group(self):
1✔
218
        return getattr(settings, 'ALLOWED_CANVAS_SPONSORED_USERS')
1✔
219

220
    def update_members(self, group_id, message):
1✔
221
        member_count = 0
×
222
        for el in message.findall('./add-members/add-member'):
×
223
            if self._valid_member(el.text):
×
224
                self._add_user(el.text, flag_user=True)
×
225
                member_count += 1
×
226

227
        for el in message.findall('./delete-members/delete-member'):
×
228
            if self._valid_member(el.text):
×
229
                self._add_user(el.text, flag_user=True)
×
230
                member_count += 1
×
231

232
        if member_count:
×
233
            self._log_update()
×
234

235
        return member_count
×
236

237

238
class StudentLoginGroupDispatch(LoginGroupDispatch):
1✔
239
    def group(self):
1✔
240
        return getattr(settings, 'ALLOWED_CANVAS_STUDENT_USERS')
1✔
241

242
    def update_members(self, group_id, message):
1✔
243
        member_count = 0
×
244
        for el in message.findall('./add-members/add-member'):
×
245
            if self._valid_member(el.text):
×
246
                user_exists = User.objects.filter(net_id=el.text).exists()
×
247
                self._add_user(el.text, flag_user=user_exists)
×
248
                member_count += 1
×
249

250
        if member_count:
×
251
            self._log_update()
×
252

253
        return member_count
×
254

255

256
class CourseGroupDispatch(Dispatch):
1✔
257
    """
258
    Course Group Dispatcher
259
    """
260
    def mine(self, group):
1✔
261
        course = ('course_' in group and re.match(
1✔
262
            (r'^course_(20[0-9]{2})'
263
             r'([a-z]{3})-([a-z\-]+)'
264
             r'([0-9]{3})([a-z][a-z0-9]?)$'), group))
265
        if course:
1✔
266
            self._course_sis_id = '-'.join([
1✔
267
                course.group(1),
268
                {'win': 'winter', 'spr': 'spring', 'sum': 'summer',
269
                    'aut': 'autumn'}[course.group(2)],
270
                re.sub(r'\-', ' ', course.group(3).upper()),
271
                course.group(4), course.group(5).upper()
272
            ])
273
            return True
1✔
274
        return False
1✔
275

276
    def update_members(self, group, message):
1✔
277
        # body contains list of members to be added or removed
278
        self._log.info('{} IGNORE course group update: {}'.format(
×
279
            log_prefix, self._course_sis_id))
280
        return 0
×
281

282
    def put_group(self, group_id, message):
1✔
283
        self._log.info('{} IGNORE course group put-group: {}'.format(
×
284
            log_prefix, group_id))
285
        return 0
×
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

© 2025 Coveralls, Inc