work in progress - passes simple tests
[plcapi.git] / PLC / Sites.py
1 from types import StringTypes
2 import string
3
4 from PLC.Faults import *
5 from PLC.Parameter import Parameter, Mixed
6 from PLC.Filter import Filter
7 from PLC.Debug import profile
8 from PLC.Table import Row, Table
9 from PLC.Slices import Slice, Slices
10 from PLC.PCUs import PCU, PCUs
11 from PLC.Nodes import Node, Nodes
12 from PLC.Addresses import Address, Addresses
13 from PLC.Persons import Person, Persons
14
15 class Site(Row):
16     """
17     Representation of a row in the sites table. To use, optionally
18     instantiate with a dict of values. Update as you would a
19     dict. Commit to the database with sync().
20     """
21
22     table_name = 'sites'
23     primary_key = 'site_id'
24     join_tables = ['person_site', 'site_address', 'peer_site']
25     fields = {
26         'site_id': Parameter(int, "Site identifier"),
27         'name': Parameter(str, "Full site name", max = 254),
28         'abbreviated_name': Parameter(str, "Abbreviated site name", max = 50),
29         'login_base': Parameter(str, "Site slice prefix", max = 20),
30         'is_public': Parameter(bool, "Publicly viewable site"),
31         'enabled': Parameter(bool, "Has been enabled"),
32         'latitude': Parameter(float, "Decimal latitude of the site", min = -90.0, max = 90.0, nullok = True),
33         'longitude': Parameter(float, "Decimal longitude of the site", min = -180.0, max = 180.0, nullok = True),
34         'url': Parameter(str, "URL of a page that describes the site", max = 254, nullok = True),
35         'date_created': Parameter(int, "Date and time when site entry was created, in seconds since UNIX epoch", ro = True),
36         'last_updated': Parameter(int, "Date and time when site entry was last updated, in seconds since UNIX epoch", ro = True),
37         'max_slices': Parameter(int, "Maximum number of slices that the site is able to create"),
38         'max_slivers': Parameter(int, "Maximum number of slivers that the site is able to create"),
39         'person_ids': Parameter([int], "List of account identifiers"),
40         'slice_ids': Parameter([int], "List of slice identifiers"),
41         'address_ids': Parameter([int], "List of address identifiers"),
42         'pcu_ids': Parameter([int], "List of PCU identifiers"),
43         'node_ids': Parameter([int], "List of site node identifiers"),
44         'peer_id': Parameter(int, "Peer to which this site belongs", nullok = True),
45         'peer_site_id': Parameter(int, "Foreign site identifier at peer", nullok = True),
46         'ext_consortium_id': Parameter(int, "external consortium id", nullok = True)
47         }
48     related_fields = {
49         'persons': [Mixed(Parameter(int, "Person identifier"),
50                           Parameter(str, "Email address"))],
51         'addresses': [Mixed(Parameter(int, "Address identifer"),
52                             Filter(Address.fields))]
53         }
54
55     def validate_name(self, name):
56         if not len(name):
57             raise PLCInvalidArgument, "Name must be specified"
58
59         return name
60
61     validate_abbreviated_name = validate_name
62
63     def validate_login_base(self, login_base):
64         if not len(login_base):
65             raise PLCInvalidArgument, "Login base must be specified"
66
67         if not set(login_base).issubset(string.lowercase + string.digits):
68             raise PLCInvalidArgument, "Login base must consist only of lowercase ASCII letters or numbers"
69
70         conflicts = Sites(self.api, [login_base])
71         for site in conflicts:
72             if 'site_id' not in self or self['site_id'] != site['site_id']:
73                 raise PLCInvalidArgument, "login_base already in use"
74
75         return login_base
76
77     def validate_latitude(self, latitude):
78         if not self.has_key('longitude') or \
79            self['longitude'] is None:
80             raise PLCInvalidArgument, "Longitude must also be specified"
81
82         return latitude
83
84     def validate_longitude(self, longitude):
85         if not self.has_key('latitude') or \
86            self['latitude'] is None:
87             raise PLCInvalidArgument, "Latitude must also be specified"
88
89         return longitude
90
91     validate_date_created = Row.validate_timestamp
92     validate_last_updated = Row.validate_timestamp
93
94     add_person = Row.add_object(Person, 'person_site')
95     remove_person = Row.remove_object(Person, 'person_site')
96
97     add_address = Row.add_object(Address, 'site_address')
98     remove_address = Row.remove_object(Address, 'site_address')
99
100     def update_last_updated(self, commit = True):
101         """
102         Update last_updated field with current time
103         """
104
105         assert 'site_id' in self
106         assert self.table_name
107
108         self.api.db.do("UPDATE %s SET last_updated = CURRENT_TIMESTAMP " % (self.table_name) + \
109                        " where site_id = %d" % (self['site_id']) )
110         self.sync(commit)    
111
112
113     def associate_persons(self, auth, field, value):
114         """
115         Adds persons found in value list to this site (using AddPersonToSite).
116         Deletes persons not found in value list from this site (using DeletePersonFromSite).
117         """
118         
119         assert 'person_ids' in self
120         assert 'site_id' in self
121         assert isinstance(value, list)
122
123         (person_ids, emails) = self.separate_types(value)[0:2]
124
125         # Translate emails into person_ids
126         if emails:
127             persons = Persons(self.api, emails, ['person_id']).dict('person_id')
128             person_ids += persons.keys()
129
130         # Add new ids, remove stale ids
131         if self['person_ids'] != person_ids:
132             from PLC.Methods.AddPersonToSite import AddPersonToSite
133             from PLC.Methods.DeletePersonFromSite import DeletePersonFromSite
134             new_persons = set(person_ids).difference(self['person_ids'])
135             stale_persons = set(self['person_ids']).difference(person_ids)
136          
137             for new_person in new_persons:
138                 AddPersonToSite.__call__(AddPersonToSite(self.api), auth, new_person, self['site_id'])
139             for stale_person in stale_persons:
140                 DeletePersonFromSite.__call__(DeletePersonFromSite(self.api), auth, stale_person, self['site_id'])              
141
142     def associate_addresses(self, auth, field, value):
143         """
144         Deletes addresses_ids not found in value list (using DeleteAddress).  
145         Adds address if slice_fields w/o address_id found in value list (using AddSiteAddress).
146         Update address if slice_fields w/ address_id found in value list (using UpdateAddress).
147         """
148         
149         assert 'address_ids' in self
150         assert 'site_id' in self
151         assert isinstance(value, list)
152
153         (address_ids, blank, addresses) = self.separate_types(value)
154
155         for address in addresses:
156             if 'address_id' in address:
157                 address_ids.append(address['address_id'])               
158
159         # Add new ids, remove stale ids
160         if self['address_ids'] != address_ids:
161             from PLC.Methods.DeleteAddress import DeleteAddress
162             stale_addresses = set(self['address_ids']).difference(address_ids)
163
164             for stale_address in stale_addresses:
165                 DeleteAddress.__call__(DeleteAddress(self.api), auth, stale_address)    
166         
167         if addresses:
168             from PLC.Methods.AddSiteAddress import AddSiteAddress
169             from PLC.Methods.UpdateAddress import UpdateAddress
170                 
171             updated_addresses = filter(lambda address: 'address_id' in address, addresses)
172             added_addresses = filter(lambda address: 'address_id' not in address, addresses)
173                 
174             for address in added_addresses:
175                 AddSiteAddress.__call__(AddSiteAddress(self.api), auth, self['site_id'], address)       
176             for address in updated_addresses:
177                 address_id = address.pop('address_id')
178                 UpdateAddress.__call__(UpdateAddress(self.api), auth, address_id, address)
179
180     def delete(self, commit = True):
181         """
182         Delete existing site.
183         """
184
185         assert 'site_id' in self
186
187         # Delete accounts of all people at the site who are not
188         # members of at least one other non-deleted site.
189         persons = Persons(self.api, self['person_ids'])
190         for person in persons:
191             delete = True
192
193             person_sites = Sites(self.api, person['site_ids'])
194             for person_site in person_sites:
195                 if person_site['site_id'] != self['site_id']:
196                     delete = False
197                     break
198
199             if delete:
200                 person.delete(commit = False)
201
202         # Delete all site addresses
203         addresses = Addresses(self.api, self['address_ids'])
204         for address in addresses:
205             address.delete(commit = False)
206
207         # Delete all site slices
208         slices = Slices(self.api, self['slice_ids'])
209         for slice in slices:
210             slice.delete(commit = False)
211
212         # Delete all site PCUs
213         pcus = PCUs(self.api, self['pcu_ids'])
214         for pcu in pcus:
215             pcu.delete(commit = False)
216
217         # Delete all site nodes
218         nodes = Nodes(self.api, self['node_ids'])
219         for node in nodes:
220             node.delete(commit = False)
221
222         # Clean up miscellaneous join tables
223         for table in self.join_tables:
224             self.api.db.do("DELETE FROM %s WHERE site_id = %d" % \
225                            (table, self['site_id']))
226
227         # Mark as deleted
228         self['deleted'] = True
229         self.sync(commit)
230
231 class Sites(Table):
232     """
233     Representation of row(s) from the sites table in the
234     database.
235     """
236
237     def __init__(self, api, site_filter = None, columns = None):
238         Table.__init__(self, api, Site, columns)
239
240         sql = "SELECT %s FROM view_sites WHERE deleted IS False" % \
241               ", ".join(self.columns)
242
243         if site_filter is not None:
244             if isinstance(site_filter, (list, tuple, set)):
245                 # Separate the list into integers and strings
246                 ints = filter(lambda x: isinstance(x, (int, long)), site_filter)
247                 strs = filter(lambda x: isinstance(x, StringTypes), site_filter)
248                 site_filter = Filter(Site.fields, {'site_id': ints, 'login_base': strs})
249                 sql += " AND (%s) %s" % site_filter.sql(api, "OR")
250             elif isinstance(site_filter, dict):
251                 site_filter = Filter(Site.fields, site_filter)
252                 sql += " AND (%s) %s" % site_filter.sql(api, "AND")
253             elif isinstance (site_filter, StringTypes):
254                 site_filter = Filter(Site.fields, {'login_base':[site_filter]})
255                 sql += " AND (%s) %s" % site_filter.sql(api, "AND")
256             elif isinstance (site_filter, int):
257                 site_filter = Filter(Site.fields, {'site_id':[site_filter]})
258                 sql += " AND (%s) %s" % site_filter.sql(api, "AND")
259             else:
260                 raise PLCInvalidArgument, "Wrong site filter %r"%site_filter
261
262         self.selectall(sql)