Showing posts with label webapp. Show all posts
Showing posts with label webapp. Show all posts

Mar 2, 2018

Automatically make web apps use HTTPS with Let's Encrypt, Nginx, and Docker

  1. Make sure you have docker already installed.
  2. Install the Nginx proxy with docker-gen
    
    sudo docker run --name=Nginx -d \
    --restart=always \
    -p 80:80 -p 443:443 \
    -v /data/certs:/etc/nginx/certs:ro \
    -v /var/run/docker.sock:/tmp/docker.sock:ro \
    -v /data/Nginx/vhost.d:/etc/nginx/vhost.d \
    -v /data/Nginx/html:/usr/share/nginx/html \
    --label com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy \
    jwilder/nginx-proxy
    

  3. Since I run portainer, start it up with the VIRTUAL_HOST and VIRTUAL_PORT environment variables so that docker-gen can pick it up. You can do this with any app you desire.
    
    sudo docker run --name Portainer -d \
    --restart=always \
    -p 9000:9000 \
    -v /var/run/docker.sock:/var/run/docker.sock \
    -v portainer_data:/data \
    -e VIRTUAL_HOST=portainer.local.network \
    -e VIRTUAL_PORT=9000 \
    portainer/portainer
    

  4. Now to use the Let's encrypt container to make certificates for our docker containers:
    
    sudo docker run --name=Letsencrypt -d \
    --restart=always \
    -v /data/certs:/etc/nginx/certs:rw \
    -v /var/run/docker.sock:/var/run/docker.sock:ro \
    --volumes-from Nginx \
    jrcs/letsencrypt-nginx-proxy-companion
    

  5. To enable SSL for your site, set the environment variables VIRTUAL_PROTO=https, VIRTUAL_PORT=433 environment as well as the LETSENCRYPT_HOST and LETSENCRYPT_EMAIL variables so that docker-gen can pick it up. You can do this with any app you desire. You will also need to mount the certificates and keys within the SSL folder of the container for the container to use the LetsEncrypt keys.

Oct 8, 2013

Laravel 4: Setting up and basics

This guide will go through one method of setting up a basic Laravel 4 environment. There are quick-start instructions, however in this guide we are not going to rely on a pre-installed global Composer binary.

These instructions require that you have git installed.

Installation

  1. Clone the laravel framework repository. This contains a basic layout for an application, although composer will be required to install all the dependencies.

    git clone https://github.com/laravel/laravel.git
  2. Rename the directory to one more suited for your project

    mv laravel/ [project]
  3. Change the permissions and owner of your project

    chown -R [user]:[group] [project]
  4. Go into your project directory

    cd [project]
  5. Download a copy of composer for your project

    curl -sS https://getcomposer.org/installer | php
  6. Since composer will now handle all dependancies, we no longer need git to mirror the laravel repository. Remove it:

    git remote -vgit remote rm origin
    If you wish to keep the repository around, you should just rename it:

    git remote rename origin laravel
  7. Edit your composer dependencies, which are stored in composer.json. If nothing else, edit the name, description, keywords, and license fields to suit your project.
  8. Ensure your composer.json is valid by running:

    php composer.phar validate
  9. Install all dependancies via composer

    php composer.phar install
  10. Run the in-built PHP development server to check that everything is working correctly

    php artisan server

    You should now be able to view your new application through http://localhost:8000/. Use the --help flag to see more configuration options.
  11. If you make changes to your composer dependancies, just run:

    php composer.phar update

Netbeans and Laravel set-up

  1. Follow the above instructions to create a new instance of Laravel
  2. Start a new project by clicking on File -> New Project
  3. Select PHP -> PHP Application  with existing sources
  4. Select the sources folder as the project directory you created earlier
  5. Set the 'Run As' configuration as PHP Built-in Web Server with the router script set to public/index.php. Note that other files (such as css and js files) stored in the public directory will not be served.

Eclipse and Laravel set-up

  1. Ensure you have installed the PHP development extensions
  2. Select File -> New Project -> Project
  3. Select PHP -> PHP Project
  4. Enter in a project name and choose the existing source folder. You can finish the settings now, or configure the project further.
  5. I could not get the Eclipse PHP server to work, so you will have to figure that one our for yourself.

Composer overview

  • The command php composer.phar will list all the available commands in composer
  • The composer.json file will list dependencies and configuration defaults for the composer binary.
  • The composer.lock file is generated after you run the install or update composer command. This file will store the exact version downloaded and any local configuration.

Resources

Sep 7, 2013

AJAX with JQuery and Google App Engine: Python

This is an adaption of AJAX with JQuery and PHP. It is a continuation of my Python 2.7 and Google App Engine series, and portions of the code builds upon my earlier work.

A first example...

AJAX stands for Asynchronous JavaScript And XML, and allows us to make our websites seem more dynamic. Let's start with the JavaScript side of things...

Create a file called static.html and add the following:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
    <head>
       <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
       <title>Ajax With Jquery</title>

       <!-- Grab the JQuery API from the Google servers -->
       <script src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js" type="text/javascript" charset="utf-8"></script>

       <!-- Our own JQuery code to do the AJAX-y stuff -->
       <script type="text/javascript" charset="utf-8">

          $(document).ready(function(){
             $('#txtValue').keyup(function(){
                 $('#display').html($(this).val());
             });
          });

       </script>
    </head>

    <body>
       <label for="txtValue">Enter a value : </label>
       <input type="text" name="txtValue" value="" id="txtValue">

        <div id="display"></div>
    </body>
</html>

Create a file called app.yaml and add the following:
application: almightynassar
version: 1
runtime: python27
api_version: 1
threadsafe: yes

handlers:
- url: /(.*).html
  static_files: \1.html
  upload: (.*).html

The above will simply serve ALL html page requests directly (which is enough for this example).

Now, if you are using Eclipse just right click on the project, select 'Run As' and choose the 'PyDev: Google App Run' option. Depending on how you configured your Google SDK, it will start up a server on your machine on port 8080. Open up a new tab and navigate to http://localhost:8080/static.html. You should now see a text box; when you type into the box it will automatically display the text in the space below.

Something a little more AJAX - The HTML

So our original example just showed how we could manipulate the DOM (Document Object Model) of the browser during run-time, without a page refresh. How can we use this knowledge utilizing the power of Google App Engine? Well, first let us edit static.html to something we can manipulate:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
       <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
       <title>Ajax With Jquery</title>
     
       <!-- Grab the JQuery API from the Google servers -->
       <script src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js" type="text/javascript" charset="utf-8"></script>

       <!-- Our own JQuery code to do the AJAX-y stuff -->
       <script type="text/javascript" charset="utf-8">
       $(document).ready(function(){
            // This will run when the item of class 'button' is clicked
            $(".button").click(function() {
               
                // Grabs the text input
                var name = $("input#txtValue").val();
        
                var dataString = 'txtValue='+ name;  
                // This creates the AJAX connection
                $.ajax({
                    type: "POST",
                    url: "/tutorial",
                    data: dataString,
                    success: function(data) {
                        $('#display').html(data.text);
                    }
                });
            return false;
            });
        });
       </script>
    </head>

    <body>
         <form name="contact" method="post" action="">
            <label for="txtValue">Enter a value : </label>
            <input type="text" name="txtValue" value="" id="txtValue">
            <input type="submit" name="submit" class="button" id="submit_btn" value="Send" />
         </form>
         <div id="display">
         </div>
   </body>
</html> 

Our JavaScript now sends some data to an external URL (in this case /tutorial) and inserts the returned data into the display div. Now all we need to do is code our Python script....

Our Python webapp

Create a file called tutorial.py and add the following code:

# The webapp2 framework
import webapp2

# The JSON library
import json

# Our JSON handler class
class JsonPage(webapp2.RequestHandler):
    # The POST handler
    def post(self):
        # Our POST Input
        txtinput = self.request.get('txtValue')
       
        # Create an array
        array = {'text': txtinput}
       
        # Output the JSON
        self.response.headers['Content-Type'] = 'application/json'
        self.response.out.write(json.dumps(array))

# URL map the JSON function
app = webapp2.WSGIApplication([('/tutorial', JsonPage)], debug=True)

Now we just need to edit app.yaml so it can serve up our tutorial script:

application: almightynassar
version: 1
runtime: python27
api_version: 1
threadsafe: no

handlers:
- url: /(.*).html
  static_files: \1.html
  upload: (.*).html

- url: /(.*)
  script: tutorial.app

Open up a new tab and navigate to http://localhost:8080/static.html. You should now see a text box with a button called 'Send'. Typing into this box and then clicking 'Send' will transmit the text to our tutorial script and then display the text in the space below.

References:

Nov 12, 2012

Get page width dynamically with Javascript (or JQuery)

This quick script will display the current width of your browser window (useful for creating responsive web designs).

The HTML

Simply make a <div> element, which we will be used by JQuery to print out the window width:
<html>
  <body>
    <div id="dimensions">
      <span class="width"></span>
    </div>
  </body>
</html>

JQuery method

If you already use JQuery, then this is for you. If you aren't using it, you should consider it; it makes programming in Javascript a breeze!

To use this snippet, just download the JQuery library, link it into your page and add the following script:

<script>
  $("#dimensions .width").html($(window).width());
  $(window).resize(function(){
    $("#dimensions .width").html($(window).width());
  });

</script>

Javascript method

If you don't have JQuery this should work equally well:

<script>
  window.onresize = displayWindowSize;
  window.onload = displayWindowSize;
  function displayWindowSize() {
    // your size calculation code here
    document.getElementById("dimensions").innerHTML = myWidth + "x" + myHeight;
  };
</script>

Further reading

You can also browse my other content such as:

Sep 11, 2012

Sending e-mail via PHP and MSMTP

Normally I would have set up a fully-fledged mailing systems like Postfix or Sendmail to handle my mailing needs. This means spending hours configuring, tweaking and securing another service running on my server. But I have found a simpler way.

MSMTP.

  1. Install MSMTP. In Ubuntu you would do the following:

    sudo apt-get install msmtp ca-certificates
  2. Edit the configuration file /etc/msmtprc with the following:

    #set defaults
    defaults

    # Enable or disable TLS/SSL encryption
    tls off
    tls_starttls on
    tls_trust_file /etc/ssl/certs/ca-certificates.crt

    # account settings
    account default
    host mail.optusnet.com.au # CHANGE THIS!!!
    port 25
    auth off
    from do-not-reply@domain.com.au
    logfile /var/log/msmtp/msmtp.log
  3. Edit the configuration of PHP to use MSMTP. Edit the configuration file with the following (in Ubuntu and PHP5, the file is stored in /etc/php5/cli/php.ini

    sendmail_path = /usr/bin/msmtp -t
  4. Create the log file directory and set proper permissions (depends on how your machine is set up:

    sudo mkdir /var/log/msmtp
    sudo chown [user]:[group] /var/log/msmtp
  5. Tell our system to rotate the logs so that they do not get too large by creating the file /etc/logrotate.d/msmtp:
    /var/log/msmtp/*.log {
    rotate 12
    monthly
    compress
    missingok
    notifempty

    }
  6. You can now test it with the following PHP script:

    <?php
    $to = "your-email@domain.com";
    $subject = "Test mail";
    $message = "Hello! This is a simple email message.";
    if (mail($to,$subject,$message)) {
        echo "Mail Sent.";
    } else {
        echo "NOT SENT.";
    }

References:

Sep 1, 2012

CodeIgniter web app authentication with PostgreSQL and TankAuth

Some of you may note that I had earlier created my own simple authentication system in CodeIgniter. However there comes a time when you just need something fully featured by you don't have the time create it yourself; step into TankAuth (it is also a hosted project on GitHub).

This post will focus on how to get TankAuth working with PostgreSQL .

Some problems....

TankAuth was designed around a MySQL database, and hence some SQL commands won't work. This means that you will get some odd behavior; unfortunately fixing all of these issues is outside the scope of this post.

Some Prerequisite knowledge...

I am going to dive right into the deep end here so if you don't know anything about PostgreSQL, PHP, CodeIgniter or Relational Database theory I strongly suggest you go and do some quick research.

I have a series on PostgreSQL that include an installation guide for Ubuntu, useful functions and operators, a guide to improving Postgre performance, how to check for installed procedural languages, and an overview of some basic concepts and some more advanced ones.

You can also check out my notes on designing relational databases. As for PHP, you can check out my blog posts such as my AJAX with JQuery and PHP tricks. You can also check out this list of useful CodeIgniter Tutorials.

Setting up PostgreSQL

We are going to assume that you have already installed PostgreSQL and have correctly download and setup CodeIgniter (and it is correctly serving the default web page). Make sure you have installed the PHP PostgreSQL plug-ins (php5-pgsql and php5-odbc).

Use this script to check if you have correctly set-up PostgreSQL and PHP.

Open up a terminal and create a user and database for CodeIgniter and TankAuth like we did in this earlier post:

createuser -h localhost -U postgres tankauth -W -S -D -R -P
createdb -h localhost -U postgres -O tankauth -W  tankauthdb
NOTE: These commands assume that you have password protected the postgres user; if you have a default install you can ditch the -W trigger.


Once we have created a user and a database, connect to the database server via the terminal command:



psql -h localhost -d tankauthdb -U tankauth -W


Now create a schema for our project to use. In the psql terminal run the following:

tankauthdb=> CREATE SCHEMA codeigniter;

PostgreSQL is now prepped and ready for CodeIgniter...


Setting up CodeIgniter

To get CodeIgniter working with PostgreSQL you will simply need to edit two configuration files. First, edit ./application/config/database.php with the following:


$db['default']['hostname'] = 'localhost';
$db['default']['username'] = 'tankauth';
$db['default']['password'] = 'tankauth';
$db['default']['database'] = 'tankauthdb';
$db['default']['dbdriver'] = 'postgre';

$db['default']['dbprefix'] = 'codeigniter.';


Now we will autoload the database (just because it is easier than calling it for every controller); edit ./application/config/autoload.php with:

$autoload['libraries'] = array('view', 'database');

And that's it! CodeIgniter will now connect to PostgreSQL.

Setting up TankAuth

  1. Download and extract TankAuth.
  2. Copy the application folder content to your CI application folder.
  3. Copy the captcha folder to your CI folder. Make sure this folder is writable by web server.
  4. Open the application/config/config.php file in your CI installation and change $config['sess_use_database'] value to TRUE.
  5. Create an encryption key in application/config/config.php and editing $config['encryption_key']
  6. Finally turn off captcha's in by editing application/config/tank_auth.php with $config['captcha_registration'] = FALSE;
Now we will need to create the databases that TankAuth requires. First we will create a table for storing CodeIgniter sessions:

CREATE TABLE codeigniter.ci_sessions
(
  session_id character varying(40) COLLATE pg_catalog."en_AU.utf8" NOT NULL DEFAULT '0',
  ip_address character varying(16) COLLATE pg_catalog."en_AU.utf8" NOT NULL DEFAULT '0',
  user_agent character varying(150) COLLATE pg_catalog."en_AU.utf8" NOT NULL,
  last_activity integer NOT NULL DEFAULT 0,
  user_data text COLLATE pg_catalog."en_AU.utf8" NOT NULL,
  CONSTRAINT ci_sessions_pkey PRIMARY KEY (session_id )
)
WITH (
  OIDS=FALSE
);
ALTER TABLE codeigniter.ci_sessions
  OWNER TO tankauth; 



This will create a table for recording login attempts AND a function and trigger for automatically updating the timestamp:


CREATE TABLE codeigniter.login_attempts
(
  id serial NOT NULL,
  ip_address character varying(40) COLLATE pg_catalog."en_AU.utf8" NOT NULL,
  login character varying(50) COLLATE pg_catalog."en_AU.utf8" NOT NULL,
  "time" timestamp without time zone NOT NULL DEFAULT now(),
  CONSTRAINT login_attempts_pkey PRIMARY KEY (id )
)
WITH (
  OIDS=FALSE
);
ALTER TABLE codeigniter.login_attempts
  OWNER TO tankauth;
CREATE OR REPLACE FUNCTION codeigniter.update_modified_column()
RETURNS TRIGGER AS $$
BEGIN
    NEW.time = now();
    RETURN NEW;
END;
$$ language 'plpgsql';

CREATE TRIGGER update_login_attempts_modtime BEFORE UPDATE ON codeigniter.login_attempts FOR EACH ROW EXECUTE PROCEDURE codeigniter.update_modified_column();


This will create a table for tracking users who choose to use the autologin feature (and the requisite triggers):


CREATE TABLE codeigniter.user_autologin
(
  key_id character(32) COLLATE pg_catalog."en_AU.utf8" NOT NULL,
  user_id integer NOT NULL DEFAULT 0,
  user_agent character varying(150) COLLATE pg_catalog."en_AU.utf8" NOT NULL,
  last_ip character varying(40) COLLATE pg_catalog."en_AU.utf8" NOT NULL,
  last_login timestamp without time zone NOT NULL DEFAULT now(),
  CONSTRAINT user_autologin_pkey PRIMARY KEY (key_id, user_id )
)
WITH (
  OIDS=FALSE
);
ALTER TABLE codeigniter.user_autologin
  OWNER TO tankauth;

CREATE OR REPLACE FUNCTION codeigniter.update_modified_login()
RETURNS TRIGGER AS $$
BEGIN
    NEW.last_login = now();
    RETURN NEW;
END;
$$ language 'plpgsql';

CREATE TRIGGER update_autologin_modtime BEFORE UPDATE ON codeigniter.user_autologin FOR EACH ROW EXECUTE PROCEDURE codeigniter.update_modified_login();


This will create our user profiles:


CREATE TABLE codeigniter.user_profiles
(
  id serial NOT NULL,
  user_id integer NOT NULL,
  country character varying(20) COLLATE pg_catalog."en_AU.utf8" DEFAULT NULL,
  website character varying(255) COLLATE pg_catalog."en_AU.utf8" DEFAULT NULL,
  CONSTRAINT user_profiles_pkey PRIMARY KEY (id )
)
WITH (
  OIDS=FALSE
);
ALTER TABLE codeigniter.user_profiles
  OWNER TO tankauth;
And finally this is our users login table:


CREATE TABLE codeigniter.users
(
  id serial NOT NULL,
  username character varying(50) COLLATE pg_catalog."en_AU.utf8" NOT NULL,
  password character varying(255) COLLATE pg_catalog."en_AU.utf8" NOT NULL,
  email character varying(100) COLLATE pg_catalog."en_AU.utf8" NOT NULL,
  activated smallint NOT NULL DEFAULT 1,
  banned smallint NOT NULL DEFAULT 1,
  ban_reason character varying(255) COLLATE pg_catalog."en_AU.utf8" DEFAULT NULL,
  new_password_key character varying(50) COLLATE pg_catalog."en_AU.utf8" DEFAULT NULL,
  new_password_requested timestamp without time zone DEFAULT NULL,
  new_email character varying(100) COLLATE pg_catalog."en_AU.utf8" DEFAULT NULL,
  new_email_key character varying(50) COLLATE pg_catalog."en_AU.utf8" DEFAULT NULL,
  last_ip character varying(40) COLLATE pg_catalog."en_AU.utf8" NOT NULL,
  last_login timestamp without time zone NOT NULL DEFAULT now(),
  created timestamp without time zone NOT NULL DEFAULT now(),
  modified timestamp without time zone NOT NULL DEFAULT now(),
  CONSTRAINT user_pkey PRIMARY KEY (id )
)
WITH (
  OIDS=FALSE
);
ALTER TABLE codeigniter.users
  OWNER TO tankauth;

CREATE TRIGGER update_users_login BEFORE UPDATE ON codeigniter.users FOR EACH ROW EXECUTE PROCEDURE codeigniter.update_modified_login();

CREATE OR REPLACE FUNCTION codeigniter.update_modified()
RETURNS TRIGGER AS $$
BEGIN
    NEW.modified = now();
    RETURN NEW;
END;
$$ language 'plpgsql';

CREATE TRIGGER update_users_modtime BEFORE UPDATE ON codeigniter.users FOR EACH ROW EXECUTE PROCEDURE codeigniter.update_modified();


And that is TankAuth set up!!!

One more thing...

For those keeping track, they may have noticed my warning about TankAuth using MySQL specific functions. To get the basic stuff working, you will need to do edit application/models/tank_auth/login_attempts.php and replace:

$this->db->or_where('UNIX_TIMESTAMP(time) <', time() - $expire_period);

with:

$this->db->or_where('extract(epoch FROM time) <', time() - $expire_period);
And you are good to go!

Aug 31, 2012

HTML5 and CSS3 drop-down menu

Don't forget to visit more of CSS3 tips and tricks!

The HTML5

All you will need is a simple menu structure using <nav> and nested <ul> tags, like this:
<nav>
    <ul>
        <li><a href="/">Home</a>
            <ul>
                <li><a href="/about">About</a></li>
                <li><a href="/contact">Contact</a></li>
            </ul>
        </li>
        <li><a href="/service">Services</a>
            <ul>
                <li><a href="/service/transport">Transport</a></li>
            </ul>
        </li>
        <li><a href='/welcome/language'>Change Language</a></li>
    </ul>
</nav>

The CSS3

I am just going to comment the CSS code; hopefully that is enough explanation for you...
/********************* Nav elements ****************************/

/* Navigation menu HTML5 tag*/
nav
{
    /* No border*/
    border:none;
    border:0px;
   
    /* Ensures the text is aligned properly*/
    text-align: left;
   
    /* Margins and padding*/
    margin:0px;
    padding:0px;
}

/* Our top menu list */
nav ul
{
    /* Set's how the element will interact with adjacent elements */
    display:block;
   
    /* Sets the height of the element (needs to be larger than the text) */
    height: 35px;
   
    /* Buffer space between other containers */
    padding-top: 5px;
    padding-bottom: 5px;
    padding-left: 1%;
    margin: 0;
   
    /* Does not insert bullet points*/
    list-style:none;
}

/* Float all <li> elements */
nav li{
    float:left;
}

/* Display top menu <li> items as inline */
nav ul li
{
    /* Set's how the element will interact with adjacent elements */
    display:inline;
}

/* The main menu link */
nav ul li a
{
    /* Changes the text to bold and uppercase */
    font-weight: bold;
    text-decoration:none;
    text-transform: uppercase;
   
    /* Default text colour */
    color:#CCCCCC;
   
    /* Pads the text so that it is not right up against the parent element*/
    padding: 5px;
   
    /* Specifies the line-height of the text */
    line-height:30px;
   
    /* Background colour */
    background-color: #FFFFFF;
   
    /* Border colour */
    border: 2px solid #CCCCCC;
   
        /* CSS3 Rounded Border */
    -webkit-border-radius: 8px; /* Saf3-4, iOS 1-3.2, Android ≤1.6 */
        -moz-border-radius: 8px; /* FF1-3.6 */
        border-radius: 8px; /* Opera 10.5, IE9, Saf5, Chrome, FF4, iOS 4, Android 2.1+ */

      /* useful if you don't want a bg color from leaking outside the border: */
      -moz-background-clip: padding; -webkit-background-clip: padding-box; background-clip: padding-box;
}

/* When we hover over (or for mobile devices, click on the element) */
nav ul li a:focus, nav ul li:focus a, nav ul li a:hover, nav ul li:hover a
{
    /* Underlines the text when we hover over it*/
    text-decoration:underline;
   
    /* Change the background colour*/
    background-color: #CCCCCC;
   
    /* Default text colour */
    color:#336666;
   
    /* Border colour */
    border: 2px solid #336666;
}

/* Our sub menus */
nav li ul
{
    /* Our background colour */
    background-color: #CCCCCC;
   
    /* HIDES the element until needed! */
    display:none;
   
    /* Let the browser determine the element height */
    height:auto;
   
    /* No padding or margins required */
    padding:0px;
    margin:0px;
   
    /* Minimum width of 120px */   
    min-width: 120px;
    width: auto;
   
    /* Use the absolute positioning method*/
    position:absolute;
   
    /* Stack this element right at the front of all elements*/
    z-index:200;
   
    /* Default text colour */
    color:#336666;
   
    /* Border colour */
    border: 2px solid #FFFFFF;
   
    /* CSS3 Rounded Border */
    -webkit-border-radius: 8px; /* Saf3-4, iOS 1-3.2, Android ≤1.6 */
        -moz-border-radius: 8px; /* FF1-3.6 */
        border-radius: 8px; /* Opera 10.5, IE9, Saf5, Chrome, FF4, iOS 4, Android 2.1+ */

      /* useful if you don't want a bg color from leaking outside the border: */
      -moz-background-clip: padding; -webkit-background-clip: padding-box; background-clip: padding-box;
}
nav li:focus ul, nav li:hover ul
{
    /* Set's how the element will interact with adjacent elements */
    display:block;
}
nav li li
{
    /* Set's how the element will interact with adjacent elements */
    display:block;
   
    /* Do not allow the element to float around */
    float:none;
   
    /* Do not set margin */
    margin: 0;
   
    /* Take up all space given by parent element*/
    /*width:100%;*/
   
    /* Border colour */
    border: none;
}
nav li:focus li a, nav li:hover li a
{
    /* Turn off the background */
    background:none;
    border:none;
    text-decoration:none;
}
nav li ul a
{
    /* Set's how the element will interact with adjacent elements */
    display:block;
   
    /* Sets the height of the element (needs to be larger than the text) */
    /*height:35px;*/
   
    /* No margin */
    margin:0px;
    /*line-height: 20px;*/
   
    /* No border or underlines*/
    text-decoration:none;
    border:none;
   
    /* Default text colour */
    color:#CCCCCC;
}
nav li ul a:hover, nav ul li ul li:hover a
{
    /* Underlines the text when we hover over it*/
    text-decoration:underline;
   
    /* Change the background colour*/
    background-color: #336666;
   
    color: #CCCCCC;
}
nav p
{
    /* No floating elements are allowed to the left of this element */
    clear:left;
}

Aug 30, 2012

CodeIgniter: Dynamically setting the website's language

I have been writing up my own View subsystem for CodeIgniter that seperates the  View more clearly from the controller. I have also designed it with Language support in mind, but for that I had to do some minor edits to the default Language class.

Even though you can load a specific language file in another language, the system will load error messages in the default language. This led to some cases where system errors would be in English, but the site language was something else.

With my new system I can have my default site language can be dynamically set by a user cookie… see the following code:

$lang_temp = $this->input->cookie('language');
if ( $lang_temp )
{
     // NOTE: getDefault() is one of my core Lang.php modifications
     if (!($lang_temp === $this->lang->getDefault()))
     {
        // NOTE: setDefault() is one of my core Lang.php modifications
        $this->lang->setDefault($lang_temp);
     }
} else {
     // Set a cookie with system default
     $this->input->set_cookie('language', $this->lang->getDefault(), '7200', '.' . $_SERVER['HTTP_HOST'], '/', NULL, FALSE);
}
 
But to make the above code work I had to make some changes to the core:



<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
/**
* CodeIgniter
*
* An open source application development framework for PHP 5.1.6 or newer
*
* @package CodeIgniter
* @author ExpressionEngine Dev Team
* @copyright Copyright (c) 2008 - 2011, EllisLab, Inc.
* @license http://codeigniter.com/user_guide/license.html
* @link http://codeigniter.com
* @since Version 1.0
* @filesource
*/

// ------------------------------------------------------------------------

/**
* Language Class
*
* @package CodeIgniter
* @subpackage Libraries
* @category Language
* @author ExpressionEngine Dev Team
* @link http://codeigniter.com/user_guide/libraries/language.html
*/
class CI_Lang {

/**
* List of translations
*
* @var array
*/
var $language = array();
/**
* List of loaded language files
*
* @var array
*/
var $is_loaded = array();

/**
* Default language
*
* @var array
*/
var $default_lang;

/**
* Constructor
*
* @access public
*/
function __construct()
{
    $config =& get_config();
    $this->default_lang = ( ! isset($config['language'])) ? 'en' : $config['language'];

    log_message('debug', "Language Class Initialized");
}

// --------------------------------------------------------------------

/**
* Load a language file
*
* @access public
* @param mixed the name of the language file to be loaded. Can be an array
* @param string the language (english, etc.)
* @param bool return loaded array of translations
* @param bool add suffix to $langfile
* @param string alternative path to look for language file
* @return mixed
*/
function load($langfile = '', $idiom = '', $return = FALSE, $add_suffix = TRUE, $alt_path = '')
{
    $langfile = str_replace('.php', '', $langfile);

    if ($add_suffix == TRUE)
    {
       $langfile = str_replace('_lang.', '', $langfile).'_lang';
    }

    $langfile .= '.php';

    if (in_array($langfile, $this->is_loaded, TRUE))
    {
       return;
    }

    if ($idiom == '')
    {
       $idiom = ($this->default_lang == '') ? 'en' : $this->default_lang;
    }

    // Determine where the language file is and load it
    if ($alt_path != '' && file_exists($alt_path.'language/'.$idiom.'/'.$langfile))
    {
       include($alt_path.'language/'.$idiom.'/'.$langfile);
    }
    else
    {
       $found = FALSE;

       foreach (get_instance()->load->get_package_paths(TRUE) as $package_path)
       {
          if (file_exists($package_path.'language/'.$idiom.'/'.$langfile))
          {
              include($package_path.'language/'.$idiom.'/'.$langfile);
              $found = TRUE;
              break;
          }
      }

      if ($found !== TRUE)
      {
         show_error('Unable to load the requested language file: language/'.$idiom.'/'.$langfile);
      }
   }


    if ( ! isset($lang))
   {
      log_message('error', 'Language file contains no data: language/'.$idiom.'/'.$langfile);
      return;
   }

   if ($return == TRUE)
   {
      return $lang;
   }

   $this->is_loaded[] = $langfile;
   $this->language = array_merge($this->language, $lang);
   unset($lang);

   log_message('debug', 'Language file loaded: language/'.$idiom.'/'.$langfile);
   return TRUE;
}

// --------------------------------------------------------------------

/**
* Fetch a single line of text from the language array
*
* @access public
* @param string $line the language line
* @return string
*/
function line($line = '')
{
   $value = ($line == '' OR ! isset($this->language[$line])) ? FALSE : $this->language[$line];

   // Because killer robots like unicorns!
   if ($value === FALSE)
   {
      log_message('error', 'Could not find the language line "'.$line.'"');
   }

   return $value;
}

/**
* Fetch Default language
*
* @access public
* @param void
* @return string Default language code
*/
function getDefault()
{
   return $this->default_lang;
}

/**
* Set Default language
*
* @access public
* @param string The default language
* @return void
*/
function setDefault($language)
{
   $this->default_lang = $language;
}

}
// END Language Class

/* End of file Lang.php */
/* Location: ./system/core/Lang.php */

I am sure you can find other uses for this code.....

My other posts on CodeIgniter include:

Jul 18, 2012

AJAX, JQuery, CodeIgniter and PHP

This is an extension of an earlier blog of mine, but this time I have built it using the CodeIgniter PHP framework. I have also spiced it up with a dynamic JQuery form (kudos to Charlie Griefer and this blog post for the code!)

If you want to get your hands dirty doing something a little harder then I suggest you check out my earlier blog on creating a login system with CodeIgniter.

If you are just getting started, have a look at my other blog posts, 'Ubuntu, LAMP and CodeIgniter' and the 'Extended JQuery Tutorial'.

Getting started

I am going to assume that you are working with a fresh installation of CodeIgniter that simply displays the welcome message. I am going to start from there so we don't get into any confusion!

First, unzip the CodeIgniter source into your web-server's root directory (if you followed my LAMP guide above, this will default to /var/www/). Rename the extracted folder to test (so in the default example, the CodeIgniter framework will be stored in /var/www/test/).

Now delete ALL the php files in /var/www/test/application/controllers and /var/www/test/application/views. We are going to start completely fresh!

The final step is to configure the framework to use our new controller (once we create it...), so edit the file /var/www/test/application/config/routes.php with the following line:
$route['default_controller'] = "home";
Now to make our main controller....

The Home page controller

  1. Create a file in /var/www/test/application/controllers named home.php
  2. Add in the following code:
    <?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');

    class Home extends CI_Controller {

        /**
         * Index Page for the home controller.
         *
         * Maps to the following URL
         *         http://example.com/
    index.php
         *    - or - 
         *         http://example.com/index.php/home
         *    - or -
         * Since this controller is set as the default controller in
         * config/routes.php, it's displayed at http://example.com/
         *
         * So any other public methods not prefixed with an underscore will
         * map to /index.php/home/<method_name>
         * @see http://codeigniter.com/user_guide/general/urls.html
         */
        public function index()
        {
            $this->load->view('view');
        }
    }

    /* End of file ajax.php */
    /* Location: ./application/controllers/home.php */
  3. That's it. All this does is load up our view, which is our next step.

The Home page view

  1. Create a file in /var/www/test/application/views named view.php
  2. Add in the following code:

    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
    "http://www.w3.org/TR/html4/loose.dtd">
    <html>
    <head>
    <title>Test</title>

    <!-- JQuery code hosted by Google -->
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3/jquery.min.js"></script>

    <!-- Adding and Deleting form buttons -->
    <script type="text/javascript" src="form.js"></script>

    <!-- AJAX functionality -->
    <script type="text/javascript" src="ajax.js"></script>
    </head>

    <body>

    <form id="myForm">
    <div id="input1" style="margin-bottom:4px;" class="clonedInput">
    Name: <input type="text" class="name" name="name1" id="name1" />
    </div>

    <div>
    <input type="button" id="btnAdd" value="add another name" />
    <input type="button" id="btnDel" value="remove name" />
    <input type="button" name="submit" class="button" id="submit" value="Send" />
    </div>

    <div id="display">
    </div>
    </form>

    </body>
    </html>
  3. Our basic HTML page includes three JavaScript files (One is the JQuery API, the other two we write ourselves; see below). We have a form with an input text box and three buttons. The first two buttons add and remove input boxes from our form, while the last button performs our AJAX operation.

form.js - Dynamically adding form elements

  1. Create a file called form.js in /var/www/test/
  2. Add the following code:
    $(document).ready(function() {
                $('#btnAdd').click(function() {
                   

                    // how many "duplicatable" input fields we currently have

                    var num     = $('.clonedInput').length;                // the numeric ID of the new input field being added
                    var newNum  = new Number(num + 1);
     
                    // create the new element via clone(),
                    // and manipulate it's ID using newNum value
                    var newElem = $('#input' + num).clone().attr('id', 'input' + newNum);
     
                    // manipulate the name/id values of the input inside the new element
                    newElem.children(':first').attr('id', 'name' + newNum).attr('name', 'name' + newNum);
     
                    // insert the new element after the last "duplicatable" input field
                    $('#input' + num).after(newElem);
     
                    // enable the "remove" button
                    $('#btnDel').attr('disabled','');
     
                    // business rule: you can only add 5 names
                    if (newNum == 5)
                        $('#btnAdd').attr('disabled','disabled');
                });
     
                $('#btnDel').click(function() {
                   
    // how many "duplicatable" input fields we currently have
                    var num = $('.clonedInput').length;

                   
    // remove the last element
                    $('#input' + num).remove();
     
                    // enable the "add" button
                    $('#btnAdd').attr('disabled','');
     
                    // if only one element remains, disable the "remove" button
                    if (num-1 == 1)
                        $('#btnDel').attr('disabled','disabled');
                });
     
                $('#btnDel').attr('disabled','disabled');
            });
  3. This function attaches a JavaScript function to the add and remove button. The code is pretty well commented by Charlie Griefer, so if you want to know more I suggest you go visit his blog post.

ajax.js - AJAX with JQuery

  1.  Create a file called ajax.js in /var/www/test/
  2. Add the following code:
    $(document).ready(function() {
        // This will run when the item of id 'submit' is clicked
        $("#submit").click(function() {

        // Grabs the text input
    from the form
        var name = [];
        $(".name").each(function() { name.push($(this).val()) });

        // Create the key-value pair for POST

        var dataString = 'string=' + name.join(' ') + '';

        // This creates the AJAX connection
        $.ajax({
           type: "POST",
           url: "index.php/ajax",
           data: dataString,
           dataType: 'json',
           success: function(obj) {
              $('#display').html(obj.message);
           }
        });
        return false;
        });
    });
  3. This will send all of our dynamically generated form elements to index.php/ajax (which we will now create). Hopefully the code and comments are self explanatory; if you have any problems just shout out in the comments!

The AJAX controller

  1. Create a file in /var/www/test/application/controllers named ajax.php
  2. Add the following code:
    <?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');

    class Ajax extends CI_Controller {


       /**

        * Index Page for this controller.
        *
        * Maps to the following URL
        * http://example.com/index.php/ajax
        * - or -
        * http://example.com/index.php/ajax/index
        *
        * So any other public methods not prefixed with an underscore will
        * map to /index.php/ajax/<method_name>
        * @see http://codeigniter.com/user_guide/general/urls.html
       */
       public function index()
       {
           $txtValue = "We have recieved: " . $this->input->post('string');

           echo json_encode(array('message' => $txtValue));

       }
    }

    /* End of file ajax.php */

    /* Location: ./application/controllers/ajax.php */
  3. You should now be able to run this on your web-server and have a dynamic form that sends and recieves AJAX!

References:

Jun 26, 2012

Handling HTTP GET requests with webapp2 and Google App Engine: Python

This is a continuation of my Python 2.7 and Google App Engine series. This particular blog post builds upon the code given in my previous posts URL Routing and  Cron and Datastore in Google App Engine: Python, which in turn builds upon my earlier work. If you don't understand parts of the code I highly suggest you browse my earlier blog posts so you can understand some of the design decisions I have made.

A brief overview...

For those who are diving straight in, let me explain the old code and how I will update it:

I have a script feed.py that I have mapped using app.yaml. A cron script (configured by cron.yaml) simply connects to my Twitter account and converts my status updates into an RSS feed. It then stores the RSS feed into a Google Datastore object.

The feed script takes the Datastore object and displays it. We use another script (entity.py) to define the Datastore object.

We will now configure the system so that it can convert multiple twitter accounts into an RSS feed. To display a particular RSS feed we will use a HTTP GET request.

The main application

We will create a file called feed.py. This script will be our controller; it simply gets the HTTP requests and maps them to certain classes. These classes will then call other functions to perform the required tasks.

# The webapp2 framework
import webapp2

# Our datastore interface
from google.appengine.ext import db

# Our entity library
import entity

# Our XML2RSS library
import XML2RSS

# Output the XML in a HTML friendly manner
class Cron(webapp2.RequestHandler):
    # Respond to a HTTP GET request
    def get(self):
        # A try-catch statement
        try:
            XML2RSS.getTweets("almightyolive")
            XML2RSS.getTweets("founding")
            XML2RSS.getTweets("ABCNews24")
            XML2RSS.getTweets("SBSNews")
       
        # Our exception code
        except (TypeError, ValueError):
            self.response.out.write("<html><body><p>Invalid inputs</p></body></html>")

# Fetches an XML document and parses it
class MainPage(webapp2.RequestHandler):
    # Respond to a HTTP GET request
    def get(self):
        # A try-catch statement
        try:
            account = self.request.get('account')
           
            feed = entity.Rss()
            feed_k = db.Key.from_path('Rss', account)
            feed = db.get(feed_k)
           
            # Outputs the RSS
            self.response.out.write(feed.content)

        # Our exception code
        except (TypeError,ValueError):
            self.response.out.write("<html><body><p>Invalid inputs (Type Error)</p></body></html>")
        except:
            self.response.out.write("<html><body><p>Unspecified Error</p></body></html>")

# Create our application instance that maps the root to our
# MainPage handler
app = webapp2.WSGIApplication([('/', MainPage),('/cron', Cron)], debug=True)

The XML2RSS script

As you may have noticed,the feed.py script made reference to an XML2RSS object. This is a separate script that outsources the conversion of XML to RSS into easy-to-call functions. Create a new file called XML2RSS.py and add the following:

# The minidom library for XML parsing
from xml.dom.minidom import parseString

# The URL Fetch library
from google.appengine.api import urlfetch

# Our entity library
import entity

# Detects if it is a URL link and adds the HTML tags
def linkify(text):
    # If http is present in, add the link tag
    if "http" in text:
        text = "&lt;a href='" + text + "'&gt;" + text + "&lt;/a&gt;"
    elif "@" in text:
        text = "&lt;a href='http://twitter.com/#!/" + text.split("@")[1] + "'&gt;" + text + "&lt;/a&gt;"
    elif "#" in text:
        text = "&lt;a href='https://twitter.com/#!/search/%23" + text.split("#")[1] + "'&gt;" + text + "&lt;/a&gt;"
       
    return text

# Output the XML in a HTML friendly manner
def outputRSS(xml, account):
    # The get the states list
    statuses = xml.getElementsByTagName("status")
   
    # Our return string
    outputString = "<?xml version='1.0'?>\n<rss version='2.0'>\n\t<channel>\n\t\t<title>Twitter: " + account + "</title>\n\t\t"
    outputString+= "<link>https://twitter.com/#!/almightyolive</link>\n\t\t<description>The twitter feed for " + account + "</description>"
   
    # Cycled through the states
    for status in statuses:
        #Gets the statuses
        text = status.getElementsByTagName("text")[0].firstChild.data
        date = status.getElementsByTagName("created_at")[0].firstChild.data
        tweet = status.getElementsByTagName("id")[0].firstChild.data
       
        # Insert links into the text
        words = text.split()
       
        for i in range (len(words)):
            words[i] = linkify(words[i])
       
        # Recompile words
        text = " ".join(words)
       
        # Creates our output
        string = "\n\t\t<item>\n\t\t\t<title>" + str(date) + "</title>\n\t\t\t<link>https://twitter.com/AlmightyOlive/status/" + tweet + "</link>\n\t\t\t<description>" + str(text) + "</description>\n\t\t</item>"
        outputString+=string
       
    # Output string
    outputString += "\n\t</channel>\n</rss>"
    return outputString   

# Our RSS storage function
def getTweets(account):
    # Grabs the XML
    url = urlfetch.fetch('https://api.twitter.com/1/statuses/user_timeline.xml?screen_name=' + account + '&count=10&trim_user=true')
           
    # Parses the document
    xml = parseString(url.content)

    # Converts the XML into RSS
    content = outputRSS(xml, account)
   
    # Our RSS storage entity
    rssStore = entity.Rss(key_name='' + account)

    # Elements of our RSS
    rssStore.feed = '' + account
    rssStore.content = content

    # Stores our RSS Feed into the datastore
    rssStore.put()

The pieces to make it all work

If you have been following on from my previous work, then you should already have most of this code. I won't bother explaining it here because it is mostly self-explanatory.

app.yaml:
application: almightynassar
version: 1
runtime: python27
api_version: 1
threadsafe: yes

handlers:
- url: /cron
  script: feed.app
  login: admin
 
- url: /.*
  script: feed.app

cron.yaml:


cron:
- description: daily summary job
  url: /cron
  schedule: every 1 hours

entity.py:

# Our datastore interface
from google.appengine.ext import db

# Our RSS entity object
class Rss(db.Model):
    feed = db.StringProperty()
    content = db.TextProperty()

And that's it! You now have a fully functional application that just uses the webapp2 framework!

If you navigate to http://localhost:8080/?account=almightyolive you should now see the RSS feed. You can test if your mapping works by navigating to http://localhost:8080/?account=founding; you should see the Founding Institute twitter account instead!


References:

Jun 25, 2012

URL Routing through WebApp2 in Google App Engine: Python

This is a continuation of my Python 2.7 and Google App Engine series. This particular blog post builds upon the code given in my previous post Cron and Datastore in Google App Engine: Python, which in turns builds upon my earlier work. If you don't understand parts of the code I highly suggest you browse my earlier blog posts so you can understand some of the design decisions I have made.

A brief overview...

For those who are diving straight in, let me explain the old code and how I will update it:

I have two scripts (feed.py and cron.py) that I have mapped using app.yaml. The cron script simply connects to my Twitter account and converts my status updates into an RSS feed. It then stores the RSS feed into a Google Datastore object.

The feed script takes the Datastore object and displays it. Both scripts use a third script (entity.py) to define the Datastore object.

Currently the set-up is not thread-safe because I have to use two different scripts to handle my incoming requests. The plan is to replace this set-up with one that is thread-safe. Effectively, we will be using the URL routing functionality provided by the webapp2 framework.

Combining the scripts

The first thing we will do is combine both cron.py and feed.py into one script. The following code should be saved to a file called feed.py:

# The webapp2 framework
import webapp2

# Our datastore interface
from google.appengine.ext import db

# The minidom library for XML parsing
from xml.dom.minidom import parseString

# The URL Fetch library
from google.appengine.api import urlfetch

# Our entity library
import entity

# Detects if it is a URL link and adds the HTML tags
def linkify(text):
    # If http is present in, add the link tag
    if "http" in text:
        text = "&lt;a href='" + text + "'&gt;" + text + "&lt;/a&gt;"
    elif "@" in text:
        text = "&lt;a href='http://twitter.com/#!/" + text.split("@")[1] + "'&gt;" + text
        text+= "&lt;/a&gt;"
    elif "#" in text:
        text = "&lt;a href='https://twitter.com/#!/search/%23" + text.split("#")[1] + "'&gt;" + text + "&lt;/a&gt;"
       
    return text

# Output the XML in a HTML friendly manner
def outputRSS(xml):
    # The get the states list
    statuses = xml.getElementsByTagName("status")
   
    # Our return string
    outputString = "<?xml version='1.0'?>\n<rss version='2.0'>\n\t<channel>"
    outputString+= "\n\t\t<title>Almightyolive Twitter</title>\n\t\t"
    outputString+= "<link>https://twitter.com/#!/almightyolive</link>\n"
    outputString+= "\t\t<description>The twitter feed for the Almighty "
    outputString+= "Olive</description>"
   
    # Cycled through the states
    for status in statuses:
        #Gets the statuses
        text = status.getElementsByTagName("text")[0].firstChild.data
        date = status.getElementsByTagName("created_at")[0].firstChild.data
        tweet = status.getElementsByTagName("id")[0].firstChild.data
       
        # Insert links into the text
        words = text.split()
       
        for i in range (len(words)):
            words[i] = linkify(words[i])
       
        # Recompile words
        text = " ".join(words)
       
        # Creates our output
        string = "\n\t\t<item>\n\t\t\t<title>" + str(date) + "</title>\n"
        string+= "\t\t\t<link>https://twitter.com/AlmightyOlive/status/" + tweet
        string+= "</link>\n\t\t\t<description>" + str(text) + "</description>\n"
        string+= "\t\t</item>"
        outputString+=string
       
    # Output string
    outputString += "\n\t</channel>\n</rss>"
    return outputString   

# Output the XML in a HTML friendly manner
class Cron(webapp2.RequestHandler):
    # Respond to a HTTP GET request
    def get(self):
        # A try-catch statement
        try:
            # Grabs the XML
            url = urlfetch.fetch('https://api.twitter.com/1/statuses/user_timeline.xml?screen_name=almightyolive&count=10&trim_user=true')
           
            # Parses the document
            xml = parseString(url.content)

            content = outputRSS(xml)
            # Our RSS storage entity
            rssStore = entity.Rss(key_name='almightyolive')
           
            # Elements of our RSS
            rssStore.feed = "almightyolive"
            rssStore.content = content

            # Stores our RSS Feed into the datastore
            rssStore.put()
       
        # Our exception code
        except (TypeError, ValueError):
            self.response.out.write("<html><body><p>Invalid inputs</p></body></html>")

# Fetches an XML document and parses it
class MainPage(webapp2.RequestHandler):
    # Respond to a HTTP GET request
    def get(self):
        # A try-catch statement
        try:
            feed = entity.Rss()
            feed_k = db.Key.from_path('Rss', 'almightyolive')
            feed = db.get(feed_k)
           
            # Outputs the RSS
            self.response.out.write(feed.content)

        # Our exception code
        except (TypeError, ValueError):
            self.response.out.write("<html><body><p>Invalid inputs</p></body></html>")

# Create our application instance that maps the root to our
# MainPage handler
app = webapp2.WSGIApplication([('/', MainPage),('/cron', Cron)], debug=True)

The big changes are:
  • We have added a new class called Cron, which included all of that loose code in cron.py
  • We have added a new URL mapping to our WSGI Application. This will hand over any request for '/cron' to our new Cron class

The pieces to make it all work

If you have been following on from my previous work, then you should already have most of this code. The only thing you need to touch is one line in app.yaml, which is to map /cron to our feed webapp.

app.yaml:
application: almightynassar
version: 1
runtime: python27
api_version: 1
threadsafe: yes

handlers:
- url: /cron
  script: feed.app
  login: admin
 
- url: /.*
  script: feed.app

cron.yaml:


cron:
- description: daily summary job
  url: /cron
  schedule: every 1 hours

entity.py:

# Our datastore interface
from google.appengine.ext import db

# Our RSS entity object
class Rss(db.Model):
    feed = db.StringProperty()
    content = db.TextProperty()

And that's it! You now have a fully functional application that just uses the webapp2 framework!

References: