1 if (! window.XOSLIB_LOADED ) {
2 window.XOSLIB_LOADED=true;
4 SLIVER_API = "/plstackapi/slivers/";
5 SLICE_API = "/plstackapi/slices/";
6 SLICEROLE_API = "/plstackapi/slice_roles/";
7 NODE_API = "/plstackapi/nodes/";
8 SITE_API = "/plstackapi/sites/";
9 SITEDEPLOYMENT_API = "/plstackapi/sitedeployments/";
10 USER_API = "/plstackapi/users/";
11 USERDEPLOYMENT_API = "/plstackapi/user_deployments/";
12 DEPLOYMENT_API = "/plstackapi/deployments/";
13 IMAGE_API = "/plstackapi/images/";
14 IMAGEDEPLOYMENTS_API = "/plstackapi/imagedeployments/";
15 NETWORKTEMPLATE_API = "/plstackapi/networktemplates/";
16 NETWORK_API = "/plstackapi/networks/";
17 NETWORKSLIVER_API = "/plstackapi/networkslivers/";
18 SERVICE_API = "/plstackapi/services/";
19 SLICEPRIVILEGE_API = "/plstackapi/slice_privileges/";
20 NETWORKDEPLOYMENT_API = "/plstackapi/networkdeployments/";
21 FLAVOR_API = "/plstackapi/flavors/";
22 CONTROLLER_API = "/plstackapi/controllers/";
25 CONTROLLERSITEDEPLOYMENT_API = "/plstackapi/controllersitedeploymentses";
28 /* changed as a side effect of the big rename
29 SLICEDEPLOYMENT_API = "/plstackapi/slice_deployments/";
30 USERDEPLOYMENT_API = "/plstackapi/user_deployments/";
33 SLICEDEPLOYMENT_API = "/plstackapi/slicedeployments/";
34 USERDEPLOYMENT_API = "/plstackapi/userdeployments/";
36 SLICEPLUS_API = "/xoslib/slicesplus/";
37 TENANTVIEW_API = "/xoslib/tenantview/"
39 XOSModel = Backbone.Model.extend({
40 relatedCollections: [],
41 foreignCollections: [],
47 /* from backbone-tastypie.js */
48 //idAttribute: 'resource_uri',
50 /* from backbone-tastypie.js */
52 var url = this.attributes.resource_uri;
56 url = this.urlRoot + this.id;
58 // this happens when creating a new model.
64 // XXX I'm not sure this does anything useful
65 url = ( _.isFunction( this.collection.url ) ? this.collection.url() : this.collection.url );
66 url = url || this.urlRoot;
69 // remove any existing query parameters
70 url && ( url.indexOf("?") > -1 ) && ( url = url.split("?")[0] );
72 url && ( url += ( url.length > 0 && url.charAt( url.length - 1 ) === '/' ) ? '' : '/' );
74 url && ( url += "?no_hyperlinks=1" );
79 listMethods: function() {
81 for(var m in this) {
\r
82 if(typeof this[m] == "function") {
\r
89 save: function(attributes, options) {
93 return Backbone.Model.prototype.save.call(this, attributes, options);
96 getChoices: function(fieldName, excludeChosen) {
98 if (fieldName in this.m2mFields) {
99 for (index in xos[this.m2mFields[fieldName]].models) {
100 candidate = xos[this.m2mFields[fieldName]].models[index];
101 if (excludeChosen && idInArray(candidate.id, this.attributes[fieldName])) {
104 choices.push(candidate.id);
110 /* If a 'validate' method is supplied, then it will be called
111 automatically on save. Unfortunately, save calls neither the
112 'error' nor the 'success' callback if the validator fails.
114 For now, we're calling our validator 'xosValidate' so this
115 autoamtic validation doesn't occur.
118 xosValidate: function(attrs, options) {
121 _.each(this.validators, function(validatorList, fieldName) {
122 _.each(validatorList, function(validator) {
123 if (fieldName in attrs) {
124 validatorResult = validateField(validator, attrs[fieldName], this)
125 if (validatorResult != true) {
126 errors[fieldName] = validatorResult;
135 // backbone.js semantics -- on successful validate, return nothing
138 /* uncommenting this would make validate() call xosValidate()
139 validate: function(attrs, options) {
140 r = this.xosValidate(attrs, options);
141 console.log("validate");
147 XOSCollection = Backbone.Collection.extend({
148 objects: function() {
149 return this.models.map(function(element) { return element.attributes; });
152 initialize: function(){
153 this.isLoaded = false;
154 this.failedLoad = false;
155 this.startedLoad = false;
156 this.sortVar = 'name';
\r
157 this.sortOrder = 'asc';
\r
158 this.on( "sort", this.sorted );
\r
161 relatedCollections: [],
\r
162 foreignCollections: [],
\r
166 detailLinkFields: [],
\r
168 sorted: function() {
\r
169 //console.log("sorted " + this.modelName);
\r
172 simpleComparator: function( model ){
\r
173 parts=this.sortVar.split(".");
\r
174 result = model.get(parts[0]);
\r
175 for (index=1; index<parts.length; ++index) {
\r
176 result=result[parts[index]];
\r
181 comparator: function (left, right) {
\r
182 var l = this.simpleComparator(left);
\r
183 var r = this.simpleComparator(right);
\r
185 if (l === void 0) return -1;
\r
186 if (r === void 0) return 1;
\r
188 if (this.sortOrder=="desc") {
\r
189 return l < r ? 1 : l > r ? -1 : 0;
\r
191 return l < r ? -1 : l > r ? 1 : 0;
\r
195 fetchSuccess: function(collection, response, options) {
\r
196 //console.log("fetch succeeded " + collection.modelName);
\r
197 this.failedLoad = false;
\r
198 this.fetching = false;
\r
199 if (!this.isLoaded) {
\r
200 this.isLoaded = true;
\r
201 Backbone.trigger("xoslib:collectionLoadChange", this);
\r
203 this.trigger("fetchStateChange");
\r
204 if (options["orig_success"]) {
\r
205 options["orig_success"](collection, response, options);
\r
209 fetchFailure: function(collection, response, options) {
\r
210 //console.log("fetch failed " + collection.modelName);
\r
211 this.fetching = false;
\r
212 if ((!this.isLoaded) && (!this.failedLoad)) {
\r
213 this.failedLoad=true;
\r
214 Backbone.trigger("xoslib:collectionLoadChange", this);
\r
216 this.trigger("fetchStateChange");
\r
217 if (options["orig_failure"]) {
\r
218 options["orig_failure"](collection, response, options);
\r
222 fetch: function(options) {
\r
224 this.fetching=true;
\r
225 //console.log("fetch " + this.modelName);
\r
226 if (!this.startedLoad) {
\r
227 this.startedLoad=true;
\r
228 Backbone.trigger("xoslib:collectionLoadChange", this);
\r
230 this.trigger("fetchStateChange");
\r
231 if (options == undefined) {
\r
234 options["orig_success"] = options["success"];
\r
235 options["orig_failure"] = options["failure"];
\r
236 options["success"] = function(collection, response, options) { self.fetchSuccess.call(self, collection, response, options); };
\r
237 options["failure"] = this.fetchFailure;
\r
238 Backbone.Collection.prototype.fetch.call(this, options);
\r
241 startPolling: function() {
\r
242 if (!this._polling) {
\r
244 setInterval(function() { collection.fetch(); }, 10000);
250 refresh: function(refreshRelated) {
251 if (!this.fetching) {
254 if (refreshRelated) {
255 for (related in this.relatedCollections) {
256 related = xos[related];
257 if (!related.fetching) {
264 maybeFetch: function(options){
265 // Helper function to fetch only if this collection has not been fetched before.
267 // If this has already been fetched, call the success, if it exists
268 options.success && options.success();
269 console.log("alreadyFetched");
273 // when the original success function completes mark this collection as fetched
275 successWrapper = function(success){
277 self._fetched = true;
278 success && success.apply(this, arguments);
281 options.success = successWrapper(options.success);
282 console.log("call fetch");
286 getOrFetch: function(id, options){
287 // Helper function to use this collection as a cache for models on the server
288 var model = this.get(id);
291 options.success && options.success(model);
295 model = new this.model({
299 model.fetch(options);
302 /* filterBy: note that this yields a new collection. If you pass that
303 collection to a CompositeView, then the CompositeView won't get
304 any events that trigger on the original collection.
306 Using this function is probably wrong, and I wrote
307 FilteredCompositeView() to replace it.
310 filterBy: function(fieldName, value) {
311 filtered = this.filter(function(obj) {
312 return obj.get(fieldName) == value;
314 return new this.constructor(filtered);
317 /* from backbone-tastypie.js */
318 url: function( models ) {
319 var url = this.urlRoot || ( models && models.length && models[0].urlRoot );
320 url && ( url += ( url.length > 0 && url.charAt( url.length - 1 ) === '/' ) ? '' : '/' );
322 url && ( url += "?no_hyperlinks=1" );
324 if (this.currentUserCanSee) {
325 url && ( url += "¤t_user_can_see=1" );
331 listMethods: function() {
333 for(var m in this) {
\r
334 if(typeof this[m] == "function") {
\r
342 function get_defaults(modelName) {
343 if ((typeof xosdefaults !== "undefined") && xosdefaults[modelName]) {
344 return xosdefaults[modelName];
349 function extend_defaults(modelName, stuff) {
350 defaults = get_defaults(modelName);
352 return $.extend({}, defaults, stuff);
358 function define_model(lib, attrs) {
359 modelName = attrs.modelName;
360 modelClassName = modelName;
361 collectionClass = attrs.collectionClass || XOSCollection;
362 collectionClassName = modelName + "Collection";
364 if (!attrs.addFields) {
365 attrs.addFields = attrs.detailFields;
368 attrs.inputType = attrs.inputType || {};
369 attrs.foreignFields = attrs.foreignFields || {};
370 attrs.m2mFields = attrs.m2mFields || {};
371 attrs.readOnlyFields = attrs.readOnlyFields || [];
372 attrs.detailLinkFields = attrs.detailLinkFields || ["id","name"];
374 if (!attrs.collectionName) {
375 attrs.collectionName = modelName + "s";
377 collectionName = attrs.collectionName;
384 if ($.inArray(key, ["urlRoot", "modelName", "collectionName", "listFields", "addFields", "detailFields", "detailLinkFields", "foreignFields", "inputType", "relatedCollections", "foreignCollections", "defaults"])>=0) {
385 modelAttrs[key] = value;
386 collectionAttrs[key] = value;
388 if ($.inArray(key, ["validate", "preSave", "readOnlyFields"])) {
389 modelAttrs[key] = value;
393 if (!modelAttrs.defaults) {
394 modelAttrs.defaults = get_defaults(modelName);
397 // if ((typeof xosdefaults !== "undefined") && xosdefaults[modelName]) {
398 // modelAttrs["defaults"] = xosdefaults[modelName];
401 if ((typeof xosvalidators !== "undefined") && xosvalidators[modelName]) {
402 modelAttrs["validators"] = $.extend({}, xosvalidators[modelName], attrs["validators"] || {});
403 } else if (attrs["validators"]) {
404 modelAttrs["validators"] = attrs["validators"];
406 console.log(modelAttrs);
409 lib[modelName] = XOSModel.extend(modelAttrs);
411 collectionAttrs["model"] = lib[modelName];
413 lib[collectionClassName] = collectionClass.extend(collectionAttrs);
414 lib[collectionName] = new lib[collectionClassName]();
416 lib.allCollectionNames.push(collectionName);
417 lib.allCollections.push(lib[collectionName]);
421 this.allCollectionNames = [];
422 this.allCollections = [];
424 /* Give an id, the name of a collection, and the name of a field for models
425 within that collection, lookup the id and return the value of the field.
428 this.idToName = function(id, collectionName, fieldName) {
429 linkedObject = xos[collectionName].get(id);
430 if (linkedObject == undefined) {
433 return linkedObject.attributes[fieldName];
437 /* defining the models
439 modelName - name of the model.
441 relatedCollections - collections which should be drawn as an inline
442 list when the detail view is displayed.
443 Format: <collection>:<collectionFieldName> where
444 <collectionFieldName> is the name of the field
445 in the collection that points back to the
446 collection in the detail view.
448 foreignCollections - collections which are used in idToName() calls
449 when presenting the data to the user. Used to
450 create a listento event. Somewhat
451 redundant with foreignFields.
453 foreignFields - <localFieldName>:<collection>. Used to
454 automatically map ids into humanReadableNames
455 when presenting data to the user.
457 m2mfields - <localFieldName>:<colleciton>. Used to
458 populate choices in picker lists. Simalar to
461 listFields - fields to display in lists
463 detailFields - fields to display in detail views
465 addFields - fields to display in popup add windows
467 inputType - by default, "detailFields" will be displayed
468 as text input controls. This will let you display
469 a checkbox or a picker instead.
472 define_model(this, {urlRoot: SLIVER_API,
473 relatedCollections: {"networkSlivers": "sliver"},
474 foreignCollections: ["slices", "deployments", "images", "nodes", "users", "flavors"],
475 foreignFields: {"creator": "users", "image": "images", "node": "nodes", "deploymentNetwork": "deployments", "slice": "slices", "flavor": "flavors"},
477 listFields: ["backend_status", "id", "name", "instance_id", "instance_name", "slice", "deploymentNetwork", "image", "node", "flavor"],
478 addFields: ["slice", "deploymentNetwork", "flavor", "image", "node"],
479 detailFields: ["backend_status", "name", "instance_id", "instance_name", "slice", "deploymentNetwork", "flavor", "image", "node", "creator"],
480 preSave: function() { if (!this.attributes.name && this.attributes.slice) { this.attributes.name = xos.idToName(this.attributes.slice, "slices", "name"); } },
483 define_model(this, {urlRoot: SLICE_API,
484 relatedCollections: {"slivers": "slice", "slicePrivileges": "slice", "networks": "owner"},
485 foreignCollections: ["services", "sites"],
486 foreignFields: {"service": "services", "site": "sites"},
487 listFields: ["backend_status", "id", "name", "enabled", "description", "slice_url", "site", "max_slivers", "service"],
488 detailFields: ["backend_status", "name", "site", "enabled", "description", "slice_url", "max_slivers"],
489 inputType: {"enabled": "checkbox"},
491 xosValidate: function(attrs, options) {
492 errors = XOSModel.prototype.xosValidate.call(this, attrs, options);
493 // validate that slice.name starts with site.login_base
494 site = attrs.site || this.site;
495 if ((site!=undefined) && (attrs.name!=undefined)) {
496 site = xos.sites.get(site);
497 if (attrs.name.indexOf(site.attributes.login_base+"_") != 0) {
498 errors = errors || {};
499 errors["name"] = "must start with " + site.attributes.login_base + "_";
506 define_model(this, {urlRoot: SLICEPRIVILEGE_API,
507 foreignCollections: ["slices", "users", "sliceRoles"],
508 modelName: "slicePrivilege",
509 foreignFields: {"user": "users", "slice": "slices", "role": "sliceRoles"},
510 listFields: ["backend_status", "id", "user", "slice", "role"],
511 detailFields: ["backend_status", "user", "slice", "role"],
514 define_model(this, {urlRoot: SLICEROLE_API,
515 modelName: "sliceRole",
516 listFields: ["backend_status", "id", "role"],
517 detailFields: ["backend_status", "role"],
520 define_model(this, {urlRoot: NODE_API,
521 foreignCollections: ["sites", "deployments"],
523 foreignFields: {"site": "sites", "deployment": "deployments"},
524 listFields: ["backend_status", "id", "name", "site", "deployment"],
525 detailFields: ["backend_status", "name", "site", "deployment"],
528 define_model(this, {urlRoot: SITE_API,
529 relatedCollections: {"users": "site", "slices": "site", "nodes": "site", "siteDeployments": "site"},
531 listFields: ["backend_status", "id", "name", "site_url", "enabled", "login_base", "is_public", "abbreviated_name"],
532 detailFields: ["backend_status", "name", "abbreviated_name", "url", "enabled", "is_public", "login_base"],
533 inputType: {"enabled": "checkbox", "is_public": "checkbox"},
536 define_model(this, {urlRoot: SITEDEPLOYMENT_API,
537 foreignCollections: ["sites", "deployments", "controllers"],
538 foreignFields: {"site": "sites", "deployment": "deployments", "controller": "controllers"},
539 modelName: "siteDeployment",
540 listFields: ["backend_status", "id", "site", "deployment", "controller", "availability_zone"],
541 detailFields: ["backend_status", "site", "deployment", "controller", "availability_zone"],
542 inputType: {"enabled": "checkbox", "is_public": "checkbox"},
545 define_model(this, {urlRoot: USER_API,
546 relatedCollections: {"slicePrivileges": "user", "slices": "owner"},
547 foreignCollections: ["sites"],
549 foreignFields: {"site": "sites"},
550 listFields: ["backend_status", "id", "username", "firstname", "lastname", "phone", "user_url", "site"],
551 detailFields: ["backend_status", "username", "firstname", "lastname", "phone", "user_url", "site"],
554 define_model(this, { urlRoot: DEPLOYMENT_API,
555 relatedCollections: {"nodes": "deployment", "slivers": "deploymentNetwork"},
556 m2mFields: {"flavors": "flavors", "sites": "sites", "images": "images"},
557 modelName: "deployment",
558 listFields: ["backend_status", "id", "name", "backend_type", "admin_tenant"],
559 detailFields: ["backend_status", "name", "backend_type", "admin_tenant", "flavors", "sites", "images"],
560 inputType: {"flavors": "picker", "sites": "picker", "images": "picker"},
563 define_model(this, {urlRoot: IMAGE_API,
566 listFields: ["backend_status", "id", "name", "disk_format", "container_format", "path"],
567 detailFields: ["backend_status", "name", "disk_format", "admin_tenant"],
570 define_model(this, {urlRoot: NETWORKTEMPLATE_API,
571 modelName: "networkTemplate",
572 listFields: ["backend_status", "id", "name", "visibility", "translation", "shared_network_name", "shared_network_id"],
573 detailFields: ["backend_status", "name", "description", "visibility", "translation", "shared_network_name", "shared_network_id"],
576 define_model(this, {urlRoot: NETWORK_API,
577 relatedCollections: {"networkSlivers": "network"},
578 foreignCollections: ["slices", "networkTemplates"],
579 modelName: "network",
580 foreignFields: {"template": "networkTemplates", "owner": "slices"},
581 listFields: ["backend_status", "id", "name", "template", "ports", "labels", "owner"],
582 detailFields: ["backend_status", "name", "template", "ports", "labels", "owner"],
585 define_model(this, {urlRoot: NETWORKSLIVER_API,
586 modelName: "networkSliver",
587 foreignFields: {"network": "networks", "sliver": "slivers"},
588 listFields: ["backend_status", "id", "network", "sliver", "ip", "port_id"],
589 detailFields: ["backend_status", "network", "sliver", "ip", "port_id"],
592 define_model(this, {urlRoot: SERVICE_API,
593 modelName: "service",
594 listFields: ["backend_status", "id", "name", "enabled", "versionNumber", "published"],
595 detailFields: ["backend_status", "name", "description", "versionNumber"],
598 define_model(this, {urlRoot: FLAVOR_API,
600 m2mFields: {"deployments": "deployments"},
601 listFields: ["backend_status", "id", "name", "flavor", "order", "default"],
602 detailFields: ["backend_status", "name", "description", "flavor", "order", "default", "deployments"],
603 inputType: {"default": "checkbox", "deployments": "picker"},
606 define_model(this, {urlRoot: CONTROLLER_API,
607 modelName: "controller",
608 listFields: ["backend_status", "id", "name", "version", "backend_type"],
609 detailFields: ["backend_status", "name", "version", "backend_type", "auth_url", "admin_user", "admin_password", "admin_tenant"],
613 define_model(this, {urlRoot: CONTROLLERSITEDEPLOYMENT_API,
614 modelName: "controllerSiteDeployment",
615 foreignCollections: ["site_deployments", "controllers"],
616 foreignFields: {"site_deployment": "siteDeployments", "controller": "controllers"},
617 listFields: ["backend_status", "id", "site_deployment", "controller", "tenant_id"],
618 detailFields: ["backend_status", "site_deployment", "controller", "tenant_id"],
622 /* DELETED in site-controller branch
624 define_model(this, {urlRoot: NETWORKDEPLOYMENT_API,
625 modelName: "networkDeployment",
626 foreignFields: {"network": "networks", "deployment": "deployments"},
627 listFields: ["backend_status", "id", "network", "deployment", "net_id"],
628 detailFields: ["backend_status", "network", "deployment", "net_id"],
631 define_model(this, {urlRoot: SLICEDEPLOYMENT_API,
632 foreignCollections: ["slices", "deployments"],
633 modelName: "sliceDeployment",
634 foreignFields: {"slice": "slices", "deployment": "deployments"},
635 listFields: ["backend_status", "id", "slice", "deployment", "tenant_id"],
636 detailFields: ["backend_status", "slice", "deployment", "tenant_id"],
639 define_model(this, {urlRoot: USERDEPLOYMENT_API,
640 foreignCollections: ["users","deployments"],
641 modelName: "userDeployment",
642 foreignFields: {"deployment": "deployments", "user": "users"},
643 listFields: ["backend_status", "id", "user", "deployment", "kuser_id"],
644 detailFields: ["backend_status", "user", "deployment", "kuser_id"],
647 END stuff deleted in site-controller branch */
649 /* not deleted, but obsolete since it has degenerated to a ManyToMany with no other fields
651 define_model(this, {urlRoot: IMAGEDEPLOYMENTS_API,
652 modelName: "imageDeployment",
653 foreignCollections: ["images", "deployments"],
654 listFields: ["backend_status", "id", "image", "deployment", "glance_image_id"],
655 detailFields: ["backend_status", "image", "deployment", "glance_image_id"],
661 // XXX this really needs to somehow be combined with Slice, to avoid duplication
662 define_model(this, {urlRoot: SLICEPLUS_API,
663 relatedCollections: {"slivers": "slice", "slicePrivileges": "slice", "networks": "owner"},
664 foreignCollections: ["services", "sites"],
665 foreignFields: {"service": "services", "site": "sites"},
666 listFields: ["backend_status", "id", "name", "enabled", "description", "slice_url", "site", "max_slivers", "service"],
667 detailFields: ["backend_status", "name", "site", "enabled", "description", "slice_url", "max_slivers"],
668 inputType: {"enabled": "checkbox"},
669 modelName: "slicePlus",
670 collectionName: "slicesPlus",
671 defaults: extend_defaults("slice", {"network_ports": "", "site_allocation": []}),
672 validators: {"network_ports": ["portspec"]},
673 xosValidate: function(attrs, options) {
674 errors = XOSModel.prototype.xosValidate.call(this, attrs, options);
675 // validate that slice.name starts with site.login_base
676 site = attrs.site || this.site;
677 if ((site!=undefined) && (attrs.name!=undefined)) {
678 site = xos.sites.get(site);
679 if (attrs.name.indexOf(site.attributes.login_base+"_") != 0) {
680 errors = errors || {};
681 errors["name"] = "must start with " + site.attributes.login_base + "_";
688 define_model(this, {urlRoot: TENANTVIEW_API,
689 modelName: "tenantview",
690 collectionName: "tenantview",
695 /* by default, have slicePlus only fetch the slices the user can see */
696 this.slicesPlus.currentUserCanSee = true;
698 this.tenant = function() { return this.tenantview.models[0].attributes; };
700 this.listObjects = function() { return this.allCollectionNames; };
702 this.getCollectionStatus = function() {
703 stats = {isLoaded: 0, failedLoad: 0, startedLoad: 0};
704 for (index in this.allCollections) {
705 collection = this.allCollections[index];
706 if (collection.isLoaded) {
707 stats["isLoaded"] = stats["isLoaded"] + 1;
709 if (collection.failedLoad) {
710 stats["failedLoad"] = stats["failedLoad"] + 1;
712 if (collection.startedLoad) {
713 stats["startedLoad"] = stats["startedLoad"] + 1;
716 stats["completedLoad"] = stats["failedLoad"] + stats["isLoaded"];
723 function getCookie(name) {
724 var cookieValue = null;
\r
725 if (document.cookie && document.cookie != '') {
\r
726 var cookies = document.cookie.split(';');
\r
727 for (var i = 0; i < cookies.length; i++) {
\r
728 var cookie = jQuery.trim(cookies[i]);
\r
729 // Does this cookie string begin with the name we want?
\r
730 if (cookie.substring(0, name.length + 1) == (name + '=')) {
\r
731 cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
\r
736 return cookieValue;
\r
740 var _sync = Backbone.sync;
\r
741 Backbone.sync = function(method, model, options){
\r
742 options.beforeSend = function(xhr){
\r
743 var token = getCookie("csrftoken");
\r
744 xhr.setRequestHeader('X-CSRFToken', token);
\r
746 return _sync(method, model, options);
\r