- support new schema
[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
6 from PLC.Debug import profile
7 from PLC.Table import Row, Table
8 from PLC.Slices import Slice, Slices
9 from PLC.PCUs import PCU, PCUs
10 from PLC.Nodes import Node, Nodes
11 from PLC.NodeGroups import NodeGroup, NodeGroups
12 import PLC.Persons
13
14 class Site(Row):
15     """
16     Representation of a row in the sites table. To use, optionally
17     instantiate with a dict of values. Update as you would a
18     dict. Commit to the database with sync().
19     """
20
21     fields = {
22         'site_id': Parameter(int, "Site identifier"),
23         'name': Parameter(str, "Full site name", max = 254),
24         'abbreviated_name': Parameter(str, "Abbreviated site name", max = 50),
25         'login_base': Parameter(str, "Site slice prefix", max = 20),
26         'is_public': Parameter(bool, "Publicly viewable site"),
27         'latitude': Parameter(float, "Decimal latitude of the site", min = -90.0, max = 90.0),
28         'longitude': Parameter(float, "Decimal longitude of the site", min = -180.0, max = 180.0),
29         'url': Parameter(str, "URL of a page that describes the site", max = 254),
30         'date_created': Parameter(str, "Date and time when site entry was created"),        
31         'last_updated': Parameter(str, "Date and time when site entry was last updated"),        
32         'deleted': Parameter(bool, "Has been deleted"),
33         'max_slices': Parameter(int, "Maximum number of slices that the site is able to create"),
34         'person_ids': Parameter([int], "List of account identifiers"),
35         # 'slice_ids': Parameter([int], "List of slice identifiers"),
36         # 'pcu_ids': Parameter([int], "List of PCU identifiers"),
37         'node_ids': Parameter([int], "List of site node identifiers"),
38         }
39
40     def __init__(self, api, fields):
41         Row.__init__(self, fields)
42         self.api = api
43
44     def validate_login_base(self, login_base):
45         if not set(login_base).issubset(string.ascii_letters):
46             raise PLCInvalidArgument, "Login base must consist only of ASCII letters"
47
48         login_base = login_base.lower()
49         conflicts = Sites(self.api, [login_base])
50         for site_id, site in conflicts.iteritems():
51             if not site['deleted'] and ('site_id' not in self or self['site_id'] != site_id):
52                 raise PLCInvalidArgument, "login_base already in use"
53
54         return login_base
55
56     def validate_latitude(self, latitude):
57         if not self.has_key('longitude') or \
58            self['longitude'] is None:
59             raise PLCInvalidArgument, "Longitude must also be specified"
60
61         return latitude
62
63     def validate_longitude(self, longitude):
64         if not self.has_key('latitude') or \
65            self['latitude'] is None:
66             raise PLCInvalidArgument, "Latitude must also be specified"
67
68         return longitude
69
70     def add_person(self, person, commit = True):
71         """
72         Add person to existing site.
73         """
74
75         assert 'site_id' in self
76         assert isinstance(person, PLC.Persons.Person)
77         assert 'person_id' in person
78
79         site_id = self['site_id']
80         person_id = person['person_id']
81         self.api.db.do("INSERT INTO person_site (person_id, site_id)" \
82                        " VALUES(%(person_id)d, %(site_id)d)",
83                        locals())
84
85         if commit:
86             self.api.db.commit()
87
88         if 'person_ids' in self and person_id not in self['person_ids']:
89             self['person_ids'].append(person_id)
90
91         if 'site_ids' in person and site_id not in person['site_ids']:
92             person['site_ids'].append(site_id)
93
94     def remove_person(self, person, commit = True):
95         """
96         Remove person from existing site.
97         """
98
99         assert 'site_id' in self
100         assert isinstance(person, PLC.Persons.Person)
101         assert 'person_id' in person
102
103         site_id = self['site_id']
104         person_id = person['person_id']
105         self.api.db.do("DELETE FROM person_site" \
106                        " WHERE person_id = %(person_id)d" \
107                        " AND site_id = %(site_id)d",
108                        locals())
109
110         if commit:
111             self.api.db.commit()
112
113         if 'person_ids' in self and person_id in self['person_ids']:
114             self['person_ids'].remove(person_id)
115
116         if 'site_ids' in person and site_id in person['site_ids']:
117             person['site_ids'].remove(site_id)
118
119     def sync(self, commit = True):
120         """
121         Flush changes back to the database.
122         """
123
124         self.validate()
125
126         try:
127             if not self['name'] or \
128                not self['abbreviated_name'] or \
129                not self['login_base']:
130                 raise KeyError
131         except KeyError:
132             raise PLCInvalidArgument, "name, abbreviated_name, and login_base must all be specified"
133
134         # Fetch a new site_id if necessary
135         if 'site_id' not in self:
136             rows = self.api.db.selectall("SELECT NEXTVAL('sites_site_id_seq') AS site_id")
137             if not rows:
138                 raise PLCDBError, "Unable to fetch new site_id"
139             self['site_id'] = rows[0]['site_id']
140             insert = True
141         else:
142             insert = False
143
144         # Filter out fields that cannot be set or updated directly
145         sites_fields = self.api.db.fields('sites')
146         fields = dict(filter(lambda (key, value): key in sites_fields,
147                              self.items()))
148
149         # Parameterize for safety
150         keys = fields.keys()
151         values = [self.api.db.param(key, value) for (key, value) in fields.items()]
152
153         if insert:
154             # Insert new row in sites table
155             sql = "INSERT INTO sites (%s) VALUES (%s)" % \
156                   (", ".join(keys), ", ".join(values))
157         else:
158             # Update existing row in sites table
159             columns = ["%s = %s" % (key, value) for (key, value) in zip(keys, values)]
160             sql = "UPDATE sites SET " + \
161                   ", ".join(columns) + \
162                   " WHERE site_id = %(site_id)d"
163
164         self.api.db.do(sql, fields)
165
166         if commit:
167             self.api.db.commit()
168
169     def delete(self, commit = True):
170         """
171         Delete existing site.
172         """
173
174         assert 'site_id' in self
175
176         # Delete accounts of all people at the site who are not
177         # members of at least one other non-deleted site.
178         persons = PLC.Persons.Persons(self.api, self['person_ids'])
179         for person_id, person in persons.iteritems():
180             delete = True
181
182             person_sites = Sites(self.api, person['site_ids'])
183             for person_site_id, person_site in person_sites.iteritems():
184                 if person_site_id != self['site_id'] and \
185                    not person_site['deleted']:
186                     delete = False
187                     break
188
189             if delete:
190                 person.delete(commit = False)
191
192         # Delete all site slices
193         # slices = Slices(self.api, self['slice_ids'])
194         # for slice in slices.values():
195         #    slice.delete(commit = False)
196
197         # Delete all site PCUs
198         # pcus = PCUs(self.api, self['pcu_ids'])
199         # for pcu in pcus.values():
200         #    pcu.delete(commit = False)
201
202         # Delete all site nodes
203         nodes = Nodes(self.api, self['node_ids'])
204         for node in nodes.values():
205             node.delete(commit = False)
206
207         # Mark as deleted
208         self['deleted'] = True
209         self.sync(commit)
210
211 class Sites(Table):
212     """
213     Representation of row(s) from the sites table in the
214     database. Specify extra_fields to be able to view and modify extra
215     fields.
216     """
217
218     def __init__(self, api, site_id_or_login_base_list = None, fields = Site.fields):
219         self.api = api
220
221         sql = "SELECT %s FROM view_sites WHERE deleted IS False" % \
222               ", ".join(fields)
223
224         if site_id_or_login_base_list:
225             # Separate the list into integers and strings
226             site_ids = filter(lambda site_id: isinstance(site_id, (int, long)),
227                               site_id_or_login_base_list)
228             login_bases = filter(lambda login_base: isinstance(login_base, StringTypes),
229                                  site_id_or_login_base_list)
230             sql += " AND (False"
231             if site_ids:
232                 sql += " OR site_id IN (%s)" % ", ".join(map(str, site_ids))
233             if login_bases:
234                 sql += " OR login_base IN (%s)" % ", ".join(api.db.quote(login_bases))
235             sql += ")"
236
237         rows = self.api.db.selectall(sql)
238
239         for row in rows:
240             self[row['site_id']] = site = Site(api, row)
241             for aggregate in ['person_ids', 'slice_ids',
242                               'defaultattribute_ids', 'pcu_ids', 'node_ids']:
243                 if not site.has_key(aggregate) or site[aggregate] is None:
244                     site[aggregate] = []
245                 else:
246                     site[aggregate] = map(int, site[aggregate].split(','))