Showing posts with label rest. Show all posts
Showing posts with label rest. Show all posts

Tuesday, June 14, 2022

Enforce row count limits on ManyToManyField rows in Django models

I had a requirement where ModelA needs to limit how many ModelBs it can have. They are linked via a ManyToManyField using a through model.

It looks like this:

 class ModelA(models.Model):  
   name = models.Charfield(max_length=255)  
   b_limit = models.PositiveSmallIntegerField(help_text="Max number of Bs we can have")  
   model_b_set = models.ManyToManyFields('ModelB', through="ABJoinTable")  

 class ModelB(models.Model):  
   some_field = models.Charfield(max_length=255)    
   model_a_set = models.ManyToManyFields('ModelA', through="ABJoinTable")  

 class ABJoinTable(models.Model):   
   model_a = models.ForeignKey("ModelA", on_delete=models.CASCADE)  
   model_b = models.ForeignKey("ModelB", on_delete=models.CASCADE)  
   is_suspended = models.BooleanField(default=False)  

The through model just contains extra information is NOT the key to enforcing the rule. Remember, we want to limit how many ModelB rows ModelA can have. How many ModelB's ModelA can have it controlled by the "b_limit" field. 

The solution is to use the m2m_changed signal

from django.db.models.signals import m2m_changed  
from django.core.exceptions import ValidationError

def enforce_modelA_B_limit(sender, **kwargs):  
   modelA = kwargs['instance']  
   if modelA.model_b_set.count() >= modelA.b_limit:  
    raise ValidationError("ModelA has too many ModelBs")  

m2m_changed.connect(enforce_modelA_B_limit, sender=ModelA.model_b_set.through)  

You can place this wherever a signal function is valid. I just placed mine in the same place where the models are declared.

Why a signal?

If your gut feeling was to override the `save()` or `clean()` methods then those won't work. You cannot check the count for the ManyToManyField until you have save the primary model - ModelA. You'll get an exception saying something like: `ModelA needs to have a value for field "id" before this many-to-many relationship can be used.`.

Testing Tip

In writing a test case for this, you can use the `self.assertRaises()` context to catch the error. It's bad practice if you do a `try-catch` block to see if the rule works. The test case would look like this:

 def test_modelA_B_limit_rule(self):  
   # we assume that a setUpTestData() method exist  
   # we also assume that ModelA is limited to 1 ModelB so adding a second ModelB should throw an
   # exception
   sample_b = ModelB.objects.create(some_field="test")  
   modelA_instance = ModelA.objects.get(pk=1)  
   with self.assertRaises(ValidationError):  
     modelA_instance.model_b_set.add(sample_b) # should throw  

There's nothing to change if you have Django Rest Framework project. Your API will throw a 400 error if they violate the limit.

Refs: https://stackoverflow.com/questions/20203806/limit-maximum-choices-of-manytomanyfield

Thursday, June 9, 2022

Fixing a Django drf-yasg "serializer_class required" AssertionError

Sometimes you have an API view (ie. GenericAPIView) class without a serializer. This causes the drf-yasg generator to throw an AssertionError exception saying class should either include a 'serializer_class' attribute, or override the 'get_serializer_class()' method.

You can fix this by adding the following to the view class:

 def get_serializer(self, *args, **kwargs):  
     pass  
 def get_serializer_class(self):  
     pass  

This should pass the drf-yasg serializer check.


Reference

Friday, November 16, 2018

My Guidelines for designing a restful APIs so frontend devs won't ask where I live

As a backend developer, I have to play nice with the frontend people. Let's face it, I don't want to be prison shanked on the lunch line. Easier said than done because of the different ideas that people have floated around like these three.
Each one of these have pros and cons but those are academic papers that I don't care to write right now. But I've worked and talked to a lot of frontend devs and they don't really care if we use any of the three examples that I gave. 

For them, a good API:
  1. Is well documented - makes sense; I'm a fan of drfdocs. Also Swagger gets a lot of mentions.
  2. Is versioned - good point; because changes to existing ones break too many things
  3. Is idempotent - to be honest I had to look this one up but it's the same idea of pure functions. Given the same input, it should return the same value (output)
  4. Uses HTTP errors correctly - For example, don't return errors with 200 status. Oops.
  5. Uses HTTP verbs sensibly - they want none of that 'GET /v1.0/delete/100' because that shit is stupid.
  6. Is NOT SOAP - dear lord up above in heaven, not ever SOAP. Ever
I also don't like SOAP. 

Friday, August 11, 2017

NativeScript and that "status 200 url null" error

This error confused me while developing with NativeScript trying to do a rest api call. Without digging thru the error object, you only get: "Response with status: 200  for URL: null" for the error text. Not exactly useful.

I then had to run "tns debug android" and dig thru the error object by doing adding a "json()" method call to it. I then got a more verbose error message. Something like:

"Error: java.net.ConnectException: failed to connect to localhost/127.0.0.1 (port 8000): connect failed: ECONNREFUSED (Connection refused)"

The chrome devtools also was cryptic only saying that the rest api call "Stalled".

Ok. So I then visited StackOverflow looking for a solution. No dice. Until, I realized that I'm doing development using an android emulator which is basically a virtual machine. And being a virtual machine calling 'localhost' or 127.0.0.1 is calling itself. The address is loopback address of the vm NOT my machine. No wonder the rest api call was failing. Fortunately, the android emulator networking is setup to work with localhost development.

The solution is to just change the rest api call from 'http://localhost/api' to http://10.0.2.2/api'.

Now it works.

For the curious, my rest api server is a Django Rest Framework app running locally.





Saturday, April 18, 2015

Finish the REST (Part 2) with Fiddler debugging and testing

We are doing three things for this part: (1) finish coding the REST api (2) Debug with Fiddler (3) Write Frisby test against our API.

Finishing the REST api is the easy part. Make a folder with the path api/v1/ inside the routes folder. Inside the api folder is another folder called v1. I do this just out of habit. APIs will change in even in a production environments. When that happens, I just add a v2 folder. It's just a cheap and easy way to future proof REST API endpoints.

After the folders, create a posts.js file inside.

var express = require('express');
var router = express.Router();
var mongoose = require('mongoose');

// Get the model
var post = mongoose.model('Post');

// GET /api/v1/posts - GET ALL
router.get('/', function (req, res) {
    post.find({}, function (err, posts) {
        res.json(posts);
    });
});

// GET /api/v1/posts/:post_id
router.get('/:post_id', function(req, res) {
    post.findById(req.params.post_id, function(err, post) {
        if (err) res.send(err);

        res.json(post);
    });
});

// POST /api/v1/posts
router.post('/', function(req, res) {
    var newPost = new Post();
    newPost.title = req.body.title;
    newPost.body = req.body.body;
    newPost.author = req.body.author;
    newPost.published = req.body.published;

    newPost.save(function(err) {
        if (err) res.send(err);
        res.json({ message: 'Post created!' });
    });
});

// PUT /api/v1/posts
router.put('/', function(req, res) {
    post.findById(req.params.post_id, function(err, post) {
        if (err) res.send(err);

        post.title = req.body.title;
        post.body = req.body.body;
        post.author = req.body.author;
        post.published = req.body.published;
        post.meta.favs = req.body.favs;
        post.meta.dvotes = req.body.dvotes;
        post.meta.uvotes = req.body.uvotes;

        post.save(function(err) {
            if (err) res.send(err);

            res.json({ message: 'Post updated' });
        });
    });
});

// DELETE /api/v1/posts/:post_id
router.delete('/:post_id', function(req, res) { 
    post.findByIdAndRemove(req.params.post_id, function(err, post) {
        if (err) res.send(err);

        res.json({ message: 'Post deleted!' });
    });
});

module.exports = router;

The whole thing should be fairly easy to figure out since this is the actual CRUD stuff.

Run the project and we should see our web page. Now we move to Fiddler to see around our API. Remember, you can just Postman instead of fiddler.

Start here: The Composer Tab in Fiddler
After you've open Fiddler, just go to the composer tab and add the API url you're interested in. Don't forget the HTTP verb. Press the Execute button on the top right to see the results.

You'll be able to go to the other tabs to see a lot of information.

The Inspectors tab contents is what we are interested in. This is where we see what's inside the reply from our API (or lack thereof if that is what we are expecting).

And finally, tests for our REST API endpoints. I wanted to add tests for our API to ensure behavior and for regression. It just makes sure I don't break the API as we continue working on this little project of ours.

We will be using the Frisby framework. Frisby is made to test REST API on node.js projects.

So, we install Frisby. Open a terminal, navigate to the project folder and type in:

npm install -g --save frisby

That should install the latest frisby version and save it a dependency in our package.json file. When frisby installed, create a folder named spec and inside that another folder named api. Inside these folders, a javascript file named posts_spec.js.

var frisby = require('frisby');
var baseURL = 'http://localhost:1337/'; // just replace if on live server

frisby.create('Status 200 for GET /api/v1/posts is returned')
    .get(baseURL + 'api/v1/posts')
    .expectStatus(200)
    .toss();

frisby.create('Status 200 for GET /api/v1/posts/:id')
    .get(baseURL + 'api/v1/posts/5525f1e3f6a7f3a00b234a09')
    .expectStatus(200)
    .expectJSON({ 'author': 'jaypax' })
    .toss();


Here we have two tests. Feel free to add more.

Now we just need to run it for which we need jasmine-node.

npm install -g --save jasmine-node

After NPM is done, you should be able to run the command in your terminal: jasmine-code spec/api/


Friby results after running the test 
By the way, you need the project to be running in order to run the test.

You basically have two terminals open: 1 terminal running the project and other terminal to run the test from.

Clone the code at the github repo.

Saturday, July 20, 2013

DAT Angular

This is part 5 of my Resteasy Tapestry with AngularJS tutorial. This is last part of the tutorial where you combine both. You should end up with a Resteasy-Tapestry5 server with the Ajax APIs and a second web application that uses the APIs via AngularJS. 

Using Ajax APIs with AngularJS is easy if you use Angular-Resource. Angular-Resource is not a default add-on for AngularJS. You don't have to download it since Google's CDNs also serve it.

<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js"></script>  
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular-resource.js"></script>  
<script type="text/javascript" src="js/tangled.js"></script>  

The tangled.js script there is where we write the interesting parts. Here's a partial of that script:

angular.module('tangled-app',['ngResource'])
    .controller('tangledCtrl', function($scope, $resource){
        var Customer = $resource('http://localhost\\:8080/tangled/rest/customers');
        
        $scope.appName = "Tangled App";             // App name
        $scope.customers = Customer.query();        // Calls our Resteasy-Tapestry REST API /GET customers
    })
........

The script basically just means that I'm working with a Angular app called "tangled-app" and then AngularJS inits to also load the Angular-Resource library so we can use the $resource variable. The whole thing then just hinges on the var Customer = $resource('http://localhost\\:8080/tangled/rest/customers'); line. Think of this line as a "promise", it promises to return data when you call the query or equivalent function.

After the script all you have to do is add the tangled-app value to the ng-app directive and the tangledCtrl to a ng-controller:
 <!DOCTYPE html>  
 <html ng-app="tangled-app">  
   <head>  
......... 
   </head>  
   <body>  
     <div class="container" ng-controller="tangledCtrl">  
       <div class="row-fluid">  
         <h3>Abstract</h3>  
         <p>This a simple customer ajax app with   
           <a href="http://tynamo.org/tapestry-resteasy+guide">Tapestry-resteasy</a> as the backend   
           and <a href="http://angularjs.org/">AngularJS</a> on the front. It also uses   
           <a href="http://twitter.github.io/bootstrap/">Twitter-Bootstrap</a>.</p>  
       </div>  
       <div class="customer-table">  
         <table class="table table-striped table-bordered">  
           <thead>  
             <tr>  
               <th>#</th>  
               <th>Name</th>  
               <th>Address</th>  
               <th>City</th>  
               <th>Country</th>  
               <th>Code</th>  
               <th>Credit Limit</th>  
             </tr>  
           </thead>  
           <tbody>  
             <tr ng-repeat="customer in customers">  
               <td>{{customer.customerNumber}}</td>  
               <td>{{customer.customerName}}</td>  
               <td>{{customer.addressLine1}} {{customer.addressLine2}}</td>  
               <td>{{customer.city}}</td>  
               <td>{{customer.country}}</td>  
               <td>{{customer.postalCode}}</td>  
               <td>{{customer.creditLimit}}</td>  
             </tr>  
           </tbody>  
         </table>  
       </div>  
     </div>  
........
     <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js"></script>  
     <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular-resource.js"></script>  
     <script type="text/javascript" src="js/tangled.js"></script>  
   </body>  
 </html>

BTW, if you're using Chrome, you'll get an "access-control-allow-origin" error. To fix this and allow you to continue developing in a local machine then just run Chrome with --disable-web-security. Don't do this though when browsing the internetz.

Tuesday, July 2, 2013

Designing with the end in mind: tuktuk style

This is part 4 of my Resteasy Tapestry with AngularJS tutorial. This is the design part of the tutorial. Now a days, what good is a web applications if it can't look good. I'm not a UI design expert by any kind of measure, in fact, I pretty much suck at it. So, I'm using somebody else's stuff to get me to look half decent, hence I'm using Javi Jimenez's tuktuk CSS framework.


Tapestry5 Layout sources
In Tapestry, the look of the website is a layout component. It works like pretty much like any other templating system you probably have seen or tried. You have a main template where the smaller page specific templates are "added" into. I have also write about tapestry layouts in a previous tutorial.

The first thing we have to do is add the tuktuk assets like the style sheets and scripts. 

1. Copy the style sheets into the layout folder. You should be adding 3 stylesheets: tuktuk.css, tuktuk.icons.css and tuktuk.theme.css

2. If the folder doesn't exist yet, create a "js" folder in the layout folder and copy the tuktuk.js file into it.

With the assets in place, we need to tell Tapestry5 about it. Open up the layout.java source file in the components folder. And right on top of the class declaration is an @import annotation, this what we change so our Tapestry webapp will be using the tuktuk assets. Change it to:

@Import( stylesheet = {"context:layout/tuktuk.css","context:layout/tuktuk.icons.css",
                       "context:layout/tuktuk.theme.css","context:layout/layout.css"},
        library = {"context:layout/js/tuktuk.js"})

That layout.css stylesheet is your own stylesheet seperate from the tuktuk.css. It's a bad idea to add your own style rules to the tuktuk.css. It's better to use your own.

The last step is to edit the layout.tml. The layout.tml is a very simple HTML template file so it should fairly easy to understand. It's a HTML file for christ sakes!

Saturday, June 15, 2013

Easy restful services with tapestry and tynamo-resteasy

This is part 3 of my Resteasy Tapestry with AngularJS tutorial. We will be building the restful api for this tutorial/project. I'll be trying to keep it simple with just CRUD equivalents like GET,  POST, PUT, and DELETE.

First thing first, we will be using Tynamo Resteasy for building the api. We could build our own from scratch but why re-invent the wheel? To add it, you will leverage Maven. Open up your POM file which should be on the root portion of the project and edit it:


<dependency>  
      <groupId>org.tynamo</groupId>  
      <artifactId>tapestry-resteasy</artifactId>  
      <version>0.3.1</version>  
 </dependency>    
 <dependency>  
      <groupId>org.jboss.resteasy</groupId>  
      <artifactId>resteasy-jackson-provider</artifactId>  
      <version>3.0-beta-4</version>  
 </dependency>

We need a JSON marshaller/unmarshaller, hence the resteasy-jackson-provider. We will be working with JSON, no XML. Read the Tynamo-resteasy guide for clarification on this. Now, simply clean and build the project and let Maven do its thing and get all them jars.

The next step is to create the Tapestry service that will respond to AJAX calls. In keeping with Tapestry convention, I'm putting this service inside a package. I called mine: com.demo.tangled.rest. So, it doesn't look so cluttered, I'm just showing the GET method of our restful API inside this service.
package com.demo.tangled.rest;

import com.demo.tangled.dao.contactsDAO;
import com.demo.tangled.entities.Contacts;
import org.apache.tapestry5.ioc.annotations.Inject;
import javax.ws.rs.*;
import javax.ws.rs.core.Response;
import java.util.List;

@Path("/customers")                     // URL for API call     
public class contactsResource{
    
    @Inject                             // Get that DAO service
    private ContactsDAO contactsDAO;
    
    @GET
    @Produces({"application/json"})     // Set output format to JSON
    public List<contacts> getAllContacts(){
        return contactsDAO.getAll();
    }
    .....    
}

The interesting parts of the code are the annotations. The first annotation, @Path, is the URL for API which is appended to rest sub-domain path. So, the address of this resource is http://localhost:8080/tangled/rest/customers if this was running on a local development machine. The other request methods like POST, PUT and DELETE are written in the same manner as the getAllContacts method. Notice the @GET annotation.

I have already wrote about how to make DAO services in my previous Tapestry5 tutorial.

Saturday, June 1, 2013

Creating the project in Netbeans with Maven

Modern software development is partly about the tools you use. Just don't insist yours is better with other programmers unless you want to start a religious war.

I use Maven. Maven is a software project management and comprehension tool. It's easy to install and use and Netbeans, my preferred IDE, can work with it by default unlike the other leading IDE which needs a plugin.  

Once you've installed Maven, you'll need to configure Netbeans to use the installed one unless you want to use the bundled one. You can do that via the menu Tools -> Options -> Java(tab) -> Maven. It should auto-detect the Maven home if you installed Maven correctly. After this, you're pretty much set.

Project from Archetype
Now all you need to do is create our Tapestry5.

Create a new project and choose Maven and then Project from Archetype.

We will be using the quickstart Tapestry5 project to get us rolling fairly quickly. The current version should 5.3.7.

On the "next" window, set the values to the following:
  • Group ID: org.apache.tapestry
  • Version: 5.3.7
  • Artifact ID: quickstart
  • Repository: http://tapestry.apache.org
You should be able to figure out the next page. After you clicked on that Finish button, you should see the your freshly created Tapestry5 project. 

Before you can run a Tapestry5 project, you need to satisfy a sample requirement which is a create a custom  Maven goal. You'll need this if you want to use the Live Reloading feature. I already wrote a how-to on this in an old blog entry. Just do steps 1, 2 and 3. You don't need to do the rest.

The Tapestry5 running
When you're done, you should be able to run the project and be able to open the web application in your favorite browser. See left. 

If you're wondering, I called my project tangled.

Next week, we'll be working on your database.

Resteasy Tapestry with AngularJS tutorial

I have been on a binge with AngularJS and so far I haven't gone to the hospital. And there's Tapestry5. I have been in-love with it for the longest time.

I think its time for a tutorial.

I'm basically gonna build a simple Tapestry-based restful API and use it with AngularJS. It's not that hard as it sounds. We'll be using Netbeans for this.

Here's what I have planned: