Revision 0caf68e9

/dev/null
1
[
2
    {
3
        "model": "auth.group",
4
        "pk": 1,
5
        "fields": {
6
            "name": "default"
7
        }
8
    },
9
    {
10
        "model": "auth.group",
11
        "pk": 2,
12
        "fields": {
13
            "name": "academic"
14
        }
15
    },
16
    {
17
        "model": "auth.group",
18
        "pk": 3,
19
        "fields": {
20
            "name": "shibboleth"
21
        }
22
    },
23
    {
24
        "model": "auth.group",
25
        "pk": 4,
26
        "fields": {
27
            "name": "helpdesk"
28
        }
29
    },
30
    {
31
        "model": "auth.group",
32
        "pk": 4,
33
        "fields": {
34
            "name": "faculty"
35
        },
36
        "permissions": ""
37
    },
38
    {
39
        "model": "auth.group",
40
        "pk": 4,
41
        "fields": {
42
            "name": "ugrad"
43
        }
44
    },
45
    {
46
        "model": "auth.group",
47
        "pk": 4,
48
        "fields": {
49
            "name": "grad"
50
        }
51
    },
52
    {
53
        "model": "auth.group",
54
        "pk": 4,
55
        "fields": {
56
            "name": "researcher"
57
        }
58
    },
59
    {
60
        "model": "auth.group",
61
        "pk": 4,
62
        "fields": {
63
            "name": "associate"
64
        }
65
    },
66
    {
67
        "model": "im.GroupKind",
68
        "pk": 1,
69
        "fields": {
70
            "name": "course"
71
        }
72
    },
73
    {
74
        "model": "im.GroupKind",
75
        "pk": 2,
76
        "fields": {
77
            "name": "project"
78
        }
79
    },
80
    {
81
        "model": "im.GroupKind",
82
        "pk": 3,
83
        "fields": {
84
            "name": "laboratory"
85
        }
86
    },
87
    {
88
        "model": "im.GroupKind",
89
        "pk": 4,
90
        "fields": {
91
            "name": "organization"
92
        }
93
    }
94
]
b/snf-astakos-app/astakos/im/management/commands/user_add.py
41 41
from django.core.management.base import BaseCommand, CommandError
42 42
from django.core.validators import validate_email
43 43
from django.core.exceptions import ValidationError
44
from django.contrib.auth.models import Group
45 44

  
46
from astakos.im.models import AstakosUser
45
from astakos.im.models import AstakosUser, AstakosGroup
47 46
from astakos.im.util import reserved_email
48 47

  
49 48
from ._common import add_user_permission
......
121 120
            groupname = options.get('add-group')
122 121
            if groupname is not None:
123 122
                try:
124
                    group = Group.objects.get(name=groupname)
125
                    user.groups.add(group)
123
                    group = AstakosGroup.objects.get(name=groupname)
124
                    user.astakos_groups.add(group)
126 125
                    self.stdout.write('Group: %s added successfully\n' % groupname)
127
                except Group.DoesNotExist, e:
126
                except AstakosGroup.DoesNotExist, e:
128 127
                    self.stdout.write('Group named %s does not exist\n' % groupname)
129 128
            
130 129
            pname = options.get('add-permission')
b/snf-astakos-app/astakos/im/management/commands/user_list.py
85 85
            active = format_bool(user.is_active)
86 86
            admin = format_bool(user.is_superuser)
87 87
            fields = (id, user.email, user.realname, active, admin, user.provider,
88
                      ','.join([g.name for g in user.groups.all()]))
88
                      ','.join([g.name for g in user.astakos_groups.all()]))
89 89
            
90 90
            if options['csv']:
91 91
                line = '|'.join(fields)
b/snf-astakos-app/astakos/im/management/commands/user_update.py
32 32
# or implied, of GRNET S.A.
33 33

  
34 34
from optparse import make_option
35
from datetime import datetime
35 36

  
36 37
from django.core.management.base import BaseCommand, CommandError
37
from django.contrib.auth.models import Group, Permission
38
from django.contrib.auth.models import Permission
38 39
from django.contrib.contenttypes.models import ContentType
39 40
from django.core.exceptions import ValidationError
41
from django.db.utils import IntegrityError
40 42

  
41
from astakos.im.models import AstakosUser
43
from astakos.im.models import AstakosUser, AstakosGroup, Membership
42 44
from ._common import remove_user_permission, add_user_permission
43 45

  
44 46
class Command(BaseCommand):
......
135 137
        groupname = options.get('add-group')
136 138
        if groupname is not None:
137 139
            try:
138
                group = Group.objects.get(name=groupname)
139
                user.groups.add(group)
140
            except Group.DoesNotExist, e:
140
                group = AstakosGroup.objects.get(name=groupname)
141
                m = Membership(person=user, group=group, date_joined=datetime.now())
142
                m.save()
143
            except AstakosGroup.DoesNotExist, e:
141 144
                self.stdout.write("Group named %s does not exist\n" % groupname)
145
            except IntegrityError, e:
146
                self.stdout.write("User is already member of %s\n" % groupname)
142 147
        
143 148
        groupname = options.get('delete-group')
144 149
        if groupname is not None:
145 150
            try:
146
                group = Group.objects.get(name=groupname)
147
                user.groups.remove(group)
148
            except Group.DoesNotExist, e:
151
                group = AstakosGroup.objects.get(name=groupname)
152
                m = Membership.objects.get(person=user, group=group)
153
                m.delete()
154
            except AstakosGroup.DoesNotExist, e:
149 155
                self.stdout.write("Group named %s does not exist\n" % groupname)
156
            except Membership.DoesNotExist, e:
157
                self.stdout.write("User is not a member of %s\n" % groupname)
150 158
        
151 159
        pname = options.get('add-permission')
152 160
        if pname is not None:
b/snf-astakos-app/astakos/im/migrations/0016_populate_group_data.py
1
# encoding: utf-8
2
import datetime
3
from south.db import db
4
from south.v2 import DataMigration
5
from django.db import models
6

  
7
class Migration(DataMigration):
8
    def forwards(self, orm):
9
        
10
        def _create_groupkind(name):
11
            try:
12
                orm.GroupKind(name=name).save()
13
            except:
14
                pass
15
                                        
16
        t = ('default', 'course', 'project', 'laboratory', 'organization')
17
        map(_create_groupkind, t)
18
        
19
        default = orm.GroupKind.objects.get(name='default')
20
        
21
        def _create_astakogroup(name):
22
            try:
23
                orm.AstakosGroup.objects.get(name=name)
24
            except orm.AstakosGroup.DoesNotExist:
25
                try:
26
                    g = orm['auth.Group'].objects.get(name=name)                    
27
                    extended = orm.AstakosGroup(group_ptr_id=g.pk)
28
                    extended.__dict__.update(g.__dict__)
29
                    extended.kind = default
30
                    extended.approval_date = datetime.datetime.now()
31
                    extended.issue_date = datetime.datetime.now()
32
                    extended.moderation_enabled = False
33
                    extended.save()
34
                    map(lambda u:orm.Membership(    group=extended, 
35
                                                    person=orm.AstakosUser.objects.get(id=u.id), 
36
                                                    date_joined=datetime.datetime.now()).save(), 
37
                        g.user_set.all())
38
                except orm['auth.Group'].DoesNotExist:
39
                    orm.AstakosGroup(   name=name, 
40
                                        kind=default, 
41
                                        approval_date=datetime.datetime.now(),
42
                                        issue_date=datetime.datetime.now(),
43
                                        moderation_enabled=False).save()
44
        
45
        # catch integrate 
46
        t = ('default', 'shibboleth', 'helpdesk', 'faculty', 'ugrad', 'grad', 'researcher', 'associate')
47
        map(_create_astakogroup, t)
48
        
49
    def backwards(self, orm):
50
        def _delete_groupkind(name):
51
            try:
52
                orm.GroupKind.objects.get(name=name).delete()
53
            except orm.GroupKind.DoesNotExist:
54
                pass
55
        
56
        def _delete_astakosgroup(name):
57
            try:
58
                orm.AstakosGroup.objects.get(name=name).delete()
59
            except orm.AstakosGroup.DoesNotExist:
60
                pass
61
        
62
        t = ('default', 'shibboleth', 'helpdesk', 'faculty', 'ugrad', 'grad', 'researcher', 'associate')
63
        map(_delete_astakosgroup, t)
64
        
65
        t = ('default', 'course', 'project', 'laboratory', 'organization')
66
        map(_delete_groupkind, t)
67

  
68
    models = {
69
        'auth.group': {
70
            'Meta': {'object_name': 'Group'},
71
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
72
            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
73
            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
74
        },
75
        'auth.permission': {
76
            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
77
            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
78
            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
79
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
80
            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
81
        },
82
        'auth.user': {
83
            'Meta': {'object_name': 'User'},
84
            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
85
            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
86
            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
87
            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
88
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
89
            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
90
            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
91
            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
92
            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
93
            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
94
            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
95
            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
96
            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
97
        },
98
        'contenttypes.contenttype': {
99
            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
100
            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
101
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
102
            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
103
            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
104
        },
105
        'im.additionalmail': {
106
            'Meta': {'object_name': 'AdditionalMail'},
107
            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}),
108
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
109
            'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"})
110
        },
111
        'im.approvalterms': {
112
            'Meta': {'object_name': 'ApprovalTerms'},
113
            'date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 8, 8, 12, 40, 8, 181485)', 'db_index': 'True'}),
114
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
115
            'location': ('django.db.models.fields.CharField', [], {'max_length': '255'})
116
        },
117
        'im.astakosgroup': {
118
            'Meta': {'object_name': 'AstakosGroup', '_ormbases': ['auth.Group']},
119
            'approval_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
120
            'creation_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 8, 8, 12, 40, 8, 175548)'}),
121
            'desc': ('django.db.models.fields.TextField', [], {'null': 'True'}),
122
            'estimated_participants': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}),
123
            'expiration_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
124
            'group_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.Group']", 'unique': 'True', 'primary_key': 'True'}),
125
            'issue_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
126
            'kind': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.GroupKind']"}),
127
            'moderation_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
128
            'policy': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.AstakosGroupQuota']", 'blank': 'True'})
129
        },
130
        'im.astakosgroupquota': {
131
            'Meta': {'unique_together': "(('resource', 'group'),)", 'object_name': 'AstakosGroupQuota'},
132
            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosGroup']", 'blank': 'True'}),
133
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
134
            'limit': ('django.db.models.fields.PositiveIntegerField', [], {}),
135
            'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"})
136
        },
137
        'im.astakosuser': {
138
            'Meta': {'unique_together': "(('provider', 'third_party_identifier'),)", 'object_name': 'AstakosUser', '_ormbases': ['auth.User']},
139
            'activation_sent': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
140
            'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
141
            'astakos_groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.AstakosGroup']", 'symmetrical': 'False', 'through': "orm['im.Membership']", 'blank': 'True'}),
142
            'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
143
            'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
144
            'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
145
            'date_signed_terms': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
146
            'email_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
147
            'has_credits': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
148
            'has_signed_terms': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
149
            'invitations': ('django.db.models.fields.IntegerField', [], {'default': '100'}),
150
            'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
151
            'level': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
152
            'owner': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'owner'", 'null': 'True', 'to': "orm['im.AstakosGroup']"}),
153
            'policy': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.AstakosUserQuota']", 'symmetrical': 'False'}),
154
            'provider': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
155
            'third_party_identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
156
            'updated': ('django.db.models.fields.DateTimeField', [], {}),
157
            'user_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'primary_key': 'True'})
158
        },
159
        'im.astakosuserquota': {
160
            'Meta': {'unique_together': "(('resource', 'user'),)", 'object_name': 'AstakosUserQuota'},
161
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
162
            'limit': ('django.db.models.fields.PositiveIntegerField', [], {}),
163
            'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}),
164
            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"})
165
        },
166
        'im.emailchange': {
167
            'Meta': {'object_name': 'EmailChange'},
168
            'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40', 'db_index': 'True'}),
169
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
170
            'new_email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75'}),
171
            'requested_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 8, 8, 12, 40, 8, 183025)'}),
172
            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emailchange_user'", 'unique': 'True', 'to': "orm['im.AstakosUser']"})
173
        },
174
        'im.groupkind': {
175
            'Meta': {'object_name': 'GroupKind'},
176
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
177
            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'})
178
        },
179
        'im.invitation': {
180
            'Meta': {'object_name': 'Invitation'},
181
            'code': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
182
            'consumed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
183
            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
184
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
185
            'inviter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invitations_sent'", 'null': 'True', 'to': "orm['im.AstakosUser']"}),
186
            'is_consumed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
187
            'realname': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
188
            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'})
189
        },
190
        'im.membership': {
191
            'Meta': {'unique_together': "(('person', 'group'),)", 'object_name': 'Membership'},
192
            'date_joined': ('django.db.models.fields.DateField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
193
            'date_requested': ('django.db.models.fields.DateField', [], {'default': 'datetime.datetime(2012, 8, 8, 12, 40, 8, 179349)', 'blank': 'True'}),
194
            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosGroup']"}),
195
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
196
            'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"})
197
        },
198
        'im.resource': {
199
            'Meta': {'object_name': 'Resource'},
200
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
201
            'meta': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.ResourceMetadata']", 'symmetrical': 'False'}),
202
            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}),
203
            'service': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Service']"})
204
        },
205
        'im.resourcemetadata': {
206
            'Meta': {'object_name': 'ResourceMetadata'},
207
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
208
            'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}),
209
            'value': ('django.db.models.fields.CharField', [], {'max_length': '255'})
210
        },
211
        'im.service': {
212
            'Meta': {'object_name': 'Service'},
213
            'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
214
            'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
215
            'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
216
            'icon': ('django.db.models.fields.FilePathField', [], {'max_length': '100', 'blank': 'True'}),
217
            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
218
            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}),
219
            'url': ('django.db.models.fields.FilePathField', [], {'max_length': '100'})
220
        }
221
    }
222

  
223
    complete_apps = ['im']
b/snf-astakos-app/astakos/im/models.py
42 42
from urlparse import urlparse, urlunparse
43 43
from random import randint
44 44
from collections import defaultdict
45
from south.signals import post_migrate
45 46

  
46 47
from django.db import models, IntegrityError
47 48
from django.contrib.auth.models import User, UserManager, Group
......
51 52
from django.core.mail import send_mail
52 53
from django.db import transaction
53 54
from django.db.models.signals import post_save, post_syncdb
54
from django.db.models import Q
55
from django.db.models import Q, Count
55 56

  
56 57
from astakos.im.settings import DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL, \
57 58
    AUTH_TOKEN_DURATION, BILLING_FIELDS, QUEUE_CONNECTION, SITENAME, \
......
236 237
        super(AstakosUser, self).__init__(*args, **kwargs)
237 238
        self.__has_signed_terms = self.has_signed_terms
238 239
        if self.id:
239
            self.__groupnames = [g.name for g in self.groups.all()]
240
            self.__groupnames = [g.name for g in self.astakos_groups.all()]
240 241
        else:
241 242
            self.is_active = False
242 243
    
......
305 306
        groupname = 'shibboleth' if self.provider == 'shibboleth' else 'default'
306 307
        if groupname not in self.__groupnames:
307 308
            try:
308
                group = Group.objects.get(name = groupname)
309
                self.groups.add(group)
310
            except Group.DoesNotExist, e:
309
                group = AstakosGroup.objects.get(name = groupname)
310
                Membership(group=group, person=self, date_joined=datetime.now()).save()
311
            except AstakosGroup.DoesNotExist, e:
311 312
                logger.exception(e)
312 313
    
313 314
    def renew_token(self):
......
562 563

  
563 564
post_syncdb.connect(superuser_post_syncdb)
564 565

  
566
def set_default_group(sender, **kwargs):
567
    try:
568
        default = AstakosGroup.objects.get(name='default')
569
        orphans = AstakosUser.objects.annotate(num_groups=Count('astakos_groups')).filter(num_groups = 0)
570
        map ( lambda u: Membership(group=default, person=u).save(), orphans )
571
    except AstakosGroup.DoesNotExist:
572
        pass
573
    
574
post_migrate.connect(set_default_group)
575

  
565 576
def superuser_post_save(sender, instance, **kwargs):
566 577
    if instance.is_superuser:
567 578
        create_astakos_user(instance)

Also available in: Unified diff