Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
itminedu
synnefo
Commits
3ffacf08
Commit
3ffacf08
authored
Dec 13, 2012
by
Georgios D. Tsoukalas
Browse files
wip sync with per membership state
parent
2c3779e5
Changes
1
Hide whitespace changes
Inline
Side-by-side
snf-astakos-app/astakos/im/models.py
View file @
3ffacf08
...
@@ -41,7 +41,7 @@ from base64 import b64encode
...
@@ -41,7 +41,7 @@ from base64 import b64encode
from
urlparse
import
urlparse
from
urlparse
import
urlparse
from
urllib
import
quote
from
urllib
import
quote
from
random
import
randint
from
random
import
randint
from
collections
import
defaultdict
from
collections
import
defaultdict
,
namedtuple
from
django.db
import
models
,
IntegrityError
from
django.db
import
models
,
IntegrityError
from
django.contrib.auth.models
import
User
,
UserManager
,
Group
,
Permission
from
django.contrib.auth.models
import
User
,
UserManager
,
Group
,
Permission
...
@@ -1101,12 +1101,82 @@ def get_closed_leave():
...
@@ -1101,12 +1101,82 @@ def get_closed_leave():
_closed_leave
=
closed
_closed_leave
=
closed
return
closeds
return
closeds
### PROJECTS ###
################
class
SyncedModel
(
models
.
Model
):
new_state
=
models
.
BigIntegerField
()
synced_state
=
models
.
BigIntegerField
()
STATUS_SYNCED
=
0
STATUS_PENDING
=
1
sync_status
=
models
.
IntegerField
(
db_index
=
True
)
class
Meta
:
abstract
=
True
class
NotSynced
(
Exception
):
pass
def
sync_init_state
(
self
,
state
):
self
.
synced_state
=
state
self
.
new_state
=
state
self
.
sync_status
=
self
.
STATUS_SYNCED
def
sync_get_status
(
self
):
return
self
.
sync_status
def
sync_set_status
(
self
):
if
self
.
new_state
!=
self
.
synced_state
:
self
.
sync_status
=
self
.
STATUS_PENDING
else
:
self
.
sync_status
=
self
.
STATUS_SYNCED
def
sync_set_synced
(
self
):
self
.
synced_state
=
self
.
new_state
self
.
sync_status
=
self
.
STATUS_SYNCED
def
sync_get_synced_state
(
self
):
return
self
.
synced_state
def
sync_set_new_state
(
self
,
new_state
):
self
.
new_state
=
new_state
self
.
sync_set_status
()
def
sync_get_new_state
(
self
):
return
self
.
new_state
def
sync_set_synced_state
(
self
,
synced_state
):
self
.
synced_state
=
synced_state
self
.
sync_set_status
()
def
sync_get_pending_objects
(
self
):
return
self
.
objects
.
filter
(
sync_status
=
self
.
STATUS_PENDING
)
def
sync_get_synced_objects
(
self
):
return
self
.
objects
.
filter
(
sync_status
=
self
.
STATUS_SYNCED
)
def
sync_verify_get_synced_state
(
self
):
status
=
self
.
sync_get_status
()
state
=
self
.
sync_get_synced_state
()
verified
=
(
status
==
self
.
STATUS_SYNCED
)
return
state
,
verified
class
ProjectResourceGrant
(
models
.
Model
):
class
ProjectResourceGrant
(
models
.
Model
):
objects
=
ExtendedManager
()
member_limit
=
models
.
BigIntegerField
(
null
=
True
)
project_limit
=
models
.
BigIntegerField
(
null
=
True
)
resource
=
models
.
ForeignKey
(
Resource
)
resource
=
models
.
ForeignKey
(
Resource
)
project_application
=
models
.
ForeignKey
(
ProjectApplication
,
blank
=
True
)
project_application
=
models
.
ForeignKey
(
ProjectApplication
,
blank
=
True
)
project_capacity
=
models
.
BigIntegerField
(
null
=
True
)
project_import_limit
=
models
.
BigIntegerField
(
null
=
True
)
project_export_limit
=
models
.
BigIntegerField
(
null
=
True
)
member_capacity
=
models
.
BigIntegerField
(
null
=
True
)
member_import_limit
=
models
.
BigIntegerField
(
null
=
True
)
member_export_limit
=
models
.
BigIntegerField
(
null
=
True
)
objects
=
ExtendedManager
()
class
Meta
:
class
Meta
:
unique_together
=
(
"resource"
,
"project_application"
)
unique_together
=
(
"resource"
,
"project_application"
)
...
@@ -1114,7 +1184,7 @@ class ProjectResourceGrant(models.Model):
...
@@ -1114,7 +1184,7 @@ class ProjectResourceGrant(models.Model):
class
ProjectApplication
(
models
.
Model
):
class
ProjectApplication
(
models
.
Model
):
states_list
=
[
PENDING
,
APPROVED
,
REPLACED
,
UNKNOWN
]
states_list
=
[
PENDING
,
APPROVED
,
REPLACED
,
UNKNOWN
]
states
=
dict
((
k
,
v
)
for
k
,
v
in
enumerate
(
states_list
))
states
=
dict
((
k
,
v
)
for
v
,
k
in
enumerate
(
states_list
))
applicant
=
models
.
ForeignKey
(
applicant
=
models
.
ForeignKey
(
AstakosUser
,
AstakosUser
,
...
@@ -1241,8 +1311,8 @@ class ProjectApplication(models.Model):
...
@@ -1241,8 +1311,8 @@ class ProjectApplication(models.Model):
new_project_name
=
self
.
definition
.
name
new_project_name
=
self
.
definition
.
name
if
self
.
state
!=
PENDING
:
if
self
.
state
!=
PENDING
:
m
=
_
(
"cannot approve: project '%s' in state '%s'"
m
=
_
(
"cannot approve: project '%s' in state '%s'"
)
%
(
%
(
new_project_name
,
self
.
state
)
)
new_project_name
,
self
.
state
)
raise
PermissionDenied
(
m
)
# invalid argument
raise
PermissionDenied
(
m
)
# invalid argument
now
=
datetime
.
now
()
now
=
datetime
.
now
()
...
@@ -1252,8 +1322,8 @@ class ProjectApplication(models.Model):
...
@@ -1252,8 +1322,8 @@ class ProjectApplication(models.Model):
conflicting_project
=
Project
.
objects
.
get
(
name
=
new_project_name
)
conflicting_project
=
Project
.
objects
.
get
(
name
=
new_project_name
)
if
conflicting_project
.
is_alive
:
if
conflicting_project
.
is_alive
:
m
=
_
(
"cannot approve: project with name '%s' "
m
=
_
(
"cannot approve: project with name '%s' "
"already exists (serial: %s)"
"already exists (serial: %s)"
)
%
(
%
(
new_project_name
,
conflicting_project
.
id
)
)
new_project_name
,
conflicting_project
.
id
)
raise
PermissionDenied
(
m
)
# invalid argument
raise
PermissionDenied
(
m
)
# invalid argument
except
Project
.
DoesNotExist
:
except
Project
.
DoesNotExist
:
pass
pass
...
@@ -1292,7 +1362,7 @@ class ProjectApplication(models.Model):
...
@@ -1292,7 +1362,7 @@ class ProjectApplication(models.Model):
logger
.
error
(
e
.
messages
)
logger
.
error
(
e
.
messages
)
class
Project
(
models
.
Model
):
class
Project
(
Synced
Model
):
application
=
models
.
OneToOneField
(
application
=
models
.
OneToOneField
(
ProjectApplication
,
ProjectApplication
,
related_name
=
'project'
,
related_name
=
'project'
,
...
@@ -1306,9 +1376,6 @@ class Project(models.Model):
...
@@ -1306,9 +1376,6 @@ class Project(models.Model):
AstakosUser
,
AstakosUser
,
through
=
'ProjectMembership'
)
through
=
'ProjectMembership'
)
current_membership_serial
=
models
.
BigIntegerField
()
synced_membership_serial
=
models
.
BigIntegerField
()
termination_start_date
=
models
.
DateTimeField
(
null
=
True
)
termination_start_date
=
models
.
DateTimeField
(
null
=
True
)
termination_date
=
models
.
DateTimeField
(
null
=
True
)
termination_date
=
models
.
DateTimeField
(
null
=
True
)
...
@@ -1379,16 +1446,10 @@ class Project(models.Model):
...
@@ -1379,16 +1446,10 @@ class Project(models.Model):
return
True
return
True
return
False
return
False
@
property
def
is_synchronized
(
self
):
return
self
.
last_application_approved
==
self
.
application
and
\
not
self
.
membership_dirty
and
\
(
not
self
.
termination_start_date
or
termination_date
)
@
property
@
property
def
approved_members
(
self
):
def
approved_members
(
self
):
return
[
m
.
person
for
m
in
self
.
projectmembership_set
.
filter
(
~
Q
(
acceptance_date
=
None
))]
return
[
m
.
person
for
m
in
self
.
projectmembership_set
.
filter
(
~
Q
(
acceptance_date
=
None
))]
def
sync
(
self
,
specific_members
=
()):
def
sync
(
self
,
specific_members
=
()):
if
self
.
is_synchronized
:
if
self
.
is_synchronized
:
return
return
...
@@ -1504,7 +1565,48 @@ class Project(models.Model):
...
@@ -1504,7 +1565,48 @@ class Project(models.Model):
logger
.
error
(
e
.
messages
)
logger
.
error
(
e
.
messages
)
class
ProjectMembership
(
models
.
Model
):
QuotaLimits
=
namedtuple
(
'QuotaLimits'
,
(
'holder'
,
'capacity'
,
'import_limit'
,
'export_limit'
))
class
ExclusiveOrRaise
(
object
):
"""Context Manager to exclusively execute a critical code section.
The exclusion must be global.
(IPC semaphores will not protect across OS,
DB locks will if it's the same DB)
"""
class
Busy
(
Exception
):
pass
def
__init__
(
self
,
locked
=
False
):
init
=
0
if
locked
else
1
from
multiprocess
import
Semaphore
self
.
_sema
=
Semaphore
(
init
)
def
enter
(
self
):
acquired
=
self
.
_sema
.
acquire
(
False
)
if
not
acquired
:
raise
self
.
Busy
()
def
leave
(
self
):
self
.
_sema
.
release
()
def
__enter__
(
self
):
self
.
enter
()
return
self
def
__exit__
(
self
,
exc_type
,
exc_value
,
exc_traceback
):
self
.
leave
()
exclusive_or_raise
=
ExclusiveOrRaise
(
locked
=
False
)
class
ProjectMembership
(
SyncedModel
):
person
=
models
.
ForeignKey
(
AstakosUser
)
person
=
models
.
ForeignKey
(
AstakosUser
)
project
=
models
.
ForeignKey
(
Project
)
project
=
models
.
ForeignKey
(
Project
)
request_date
=
models
.
DateField
(
default
=
datetime
.
now
())
request_date
=
models
.
DateField
(
default
=
datetime
.
now
())
...
@@ -1512,9 +1614,24 @@ class ProjectMembership(models.Model):
...
@@ -1512,9 +1614,24 @@ class ProjectMembership(models.Model):
acceptance_date
=
models
.
DateField
(
null
=
True
,
db_index
=
True
)
acceptance_date
=
models
.
DateField
(
null
=
True
,
db_index
=
True
)
leave_request_date
=
models
.
DateField
(
null
=
True
)
leave_request_date
=
models
.
DateField
(
null
=
True
)
REQUESTED
=
0
ACCEPTED
=
1
REMOVED
=
2
REJECTED
=
3
# never seen, because .delete()
class
Meta
:
class
Meta
:
unique_together
=
(
"person"
,
"project"
)
unique_together
=
(
"person"
,
"project"
)
def
__str__
(
self
):
return
_
(
"<'%s' membership in project '%s'>"
)
%
(
self
.
person
.
username
,
self
.
project
.
application
)
__repr__
=
__str__
def
__init__
(
self
,
*
args
,
**
kwargs
):
self
.
sync_init_state
(
self
.
REQUEST
)
super
(
ProjectMembership
,
self
).
__init__
(
*
args
,
**
kwargs
)
def
_set_history_item
(
self
,
reason
,
date
=
None
):
def
_set_history_item
(
self
,
reason
,
date
=
None
):
if
isinstance
(
reason
,
basestring
):
if
isinstance
(
reason
,
basestring
):
reason
=
ProjectMembershipHistory
.
reasons
.
get
(
reason
,
-
1
)
reason
=
ProjectMembershipHistory
.
reasons
.
get
(
reason
,
-
1
)
...
@@ -1529,142 +1646,100 @@ class ProjectMembership(models.Model):
...
@@ -1529,142 +1646,100 @@ class ProjectMembership(models.Model):
serial
=
history_item
.
id
serial
=
history_item
.
id
def
accept
(
self
):
def
accept
(
self
):
if
not
self
.
acceptance_d
ate
:
state
,
verified
=
self
.
sync_verify_get_synced_st
ate
()
now
=
datetime
.
now
()
if
not
verified
:
self
.
acceptance_date
=
now
new_state
=
self
.
sync_get_new_state
()
serial
=
self
.
_set_history_item
(
reason
=
'ACCEPT'
,
date
=
now
)
m
=
_
(
"%s: cannot accept: not synched (%s -> %s)"
)
%
(
self
.
project
.
current_membership_serial
=
serial
self
,
state
,
new_state
)
self
.
save
(
)
raise
self
.
NotSynced
(
m
)
def
remove
(
self
):
if
state
!=
self
.
REQUESTED
:
serial
=
self
.
_set_history_item
(
reason
=
'REMOVE'
)
m
=
_
(
"%s: attempt to accept in state '%s'"
)
%
(
self
,
state
)
self
.
project
.
current_membership_serial
=
serial
raise
AssertionError
(
m
)
self
.
delete
()
def
reject
(
self
):
now
=
datetime
.
now
()
self
.
_set_history_item
(
reason
=
'REJECT'
)
self
.
acceptance_date
=
now
self
.
delete
()
self
.
_set_history_item
(
reason
=
'ACCEPT'
,
date
=
now
)
self
.
sync_set_new_state
(
self
.
ACCEPTED
)
self
.
save
()
def
remove
(
self
):
state
,
verified
=
self
.
sync_verify_get_synced_state
()
if
not
verified
:
new_state
=
self
.
sync_get_new_state
()
m
=
_
(
"%s: cannot remove: not synched (%s -> %s)"
)
%
(
self
,
state
,
new_state
)
raise
self
.
NotSynced
(
m
)
### Views to be moved to views ###
if
state
!=
self
.
ACCEPTED
:
m
=
_
(
"%s: attempt to remove in state '%s'"
)
%
(
self
,
state
)
raise
AssertionError
(
m
)
def
accept_view
(
self
,
delete_on_failure
=
False
,
request_user
=
None
):
serial
=
self
.
_set_history_item
(
reason
=
'REMOVE'
)
"""
self
.
sync_set_new_state
(
self
.
REMOVED
)
Raises:
django.exception.PermissionDenied
"""
try
:
if
request_user
and
\
(
not
self
.
project
.
current_application
.
owner
==
request_user
and
\
not
request_user
.
is_superuser
):
raise
PermissionDenied
(
_
(
astakos_messages
.
NOT_ALLOWED
))
if
not
self
.
project
.
is_alive
:
raise
PermissionDenied
(
_
(
astakos_messages
.
NOT_ALIVE_PROJECT
)
%
self
.
project
.
__dict__
)
if
len
(
self
.
project
.
approved_members
)
+
1
>
self
.
project
.
definition
.
limit_on_members_number
:
raise
PermissionDenied
(
_
(
astakos_messages
.
MEMBER_NUMBER_LIMIT_REACHED
))
except
PermissionDenied
,
e
:
if
delete_on_failure
:
self
.
delete
()
raise
if
self
.
acceptance_date
:
return
self
.
acceptance_date
=
datetime
.
now
()
self
.
save
()
self
.
save
()
self
.
sync
()
try
:
def
reject
(
self
):
notification
=
build_notification
(
state
,
verified
=
self
.
sync_verify_get_synced_state
()
settings
.
SERVER_EMAIL
,
if
not
verified
:
[
self
.
person
.
email
],
new_state
=
self
.
sync_get_new_state
()
_
(
PROJECT_MEMBERSHIP_CHANGE_SUBJECT
)
%
self
.
project
.
definition
.
__dict__
,
m
=
_
(
"%s: cannot reject: not synched (%s -> %s)"
)
%
(
template
=
'im/projects/project_membership_change_notification.txt'
,
self
,
state
,
new_state
))
dictionary
=
{
'object'
:
self
.
project
.
current_application
,
'action'
:
'accepted'
}
raise
self
.
NotSynced
(
m
)
).
send
()
except
NotificationError
,
e
:
if
state
!=
self
.
REQUESTED
:
logger
.
error
(
e
.
messages
)
m
=
_
(
"%s: attempt to remove in state '%s'"
)
%
(
self
,
state
)
raise
AssertionError
(
m
)
def
reject_view
(
self
,
request_user
=
None
):
"""
# rejected requests don't need sync,
Raises:
# because they were never effected
django.exception.PermissionDenied
self
.
_set_history_item
(
reason
=
'REJECT'
)
"""
if
request_user
and
\
(
not
self
.
project
.
current_application
.
owner
==
request_user
and
\
not
request_user
.
is_superuser
):
raise
PermissionDenied
(
_
(
astakos_messages
.
NOT_ALLOWED
))
if
not
self
.
project
.
is_alive
:
raise
PermissionDenied
(
_
(
astakos_messages
.
NOT_ALIVE_PROJECT
)
%
project
.
__dict__
)
history_item
=
ProjectMembershipHistory
(
person
=
self
.
person
,
project
=
self
.
project
,
request_date
=
self
.
request_date
,
rejection_date
=
datetime
.
now
()
)
self
.
delete
()
self
.
delete
()
history_item
.
save
()
try
:
notification
=
build_notification
(
settings
.
SERVER_EMAIL
,
[
self
.
person
.
email
],
_
(
PROJECT_MEMBERSHIP_CHANGE_SUBJECT
)
%
self
.
project
.
definition
.
__dict__
,
template
=
'im/projects/project_membership_change_notification.txt'
,
dictionary
=
{
'object'
:
self
.
project
.
current_application
,
'action'
:
'rejected'
}
).
send
()
except
NotificationError
,
e
:
logger
.
error
(
e
.
messages
)
def
remove_view
(
self
,
request_user
=
None
):
def
get_quotas
(
self
,
limits_list
=
None
,
factor
=
1
):
"""
holder
=
self
.
person
.
username
Raises:
if
limits_list
is
None
:
django.exception.PermissionDenied
limits_list
=
[]
"""
append
=
limits_list
.
append
if
request_user
and
\
all_grants
=
self
.
project
.
application
.
resource_grants
.
all
()
(
not
self
.
project
.
current_application
.
owner
==
request_user
and
\
for
grant
in
all_grants
:
not
request_user
.
is_superuser
):
append
(
QuotaLimits
(
holder
=
holder
,
raise
PermissionDenied
(
_
(
astakos_messages
.
NOT_ALLOWED
))
resource
=
grant
.
resource
.
name
,
if
not
self
.
project
.
is_alive
:
capacity
=
factor
*
grant
.
member_capacity
,
raise
PermissionDenied
(
_
(
astakos_messages
.
NOT_ALIVE_PROJECT
)
%
self
.
project
.
__dict__
)
import_limit
=
factor
*
grant
.
member_import_limit
,
history_item
=
ProjectMembershipHistory
(
export_limit
=
factor
*
grant
.
member_export_limit
))
id
=
self
.
id
,
return
limits_list
person
=
self
.
person
,
project
=
self
.
project
,
def
do_sync
(
self
):
request_date
=
self
.
request_date
,
state
=
self
.
sync_get_synced_state
()
removal_date
=
datetime
.
now
()
new_state
=
self
.
sync_get_new_state
()
)
self
.
delete
()
if
state
==
self
.
REQUESTED
and
new_state
==
self
.
ACCEPTED
:
history_item
.
save
()
factor
=
1
self
.
sync
()
elif
state
==
self
.
ACCEPTED
and
new_state
==
self
.
REMOVED
:
factor
=
-
1
else
:
m
=
_
(
"%s: sync: called on invalid state ('%s' -> '%s')"
)
%
(
self
,
state
,
new_state
)
raise
AssertionError
(
m
)
quotas
=
self
.
get_quotas
(
factor
=
factor
)
try
:
try
:
notification
=
build_notification
(
failure
=
add_quotas
(
quotas
)
settings
.
SERVER_EMAIL
,
if
failure
:
[
self
.
person
.
email
],
m
=
"%s: sync: add_quotas failed"
%
(
self
,)
_
(
PROJECT_MEMBERSHIP_CHANGE_SUBJECT
)
%
self
.
project
.
definition
.
__dict__
,
raise
RuntimeError
(
m
)
template
=
'im/projects/project_membership_change_notification.txt'
,
except
Exception
:
dictionary
=
{
'object'
:
self
.
project
.
current_application
,
'action'
:
'removed'
}
raise
).
send
()
except
NotificationError
,
e
:
logger
.
error
(
e
.
messages
)
def
leave_view
(
self
):
leave_policy
=
self
.
project
.
current_application
.
definition
.
member_leave_policy
if
leave_policy
==
get_auto_accept_leave
():
self
.
remove
()
else
:
else
:
self
.
leave_request_date
=
datetime
.
now
()
self
.
sync_set_synced
()
self
.
save
()
if
new_state
==
self
.
REMOVED
:
self
.
delete
()
def
sync
(
self
):
def
sync
(
self
):
# set membership_dirty flag
with
exclusive_or_raise
:
self
.
project
.
membership_dirty
=
True
self
.
do_sync
()
self
.
project
.
save
()
rejected
=
self
.
project
.
sync
(
specific_members
=
[
self
.
person
])
if
not
rejected
:
# if syncing was successful unset membership_dirty flag
self
.
membership_dirty
=
False
self
.
project
.
save
()
class
ProjectMembershipHistory
(
models
.
Model
):
class
ProjectMembershipHistory
(
models
.
Model
):
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment